Compare commits

..

16 commits
v0.9.4 ... main

Author SHA1 Message Date
Pierre Alain
5257071810
Merge pull request #218 from mirage/tst
minor change
2025-03-19 10:30:40 +01:00
Pierre Alain
64d2b16c3a fix hashsum 2025-03-18 15:52:32 +01:00
Pierre Alain
0398036a14
Merge pull request #217 from hannesm/fix-formatting
fix formatting action
2025-03-18 15:06:53 +01:00
Automated ocamlformat GitHub action, developed by robur.coop
4d89b85892 formatted code 2025-03-18 08:16:13 +00:00
Pierre Alain
511ac0adfb trigger format on push rather than pull_request 2025-03-18 09:10:17 +01:00
Hannes Mehnert
17941c7fbc minor change 2025-03-17 12:59:22 +01:00
Hannes Mehnert
edba36b97b another try 2025-03-17 12:35:47 +01:00
Hannes Mehnert
4de45e2f67 try 2025-03-17 12:25:34 +01:00
Hannes Mehnert
bc3fdaf3d5 fix formatting action 2025-03-17 12:23:10 +01:00
Pierre Alain
3138ef53ee
Merge pull request #215 from hannesm/mirage-49
update to mirage 4.9.0
2025-03-13 11:07:55 +01:00
Pierre Alain
85c8b7a661 add ocamlformat and autoformat in github action 2025-03-13 10:57:13 +01:00
Pierre Alain
a756effb14 update hashsum 2025-03-12 11:56:51 +01:00
Pierre Alain
5d515c360d update opam version, opam-repository and overlays hash 2025-03-12 11:56:33 +01:00
Hannes Mehnert
592f53777e update to mirage 4.9.0 2025-03-10 13:51:20 +01:00
Pierre Alain
56a823ab5e
Merge pull request #214 from palainp/v0.9.4
release v0.9.4
2025-02-10 11:36:36 +01:00
Pierre Alain
5f5fe82b9b release v0.9.4 2025-02-10 11:25:57 +01:00
28 changed files with 1495 additions and 1259 deletions

