diff --git a/.gitignore b/.gitignore index f5cd959..bd2f111 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -Makefile +/Makefile _build/ log key_gen.ml diff --git a/README.md b/README.md index bfbef5f..960e568 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,29 @@ qvm-prefs --set my-app-vm netvm mirage-firewall Alternatively, you can configure `mirage-firewall` to be your default firewall VM. +### Components + +This diagram show the main components (each box corresponds to a source `.ml` file with the same name): + +

+ +

+ +Ethernet frames arrives from client qubes (such as `work` or `personal`) or from `sys-net`. +Internet (IP) packets are sent to `firewall`, which consults `rules` to decide what to do with the packet. +If it should be sent on, it uses `router` to send it to the chosen destination. +`client_net` watches the XenStore database provided by dom0 +to find out when clients need to be added or removed. + +The boot process: + +- `config.ml` describes the libraries used and static configuration settings (NAT table size). + The `mirage` tool uses this to generate `main.ml`. +- `main.ml` initialises the drivers selected by `config.ml` + and calls the `start` function in `unikernel.ml`. +- `unikernel.ml` connects the Qubes agents, sets up the networking components, + and then waits for a shutdown request. + ### Easy deployment for developers For development, use the [test-mirage][] scripts to deploy the unikernel (`qubes_firewall.xen`) from your development AppVM. diff --git a/build-with-docker.sh b/build-with-docker.sh index d14c057..7345ca5 100755 --- a/build-with-docker.sh +++ b/build-with-docker.sh @@ -5,5 +5,5 @@ docker build -t qubes-mirage-firewall . echo Building Firewall... docker run --rm -i -v `pwd`:/home/opam/qubes-mirage-firewall qubes-mirage-firewall echo "SHA2 of build: $(sha256sum qubes_firewall.xen)" -echo "SHA2 last known: dbf7460fa628bea5d132a96fe7ba2cd832e3d9da7005ae74f6a124957f4848ea" +echo "SHA2 last known: 888cfd66e54c14da75be2bc4272efdb74c2ec8f9f144979f508a09410121482e" echo "(hashes should match for released versions)" diff --git a/client_net.ml b/client_net.ml index 0649567..68fe6d3 100644 --- a/client_net.ml +++ b/client_net.ml @@ -56,7 +56,7 @@ let input_arp ~fixed_arp ~iface request = iface#writev `ARP (fun b -> Arp_packet.encode_into response b; Arp_packet.size) (** Handle an IPv4 packet from the client. *) -let input_ipv4 ~client_ip ~router packet = +let input_ipv4 ~iface ~router packet = match Nat_packet.of_ipv4_packet packet with | Error e -> Log.warn (fun f -> f "Ignored unknown IPv4 message: %a" Nat_packet.pp_error e); @@ -64,10 +64,10 @@ let input_ipv4 ~client_ip ~router packet = | Ok packet -> let `IPv4 (ip, _) = packet in let src = ip.Ipv4_packet.src in - if src = client_ip then Firewall.ipv4_from_client router packet + if src = iface#other_ip then Firewall.ipv4_from_client router ~src:iface packet else ( Log.warn (fun f -> f "Incorrect source IP %a in IP packet from %a (dropping)" - Ipaddr.V4.pp src Ipaddr.V4.pp client_ip); + Ipaddr.V4.pp src Ipaddr.V4.pp iface#other_ip); return () ) @@ -94,7 +94,7 @@ let add_vif { Dao.ClientVif.domid; device_id } ~client_ip ~router ~cleanup_tasks | Ok (eth, payload) -> match eth.Ethernet_packet.ethertype with | `ARP -> input_arp ~fixed_arp ~iface payload - | `IPv4 -> input_ipv4 ~client_ip ~router payload + | `IPv4 -> input_ipv4 ~iface ~router payload | `IPv6 -> return () (* TODO: oh no! *) ) >|= or_raise "Listen on client interface" Netback.pp_error diff --git a/diagrams/Makefile b/diagrams/Makefile new file mode 100644 index 0000000..a6fbc5f --- /dev/null +++ b/diagrams/Makefile @@ -0,0 +1,6 @@ +# Requires https://github.com/blampe/goat + +all: components.svg + +%.svg: %.txt + goat $^ > $@ diff --git a/diagrams/components.svg b/diagrams/components.svg new file mode 100644 index 0000000..1e996b1 --- /dev/null +++ b/diagrams/components.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +l +y +s +t +k +r +u +l +n +_ +r +i +e +l +o +n +k +n +o +o +e +e +e +l +s +t +( +f +p +i +i +o +w +t +u +n +- +a +o +X +S +r +m +u +c +r +] +e +r +i +n +s +t +e +k +s +w +e +. +n +e +l +r +s +e +s +r +l +[ +. +p +n +t +o +o +c +h +. +c +t +m +a +e +r +d +0 +) + + diff --git a/diagrams/components.txt b/diagrams/components.txt new file mode 100644 index 0000000..62e4f9e --- /dev/null +++ b/diagrams/components.txt @@ -0,0 +1,20 @@ + +----------+ + | rules | + +----------+ + ^ + |checks + | + +------------+ +-----+----+ + work <---->| +---->| firewall |<--------. + | | +-----+----+ | + | | | +----+---+ + [...] <---->| client_net | | | uplink |<----> sys-net + | | v +--------+ + | | +----------+ ^ +personal <---->| |<----+ router +---------' + +------+-----+ +----------+ + | + |monitors + v + XenStore + (dom0) diff --git a/firewall.ml b/firewall.ml index 39254d3..cbb47b7 100644 --- a/firewall.ml +++ b/firewall.ml @@ -48,8 +48,21 @@ let forward_ipv4 t packet = (* Packet classification *) -let classify t packet = - let `IPv4 (ip, transport) = packet in +let parse_ips ips = List.map (fun (ip_str, id) -> (Ipaddr.of_string_exn ip_str, id)) ips + +let clients = parse_ips Rules.clients +let externals = parse_ips Rules.externals + +let resolve_client client = + `Client (try List.assoc (Ipaddr.V4 client#other_ip) clients with Not_found -> `Unknown) + +let resolve_host = function + | `Client c -> resolve_client c + | `External ip -> `External (try List.assoc ip externals with Not_found -> `Unknown) + | (`Client_gateway | `Firewall_uplink | `NetVM) as x -> x + +let classify ~src ~dst packet = + let `IPv4 (_ip, transport) = packet in let proto = match transport with | `TCP ({Tcp.Tcp_packet.src_port; dst_port; _}, _) -> `TCP {sport = src_port; dport = dst_port} @@ -58,8 +71,8 @@ let classify t packet = in Some { packet; - src = Router.classify t (Ipaddr.V4 ip.Ipv4_packet.src); - dst = Router.classify t (Ipaddr.V4 ip.Ipv4_packet.dst); + src; + dst; proto; } @@ -80,7 +93,10 @@ let pp_proto fmt = function | `ICMP -> Format.pp_print_string fmt "ICMP" | `Unknown -> Format.pp_print_string fmt "UnknownProtocol" -let pp_packet fmt {src; dst; proto; packet = _} = +let pp_packet t fmt {src = _; dst = _; proto; packet} = + let `IPv4 (ip, _transport) = packet in + let src = Router.classify t (Ipaddr.V4 ip.Ipv4_packet.src) in + let dst = Router.classify t (Ipaddr.V4 ip.Ipv4_packet.dst) in Format.fprintf fmt "[src=%a dst=%a proto=%a]" pp_host src pp_host dst @@ -125,18 +141,18 @@ let nat_to t ~host ~port packet = (* Handle incoming packets *) -let apply_rules t rules info = +let apply_rules t rules ~dst info = let packet = info.packet in - match rules info, info.dst with + match rules info, dst with | `Accept, `Client client_link -> transmit_ipv4 packet client_link | `Accept, (`External _ | `NetVM) -> transmit_ipv4 packet t.Router.uplink | `Accept, (`Firewall_uplink | `Client_gateway) -> - Log.warn (fun f -> f "Bad rule: firewall can't accept packets %a" pp_packet info); + Log.warn (fun f -> f "Bad rule: firewall can't accept packets %a" (pp_packet t) info); return () | `NAT, _ -> add_nat_and_forward_ipv4 t packet | `NAT_to (host, port), _ -> nat_to t packet ~host ~port | `Drop reason, _ -> - Log.info (fun f -> f "Dropped packet (%s) %a" reason pp_packet info); + Log.info (fun f -> f "Dropped packet (%s) %a" reason (pp_packet t) info); return () let handle_low_memory t = @@ -147,7 +163,7 @@ let handle_low_memory t = `Memory_critical | `Ok -> Lwt.return `Ok -let ipv4_from_client t packet = +let ipv4_from_client t ~src packet = handle_low_memory t >>= function | `Memory_critical -> return () | `Ok -> @@ -156,23 +172,28 @@ let ipv4_from_client t packet = | Some frame -> forward_ipv4 t frame (* Some existing connection or redirect *) | None -> (* No existing NAT entry. Check the firewall rules. *) - match classify t packet with + let `IPv4 (ip, _transport) = packet in + let dst = Router.classify t (Ipaddr.V4 ip.Ipv4_packet.dst) in + match classify ~src:(resolve_client src) ~dst:(resolve_host dst) packet with | None -> return () - | Some info -> apply_rules t Rules.from_client info + | Some info -> apply_rules t Rules.from_client ~dst info let ipv4_from_netvm t packet = handle_low_memory t >>= function | `Memory_critical -> return () | `Ok -> - match classify t packet with + let `IPv4 (ip, _transport) = packet in + let src = Router.classify t (Ipaddr.V4 ip.Ipv4_packet.src) in + let dst = Router.classify t (Ipaddr.V4 ip.Ipv4_packet.dst) in + match classify ~src ~dst:(resolve_host dst) packet with | None -> return () | Some info -> - match info.src with + match src with | `Client _ | `Firewall_uplink | `Client_gateway -> - Log.warn (fun f -> f "Frame from NetVM has internal source IP address! %a" pp_packet info); + Log.warn (fun f -> f "Frame from NetVM has internal source IP address! %a" (pp_packet t) info); return () - | `External _ | `NetVM -> + | `External _ | `NetVM as src -> translate t packet >>= function | Some frame -> forward_ipv4 t frame | None -> - apply_rules t Rules.from_netvm info + apply_rules t Rules.from_netvm ~dst { info with src } diff --git a/firewall.mli b/firewall.mli index 3909ee0..9900f56 100644 --- a/firewall.mli +++ b/firewall.mli @@ -6,6 +6,6 @@ val ipv4_from_netvm : Router.t -> Nat_packet.t -> unit Lwt.t (** Handle a packet from the outside world (this module will validate the source IP). *) -val ipv4_from_client : Router.t -> Nat_packet.t -> unit Lwt.t +val ipv4_from_client : Router.t -> src:Fw_utils.client_link -> Nat_packet.t -> unit Lwt.t (** Handle a packet from a client. Caller must check the source IP matches the client's before calling this. *) diff --git a/packet.ml b/packet.ml index a9fa4e7..d9b49bb 100644 --- a/packet.ml +++ b/packet.ml @@ -13,9 +13,25 @@ type ports = { type host = [ `Client of client_link | `Client_gateway | `Firewall_uplink | `NetVM | `External of Ipaddr.t ] -type info = { +type ('src, 'dst) info = { packet : Nat_packet.t; - src : host; - dst : host; + src : 'src; + dst : 'dst; proto : [ `UDP of ports | `TCP of ports | `ICMP | `Unknown ]; } + +(* The first message in a TCP connection has SYN set and ACK clear. *) +let is_tcp_start = function + | `IPv4 (_ip, `TCP (hdr, _body)) -> Tcp.Tcp_packet.(hdr.syn && not hdr.ack) + | _ -> false + +(* The possible actions we can take for a packet: *) +type action = [ + | `Accept (* Send the packet to its destination. *) + | `NAT (* Rewrite the packet's source field so packet appears to + have come from the firewall, via an unused port. + Also, add NAT rules so related packets will be translated accordingly. *) + | `NAT_to of host * port (* As for [`NAT], but also rewrite the packet's + destination fields so it will be sent to [host:port]. *) + | `Drop of string (* Drop the packet and log the given reason. *) +] diff --git a/rules.ml b/rules.ml index 7e62790..3959d14 100644 --- a/rules.ml +++ b/rules.ml @@ -1,39 +1,62 @@ (* Copyright (C) 2015, Thomas Leonard See the README file for details. *) -(** Put your firewall rules here. *) +(** Put your firewall rules in this file. *) -open Packet +open Packet (* Allow us to use definitions in packet.ml *) + +(* List your AppVM IP addresses here if you want to match on them in the rules below. + Any client not listed here will appear as [`Client `Unknown]. *) +let clients = [ + (* + "10.137.0.12", `Dev; + "10.137.0.14", `Untrusted; + *) +] + +(* List your external (non-AppVM) IP addresses here if you want to match on them in the rules below. + Any external machine not listed here will appear as [`External `Unknown]. *) +let externals = [ + (* + "8.8.8.8", `GoogleDNS; + *) +] (* OCaml normally warns if you don't match all fields, but that's OK here. *) [@@@ocaml.warning "-9"] -(** {2 Actions} +(** This function decides what to do with a packet from a client VM. - The possible actions are: + It takes as input an argument [info] (of type [Packet.info]) describing the + packet, and returns an action (of type [Packet.action]) to perform. - - [`Accept] : Send the packet to its destination. + See packet.ml for the definitions of [info] and [action]. - - [`NAT] : Rewrite the packet's source field so packet appears to - have come from the firewall, via an unused port. - Also, add NAT rules so related packets will be translated accordingly. - - - [`NAT_to (host, port)] : - As for [`NAT], but also rewrite the packet's destination fields so it - will be sent to [host:port]. - - - [`Drop reason] drop the packet and log the reason. -*) - -(** Decide what to do with a packet from a client VM. Note: If the packet matched an existing NAT rule then this isn't called. *) -let from_client = function +let from_client (info : ([`Client of _], _) Packet.info) : Packet.action = + match info with + (* Examples (add your own rules here): + + 1. Allows Dev to send SSH packets to Untrusted. + Note: responses are not covered by this! + 2. Allows Untrusted to reply to Dev. + 3. Blocks an external site. + + In all cases, make sure you've added the VM name to [clients] or [externals] above, or it won't + match anything! *) + (* + | { src = `Client `Dev; dst = `Client `Untrusted; proto = `TCP { dport = 22 } } -> `Accept + | { src = `Client `Untrusted; dst = `Client `Dev; proto = `TCP _; packet } + when not (is_tcp_start packet) -> `Accept + | { dst = `External `GoogleDNS } -> `Drop "block Google DNS" + *) | { dst = (`External _ | `NetVM) } -> `NAT | { dst = `Client_gateway; proto = `UDP { dport = 53 } } -> `NAT_to (`NetVM, 53) | { dst = (`Client_gateway | `Firewall_uplink) } -> `Drop "packet addressed to firewall itself" - | { dst = `Client _ } -> `Drop "prevent communication between client VMs" + | { dst = `Client _ } -> `Drop "prevent communication between client VMs by default" (** Decide what to do with a packet received from the outside world. Note: If the packet matched an existing NAT rule then this isn't called. *) -let from_netvm = function +let from_netvm (info : ([`NetVM | `External of _], _) Packet.info) : Packet.action = + match info with | _ -> `Drop "drop by default"