-
Notifications
You must be signed in to change notification settings - Fork 3
/
unikernel.ml
145 lines (130 loc) · 6.14 KB
/
unikernel.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
(* This unikernel is largely inspired by the example at https://github.com/mirage/mirage-nat/ *)
open Lwt.Infix
module Main
(* our unikernel is functorized over the physical, and ethernet modules
for the public and private interfaces, so each one shows up as a module
argument. *)
(Public_net: Mirage_net.S) (Private_net: Mirage_net.S)
(Public_ethernet : Ethernet.S) (Private_ethernet : Ethernet.S)
(Random : Mirage_crypto_rng_mirage.S) (Clock : Mirage_clock.MCLOCK)
= struct
(* configure logs, so we can use them later *)
let log = Logs.Src.create "fw" ~doc:"FW device"
module Log = (val Logs.src_log log : Logs.LOG)
(* the specific impls we're using show up as arguments to start. *)
let start public_netif private_netif
_public_ethernet _private_ethernet
_rng () =
(* Creates a set of rules (empty) and a default condition (accept) *)
let filter_rules =
Rules.init false
in
(* Takes an IPv4 [packet], unmarshal it, check if we're the destination and
the payload is some sort of rule update, apply that, and if we're not the
destination, use the filter_rules to [out] the packet or not. *)
let is_forwardable packet =
(* Handle IPv4 only... *)
match Ipv4_packet.Unmarshal.of_cstruct packet with
| Result.Error s ->
Logs.err (fun m -> m "Can't parse IPv4 packet: %s" s);
false
(* If the packet is a UDP packet to us, decode *)
(* | Result.Ok (ipv4_hdr, payload) when
Ipv4_packet.Unmarshal.int_to_protocol ipv4_hdr.Ipv4_packet.proto =
Some `UDP &&
(ipv4_hdr.dst = filter_rules.public_ipv4 || ipv4_hdr.dst = filter_rules.private_ipv4) &&
Rules.magic_is_present payload ->
Logs.info (fun m -> m "Got an update message from %a" Ipaddr.V4.pp ipv4_hdr.src);
Rules.update filter_rules payload ;
false
(* If the packet is a UDP packet to us but not an update packet, ignore *)
| Result.Ok (ipv4_hdr, _payload) when
(ipv4_hdr.dst = filter_rules.public_ipv4 || ipv4_hdr.dst = filter_rules.private_ipv4) ->
Logs.debug (fun m -> m "Got an message from %a but not an update packet" Ipaddr.V4.pp ipv4_hdr.src);
false *)
(* Otherwise try to forward (or not) the packet *)
| Result.Ok (ipv4_hdr, payload) ->
Rules.filter filter_rules (ipv4_hdr, payload)
in
(* Forward the (dest, packet) [packet] to the public interface, using [dest] to understand how to route *)
let output_public :
Cstruct.t -> unit Lwt.t
= fun packet ->
let len = Cstruct.length packet in
Public_net.write public_netif ~size:len (fun b -> Cstruct.blit packet 0 b 0 len ; len) >|= function
| Ok () -> ()
| Error e ->
Log.warn (fun f -> f "netif write errored %a" Public_net.pp_error e) ;
()
in
(* Forward the (dest, packet) [packet] to the private interface, using [dest] to understand how to route *)
let output_private :
Cstruct.t -> unit Lwt.t
= fun packet ->
(* For IPv4 only one prefix can be configured so the list is always of length 1 *)
let len = Cstruct.length packet in
Private_net.write private_netif ~size:len (fun b -> Cstruct.blit packet 0 b 0 len ; len) >|= function
| Ok () -> ()
| Error e ->
Log.warn (fun f -> f "netif write errored %a" Private_net.pp_error e) ;
()
in
(* we need to establish listeners for the private and public interfaces *)
(* we're interested in all traffic to the physical interface; we'd like to
send ARP traffic to the normal ARP listener and responder,
handle ipv4 traffic with the functions we've defined above for filtering,
and ignore all ipv6 traffic. *)
let listen_public =
let header_size = Ethernet.Packet.sizeof_ethernet
and input frame= (* Takes an ethernet packet and send it to the relevant callback *)
match Ethernet.Packet.of_cstruct frame with
| Ok (header, payload) ->
begin
match header.Ethernet.Packet.ethertype with
| `ARP -> output_private frame
| `IPv4 when is_forwardable payload -> output_private frame
| _ -> Lwt.return_unit
end
| Error s ->
Log.debug (fun f -> f "dropping Ethernet frame: %s" s);
Lwt.return_unit
in
Public_net.listen ~header_size public_netif input >>= function
| Error e -> Log.debug (fun f -> f "public interface stopped: %a"
Public_net.pp_error e); Lwt.return_unit
| Ok () -> Log.debug (fun f -> f "public interface terminated normally");
Lwt.return_unit
in
let listen_private =
let header_size = Ethernet.Packet.sizeof_ethernet
and input frame = (* Takes an ethernet packet and send it to the relevant callback *)
match Ethernet.Packet.of_cstruct frame with
| Ok (header, payload) ->
begin
match header.Ethernet.Packet.ethertype with
| `ARP -> output_public frame
| `IPv4 when is_forwardable payload -> output_public frame
| _ -> Lwt.return_unit
end
| Error s ->
Log.debug (fun f -> f "dropping Ethernet frame: %s" s);
Lwt.return_unit
in
Private_net.listen ~header_size private_netif input >>= function
| Error e -> Log.debug (fun f -> f "private interface stopped: %a"
Private_net.pp_error e); Lwt.return_unit
| Ok () -> Log.debug (fun f -> f "private interface terminated normally");
Lwt.return_unit
in
(* Notice how we haven't said anything about ICMP anywhere. The unikernel
doesn't know anything about it, so pinging this host on either interface
will just be ignored -- the only way this unikernel can be easily seen,
without sending traffic through it, is via ARP. The `arping` command
line utility might be useful in trying to see whether your unikernel is
up. *)
(* start both listeners, and continue as long as both are working. *)
Lwt.pick [
listen_public;
listen_private;
]
end