42
.github/workflows/format.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: ocamlformat
on: [push]
jobs:
format:
name: ocamlformat
strategy:
fail-fast: false
matrix:
ocaml-version: ["4.14.2"]
operating-system: [ubuntu-latest]
runs-on: ${{ matrix.operating-system }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Use OCaml ${{ matrix.ocaml-version }}
uses: ocaml/setup-ocaml@v3
with:
ocaml-compiler: ${{ matrix.ocaml-version }}
- name: Install ocamlformat
run: grep ^version .ocamlformat | cut -d '=' -f 2 | xargs -I V opam install ocamlformat=V
- name: Format code
run: git ls-files '*.ml' '*.mli' | xargs opam exec -- ocamlformat --inplace
- name: Check for modified files
id: git-check
run: echo "modified=$(if git diff-index --quiet HEAD --; then echo "false"; else echo "true"; fi)" >> $GITHUB_OUTPUT
- name: Commit and push changes
if: steps.git-check.outputs.modified == 'true'
run: |
git config --global user.name "Automated ocamlformat GitHub action, developed by robur.coop"
git config --global user.email "autoformat@robur.coop"
git commit -m "formatted code" .
git push

3
.ocamlformat Normal file
View file

@ -0,0 +1,3 @@
version = 0.27.0
profile = conventional
parse-docstrings = true

View file

@ -1,3 +1,11 @@
### 0.9.4 (2025-02-10)
- Fix an issue when qubes-mirage-firewall is used a a mullvad AppVM client. If
our netvm does not reply to our ARP requests we can not construct the ethernet
header. However in Linux VMs, Qubes adds a default netvm address associated to
`fe:ff:ff:ff:ff:ff`, so if ARP fails, we fall back on that address.
(#213, @palainp, reported in the Qubes forum #212, reviewed by @hannesm)
### 0.9.3 (2025-01-04)
- Fix an issue when qubes-mirage-firewall is used along with *BSD sys-net

View file

@ -12,10 +12,10 @@ RUN printf "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian
RUN printf "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian-security/20240419T111010Z bookworm-security main\n" >> /etc/apt/sources.list
RUN apt update && apt install --no-install-recommends --no-install-suggests -y wget ca-certificates git patch unzip bzip2 make gcc g++ libc-dev
RUN wget -O /usr/bin/opam https://github.com/ocaml/opam/releases/download/2.2.1/opam-2.2.1-i686-linux && chmod 755 /usr/bin/opam
RUN wget -O /usr/bin/opam https://github.com/ocaml/opam/releases/download/2.3.0/opam-2.3.0-i686-linux && chmod 755 /usr/bin/opam
# taken from https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh
RUN test `sha512sum /usr/bin/opam | cut -d' ' -f1` = \
"bf16d573137835ce9abbcf6b99cb94a1da69ab58804a4de7c90233f0b354d5e68e9c47ee16670ca9d59866d58c7db345d9723e6eb5fc3a1cb8dca371f0e90225" || exit
"4c0e8771889a36bad4d5f964e2e662d5b611e6f112777d3d4eea3eea919d109cd17826beba38e6cfa1ad9553a0a989d9268f911ea5485968da04b1e08efc7de2" || exit
ENV OPAMROOT=/tmp
ENV OPAMCONFIRMLEVEL=unsafe-yes
@ -23,13 +23,13 @@ ENV OPAMCONFIRMLEVEL=unsafe-yes
# Remove this line (and the base image pin above) if you want to test with the
# latest versions.
# taken from https://github.com/ocaml/opam-repository
RUN opam init --disable-sandboxing -a --bare https://github.com/ocaml/opam-repository.git#5d3f0d1d655199e596a1e785e69fae8fad78cad3
RUN opam init --disable-sandboxing -a --bare https://github.com/ocaml/opam-repository.git#8f63148a9025a7b775a069a6c0b0385c22ad51d3
RUN opam switch create myswitch 4.14.2
RUN opam exec -- opam install -y mirage opam-monorepo ocaml-solo5
RUN mkdir /tmp/orb-build
ADD config.ml /tmp/orb-build/config.ml
WORKDIR /tmp/orb-build
CMD opam exec -- sh -exc 'mirage configure -t xen --extra-repos=\
opam-overlays:https://github.com/dune-universe/opam-overlays.git#4e75ee36715b27550d5bdb87686bb4ae4c9e89c4,\
opam-overlays:https://github.com/dune-universe/opam-overlays.git#f2bec38beca4aea9e481f2fd3ee319c519124649,\
mirage-overlays:https://github.com/dune-universe/mirage-opam-overlays.git#797cb363df3ff763c43c8fbec5cd44de2878757e \
&& make depend && make unikernel'

View file

@ -48,7 +48,7 @@ It's OK to install the Docker or Podman package in a template VM if you want it
after a reboot, but the build of the firewall itself should be done in a regular AppVM.
You can also build without that script, as for any normal Mirage unikernel;
see [the Mirage installation instructions](https://mirage.io/wiki/install) for details.
see [the Mirage installation instructions](https://mirageos.org/wiki/install) for details.
The build script fixes the versions of the libraries it uses, ensuring that you will get
exactly the same binary that is in the release. If you build without it, it will build

View file

@ -4,9 +4,7 @@
type t = (unit -> unit) list ref
let create () = ref []
let on_cleanup t fn =
t := fn :: !t
let on_cleanup t fn = t := fn :: !t
let cleanup t =
let tasks = !t in

View file

@ -1,8 +1,8 @@
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
See the README file for details. *)
(** Register actions to take when a resource is finished.
Like [Lwt_switch], but synchronous. *)
(** Register actions to take when a resource is finished. Like [Lwt_switch], but
synchronous. *)
type t

View file

@ -4,19 +4,19 @@
open Fw_utils
open Lwt.Infix
let src = Logs.Src.create "client_eth" ~doc:"Ethernet networks for NetVM clients"
let src =
Logs.Src.create "client_eth" ~doc:"Ethernet networks for NetVM clients"
module Log = (val Logs.src_log src : Logs.LOG)
type t = {
mutable iface_of_ip : client_link Ipaddr.V4.Map.t;
changed : unit Lwt_condition.t; (* Fires when [iface_of_ip] changes. *)
my_ip : Ipaddr.V4.t; (* The IP that clients are given as their default gateway. *)
my_ip : Ipaddr.V4.t;
(* The IP that clients are given as their default gateway. *)
}
type host =
[ `Client of client_link
| `Firewall
| `External of Ipaddr.t ]
type host = [ `Client of client_link | `Firewall | `External of Ipaddr.t ]
let create config =
let changed = Lwt_condition.create () in
@ -32,7 +32,10 @@ let add_client t iface =
| Some old ->
(* Wait for old client to disappear before adding one with the same IP address.
Otherwise, its [remove_client] call will remove the new client instead. *)
Log.info (fun f -> f ~header:iface#log_header "Waiting for old client %s to go away before accepting new one" old#log_header);
Log.info (fun f ->
f ~header:iface#log_header
"Waiting for old client %s to go away before accepting new one"
old#log_header);
Lwt_condition.wait t.changed >>= aux
| None ->
t.iface_of_ip <- t.iface_of_ip |> Ipaddr.V4.Map.add ip iface;
@ -52,11 +55,12 @@ let lookup t ip = Ipaddr.V4.Map.find_opt ip t.iface_of_ip
let classify t ip =
match ip with
| Ipaddr.V6 _ -> `External ip
| Ipaddr.V4 ip4 ->
| Ipaddr.V4 ip4 -> (
if ip4 = t.my_ip then `Firewall
else match lookup t ip4 with
else
match lookup t ip4 with
| Some client_link -> `Client client_link
| None -> `External ip
| None -> `External ip)
let resolve t : host -> Ipaddr.t = function
| `Client client_link -> Ipaddr.V4 client_link#other_ip
@ -64,18 +68,18 @@ let resolve t : host -> Ipaddr.t = function
| `External addr -> addr
module ARP = struct
type arp = {
net : t;
client_link : client_link;
}
type arp = { net : t; client_link : client_link }
let lookup t ip =
if ip = t.net.my_ip then Some t.client_link#my_mac
else if (Ipaddr.V4.to_octets ip).[3] = '\x01' then (
Log.info (fun f -> f ~header:t.client_link#log_header
"Request for %a is invalid, but pretending it's me (see Qubes issue #5022)" Ipaddr.V4.pp ip);
Some t.client_link#my_mac
) else None
Log.info (fun f ->
f ~header:t.client_link#log_header
"Request for %a is invalid, but pretending it's me (see Qubes \
issue #5022)"
Ipaddr.V4.pp ip);
Some t.client_link#my_mac)
else None
(* We're now treating client networks as point-to-point links,
so we no longer respond on behalf of other clients. *)
(*
@ -89,19 +93,22 @@ module ARP = struct
let input_query t arp =
let req_ipv4 = arp.Arp_packet.target_ip in
let pf (f : ?header:string -> ?tags:_ -> _) fmt =
f ~header:t.client_link#log_header ("who-has %a? " ^^ fmt) Ipaddr.V4.pp req_ipv4
f ~header:t.client_link#log_header ("who-has %a? " ^^ fmt) Ipaddr.V4.pp
req_ipv4
in
if req_ipv4 = t.client_link#other_ip then (
Log.info (fun f -> pf f "ignoring request for client's own IP");
None
) else match lookup t req_ipv4 with
None)
else
match lookup t req_ipv4 with
| None ->
Log.info (fun f -> pf f "unknown address; not responding");
None
| Some req_mac ->
Log.info (fun f -> pf f "responding with %a" Macaddr.pp req_mac);
Some { Arp_packet.
operation = Arp_packet.Reply;
Some
{
Arp_packet.operation = Arp_packet.Reply;
(* The Target Hardware Address and IP are copied from the request *)
target_ip = arp.Arp_packet.source_ip;
target_mac = arp.Arp_packet.source_mac;
@ -115,18 +122,28 @@ module ARP = struct
let header = t.client_link#log_header in
match lookup t source_ip with
| Some real_mac when Macaddr.compare source_mac real_mac = 0 ->
Log.info (fun f -> f ~header "client suggests updating %s -> %s (as expected)"
(Ipaddr.V4.to_string source_ip) (Macaddr.to_string source_mac));
Log.info (fun f ->
f ~header "client suggests updating %s -> %s (as expected)"
(Ipaddr.V4.to_string source_ip)
(Macaddr.to_string source_mac))
| Some other_mac ->
Log.warn (fun f -> f ~header "client suggests incorrect update %s -> %s (should be %s)"
(Ipaddr.V4.to_string source_ip) (Macaddr.to_string source_mac) (Macaddr.to_string other_mac));
Log.warn (fun f ->
f ~header "client suggests incorrect update %s -> %s (should be %s)"
(Ipaddr.V4.to_string source_ip)
(Macaddr.to_string source_mac)
(Macaddr.to_string other_mac))
| None ->
Log.warn (fun f -> f ~header "client suggests incorrect update %s -> %s (unexpected IP)"
(Ipaddr.V4.to_string source_ip) (Macaddr.to_string source_mac))
Log.warn (fun f ->
f ~header
"client suggests incorrect update %s -> %s (unexpected IP)"
(Ipaddr.V4.to_string source_ip)
(Macaddr.to_string source_mac))
let input t arp =
let op = arp.Arp_packet.operation in
match op with
| Arp_packet.Request -> input_query t arp
| Arp_packet.Reply -> input_gratuitous t arp; None
| Arp_packet.Reply ->
input_gratuitous t arp;
None
end

View file

@ -1,34 +1,32 @@
(* Copyright (C) 2016, Thomas Leonard <thomas.leonard@unikernel.com>
See the README file for details. *)
(** The ethernet networks connecting us to our client AppVMs.
Note: each AppVM is on a point-to-point link, each link being considered to be a separate Ethernet network. *)
(** The ethernet networks connecting us to our client AppVMs. Note: each AppVM
is on a point-to-point link, each link being considered to be a separate
Ethernet network. *)
open Fw_utils
type t
(** A collection of clients. *)
type host =
[ `Client of client_link
| `Firewall
| `External of Ipaddr.t ]
type host = [ `Client of client_link | `Firewall | `External of Ipaddr.t ]
(* Note: Qubes does not allow us to distinguish between an external address and a
disconnected client.
See: https://github.com/talex5/qubes-mirage-firewall/issues/9#issuecomment-246956850 *)
val create : Dao.network_config -> t Lwt.t
(** [create ~client_gw] is a network of client machines.
Qubes will have configured the client machines to use [client_gw] as their default gateway. *)
(** [create ~client_gw] is a network of client machines. Qubes will have
configured the client machines to use [client_gw] as their default gateway.
*)
val add_client : t -> client_link -> unit Lwt.t
(** [add_client t client] registers a new client. If a client with this IP address is already registered,
it waits for [remove_client] to be called on that before adding the new client and returning. *)
(** [add_client t client] registers a new client. If a client with this IP
address is already registered, it waits for [remove_client] to be called on
that before adding the new client and returning. *)
val remove_client : t -> client_link -> unit
val client_gw : t -> Ipaddr.V4.t
val classify : t -> Ipaddr.t -> host
val resolve : t -> host -> Ipaddr.t
@ -36,18 +34,18 @@ val lookup : t -> Ipaddr.V4.t -> client_link option
(** [lookup t addr] is the client with IP address [addr], if connected. *)
module ARP : sig
(** We already know the correct mapping of IP addresses to MAC addresses, so we never
allow clients to update it. We log a warning if a client attempts to set incorrect
information. *)
(** We already know the correct mapping of IP addresses to MAC addresses, so
we never allow clients to update it. We log a warning if a client attempts
to set incorrect information. *)
type arp
(** An ARP-responder for one client. *)
val create : net:t -> client_link -> arp
(** [create ~net client_link] is an ARP responder for [client_link].
It answers only for the client's gateway address. *)
(** [create ~net client_link] is an ARP responder for [client_link]. It
answers only for the client's gateway address. *)
val input : arp -> Arp_packet.t -> Arp_packet.t option
(** Process one ethernet frame containing an ARP message.
Returns a response frame, if one is needed. *)
(** Process one ethernet frame containing an ARP message. Returns a response
frame, if one is needed. *)
end

View file

@ -4,23 +4,29 @@
(** Commands we provide via qvm-run. *)
open Lwt
module Flow = Qubes.RExec.Flow
let src = Logs.Src.create "command" ~doc:"qrexec command handler"
module Log = (val Logs.src_log src : Logs.LOG)
let set_date_time flow =
Flow.read_line flow >|= function
| `Eof -> Log.warn (fun f -> f "EOF reading time from dom0"); 1
| `Ok line -> Log.info (fun f -> f "TODO: set time to %S" line); 0
| `Eof ->
Log.warn (fun f -> f "EOF reading time from dom0");
1
| `Ok line ->
Log.info (fun f -> f "TODO: set time to %S" line);
0
let handler ~user:_ cmd flow =
(* Write a message to the client and return an exit status of 1. *)
let error fmt =
fmt |> Printf.ksprintf @@ fun s ->
fmt
|> Printf.ksprintf @@ fun s ->
Log.warn (fun f -> f "<< %s" s);
Flow.ewritef flow "%s [while processing %S]" s cmd >|= fun () -> 1 in
Flow.ewritef flow "%s [while processing %S]" s cmd >|= fun () -> 1
in
match cmd with
| "QUBESRPC qubes.SetDateTime dom0" -> set_date_time flow
| "QUBESRPC qubes.WaitForSession none" -> return 0 (* Always ready! *)

View file

@ -1,4 +1,4 @@
(* mirage >= 4.8.0 & < 4.9.0 *)
(* mirage >= 4.9.0 & < 4.10.0 *)
(* Copyright (C) 2017, Thomas Leonard <thomas.leonard@unikernel.com>
See the README file for details. *)
@ -8,7 +8,8 @@ open Mirage
let main =
main
~packages:[
~packages:
[
package "vchan" ~min:"4.0.2";
package "cstruct";
package "tcpip" ~min:"3.7.0";
@ -24,7 +25,6 @@ let main =
package ~min:"6.4.0" "dns-client";
package "pf-qubes";
]
"Unikernel.Main" (random @-> mclock @-> time @-> job)
"Unikernel" job
let () =
register "qubes-firewall" [main $ default_random $ default_monotonic_clock $ default_time]
let () = register "qubes-firewall" [ main ]

105
dao.ml
View file

@ -5,26 +5,26 @@ open Lwt.Infix
open Qubes
let src = Logs.Src.create "dao" ~doc:"QubesDB data access"
module Log = (val Logs.src_log src : Logs.LOG)
module ClientVif = struct
type t = {
domid : int;
device_id : int;
}
type t = { domid : int; device_id : int }
let pp f { domid; device_id } = Fmt.pf f "{domid=%d;device_id=%d}" domid device_id
let pp f { domid; device_id } =
Fmt.pf f "{domid=%d;device_id=%d}" domid device_id
let compare = compare
end
module VifMap = struct
include Map.Make (ClientVif)
let rec of_list = function
| [] -> empty
| (k, v) :: rest -> add k v (of_list rest)
let find key t =
try Some (find key t)
with Not_found -> None
let find key t = try Some (find key t) with Not_found -> None
end
let directory ~handle dir =
@ -32,8 +32,7 @@ let directory ~handle dir =
| [ "" ] -> [] (* XenStore client bug *)
| items -> items
let db_root client_ip =
"/qubes-firewall/" ^ (Ipaddr.V4.to_string client_ip)
let db_root client_ip = "/qubes-firewall/" ^ Ipaddr.V4.to_string client_ip
let read_rules rules client_ip =
let root = db_root client_ip in
@ -44,42 +43,57 @@ let read_rules rules client_ip =
| None ->
Log.debug (fun f -> f "rule %d does not exist; won't look for more" n);
Ok (List.rev l)
| Some rule ->
| Some rule -> (
Log.debug (fun f -> f "rule %d: %s" n rule);
match Pf_qubes.Parse_qubes.parse_qubes ~number:n rule with
| Error e -> Log.warn (fun f -> f "Error parsing rule %d: %s" n e); Error e
| Error e ->
Log.warn (fun f -> f "Error parsing rule %d: %s" n e);
Error e
| Ok rule ->
Log.debug (fun f -> f "parsed rule: %a" Pf_qubes.Parse_qubes.pp_rule rule);
get_rule (n+1) (rule :: l)
Log.debug (fun f ->
f "parsed rule: %a" Pf_qubes.Parse_qubes.pp_rule rule);
get_rule (n + 1) (rule :: l))
in
match get_rule 0 [] with
| Ok l -> l
| Error e ->
Log.warn (fun f -> f "Defaulting to deny-all because of rule parse failure (%s)" e);
[ Pf_qubes.Parse_qubes.({action = Drop;
Log.warn (fun f ->
f "Defaulting to deny-all because of rule parse failure (%s)" e);
[
Pf_qubes.Parse_qubes.
{
action = Drop;
proto = None;
specialtarget = None;
dst = `any;
dstports = None;
icmp_type = None;
number = 0;})]
number = 0;
};
]
let vifs client domid =
let open Lwt.Syntax in
match int_of_string_opt domid with
| None -> Log.err (fun f -> f "Invalid domid %S" domid); Lwt.return []
| None ->
Log.err (fun f -> f "Invalid domid %S" domid);
Lwt.return []
| Some domid ->
let path = Fmt.str "backend/vif/%d" domid in
let vifs_of_domain handle =
let* devices = directory ~handle path in
let ip_of_vif device_id = match int_of_string_opt device_id with
let ip_of_vif device_id =
match int_of_string_opt device_id with
| None ->
Log.err (fun f -> f "Invalid device ID %S for domid %d" device_id domid);
Log.err (fun f ->
f "Invalid device ID %S for domid %d" device_id domid);
Lwt.return_none
| Some device_id ->
| Some device_id -> (
let vif = { ClientVif.domid; device_id } in
let get_client_ip () =
let* str = Xen_os.Xs.read handle (Fmt.str "%s/%d/ip" path device_id) in
let* str =
Xen_os.Xs.read handle (Fmt.str "%s/%d/ip" path device_id)
in
let client_ip = List.hd (String.split_on_char ' ' str) in
(* NOTE(dinosaure): it's safe to use [List.hd] here,
[String.split_on_char] can not return an empty list. *)
@ -88,13 +102,15 @@ let vifs client domid =
Lwt.catch get_client_ip @@ function
| Xs_protocol.Enoent _ -> Lwt.return_none
| Ipaddr.Parse_error (msg, client_ip) ->
Log.err (fun f -> f "Error parsing IP address of %a from %s: %s"
Log.err (fun f ->
f "Error parsing IP address of %a from %s: %s"
ClientVif.pp vif client_ip msg);
Lwt.return_none
| exn ->
Log.err (fun f -> f "Error getting IP address of %a: %s"
ClientVif.pp vif (Printexc.to_string exn));
Lwt.return_none
Log.err (fun f ->
f "Error getting IP address of %a: %s" ClientVif.pp vif
(Printexc.to_string exn));
Lwt.return_none)
in
Lwt_list.filter_map_p ip_of_vif devices
in
@ -105,21 +121,19 @@ let watch_clients fn =
let backend_vifs = "backend/vif" in
Log.info (fun f -> f "Watching %s" backend_vifs);
Xen_os.Xs.wait xs (fun handle ->
begin Lwt.catch
Lwt.catch
(fun () -> directory ~handle backend_vifs)
(function
| Xs_protocol.Enoent _ -> Lwt.return []
| ex -> Lwt.fail ex)
end >>= fun items ->
(function Xs_protocol.Enoent _ -> Lwt.return [] | ex -> Lwt.fail ex)
>>= fun items ->
Xen_os.Xs.make () >>= fun xs ->
Lwt_list.map_p (vifs xs) items >>= fun items ->
fn (List.concat items |> VifMap.of_list) >>= fun () ->
(* Wait for further updates *)
Lwt.fail Xs_protocol.Eagain
)
Lwt.fail Xs_protocol.Eagain)
type network_config = {
from_cmdline : bool; (* Specify if we have network configuration from command line or from qubesDB*)
from_cmdline : bool;
(* Specify if we have network configuration from command line or from qubesDB*)
netvm_ip : Ipaddr.V4.t; (* The IP address of NetVM (our gateway) *)
our_ip : Ipaddr.V4.t; (* The IP address of our interface to NetVM *)
dns : Ipaddr.V4.t;
@ -132,9 +146,12 @@ let try_read_network_config db =
let get name =
match DB.KeyMap.find_opt name db with
| None -> raise (Missing_key name)
| Some value -> Ipaddr.V4.of_string_exn value in
let our_ip = get "/qubes-ip" in (* - IP address for this VM (only when VM has netvm set) *)
let netvm_ip = get "/qubes-gateway" in (* - default gateway IP (only when VM has netvm set); VM should add host route to this address directly via eth0 (or whatever default interface name is) *)
| Some value -> Ipaddr.V4.of_string_exn value
in
let our_ip = get "/qubes-ip" in
(* - IP address for this VM (only when VM has netvm set) *)
let netvm_ip = get "/qubes-gateway" in
(* - default gateway IP (only when VM has netvm set); VM should add host route to this address directly via eth0 (or whatever default interface name is) *)
let dns = get "/qubes-primary-dns" in
let dns2 = get "/qubes-secondary-dns" in
{ from_cmdline = false; netvm_ip; our_ip; dns; dns2 }
@ -143,20 +160,22 @@ let read_network_config qubesDB =
let rec aux bindings =
try Lwt.return (try_read_network_config bindings)
with Missing_key key ->
Log.warn (fun f -> f "QubesDB key %S not (yet) present; waiting for QubesDB to change..." key);
Log.warn (fun f ->
f "QubesDB key %S not (yet) present; waiting for QubesDB to change..."
key);
DB.after qubesDB bindings >>= aux
in
aux (DB.bindings qubesDB)
let print_network_config config =
Log.info (fun f -> f "@[<v2>Current network configuration (QubesDB or command line):@,\
Log.info (fun f ->
f
"@[<v2>Current network configuration (QubesDB or command line):@,\
NetVM IP on uplink network: %a@,\
Our IP on client networks: %a@,\
DNS primary resolver: %a@,\
DNS secondary resolver: %a@]"
Ipaddr.V4.pp config.netvm_ip
Ipaddr.V4.pp config.our_ip
Ipaddr.V4.pp config.dns
Ipaddr.V4.pp config.dns2)
Ipaddr.V4.pp config.netvm_ip Ipaddr.V4.pp config.our_ip Ipaddr.V4.pp
config.dns Ipaddr.V4.pp config.dns2)
let set_iptables_error db = Qubes.DB.write db "/qubes-iptables-error"

31
dao.mli
View file

@ -4,23 +4,24 @@
(** Wrapper for XenStore and QubesDB databases. *)
module ClientVif : sig
type t = {
domid : int;
device_id : int;
}
type t = { domid : int; device_id : int }
val pp : t Fmt.t
end
module VifMap : sig
include Map.S with type key = ClientVif.t
val find : key -> 'a t -> 'a option
end
val watch_clients : (Ipaddr.V4.t VifMap.t -> unit Lwt.t) -> 'a Lwt.t
(** [watch_clients fn] calls [fn clients] with the list of backend clients
in XenStore, and again each time XenStore updates. *)
(** [watch_clients fn] calls [fn clients] with the list of backend clients in
XenStore, and again each time XenStore updates. *)
type network_config = {
from_cmdline : bool; (* Specify if we have network configuration from command line or from qubesDB*)
from_cmdline : bool;
(* Specify if we have network configuration from command line or from qubesDB*)
netvm_ip : Ipaddr.V4.t; (* The IP address of NetVM (our gateway) *)
our_ip : Ipaddr.V4.t; (* The IP address of our interface to NetVM *)
dns : Ipaddr.V4.t;
@ -28,16 +29,18 @@ type network_config = {
}
val read_network_config : Qubes.DB.t -> network_config Lwt.t
(** [read_network_config db] fetches the configuration from QubesDB.
If it isn't there yet, it waits until it is. *)
(** [read_network_config db] fetches the configuration from QubesDB. If it isn't
there yet, it waits until it is. *)
val db_root : Ipaddr.V4.t -> string
(** Returns the root path of the firewall rules in the QubesDB for a given IP address. *)
(** Returns the root path of the firewall rules in the QubesDB for a given IP
address. *)
val read_rules : string Qubes.DB.KeyMap.t -> Ipaddr.V4.t -> Pf_qubes.Parse_qubes.rule list
(** [read_rules bindings ip] extracts firewall rule information for [ip] from [bindings].
If any rules fail to parse, it will return only one rule denying all traffic. *)
val read_rules :
string Qubes.DB.KeyMap.t -> Ipaddr.V4.t -> Pf_qubes.Parse_qubes.rule list
(** [read_rules bindings ip] extracts firewall rule information for [ip] from
[bindings]. If any rules fail to parse, it will return only one rule denying
all traffic. *)
val print_network_config : network_config -> unit
val set_iptables_error : Qubes.DB.t -> string -> unit Lwt.t

View file

@ -7,18 +7,11 @@ module UplinkEth = Ethernet.Make (Netif)
let src = Logs.Src.create "dispatcher" ~doc:"Networking dispatch"
module Log = (val Logs.src_log src : Logs.LOG)
module Arp = Arp.Make (UplinkEth)
module I = Static_ipv4.Make (UplinkEth) (Arp)
module U = Udp.Make (I)
module Make
(R : Mirage_crypto_rng_mirage.S)
(Clock : Mirage_clock.MCLOCK)
(Time : Mirage_time.S) =
struct
module Arp = Arp.Make (UplinkEth) (Time)
module I = Static_ipv4.Make (R) (Clock) (UplinkEth) (Arp)
module U = Udp.Make (I) (R)
class client_iface eth ~domid ~gateway_ip ~client_ip client_mac : client_link
=
class client_iface eth ~domid ~gateway_ip ~client_ip client_mac : client_link =
let log_header = Fmt.str "dom%d:%a" domid Ipaddr.V4.pp client_ip in
object
val mutable rules = []
@ -36,8 +29,8 @@ struct
| Ok () -> ()
| Error e ->
Log.err (fun f ->
f "error trying to send to client: @[%a@]"
ClientEth.pp_error e))
f "error trying to send to client: @[%a@]" ClientEth.pp_error
e))
(fun ex ->
(* Usually Netback_shutdown, because the client disconnected *)
Log.err (fun f ->
@ -107,14 +100,19 @@ struct
let dst_ip = buf.Ipv4_packet.dst in
match Client_eth.lookup t.clients dst_ip with
| Some client_link -> Some (client_link :> interface)
| None -> ( (* if dest is not a client, transfer it to our uplink *)
| None -> (
(* if dest is not a client, transfer it to our uplink *)
match t.uplink with
| None -> (
match Client_eth.lookup t.clients t.config.netvm_ip with
| Some uplink ->
Some (uplink :> interface)
| Some uplink -> Some (uplink :> interface)
| None ->
Log.err (fun f -> f "We have a command line configuration %a but it's currently not connected to us (please check its netvm property)...%!" Ipaddr.V4.pp t.config.netvm_ip);
Log.err (fun f ->
f
"We have a command line configuration %a but it's \
currently not connected to us (please check its netvm \
property)...%!"
Ipaddr.V4.pp t.config.netvm_ip);
None)
| Some uplink -> Some uplink.interface)
@ -260,8 +258,8 @@ struct
| None -> (
match Packet.of_mirage_nat_packet ~src ~dst packet with
| None -> Lwt.return_unit
| Some packet -> apply_rules t Rules.from_netvm ~dst packet)
)))
| Some packet -> apply_rules t Rules.from_netvm ~dst packet)))
)
let ipv4_from_client resolver dns_servers t ~src packet =
match Memory_pressure.status () with
@ -312,12 +310,11 @@ struct
Log.warn (fun f ->
f "Failed to write APR to %a: %s" Ipaddr.V4.pp iface#other_ip
(Printexc.to_string ex));
Lwt.return_unit)
)
Lwt.return_unit))
(** Handle an IPv4 packet from the client. *)
let client_handle_ipv4 get_ts cache ~iface ~router dns_client dns_servers
packet =
let client_handle_ipv4 get_ts cache ~iface ~router dns_client dns_servers packet
=
let cache', r = Nat_packet.of_ipv4_packet !cache ~now:(get_ts ()) packet in
cache := cache';
match r with
@ -340,12 +337,14 @@ struct
Ipaddr.V4.pp src Ipaddr.V4.pp iface#other_ip);
Lwt.return_unit)
(** Connect to a new client's interface and listen for incoming frames and firewall rule changes. *)
let conf_vif get_ts vif backend client_eth dns_client dns_servers
~client_ip ~iface ~router ~cleanup_tasks qubesDB () =
(** Connect to a new client's interface and listen for incoming frames and
firewall rule changes. *)
let conf_vif get_ts vif backend client_eth dns_client dns_servers ~client_ip
~iface ~router ~cleanup_tasks qubesDB () =
let { Dao.ClientVif.domid; device_id } = vif in
Log.info (fun f ->
f "Client %d:%d (IP: %s) ready" domid device_id (Ipaddr.V4.to_string client_ip));
f "Client %d:%d (IP: %s) ready" domid device_id
(Ipaddr.V4.to_string client_ip));
(* update the rules whenever QubesDB notices a change for this IP *)
let qubesdb_updater =
@ -358,8 +357,7 @@ struct
let new_rules = iface#get_rules in
if current_rules = new_rules then
Log.info (fun m ->
m "Rules did not change for %s"
(Ipaddr.V4.to_string client_ip))
m "Rules did not change for %s" (Ipaddr.V4.to_string client_ip))
else (
Log.info (fun m ->
m "New firewall rules for %s@.%a"
@ -402,7 +400,8 @@ struct
Lwt.async (fun () -> Lwt.pick [ qubesdb_updater; listener ]);
Lwt.return_unit
(** A new client VM has been found in XenStore. Find its interface and connect to it. *)
(** A new client VM has been found in XenStore. Find its interface and connect
to it. *)
let add_client get_ts dns_client dns_servers ~router vif client_ip qubesDB =
let open Lwt.Syntax in
let cleanup_tasks = Cleanup.create () in
@ -420,8 +419,7 @@ struct
Cleanup.on_cleanup cleanup_tasks (fun () -> remove_client router iface);
Lwt.async (fun () ->
Lwt.catch
(fun () ->
add_client router iface)
(fun () -> add_client router iface)
(fun ex ->
Log.warn (fun f ->
f "Error with client %a: %s" Dao.ClientVif.pp vif
@ -429,9 +427,9 @@ struct
Lwt.return_unit));
let* () =
Lwt.catch (
conf_vif get_ts vif backend client_eth dns_client dns_servers ~client_ip ~iface ~router
~cleanup_tasks qubesDB)
Lwt.catch
(conf_vif get_ts vif backend client_eth dns_client dns_servers ~client_ip
~iface ~router ~cleanup_tasks qubesDB)
@@ fun exn ->
Log.warn (fun f ->
f "Error with client %a: %s" Dao.ClientVif.pp vif
@ -447,18 +445,20 @@ struct
Dao.watch_clients @@ fun new_set ->
(* Check for removed clients *)
let clean_up_clients key cleanup =
if not (Dao.VifMap.mem key new_set) then begin
if not (Dao.VifMap.mem key new_set) then (
clients := !clients |> Dao.VifMap.remove key;
Log.info (fun f -> f "client %a has gone" Dao.ClientVif.pp key);
Cleanup.cleanup cleanup
end
Cleanup.cleanup cleanup)
in
Dao.VifMap.iter clean_up_clients !clients;
(* Check for added clients *)
let rec go seq = match Seq.uncons seq with
let rec go seq =
match Seq.uncons seq with
| None -> Lwt.return_unit
| Some ((key, ipaddr), seq) when not (Dao.VifMap.mem key !clients) ->
let* cleanup = add_client get_ts dns_client dns_servers ~router key ipaddr qubesDB in
let* cleanup =
add_client get_ts dns_client dns_servers ~router key ipaddr qubesDB
in
Log.debug (fun f -> f "client %a arrived" Dao.ClientVif.pp key);
clients := Dao.VifMap.add key cleanup !clients;
go seq
@ -471,19 +471,22 @@ struct
| None ->
Log.err (fun f -> f "No uplink interface");
Lwt.return (Error (`Msg "failure"))
| Some uplink -> (
| Some uplink ->
Lwt.catch
(fun () ->
U.write ~src_port ~dst ~dst_port uplink.udp (Cstruct.of_string buf) >|= function
U.write ~src_port ~dst ~dst_port uplink.udp (Cstruct.of_string buf)
>|= function
| Error s ->
Log.err (fun f -> f "error sending udp packet: %a" U.pp_error s);
Error (`Msg "failure")
| Ok () -> Ok ())
(fun ex ->
Log.err (fun f ->
f "uncaught exception trying to send DNS request to uplink: @[%s@]"
f
"uncaught exception trying to send DNS request to uplink: \
@[%s@]"
(Printexc.to_string ex));
Lwt.return (Error (`Msg "DNS request not sent"))))
Lwt.return (Error (`Msg "DNS request not sent")))
(** Wait for packet from our uplink (we must have an uplink here...). *)
let rec uplink_listen get_ts dns_responses router =
@ -491,8 +494,7 @@ struct
match router.uplink with
| None ->
Log.err (fun f ->
f
"Uplink is connected but not found in the router, retrying...%!");
f "Uplink is connected but not found in the router, retrying...%!");
uplink_listen get_ts dns_responses router
| Some uplink ->
let listen =
@ -504,41 +506,41 @@ struct
UplinkEth.input uplink.eth ~arpv4:(Arp.input uplink.arp)
~ipv4:(fun ip ->
let cache, r =
Nat_packet.of_ipv4_packet uplink.fragments ~now:(get_ts ())
ip
Nat_packet.of_ipv4_packet uplink.fragments
~now:(get_ts ()) ip
in
uplink.fragments <- cache;
begin match r with
match r with
| Error e ->
Log.warn (fun f ->
f "Ignored unknown IPv4 message from uplink: %a"
Nat_packet.pp_error e);
Lwt.return ()
| Ok None -> Lwt.return_unit
| Ok (Some (`IPv4 (header, packet))) ->
| Ok (Some (`IPv4 (header, packet))) -> (
let open Udp_packet in
Log.debug (fun f ->
f "received ipv4 packet from %a on uplink" Ipaddr.V4.pp
header.Ipv4_packet.src);
begin match packet with
| `UDP (header, packet) when My_nat.dns_port router.nat header.dst_port ->
f "received ipv4 packet from %a on uplink"
Ipaddr.V4.pp header.Ipv4_packet.src);
match packet with
| `UDP (header, packet)
when My_nat.dns_port router.nat header.dst_port ->
Log.debug (fun f ->
f
"found a DNS packet whose dst_port (%d) was in the list of \
dns_client ports"
"found a DNS packet whose dst_port (%d) was \
in the list of dns_client ports"
header.dst_port);
Lwt_mvar.put dns_responses (header, Cstruct.to_string packet)
| _ -> ipv4_from_netvm router (`IPv4 (header, packet))
end
end)
Lwt_mvar.put dns_responses
(header, Cstruct.to_string packet)
| _ -> ipv4_from_netvm router (`IPv4 (header, packet))))
~ipv6:(fun _ip -> Lwt.return_unit)
frame)
>|= or_raise "Uplink listen loop" Netif.pp_error)
(function Lwt.Canceled ->
(function
| Lwt.Canceled ->
(* We can be cancelled if reconnect_uplink is achieved (via the Lwt_condition), so we need to disconnect and broadcast when it's done
currently we delay 1s as Netif.disconnect is non-blocking... (need to fix upstream?) *)
Log.info (fun f ->
f "disconnecting from our uplink");
Log.info (fun f -> f "disconnecting from our uplink");
U.disconnect uplink.udp >>= fun () ->
I.disconnect uplink.ip >>= fun () ->
(* mutable fragments : Fragments.Cache.t; *)
@ -552,8 +554,7 @@ struct
in
let reconnect_uplink =
Lwt_condition.wait router.uplink_disconnect >>= fun () ->
Log.info (fun f ->
f "we need to reconnect to the new uplink");
Log.info (fun f -> f "we need to reconnect to the new uplink");
Lwt.return_unit
in
Lwt.pick [ listen; reconnect_uplink ] >>= fun () ->
@ -632,4 +633,3 @@ struct
>>= fun () -> aux new_db
in
aux Qubes.DB.KeyMap.empty
end

View file

@ -20,9 +20,11 @@ class type client_link = object
method set_rules : string Qubes.DB.KeyMap.t -> unit
end
(** An Ethernet header from [src]'s MAC address to [dst]'s with an IPv4 payload. *)
(** An Ethernet header from [src]'s MAC address to [dst]'s with an IPv4 payload.
*)
let eth_header ethertype ~src ~dst =
Ethernet.Packet.make_cstruct { Ethernet.Packet.source = src; destination = dst; ethertype }
Ethernet.Packet.make_cstruct
{ Ethernet.Packet.source = src; destination = dst; ethertype }
let error fmt =
let err s = Failure s in

View file

@ -2,14 +2,14 @@
See the README file for details. *)
let src = Logs.Src.create "memory_pressure" ~doc:"Memory pressure monitor"
module Log = (val Logs.src_log src : Logs.LOG)
let fraction_free stats =
let { Xen_os.Memory.free_words; heap_words; _ } = stats in
float free_words /. float heap_words
let init () =
Gc.full_major ()
let init () = Gc.full_major ()
let status () =
let stats = Xen_os.Memory.quick_stat () in
@ -18,6 +18,4 @@ let status () =
Gc.full_major ();
Xen_os.Memory.trim ();
let stats = Xen_os.Memory.quick_stat () in
if fraction_free stats < 0.6 then `Memory_critical
else `Ok
)
if fraction_free stats < 0.6 then `Memory_critical else `Ok)

View file

@ -8,5 +8,5 @@ val status : unit -> [ `Ok | `Memory_critical ]
(** Check the memory situation. If we're running low, do a GC (work-around for
http://caml.inria.fr/mantis/view.php?id=7100 and OCaml GC needing to malloc
extra space to run finalisers). Returns [`Memory_critical] if memory is
still low - caller should take action to reduce memory use.
After GC, updates meminfo in XenStore. *)
still low - caller should take action to reduce memory use. After GC,
updates meminfo in XenStore. *)

View file

@ -1,12 +1,16 @@
open Lwt.Infix
module Transport (R : Mirage_crypto_rng_mirage.S) (C : Mirage_clock.MCLOCK) (Time : Mirage_time.S) = struct
type +'a io = 'a Lwt.t
type io_addr = Ipaddr.V4.t * int
module Dispatcher = Dispatcher.Make(R)(C)(Time)
type stack = Dispatcher.t *
(src_port:int -> dst:Ipaddr.V4.t -> dst_port:int -> string -> (unit, [ `Msg of string ]) result Lwt.t) *
(Udp_packet.t * string) Lwt_mvar.t
type stack =
Dispatcher.t
* (src_port:int ->
dst:Ipaddr.V4.t ->
dst_port:int ->
string ->
(unit, [ `Msg of string ]) result Lwt.t)
* (Udp_packet.t * string) Lwt_mvar.t
module IM = Map.Make (Int)
@ -17,26 +21,27 @@ module Transport (R : Mirage_crypto_rng_mirage.S) (C : Mirage_clock.MCLOCK) (Tim
timeout_ns : int64;
mutable requests : string Lwt_condition.t IM.t;
}
type context = t
let nameservers { protocol ; nameserver ; _ } = protocol, [ nameserver ]
let rng = R.generate ?g:None
let clock = C.elapsed_ns
let nameservers { protocol; nameserver; _ } = (protocol, [ nameserver ])
let rng = Mirage_crypto_rng.generate ?g:None
let clock = Mirage_mtime.elapsed_ns
let rec read t =
let _, _, answer = t.stack in
Lwt_mvar.take answer >>= fun (_, data) ->
if String.length data > 2 then begin
(if String.length data > 2 then
match IM.find_opt (String.get_uint16_be data 0) t.requests with
| Some cond -> Lwt_condition.broadcast cond data
| None -> ()
end;
| None -> ());
read t
let create ?nameservers ~timeout stack =
let protocol, nameserver = match nameservers with
let protocol, nameserver =
match nameservers with
| None | Some (_, []) -> invalid_arg "no nameserver found"
| Some (proto, ns :: _) -> proto, ns
| Some (proto, ns :: _) -> (proto, ns)
in
let t =
{ protocol; nameserver; stack; timeout_ns = timeout; requests = IM.empty }
@ -45,7 +50,9 @@ module Transport (R : Mirage_crypto_rng_mirage.S) (C : Mirage_clock.MCLOCK) (Tim
t
let with_timeout timeout_ns f =
let timeout = Time.sleep_ns timeout_ns >|= fun () -> Error (`Msg "DNS request timeout") in
let timeout =
Mirage_sleep.ns timeout_ns >|= fun () -> Error (`Msg "DNS request timeout")
in
Lwt.pick [ f; timeout ]
let connect (t : t) = Lwt.return (Ok (t.protocol, t))
@ -60,17 +67,15 @@ module Transport (R : Mirage_crypto_rng_mirage.S) (C : Mirage_clock.MCLOCK) (Tim
with_timeout ctx.timeout_ns
(let cond = Lwt_condition.create () in
ctx.requests <- IM.add id cond ctx.requests;
(send_udp ~src_port ~dst ~dst_port buf >|= Rresult.R.open_error_msg) >>= function
send_udp ~src_port ~dst ~dst_port buf >|= Rresult.R.open_error_msg
>>= function
| Ok () -> Lwt_condition.wait cond >|= fun dns_response -> Ok dns_response
| Error _ as e -> Lwt.return e) >|= fun result ->
| Error _ as e -> Lwt.return e)
>|= fun result ->
ctx.requests <- IM.remove id ctx.requests;
evict ();
result
let close _ = Lwt.return_unit
let bind = Lwt.bind
let lift = Lwt.return
end

View file

@ -2,26 +2,22 @@
See the README file for details. *)
let src = Logs.Src.create "my-nat" ~doc:"NAT shim"
module Log = (val Logs.src_log src : Logs.LOG)
type action = [
| `NAT
| `Redirect of Mirage_nat.endpoint
]
type action = [ `NAT | `Redirect of Mirage_nat.endpoint ]
module Nat = Mirage_nat_lru
module S =
Set.Make(struct type t = int let compare (a : int) (b : int) = compare a b end)
module S = Set.Make (struct
type t = int
type t = {
table : Nat.t;
mutable udp_dns : S.t;
last_resort_port : int
}
let compare (a : int) (b : int) = compare a b
end)
let pick_port () =
1024 + Random.int (0xffff - 1024)
type t = { table : Nat.t; mutable udp_dns : S.t; last_resort_port : int }
let pick_port () = 1024 + Random.int (0xffff - 1024)
let create ~max_entries =
let tcp_size = 7 * max_entries / 8 in
@ -32,35 +28,31 @@ let create ~max_entries =
let pick_free_port t proto =
let rec go retries =
if retries = 0 then
None
if retries = 0 then None
else
let p = 1024 + Random.int (0xffff - 1024) in
match proto with
| `Udp when S.mem p t.udp_dns || p = t.last_resort_port ->
go (retries - 1)
| `Udp when S.mem p t.udp_dns || p = t.last_resort_port -> go (retries - 1)
| _ -> Some p
in
go 10
let free_udp_port t ~src ~dst ~dst_port =
let rec go retries =
if retries = 0 then
t.last_resort_port, Fun.id
if retries = 0 then (t.last_resort_port, Fun.id)
else
let src_port =
Option.value ~default:t.last_resort_port (pick_free_port t `Udp)
in
if Nat.is_port_free t.table `Udp ~src ~dst ~src_port ~dst_port then begin
if Nat.is_port_free t.table `Udp ~src ~dst ~src_port ~dst_port then
let remove =
if src_port <> t.last_resort_port then begin
if src_port <> t.last_resort_port then (
t.udp_dns <- S.add src_port t.udp_dns;
(fun () -> t.udp_dns <- S.remove src_port t.udp_dns)
end else Fun.id
fun () -> t.udp_dns <- S.remove src_port t.udp_dns)
else Fun.id
in
src_port, remove
end else
go (retries - 1)
(src_port, remove)
else go (retries - 1)
in
go 10
@ -68,24 +60,24 @@ let dns_port t port = S.mem port t.udp_dns || port = t.last_resort_port
let translate t packet =
match Nat.translate t.table packet with
| Error (`Untranslated | `TTL_exceeded as e) ->
Log.debug (fun f -> f "Failed to NAT %a: %a"
Nat_packet.pp packet
Mirage_nat.pp_error e
);
| Error ((`Untranslated | `TTL_exceeded) as e) ->
Log.debug (fun f ->
f "Failed to NAT %a: %a" Nat_packet.pp packet Mirage_nat.pp_error e);
None
| Ok packet -> Some packet
let remove_connections t ip =
ignore (Nat.remove_connections t.table ip)
let remove_connections t ip = ignore (Nat.remove_connections t.table ip)
let add_nat_rule_and_translate t ~xl_host action packet =
let proto = match packet with
let proto =
match packet with
| `IPv4 (_, `TCP _) -> `Tcp
| `IPv4 (_, `UDP _) -> `Udp
| `IPv4 (_, `ICMP _) -> `Icmp
in
match Nat.add t.table packet xl_host (fun () -> pick_free_port t proto) action with
match
Nat.add t.table packet xl_host (fun () -> pick_free_port t proto) action
with
| Error `Overlap -> Error "Too many retries"
| Error `Cannot_NAT -> Error "Cannot NAT this packet"
| Ok () ->

View file

@ -4,17 +4,23 @@
(* Abstract over NAT interface (todo: remove this) *)
type t
type action = [ `NAT | `Redirect of Mirage_nat.endpoint ]
type action = [
| `NAT
| `Redirect of Mirage_nat.endpoint
]
val free_udp_port : t -> src:Ipaddr.V4.t -> dst:Ipaddr.V4.t -> dst_port:int ->
val free_udp_port :
t ->
src:Ipaddr.V4.t ->
dst:Ipaddr.V4.t ->
dst_port:int ->
int * (unit -> unit)
val dns_port : t -> int -> bool
val create : max_entries:int -> t
val remove_connections : t -> Ipaddr.V4.t -> unit
val translate : t -> Nat_packet.t -> Nat_packet.t option
val add_nat_rule_and_translate : t ->
xl_host:Ipaddr.V4.t -> action -> Nat_packet.t -> (Nat_packet.t, string) result
val add_nat_rule_and_translate :
t ->
xl_host:Ipaddr.V4.t ->
action ->
Nat_packet.t ->
(Nat_packet.t, string) result

View file

@ -8,9 +8,8 @@ type port = int
type host =
[ `Client of client_link | `Firewall | `NetVM | `External of Ipaddr.t ]
type transport_header = [`TCP of Tcp.Tcp_packet.t
|`UDP of Udp_packet.t
|`ICMP of Icmpv4_packet.t]
type transport_header =
[ `TCP of Tcp.Tcp_packet.t | `UDP of Udp_packet.t | `ICMP of Icmpv4_packet.t ]
type ('src, 'dst) t = {
ipv4_header : Ipv4_packet.t;
@ -19,13 +18,14 @@ type ('src, 'dst) t = {
src : 'src;
dst : 'dst;
}
let pp_transport_header f = function
| `ICMP h -> Icmpv4_packet.pp f h
| `TCP h -> Tcp.Tcp_packet.pp f h
| `UDP h -> Udp_packet.pp f h
let pp_host fmt = function
| `Client c -> Ipaddr.V4.pp fmt (c#other_ip)
| `Client c -> Ipaddr.V4.pp fmt c#other_ip
| `Unknown_client ip -> Format.fprintf fmt "unknown-client(%a)" Ipaddr.pp ip
| `NetVM -> Format.pp_print_string fmt "net-vm"
| `External ip -> Format.fprintf fmt "external(%a)" Ipaddr.pp ip
@ -33,32 +33,28 @@ let pp_host fmt = function
let to_mirage_nat_packet t : Nat_packet.t =
match t.transport_header with
| `TCP h -> `IPv4 (t.ipv4_header, (`TCP (h, t.transport_payload)))
| `UDP h -> `IPv4 (t.ipv4_header, (`UDP (h, t.transport_payload)))
| `ICMP h -> `IPv4 (t.ipv4_header, (`ICMP (h, t.transport_payload)))
| `TCP h -> `IPv4 (t.ipv4_header, `TCP (h, t.transport_payload))
| `UDP h -> `IPv4 (t.ipv4_header, `UDP (h, t.transport_payload))
| `ICMP h -> `IPv4 (t.ipv4_header, `ICMP (h, t.transport_payload))
let of_mirage_nat_packet ~src ~dst packet : ('a, 'b) t option =
let `IPv4 (ipv4_header, ipv4_payload) = packet in
let transport_header, transport_payload = match ipv4_payload with
| `TCP (h, p) -> `TCP h, p
| `UDP (h, p) -> `UDP h, p
| `ICMP (h, p) -> `ICMP h, p
let (`IPv4 (ipv4_header, ipv4_payload)) = packet in
let transport_header, transport_payload =
match ipv4_payload with
| `TCP (h, p) -> (`TCP h, p)
| `UDP (h, p) -> (`UDP h, p)
| `ICMP (h, p) -> (`ICMP h, p)
in
Some {
ipv4_header;
transport_header;
transport_payload;
src;
dst;
}
Some { ipv4_header; transport_header; transport_payload; src; dst }
(* possible actions to take for a packet: *)
type action = [
| `Accept (* Send to destination, unmodified. *)
| `NAT (* Rewrite source field to the firewall's IP, with a fresh source port.
type action =
[ `Accept (* Send to destination, unmodified. *)
| `NAT
(* Rewrite source field to the firewall's IP, with a fresh source port.
Also, add translation rules for future traffic in both directions,
between these hosts on these ports, and corresponding ICMP error traffic. *)
| `NAT_to of host * port (* As for [`NAT], but also rewrite the packet's
| `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 packet for this reason. *)
]
| `Drop of string (* Drop packet for this reason. *) ]

View file

@ -4,12 +4,10 @@ type host =
[ `Client of Fw_utils.client_link (** an IP address on the private network *)
| `Firewall (** the firewall's IP on the private network *)
| `NetVM (** the IP of the firewall's default route *)
| `External of Ipaddr.t (** an IP on the public network *)
]
| `External of Ipaddr.t (** an IP on the public network *) ]
type transport_header = [`TCP of Tcp.Tcp_packet.t
|`UDP of Udp_packet.t
|`ICMP of Icmpv4_packet.t]
type transport_header =
[ `TCP of Tcp.Tcp_packet.t | `UDP of Udp_packet.t | `ICMP of Icmpv4_packet.t ]
type ('src, 'dst) t = {
ipv4_header : Ipv4_packet.t;
@ -20,20 +18,18 @@ type ('src, 'dst) t = {
}
val pp_transport_header : Format.formatter -> transport_header -> unit
val pp_host : Format.formatter -> host -> unit
val to_mirage_nat_packet : ('a, 'b) t -> Nat_packet.t
val of_mirage_nat_packet : src:'a -> dst:'b -> Nat_packet.t -> ('a, 'b) t option
(* possible actions to take for a packet: *)
type action = [
| `Accept (* Send to destination, unmodified. *)
| `NAT (* Rewrite source field to the firewall's IP, with a fresh source port.
type action =
[ `Accept (* Send to destination, unmodified. *)
| `NAT
(* Rewrite source field to the firewall's IP, with a fresh source port.
Also, add translation rules for future traffic in both directions,
between these hosts on these ports, and corresponding ICMP error traffic. *)
| `NAT_to of host * port (* As for [`NAT], but also rewrite the packet's
| `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 packet for this reason. *)
]
| `Drop of string (* Drop packet for this reason. *) ]

View file

@ -1 +1 @@
b78d6711b502f8babcc5c4083b0352b78be8e8a6bef044189ce7a00e6e564612 dist/qubes-firewall.xen
0c3c2c0e62a834112c69d7cddc5dd6f70ecb93afa988768fb860ed26e423b1f8 dist/qubes-firewall.xen

View file

@ -1 +1 @@
0c3c2c0e62a834112c69d7cddc5dd6f70ecb93afa988768fb860ed26e423b1f8 dist/qubes-firewall.xen
ac049069b35f786fa11b18a2261d7dbecd588301af0363ef6888ec9d924dc989 dist/qubes-firewall.xen

View file

@ -8,93 +8,115 @@ open Lwt.Infix
module Q = Pf_qubes.Parse_qubes
let src = Logs.Src.create "rules" ~doc:"Firewall rules"
module Log = (val Logs.src_log src : Logs.LOG)
let dns_port = 53
module Classifier = struct
let matches_port dstports (port : int) = match dstports with
let matches_port dstports (port : int) =
match dstports with
| None -> true
| Some (Q.Range_inclusive (min, max)) -> min <= port && port <= max
let matches_proto rule dns_servers packet = match rule.Q.proto, rule.Q.specialtarget with
let matches_proto rule dns_servers packet =
match (rule.Q.proto, rule.Q.specialtarget) with
| None, None -> true
| None, Some `dns when List.mem packet.ipv4_header.Ipv4_packet.dst dns_servers -> begin
| None, Some `dns
when List.mem packet.ipv4_header.Ipv4_packet.dst dns_servers -> (
(* specialtarget=dns applies only to the specialtarget destination IPs, and
specialtarget=dns is also implicitly tcp/udp port 53 *)
match packet.transport_header with
| `TCP header -> header.Tcp.Tcp_packet.dst_port = dns_port
| `UDP header -> header.Udp_packet.dst_port = dns_port
| _ -> false
end
| _ -> false)
(* DNS rules can only match traffic headed to the specialtarget hosts, so any other destination
isn't a match for DNS rules *)
| None, Some `dns -> false
| Some rule_proto, _ -> match rule_proto, packet.transport_header with
| `tcp, `TCP header -> matches_port rule.Q.dstports header.Tcp.Tcp_packet.dst_port
| `udp, `UDP header -> matches_port rule.Q.dstports header.Udp_packet.dst_port
| `icmp, `ICMP header ->
begin
| Some rule_proto, _ -> (
match (rule_proto, packet.transport_header) with
| `tcp, `TCP header ->
matches_port rule.Q.dstports header.Tcp.Tcp_packet.dst_port
| `udp, `UDP header ->
matches_port rule.Q.dstports header.Udp_packet.dst_port
| `icmp, `ICMP header -> (
match rule.Q.icmp_type with
| None -> true
| Some rule_icmp_type ->
0 = compare rule_icmp_type @@ Icmpv4_wire.ty_to_int header.Icmpv4_packet.ty
end
| _, _ -> false
0
= compare rule_icmp_type
@@ Icmpv4_wire.ty_to_int header.Icmpv4_packet.ty)
| _, _ -> false)
let matches_dest dns_client rule packet =
let ip = packet.ipv4_header.Ipv4_packet.dst in
match rule.Q.dst with
| `any -> Lwt.return @@ `Match rule
| `hosts subnet ->
Lwt.return @@ if (Ipaddr.Prefix.mem Ipaddr.(V4 ip) subnet) then `Match rule else `No_match
| `dnsname name ->
Lwt.return
@@
if Ipaddr.Prefix.mem Ipaddr.(V4 ip) subnet then `Match rule
else `No_match
| `dnsname name -> (
Log.debug (fun f -> f "Resolving %a" Domain_name.pp name);
dns_client name >|= function
| Ok (_ttl, found_ips) ->
if Ipaddr.V4.Set.mem ip found_ips
then `Match rule
else `No_match
if Ipaddr.V4.Set.mem ip found_ips then `Match rule else `No_match
| Error (`Msg m) ->
Log.warn (fun f -> f "Ignoring rule %a, could not resolve" Q.pp_rule rule);
Log.warn (fun f ->
f "Ignoring rule %a, could not resolve" Q.pp_rule rule);
Log.debug (fun f -> f "%s" m);
`No_match
| Error _ -> assert false (* TODO: fix type of dns_client so that this case can go *)
| Error _ ->
assert
false (* TODO: fix type of dns_client so that this case can go *))
end
let find_first_match dns_client dns_servers packet acc rule =
match acc with
| `No_match ->
if Classifier.matches_proto rule dns_servers packet
then Classifier.matches_dest dns_client rule packet
if Classifier.matches_proto rule dns_servers packet then
Classifier.matches_dest dns_client rule packet
else Lwt.return `No_match
| q -> Lwt.return q
(* Does the packet match our rules? *)
let classify_client_packet dns_client dns_servers (packet : ([`Client of Fw_utils.client_link], _) Packet.t) =
let classify_client_packet dns_client dns_servers
(packet : ([ `Client of Fw_utils.client_link ], _) Packet.t) =
let (`Client client_link) = packet.src in
let rules = client_link#get_rules in
Lwt_list.fold_left_s (find_first_match dns_client dns_servers packet) `No_match rules >|= function
Lwt_list.fold_left_s
(find_first_match dns_client dns_servers packet)
`No_match rules
>|= function
| `No_match -> `Drop "No matching rule; assuming default drop"
| `Match { Q.action = Q.Accept; _ } -> `Accept
| `Match ({ Q.action = Q.Drop; _ } as rule) ->
`Drop (Format.asprintf "rule number %a explicitly drops this packet" Q.pp_rule rule)
`Drop
(Format.asprintf "rule number %a explicitly drops this packet" Q.pp_rule
rule)
let translate_accepted_packets dns_client dns_servers packet =
classify_client_packet dns_client dns_servers packet >|= function
| `Accept -> `NAT
| `Drop s -> `Drop s
(** Packets from the private interface that don't match any NAT table entry are being checked against the fw rules here *)
let from_client dns_client dns_servers (packet : ([`Client of Fw_utils.client_link], _) Packet.t) : Packet.action Lwt.t =
(** Packets from the private interface that don't match any NAT table entry are
being checked against the fw rules here *)
let from_client dns_client dns_servers
(packet : ([ `Client of Fw_utils.client_link ], _) Packet.t) :
Packet.action Lwt.t =
match packet with
| { dst = `External _ ; _ } | { dst = `NetVM; _ } -> translate_accepted_packets dns_client dns_servers packet
| { dst = `Firewall ; _ } -> Lwt.return @@ `Drop "packet addressed to firewall itself"
| { dst = `Client _ ; _ } -> classify_client_packet dns_client dns_servers packet
| { dst = `External _; _ } | { dst = `NetVM; _ } ->
translate_accepted_packets dns_client dns_servers packet
| { dst = `Firewall; _ } ->
Lwt.return @@ `Drop "packet addressed to firewall itself"
| { dst = `Client _; _ } ->
classify_client_packet dns_client dns_servers packet
| _ -> Lwt.return @@ `Drop "could not classify packet"
(** Packets from the outside world that don't match any NAT table entry are being dropped by default *)
let from_netvm (_packet : ([`NetVM | `External of _], _) Packet.t) : Packet.action Lwt.t =
(** Packets from the outside world that don't match any NAT table entry are
being dropped by default *)
let from_netvm (_packet : ([ `NetVM | `External of _ ], _) Packet.t) :
Packet.action Lwt.t =
Lwt.return @@ `Drop "drop by default"

View file

@ -2,7 +2,8 @@ open Mirage
let pin = "git+https://github.com/roburio/alcotest.git#mirage"
let packages = [
let packages =
[
package "ethernet";
package "arp";
package "arp-mirage";
@ -16,12 +17,17 @@ let packages = [
]
let client =
foreign ~packages
"Unikernel.Client" @@ random @-> time @-> mclock @-> network @-> qubesdb @-> job
foreign ~packages "Unikernel.Client"
@@ random @-> time @-> mclock @-> network @-> qubesdb @-> job
let db = default_qubesdb
let network = default_network
let () =
let job = [ client $ default_random $ default_time $ default_monotonic_clock $ network $ db ] in
let job =
[
client $ default_random $ default_time $ default_monotonic_clock $ network
$ db;
]
in
register "http-fetch" job

View file

@ -1,6 +1,8 @@
open Lwt.Infix
(* https://www.qubes-os.org/doc/vm-interface/#firewall-rules-in-4x *)
let src = Logs.Src.create "firewall test" ~doc:"Firewalltest"
module Log = (val Logs.src_log src : Logs.LOG)
(* TODO
@ -39,17 +41,23 @@ module Log = (val Logs.src_log src : Logs.LOG)
(* Point-to-point links out of a netvm always have this IP TODO clarify with Marek *)
let netvm = "10.137.0.5"
(* default "nameserver"s, which netvm redirects to whatever its real nameservers are *)
let nameserver_1, nameserver_2 = "10.139.1.1", "10.139.1.2"
module Client (R: Mirage_crypto_rng_mirage.S) (Time: Mirage_time.S) (Clock : Mirage_clock.MCLOCK) (NET: Mirage_net.S) (DB : Qubes.S.DB) = struct
(* default "nameserver"s, which netvm redirects to whatever its real nameservers are *)
let nameserver_1, nameserver_2 = ("10.139.1.1", "10.139.1.2")
module Client
(R : Mirage_crypto_rng_mirage.S)
(Time : Mirage_time.S)
(Clock : Mirage_clock.MCLOCK)
(NET : Mirage_net.S)
(DB : Qubes.S.DB) =
struct
module E = Ethernet.Make (NET)
module A = Arp.Make (E) (Time)
module I = Qubesdb_ipv4.Make (DB) (R) (Clock) (E) (A)
module Icmp = Icmpv4.Make (I)
module U = Udp.Make (I) (R)
module T = Tcp.Flow.Make (I) (Time) (Clock) (R)
module Alcotest = Alcotest_mirage.Make (Clock)
module Stack = struct
@ -66,18 +74,23 @@ module Client (R: Mirage_crypto_rng_mirage.S) (Time: Mirage_time.S) (Clock : Mir
module IPV4 = I
type t = {
net : NET.t ; eth : E.t ; arp : A.t ;
ip : I.t ; icmp : Icmp.t ; udp : U.t ; tcp : T.t ;
net : NET.t;
eth : E.t;
arp : A.t;
ip : I.t;
icmp : Icmp.t;
udp : U.t;
tcp : T.t;
udp_listeners : (int, U.callback) Hashtbl.t;
tcp_listeners : (int, T.listener) Hashtbl.t;
mutable icmp_listener : (src:Ipaddr.V4.t -> dst:Ipaddr.V4.t -> Cstruct.t -> unit Lwt.t) option ;
mutable icmp_listener :
(src:Ipaddr.V4.t -> dst:Ipaddr.V4.t -> Cstruct.t -> unit Lwt.t) option;
}
let ipv4 { ip; _ } = ip
let udpv4 { udp; _ } = udp
let tcpv4 { tcp; _ } = tcp
let icmpv4 { icmp; _ } = icmp
let listener h port = Hashtbl.find_opt h port
let udp_listener h ~dst_port = listener h dst_port
@ -97,19 +110,17 @@ module Client (R: Mirage_crypto_rng_mirage.S) (Time: Mirage_time.S) (Clock : Mir
let listen t =
let ethif_listener =
E.input
~arpv4:(A.input t.arp)
~ipv4:(
I.input
E.input ~arpv4:(A.input t.arp)
~ipv4:
(I.input
~tcp:(T.input t.tcp ~listeners:(listener t.tcp_listeners))
~udp:(U.input t.udp ~listeners:(udp_listener t.udp_listeners))
~default:(fun ~proto ~src ~dst buf ->
match proto with
| 1 ->
begin match t.icmp_listener with
| 1 -> (
match t.icmp_listener with
| None -> Icmp.input t.icmp ~src ~dst buf
| Some cb -> cb ~src ~dst buf
end
| Some cb -> cb ~src ~dst buf)
| _ -> Lwt.return_unit)
t.ip)
~ipv6:(fun _ -> Lwt.return_unit)
@ -123,7 +134,14 @@ module Client (R: Mirage_crypto_rng_mirage.S) (Time: Mirage_time.S) (Clock : Mir
| Ok _res -> Lwt.return_unit
let connect net eth arp ip icmp udp tcp =
{ net ; eth ; arp ; ip ; icmp ; udp ; tcp ;
{
net;
eth;
arp;
ip;
icmp;
udp;
tcp;
udp_listeners = Hashtbl.create 2;
tcp_listeners = Hashtbl.create 2;
icmp_listener = None;
@ -137,23 +155,31 @@ module Client (R: Mirage_crypto_rng_mirage.S) (Time: Mirage_time.S) (Clock : Mir
module Dns = Dns_client_mirage.Make (R) (Time) (Clock) (Stack)
let make_ping_packet payload =
let echo_request = { Icmpv4_packet.code = 0; (* constant for echo request/reply *)
let echo_request =
{
Icmpv4_packet.code = 0;
(* constant for echo request/reply *)
ty = Icmpv4_wire.Echo_request;
subheader = Icmpv4_packet.(Id_and_seq (0, 0)); } in
subheader = Icmpv4_packet.(Id_and_seq (0, 0));
}
in
Icmpv4_packet.Marshal.make_cstruct echo_request ~payload
let is_ping_reply src server packet =
0 = Ipaddr.V4.(compare src @@ of_string_exn server) &&
packet.Icmpv4_packet.code = 0 &&
packet.Icmpv4_packet.ty = Icmpv4_wire.Echo_reply &&
packet.Icmpv4_packet.subheader = Icmpv4_packet.(Id_and_seq (0, 0))
(0 = Ipaddr.V4.(compare src @@ of_string_exn server))
&& packet.Icmpv4_packet.code = 0
&& packet.Icmpv4_packet.ty = Icmpv4_wire.Echo_reply
&& packet.Icmpv4_packet.subheader = Icmpv4_packet.(Id_and_seq (0, 0))
let ping_denied_listener server resp_received stack =
let icmp_listener ~src ~dst:_ buf =
(* hopefully this is a reply to an ICMP echo request we sent *)
Log.info (fun f -> f "ping test: ICMP message received from %a: %a" I.pp_ipaddr src Cstruct.hexdump_pp buf);
Log.info (fun f ->
f "ping test: ICMP message received from %a: %a" I.pp_ipaddr src
Cstruct.hexdump_pp buf);
match Icmpv4_packet.Unmarshal.of_cstruct buf with
| Error e -> Log.err (fun f -> f "couldn't parse ICMP packet: %s" e);
| Error e ->
Log.err (fun f -> f "couldn't parse ICMP packet: %s" e);
Lwt.return_unit
| Ok (packet, _payload) ->
Log.info (fun f -> f "ICMP message: %a" Icmpv4_packet.pp packet);
@ -166,16 +192,21 @@ module Client (R: Mirage_crypto_rng_mirage.S) (Time: Mirage_time.S) (Clock : Mir
let resp_received = ref false in
Log.info (fun f -> f "Entering ping test: %s" server);
ping_denied_listener server resp_received stack;
Icmp.write (Stack.icmpv4 stack) ~dst:(Ipaddr.V4.of_string_exn server) (make_ping_packet (Cstruct.of_string "hi")) >>= function
| Error e -> Log.err (fun f -> f "ping test: error sending ping: %a" Icmp.pp_error e); Lwt.return_unit
Icmp.write (Stack.icmpv4 stack)
~dst:(Ipaddr.V4.of_string_exn server)
(make_ping_packet (Cstruct.of_string "hi"))
>>= function
| Error e ->
Log.err (fun f -> f "ping test: error sending ping: %a" Icmp.pp_error e);
Lwt.return_unit
| Ok () ->
Log.info (fun f -> f "ping test: sent ping to %s" server);
Time.sleep_ns 2_000_000_000L >>= fun () ->
(if !resp_received then
Log.err (fun f -> f "ping test failed: server %s got a response, block expected :(" server)
else
Log.err (fun f -> f "ping test passed: successfully blocked :)")
);
if !resp_received then
Log.err (fun f ->
f "ping test failed: server %s got a response, block expected :("
server)
else Log.err (fun f -> f "ping test passed: successfully blocked :)");
Stack.listen_icmp stack None;
Lwt.return_unit
@ -183,30 +214,44 @@ module Client (R: Mirage_crypto_rng_mirage.S) (Time: Mirage_time.S) (Clock : Mir
let resp_correct = ref false in
let echo_server = Ipaddr.V4.of_string_exn netvm in
let icmp_callback ~src ~dst:_ buf =
if Ipaddr.V4.compare src echo_server = 0 then begin
(if Ipaddr.V4.compare src echo_server = 0 then
(* TODO: check that packet is error packet *)
match Icmpv4_packet.Unmarshal.of_cstruct buf with
| Error e -> Log.err (fun f -> f "Error parsing icmp packet %s" e)
| Ok (packet, _) ->
(* TODO don't hardcode the numbers, make a datatype *)
if packet.Icmpv4_packet.code = 10 (* unreachable, admin prohibited *)
if
packet.Icmpv4_packet.code
= 10 (* unreachable, admin prohibited *)
then resp_correct := true
else Log.debug (fun f -> f "Unrelated icmp packet %a" Icmpv4_packet.pp packet)
end;
else
Log.debug (fun f ->
f "Unrelated icmp packet %a" Icmpv4_packet.pp packet));
Lwt.return_unit
in
let content = Cstruct.of_string "important data" in
Stack.listen_icmp stack (Some icmp_callback);
U.write ~src_port:1337 ~dst:echo_server ~dst_port:1338 (Stack.udpv4 stack) content >>= function
| Ok () -> (* .. listener: test with accept rule, if we get reply we're good *)
U.write ~src_port:1337 ~dst:echo_server ~dst_port:1338 (Stack.udpv4 stack)
content
>>= function
| Ok () ->
(* .. listener: test with accept rule, if we get reply we're good *)
Time.sleep_ns 1_000_000_000L >>= fun () ->
if !resp_correct
then Log.info (fun m -> m "UDP fetch test to port %d succeeded :)" 1338)
else Log.err (fun f -> f "UDP fetch test to port %d: failed. :( correct response not received" 1338);
if !resp_correct then
Log.info (fun m -> m "UDP fetch test to port %d succeeded :)" 1338)
else
Log.err (fun f ->
f
"UDP fetch test to port %d: failed. :( correct response not \
received"
1338);
Stack.listen_icmp stack None;
Lwt.return_unit
| Error e ->
Log.err (fun f -> f "UDP fetch test to port %d failed: :( couldn't write the packet: %a"
Log.err (fun f ->
f
"UDP fetch test to port %d failed: :( couldn't write the packet: \
%a"
1338 U.pp_error e);
Lwt.return_unit
@ -218,94 +263,137 @@ module Client (R: Mirage_crypto_rng_mirage.S) (Time: Mirage_time.S) (Clock : Mir
| Ok flow ->
Log.info (fun f -> f "%s passed :)" msg');
T.close flow
| Error e -> Log.err (fun f -> f "%s failed: Connection failed (%a) :(" msg' T.pp_error e);
| Error e ->
Log.err (fun f ->
f "%s failed: Connection failed (%a) :(" msg' T.pp_error e);
Lwt.return_unit
let tcp_connect_denied msg server port tcp () =
let ip = Ipaddr.V4.of_string_exn server in
let msg' = Printf.sprintf "TCP connect denied test %s to %s:%d" msg server port in
let connect = (T.create_connection tcp (ip, port) >>= function
| Ok flow ->
Log.err (fun f -> f "%s failed: Connection should be denied, but was not. :(" msg');
T.close flow
| Error e -> Log.info (fun f -> f "%s passed (error text: %a) :)" msg' T.pp_error e);
Lwt.return_unit)
let msg' =
Printf.sprintf "TCP connect denied test %s to %s:%d" msg server port
in
let timeout = (
let connect =
T.create_connection tcp (ip, port) >>= function
| Ok flow ->
Log.err (fun f ->
f "%s failed: Connection should be denied, but was not. :(" msg');
T.close flow
| Error e ->
Log.info (fun f ->
f "%s passed (error text: %a) :)" msg' T.pp_error e);
Lwt.return_unit
in
let timeout =
Time.sleep_ns 1_000_000_000L >>= fun () ->
Log.info (fun f -> f "%s passed :)" msg');
Lwt.return_unit)
Lwt.return_unit
in
Lwt.pick [ connect; timeout ]
let udp_fetch ~src_port ~echo_server_port stack () =
Log.info (fun f -> f "Entering udp fetch test: %d -> %s:%d"
src_port netvm echo_server_port);
Log.info (fun f ->
f "Entering udp fetch test: %d -> %s:%d" src_port netvm echo_server_port);
let resp_correct = ref false in
let echo_server = Ipaddr.V4.of_string_exn netvm in
let content = Cstruct.of_string "important data" in
let udp_listener : U.callback = (fun ~src ~dst:_ ~src_port buf ->
Log.debug (fun f -> f "listen_udpv4 function invoked for packet: %a" Cstruct.hexdump_pp buf);
if ((0 = Ipaddr.V4.compare echo_server src) && src_port = echo_server_port) then
let udp_listener : U.callback =
fun ~src ~dst:_ ~src_port buf ->
Log.debug (fun f ->
f "listen_udpv4 function invoked for packet: %a" Cstruct.hexdump_pp
buf);
if 0 = Ipaddr.V4.compare echo_server src && src_port = echo_server_port
then (
match Cstruct.equal buf content with
| true -> (* yay *)
Log.info (fun f -> f "UDP fetch test to port %d: passed :)" echo_server_port);
| true ->
(* yay *)
Log.info (fun f ->
f "UDP fetch test to port %d: passed :)" echo_server_port);
resp_correct := true;
Lwt.return_unit
| false -> (* oh no *)
Log.err (fun f -> f "UDP fetch test to port %d: failed. :( Packet corrupted; expected %a but got %a"
echo_server_port Cstruct.hexdump_pp content Cstruct.hexdump_pp buf);
Lwt.return_unit
else
begin
| false ->
(* oh no *)
Log.err (fun f ->
f
"UDP fetch test to port %d: failed. :( Packet corrupted; \
expected %a but got %a"
echo_server_port Cstruct.hexdump_pp content Cstruct.hexdump_pp
buf);
Lwt.return_unit)
else (
(* disregard this packet *)
Log.debug (fun f -> f "packet is not from the echo server or has the wrong source port (%d but we wanted %d)"
Log.debug (fun f ->
f
"packet is not from the echo server or has the wrong source port \
(%d but we wanted %d)"
src_port echo_server_port);
(* don't cancel the listener, since we want to keep listening *)
Lwt.return_unit
end
)
Lwt.return_unit)
in
Stack.listen_udpv4 stack ~port:src_port udp_listener;
U.write ~src_port ~dst:echo_server ~dst_port:echo_server_port (Stack.udpv4 stack) content >>= function
| Ok () -> (* .. listener: test with accept rule, if we get reply we're good *)
U.write ~src_port ~dst:echo_server ~dst_port:echo_server_port
(Stack.udpv4 stack) content
>>= function
| Ok () ->
(* .. listener: test with accept rule, if we get reply we're good *)
Time.sleep_ns 1_000_000_000L >>= fun () ->
Stack.stop_listen_udpv4 stack ~port:src_port;
if !resp_correct then Lwt.return_unit else begin
Log.err (fun f -> f "UDP fetch test to port %d: failed. :( correct response not received" echo_server_port);
Lwt.return_unit
end
if !resp_correct then Lwt.return_unit
else (
Log.err (fun f ->
f
"UDP fetch test to port %d: failed. :( correct response not \
received"
echo_server_port);
Lwt.return_unit)
| Error e ->
Log.err (fun f -> f "UDP fetch test to port %d failed: :( couldn't write the packet: %a"
Log.err (fun f ->
f
"UDP fetch test to port %d failed: :( couldn't write the packet: \
%a"
echo_server_port U.pp_error e);
Lwt.return_unit
let dns_expect_failure ~nameserver ~hostname stack () =
let lookup = Domain_name.(of_string_exn hostname |> host_exn) in
let nameserver' = `UDP, (Ipaddr.V4.of_string_exn nameserver, 53) in
let nameserver' = (`UDP, (Ipaddr.V4.of_string_exn nameserver, 53)) in
let dns = Dns.create ~nameserver:nameserver' stack in
Dns.gethostbyname dns lookup >>= function
| Error (`Msg s) when String.compare s "Truncated UDP response" <> 0 -> Log.debug (fun f -> f "DNS test to %s failed as expected: %s"
nameserver s);
Log.info (fun f -> f "DNS traffic to %s correctly blocked :)" nameserver);
| Error (`Msg s) when String.compare s "Truncated UDP response" <> 0 ->
Log.debug (fun f ->
f "DNS test to %s failed as expected: %s" nameserver s);
Log.info (fun f ->
f "DNS traffic to %s correctly blocked :)" nameserver);
Lwt.return_unit
| Error (`Msg s) ->
Log.debug (fun f -> f "DNS test to %s failed unexpectedly (truncated response): %s :("
Log.debug (fun f ->
f "DNS test to %s failed unexpectedly (truncated response): %s :("
nameserver s);
Lwt.return_unit
| Ok addr -> Log.err (fun f -> f "DNS test to %s should have been blocked, but looked up %s:%a" nameserver hostname Ipaddr.V4.pp addr);
| Ok addr ->
Log.err (fun f ->
f "DNS test to %s should have been blocked, but looked up %s:%a"
nameserver hostname Ipaddr.V4.pp addr);
Lwt.return_unit
let dns_then_tcp_denied server stack () =
let parsed_server = Domain_name.(of_string_exn server |> host_exn) in
(* ask dns about server *)
Log.debug (fun f -> f "going to make a dns thing using nameserver %s" nameserver_1);
let dns = Dns.create ~nameserver:(`UDP, ((Ipaddr.V4.of_string_exn nameserver_1), 53)) stack in
Log.debug (fun f ->
f "going to make a dns thing using nameserver %s" nameserver_1);
let dns =
Dns.create
~nameserver:(`UDP, (Ipaddr.V4.of_string_exn nameserver_1, 53))
stack
in
Log.debug (fun f -> f "OK, going to look up %s now" server);
Dns.gethostbyname dns parsed_server >>= function
| Error (`Msg s) -> Log.err (fun f -> f "couldn't look up ip for %s: %s" server s); Lwt.return_unit
| Error (`Msg s) ->
Log.err (fun f -> f "couldn't look up ip for %s: %s" server s);
Lwt.return_unit
| Ok addr ->
Log.debug (fun f -> f "looked up ip for %s: %a" server Ipaddr.V4.pp addr);
Log.debug (fun f ->
f "looked up ip for %s: %a" server Ipaddr.V4.pp addr);
Log.err (fun f -> f "Do more stuff here!!!! :(");
Lwt.return_unit
@ -316,42 +404,64 @@ module Client (R: Mirage_crypto_rng_mirage.S) (Time: Mirage_time.S) (Clock : Mir
Icmp.connect ipv4 >>= fun icmp ->
U.connect ipv4 >>= fun udp ->
T.connect ipv4 >>= fun tcp ->
let stack = Stack.connect network ethernet arp ipv4 icmp udp tcp in
Lwt.async (fun () -> Stack.listen stack);
(* put this first because tcp_connect_denied tests also generate icmp messages *)
let general_tests : unit Alcotest.test = ("firewall tests", [
("UDP fetch", `Quick, udp_fetch ~src_port:9090 ~echo_server_port:1235 stack);
let general_tests : unit Alcotest.test =
( "firewall tests",
[
( "UDP fetch",
`Quick,
udp_fetch ~src_port:9090 ~echo_server_port:1235 stack );
("Ping expect failure", `Quick, ping_expect_failure "8.8.8.8" stack);
(* TODO: ping_expect_success to the netvm, for which we have an icmptype rule in update-firewall.sh *)
("ICMP error type", `Quick, icmp_error_type stack)
] ) in
("ICMP error type", `Quick, icmp_error_type stack);
] )
in
Alcotest.run ~and_exit:false "name" [ general_tests ] >>= fun () ->
let tcp_tests : unit Alcotest.test = ("tcp tests", [
let tcp_tests : unit Alcotest.test =
( "tcp tests",
[
(* this test fails on 4.0R3
("TCP connect", `Quick, tcp_connect "when trying specialtarget" nameserver_1 53 tcp); *)
("TCP connect", `Quick, tcp_connect_denied "" netvm 53 tcp);
("TCP connect", `Quick, tcp_connect_denied "when trying below range" netvm 6667 tcp);
("TCP connect", `Quick, tcp_connect "when trying lower bound in range" netvm 6668 tcp);
("TCP connect", `Quick, tcp_connect "when trying upper bound in range" netvm 6670 tcp);
("TCP connect", `Quick, tcp_connect_denied "when trying above range" netvm 6671 tcp);
( "TCP connect",
`Quick,
tcp_connect_denied "when trying below range" netvm 6667 tcp );
( "TCP connect",
`Quick,
tcp_connect "when trying lower bound in range" netvm 6668 tcp );
( "TCP connect",
`Quick,
tcp_connect "when trying upper bound in range" netvm 6670 tcp );
( "TCP connect",
`Quick,
tcp_connect_denied "when trying above range" netvm 6671 tcp );
("TCP connect", `Quick, tcp_connect_denied "" netvm 8082 tcp);
] ) in
] )
in
(* replace the udp-related listeners with the right one for tcp *)
Alcotest.run "name" [ tcp_tests ] >>= fun () ->
(* use the stack abstraction only after the other tests have run, since it's not friendly with outside use of its modules *)
let stack_tests = "stack tests", [
("DNS expect failure", `Quick, dns_expect_failure ~nameserver:"8.8.8.8" ~hostname:"mirage.io" stack);
let stack_tests =
( "stack tests",
[
( "DNS expect failure",
`Quick,
dns_expect_failure ~nameserver:"8.8.8.8" ~hostname:"mirage.io" stack
);
(* the test below won't work on @linse's internet,
* because the nameserver there doesn't answer on TCP port 53,
* only UDP port 53. Dns_mirage_client.ml disregards our request
* to use UDP and uses TCP anyway, so this request can never work there. *)
(* If we can figure out a way to have this test unikernel do a UDP lookup with minimal pain,
* we should re-enable this test. *)
("DNS lookup + TCP connect", `Quick, dns_then_tcp_denied "google.com" stack);
] in
( "DNS lookup + TCP connect",
`Quick,
dns_then_tcp_denied "google.com" stack );
] )
in
Alcotest.run "name" [ stack_tests ]
end

View file

@ -6,10 +6,13 @@ open Qubes
open Cmdliner
let src = Logs.Src.create "unikernel" ~doc:"Main unikernel code"
module Log = (val Logs.src_log src : Logs.LOG)
let nat_table_size =
let doc = Arg.info ~doc:"The number of NAT entries to allocate." [ "nat-table-size" ] in
let doc =
Arg.info ~doc:"The number of NAT entries to allocate." [ "nat-table-size" ]
in
Mirage_runtime.register_arg Arg.(value & opt int 5_000 doc)
let ipv4 =
@ -28,40 +31,41 @@ let ipv4_dns2 =
let doc = Arg.info ~doc:"Manual Second DNS IP setting." [ "ipv4-dns2" ] in
Mirage_runtime.register_arg Arg.(value & opt string "10.139.1.2" doc)
module Main (R : Mirage_crypto_rng_mirage.S)(Clock : Mirage_clock.MCLOCK)(Time : Mirage_time.S) = struct
module Dispatcher = Dispatcher.Make(R)(Clock)(Time)
module Dns_transport = My_dns.Transport(R)(Clock)(Time)
module Dns_client = Dns_client.Make(Dns_transport)
module Dns_client = Dns_client.Make (My_dns)
(* Set up networking and listen for incoming packets. *)
let network dns_client dns_responses dns_servers qubesDB router =
(* Report success *)
Dao.set_iptables_error qubesDB "" >>= fun () ->
(* Handle packets from both networks *)
Lwt.choose [
Dispatcher.wait_clients Clock.elapsed_ns dns_client dns_servers qubesDB router ;
Lwt.choose
[
Dispatcher.wait_clients Mirage_mtime.elapsed_ns dns_client dns_servers
qubesDB router;
Dispatcher.uplink_wait_update qubesDB router;
Dispatcher.uplink_listen Clock.elapsed_ns dns_responses router
Dispatcher.uplink_listen Mirage_mtime.elapsed_ns dns_responses router;
]
(* Main unikernel entry point (called from auto-generated main.ml). *)
let start _random _clock _time =
let start () =
let open Lwt.Syntax in
let start_time = Clock.elapsed_ns () in
let start_time = Mirage_mtime.elapsed_ns () in
(* Start qrexec agent and QubesDB agent in parallel *)
let* qrexec = RExec.connect ~domid:0 () in
let agent_listener = RExec.listen qrexec Command.handler in
let* qubesDB = DB.connect ~domid:0 () in
let startup_time =
let ( - ) = Int64.sub in
let time_in_ns = Clock.elapsed_ns () - start_time in
let time_in_ns = Mirage_mtime.elapsed_ns () - start_time in
Int64.to_float time_in_ns /. 1e9
in
Log.info (fun f -> f "QubesDB and qrexec agents connected in %.3f s" startup_time);
Log.info (fun f ->
f "QubesDB and qrexec agents connected in %.3f s" startup_time);
(* Watch for shutdown requests from Qubes *)
let shutdown_rq =
Xen_os.Lifecycle.await_shutdown_request () >>= fun (`Poweroff | `Reboot) ->
Lwt.return_unit in
Lwt.return_unit
in
(* Set up networking *)
let nat = My_nat.create ~max_entries:(nat_table_size ()) in
@ -73,19 +77,24 @@ module Main (R : Mirage_crypto_rng_mirage.S)(Clock : Mirage_clock.MCLOCK)(Time :
let zero_ip = Ipaddr.V4.any in
let network_config =
if (netvm_ip = zero_ip && our_ip = zero_ip) then (* Read network configuration from QubesDB *)
Dao.read_network_config qubesDB >>= fun config ->
if netvm_ip = zero_ip && our_ip = zero_ip then (
(* Read network configuration from QubesDB *)
Dao.read_network_config qubesDB
>>= fun config ->
if config.netvm_ip = zero_ip || config.our_ip = zero_ip then
Log.info (fun f -> f "We currently have no netvm nor command line for setting it up, aborting...");
Log.info (fun f ->
f
"We currently have no netvm nor command line for setting it up, \
aborting...");
assert (config.netvm_ip <> zero_ip && config.our_ip <> zero_ip);
Lwt.return config)
else
let config : Dao.network_config =
{ from_cmdline = true; netvm_ip; our_ip; dns; dns2 }
in
Lwt.return config
else begin
let config:Dao.network_config = {from_cmdline=true; netvm_ip; our_ip; dns; dns2} in
Lwt.return config
end
in
network_config >>= fun config ->
(* We now must have a valid netvm IP address and our IP address or crash *)
Dao.print_network_config config;
@ -93,25 +102,25 @@ module Main (R : Mirage_crypto_rng_mirage.S)(Clock : Mirage_clock.MCLOCK)(Time :
let* clients = Client_eth.create config in
(* Set up routing between networks and hosts *)
let router = Dispatcher.create
~config
~clients
~nat
~uplink:None
in
let router = Dispatcher.create ~config ~clients ~nat ~uplink:None in
let send_dns_query = Dispatcher.send_dns_client_query router in
let dns_mvar = Lwt_mvar.create_empty () in
let nameservers = `Udp, [ config.Dao.dns, 53 ; config.Dao.dns2, 53 ] in
let dns_client = Dns_client.create ~nameservers (router, send_dns_query, dns_mvar) in
let nameservers = (`Udp, [ (config.Dao.dns, 53); (config.Dao.dns2, 53) ]) in
let dns_client =
Dns_client.create ~nameservers (router, send_dns_query, dns_mvar)
in
let dns_servers = [ config.Dao.dns; config.Dao.dns2 ] in
let net_listener = network (Dns_client.getaddrinfo dns_client Dns.Rr_map.A) dns_mvar dns_servers qubesDB router in
let net_listener =
network
(Dns_client.getaddrinfo dns_client Dns.Rr_map.A)
dns_mvar dns_servers qubesDB router
in
(* Report memory usage to XenStore *)
Memory_pressure.init ();
(* Run until something fails or we get a shutdown request. *)
Lwt.choose [ agent_listener; net_listener; shutdown_rq ] >>= fun () ->
(* Give the console daemon time to show any final log messages. *)
Time.sleep_ns (1.0 *. 1e9 |> Int64.of_float)
end
Mirage_sleep.ns (1.0 *. 1e9 |> Int64.of_float)