diff --git a/.dockerignore b/.dockerignore
index 85fe546..72eb1df 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -2,3 +2,6 @@
_build
*.xen
*.bz2
+*.tar.bz2
+*.tgz
+mirage-firewall-bin*
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 0000000..a5720ca
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,32 @@
+name: Main workflow
+
+on:
+ pull_request:
+ push:
+ schedule:
+ # Prime the caches every Monday
+ - cron: 0 1 * * MON
+
+jobs:
+ build:
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - run: ./build-with.sh docker
+
+ - run: sh -exc 'if [ "$(sha256sum dist/qubes-firewall.xen)" = "$(cat qubes-firewall.sha256)" ]; then echo "SHA256 MATCHES"; else exit 42; fi'
+
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: qubes-firewall.xen
+ path: qubes-firewall.xen
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
new file mode 100644
index 0000000..7970630
--- /dev/null
+++ b/.github/workflows/format.yml
@@ -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
diff --git a/.github/workflows/podman.yml b/.github/workflows/podman.yml
new file mode 100644
index 0000000..21f2bd2
--- /dev/null
+++ b/.github/workflows/podman.yml
@@ -0,0 +1,32 @@
+name: Main workflow
+
+on:
+ pull_request:
+ push:
+ schedule:
+ # Prime the caches every Monday
+ - cron: 0 1 * * MON
+
+jobs:
+ build:
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - run: ./build-with.sh podman
+
+ - run: sh -exc 'if [ "$(sha256sum dist/qubes-firewall.xen)" = "$(cat qubes-firewall.sha256)" ]; then echo "SHA256 MATCHES"; else exit 42; fi'
+
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: qubes-firewall.xen
+ path: qubes-firewall.xen
diff --git a/.gitignore b/.gitignore
index bd2f111..280a547 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ main.native
mir-qubes-test
qubes-firewall.xl.in
qubes-firewall_libvirt.xml
+.merlin
diff --git a/.merlin b/.merlin
deleted file mode 100644
index 2b4d411..0000000
--- a/.merlin
+++ /dev/null
@@ -1,3 +0,0 @@
-S .
-B _build
-PKG vchan.xen lwt mirage mirage-net-xen tcpip mirage-nat
diff --git a/.ocamlformat b/.ocamlformat
new file mode 100644
index 0000000..d6d9647
--- /dev/null
+++ b/.ocamlformat
@@ -0,0 +1,3 @@
+version = 0.27.0
+profile = conventional
+parse-docstrings = true
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index fb11f9a..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-language: c
-script:
- - echo 'ADD . /home/opam/qubes-mirage-firewall' >> Dockerfile
- - echo 'RUN sudo chown -R opam /home/opam/qubes-mirage-firewall' >> Dockerfile
- - docker build -t qubes-mirage-firewall .
- - docker run --rm -i qubes-mirage-firewall
-sudo: required
-dist: trusty
diff --git a/CHANGES.md b/CHANGES.md
index 7fde759..41d0026 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,169 @@
+### 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
+ (#209, @palainp, reported in the Qubes forum #208, reviewed by @dinosaure)
+
+### 0.9.2 (2024-10-16)
+
+- Code refactoring and improvements (#197, @dinosaure)
+- Build tooling updates: opam 2.2.1, solo5 0.9, mirage 4.8.1 (#199, #201, #202,
+ #203, @hannesm)
+
+### 0.9.1 (2024-05-10)
+
+- Drop astring dependency, update mirage-net-xen, and OCaml 4.14.2 -- the
+ latest LTS release (#193, @hannesm)
+- Allow the firewall to use domains requests in rules (#193, @palainp,
+ reported in the Qubes forum, fix confirmed by @neoniobium)
+
+### 0.9.0 (2024-04-24)
+
+- Fix an incorrect free memory estimation (fix in mirage/ocaml-solo5#135
+ @palainp)
+- Update to mirage 4.5.0, allowing openBSD to be used as netvm (#146 reported
+ by @Szewcson), and recover from a netvm change (#156 reported by @xaki-23)
+ (#178 @palainp)
+
+### 0.8.6 (2023-11-08)
+
+- Fix Docker build issue with newest SELinux policies (#183 @palainp, reported
+ by @Szewcson)
+- Update build script (change to debian repositories, update debian image, update
+ opam-repository commit, set commit for opam-overlay and mirage-overlay) (#184
+ @palainp, reported by @ben-grande)
+- Update disk usage value during local compilation (#186 @palainp, reported by
+ @ben-grande)
+
+### 0.8.5 (2023-07-05)
+
+- Remove memreport to Xen to avoid Qubes trying to get back some memory
+ (#176 @palainp)
+- Use bookworm and snapshot.notset.fr debian packages for reproducibility
+ (#175 @palainp)
+
+### 0.8.4 (2022-12-07)
+
+- Fix remote denial of service due to excessive console output (#166 @burghardt,
+ fix in solo5/solo5#538 by @palainp)
+- Use Ubuntu container for build, now GitHub action, ./build-with-docker.sh and
+ builds.robur.coop are synchronized (and result in the same artifact)
+ (#164 @hannesm)
+
+### 0.8.3 (2022-11-11)
+
+- Fix "DNS issues", a firewall ruleset with a domain name lead to 100% CPU usage
+ (reported by fiftyfourthparallel on
+ https://forum.qubes-os.org/t/mirage-firewall-0-8-2-broken-new-users-should-install-0-8-1/14566,
+ re-reported by @palainp in #158, fixed by @hannesm in mirage/mirage-nat#48
+ (release 3.0.1)) - underlying issue was a wrong definition of `is_port_free`
+ (since 3.0.0, used since mirage-qubes-firewall 0.8.2).
+- Fix "crash on downstream vm start", after more than 64 client VMs have been
+ connected and disconnected with the qubes-mirage-firewall (reported by @xaki23
+ in #155, fixed by @hannesm in #161) - underlying issue was a leak of xenstore
+ watchers and a hard limit in xen on the amount of watchers
+- Fix "detach netvm fails" (reported by @rootnoob in #157, fixed by @palainp
+ in mirage/mirage-net-xen#105 (release 2.1.2)) - underlying issue was that the
+ network interface state was never set to closed, but directly removed
+- Fix potential DoS in handling DNS replies (#162 @hannesm)
+- Avoid potential forever loop in My_nat.free_udp_port (#159 @hannesm)
+- Assorted code removals (#161 @hannesm)
+- Update to dns 6.4.0 changes (#154, @hannesm)
+
+### 0.8.2 (2022-10-12)
+
+- Advise to use 32 MB memory, which is sufficient (#150, @palainp)
+- Improve documentation (#150, @palainp)
+- Remove unneeded memory management code and log messages (#150, @palainp)
+- Use mirage-nat 3.0.0, remove global mutable state (#151, @hannesm)
+
+### 0.8.1 (2022-09-14)
+
+- support qrexec protocol version 3 (@reynir @palainp in mirage-qubes 0.9.3)
+- remove special DNS rule (which used to be required for Qubes 3, issue #63, fix #142, @hannesm)
+- use DNS servers from QubesDB instead of hardcoded ones for evaluation of the DNS rule (#142 @hannesm)
+- remove the GUI code (not needed in Qubes 4.1 anymore, issue #62, fix #144, @palainp)
+- trigger GC slightly earlier (at < 50% free space, issue #143, fix #147, @palainp)
+
+### 0.8.0
+
+The major change is to use PVH instead of PV. The effort was in solo5 (https://github.com/solo5/solo5) which since 0.6.6 supports Xen and PVH (developed by @mato, with some fixes (multiboot, mem size computed uniformly, not skipping first token of command line arguments) by @marmarek, @xaki23, @palainp, and @hannesm).
+
+Another user-visible change is that the DNS resolver is read from QubesDB /qubes-primary-dns instead of using a hardcoded IP address (@palainp and @hannesm).
+
+Also, the qrexec version negotiation has been implemented (in mirage-qubes by @reynir).
+
+Thanks to @palainp and @winux138 keeping track of memory allocation has been improved, and also memory can be freed now.
+
+This release uses the latest mirage release (4.2.1). It can be built with a Fedora 35 container. It uses OCaml 4.14.0.
+
+Thanks to @talex5 for lots of code cleanups, reviews, and merges. Also thanks to @xaki23 for early and detailed feedback. Testing was done by @Tommytran732 and @Szewcson. Thanks to @burghardt for documentation improvements.
+
+### 0.7.1
+
+Bugfixes:
+
+- More robust parsing of IP address in Xenstore, which may contain both IPv4 and IPv6 addresses (@linse, #103, reported by @grote)
+
+- Avoid stack overflow with many connections in the NAT table (@linse and @hannesm, reported by @talex5 in #105, fixed by mirage-nat 2.2.2 release)
+
+### 0.7
+
+This version adapts qubes-mirage-firewall with
+- dynamic rulesets via QubesDB (as defined in Qubes 4.0), and
+- adds support for DNS hostnames in rules, using the pf-qubes library for parsing.
+
+The DNS client is provided by DNS (>= 4.2.0) which uses a cache for name lookups. Not every packet will lead to a DNS lookup if DNS rules are in place.
+
+A test unikernel is available in the test subdirectory.
+
+This project was done by @linse and @yomimono in summer 2019, see PR #96.
+
+Additional changes and bugfixes:
+
+- Support Mirage 3.7 and mirage-nat 2.0.0 (@hannesm, #89).
+ The main improvement is fragmentation and reassembly support.
+
+- Use the smaller OCurrent images as the base for building the Docker images (@talex5, #80).
+ - Before: 1 GB (ocaml/opam2:debian-10-ocaml-4.08)
+ - Now: 309 MB (ocurrent/opam:alpine-3.10-ocaml-4.08)
+
+- Removed unreachable `Lwt.catch` (@hannesm, #90).
+
+Documentation:
+
+- Add note that AppVM used to build from source may need a private image larger than the default 2048MB (@marmot1791, #83).
+
+- README: create the symlink-redirected docker dir (@xaki23, #75). Otherwise, installing the docker package removes the dangling symlink.
+
+- Note that mirage-firewall cannot be used as UpdateVM (@talex5, #68).
+
+- Fix ln(1) call in build instructions (@jaseg, #69). The arguments were backwards.
+
+Keeping up with upstream changes:
+
+- Support mirage-3.7 via qubes-builder (@xaki23, #91).
+
+- Remove unused `Clock` argument to `Uplink` (@talex5, #90).
+
+- Rename things for newer mirage-xen versions (@xaki23, #80).
+
+- Adjust to ipaddr-4.0.0 renaming `_bytes` to `_octets` (@xaki23, #75).
+
+- Use OCaml 4.08.0 for qubes-builder builds (was 4.07.1) (@xaki23, #75).
+
+- Remove netchannel pin as 1.11.0 is now released (@talex5, #72).
+
+- Remove cmdliner pin as 1.0.4 is now released (@talex5, #71).
+
+
### 0.6
Changes to rules language:
@@ -19,7 +185,7 @@ Changes to rules language:
Now, `from_client` knows that `src` must be a `Client`,
and `from_netvm` knows that `src` is `External` or `NetVM`.
-- Combine `Client_gateway` and `Firewall_uplink` (@talex5, #65).
+- Combine `Client_gateway` and `Firewall_uplink` (@talex5, #64).
Before, we used `Client_gateway` for the IP address of the firewall on the client network
and `Firewall_uplink` for its address on the uplink network.
However, Qubes 4 uses the same IP address for both, so we can't separate these any longer,
diff --git a/Dockerfile b/Dockerfile
index 1cbe558..bd6e343 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,20 +1,35 @@
# Pin the base image to a specific hash for maximum reproducibility.
-# It will probably still work on newer images, though, unless Debian
+# It will probably still work on newer images, though, unless an update
# changes some compiler optimisations (unlikely).
-#FROM ocaml/opam2:debian-9-ocaml-4.07
-FROM ocaml/opam2@sha256:f7125924dd6632099ff98b2505536fe5f5c36bf0beb24779431bb62be5748562
+# bookworm-slim taken from https://hub.docker.com/_/debian/tags?page=1&name=bookworm-slim
+FROM debian@sha256:3d5df92588469a4c503adbead0e4129ef3f88e223954011c2169073897547cac
+# install remove default packages repository
+RUN rm /etc/apt/sources.list.d/debian.sources
+# and set the package source to a specific release too
+# taken from https://snapshot.debian.org/archive/debian
+RUN printf "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/20240419T024211Z bookworm main\n" > /etc/apt/sources.list
+# taken from https://snapshot.debian.org/archive/debian-security/
+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.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` = \
+"4c0e8771889a36bad4d5f964e2e662d5b611e6f112777d3d4eea3eea919d109cd17826beba38e6cfa1ad9553a0a989d9268f911ea5485968da04b1e08efc7de2" || exit
+
+ENV OPAMROOT=/tmp
+ENV OPAMCONFIRMLEVEL=unsafe-yes
# Pin last known-good version for reproducible builds.
# Remove this line (and the base image pin above) if you want to test with the
# latest versions.
-RUN git fetch origin && git reset --hard d1b2a1cbc28d43926b37e61f46fc403b48ab9c23 && opam update
-
-RUN sudo apt-get install -y m4 libxen-dev pkg-config
-RUN opam pin add -yn cmdliner 'https://github.com/talex5/cmdliner.git#repro-builds'
-RUN opam install -y vchan mirage-xen-ocaml mirage-xen-minios io-page mirage-xen mirage mirage-nat mirage-qubes
-RUN mkdir /home/opam/qubes-mirage-firewall
-ADD config.ml /home/opam/qubes-mirage-firewall/config.ml
-WORKDIR /home/opam/qubes-mirage-firewall
-RUN opam config exec -- mirage configure -t xen && make depend
-CMD opam config exec -- mirage configure -t xen && \
- opam config exec -- make tar
+# taken from https://github.com/ocaml/opam-repository
+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#f2bec38beca4aea9e481f2fd3ee319c519124649,\
+mirage-overlays:https://github.com/dune-universe/mirage-opam-overlays.git#797cb363df3ff763c43c8fbec5cd44de2878757e \
+&& make depend && make unikernel'
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..23ec3d0
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,23 @@
+Copyright (X) 2015-2024, the Qubes Mirage Firewall authors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/Makefile.builder b/Makefile.builder
index 098463d..53b860d 100644
--- a/Makefile.builder
+++ b/Makefile.builder
@@ -1,2 +1,7 @@
-MIRAGE_KERNEL_NAME = qubes_firewall.xen
-OCAML_VERSION ?= 4.07.1
+MIRAGE_KERNEL_NAME = dist/qubes-firewall.xen
+OCAML_VERSION ?= 4.14.2
+SOURCE_BUILD_DEP := firewall-build-dep
+
+firewall-build-dep:
+ opam install -y mirage
+
diff --git a/Makefile.user b/Makefile.user
index da810cd..7188982 100644
--- a/Makefile.user
+++ b/Makefile.user
@@ -1,7 +1,10 @@
-tar: build
- rm -rf _build/mirage-firewall
- mkdir _build/mirage-firewall
- cp qubes_firewall.xen _build/mirage-firewall/vmlinuz
- touch _build/mirage-firewall/modules.img
- cat /dev/null | gzip -n > _build/mirage-firewall/initramfs
- tar cjf mirage-firewall.tar.bz2 -C _build --mtime=./build-with-docker.sh mirage-firewall
+unikernel: build
+ cp dist/qubes-firewall.xen dist/qubes-firewall.xen.debug
+ strip dist/qubes-firewall.xen
+ cp dist/qubes-firewall.xen .
+ sha256sum qubes-firewall.xen
+
+fetchmotron: qubes_firewall.xen
+ test-mirage qubes_firewall.xen mirage-fw-test &
+ sleep 1
+ boot-mirage fetchmotron
diff --git a/README.md b/README.md
index 960e568..ce64ba6 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,6 @@
A unikernel that can run as a QubesOS ProxyVM, replacing `sys-firewall`.
It uses the [mirage-qubes][] library to implement the Qubes protocols.
-Note: This firewall *ignores the rules set in the Qubes GUI*. See `rules.ml` for the actual policy.
-
See [A Unikernel Firewall for QubesOS][] for more details.
@@ -15,62 +13,86 @@ See the [Deploy](#deploy) section below for installation instructions.
## Build from source
-Create a new Fedora-29 AppVM (or reuse an existing one). Open a terminal.
-Clone this Git repository and run the `build-with-docker.sh` script:
+Note: The most reliable way to build is using Docker or Podman.
+Fedora 38 works well for this, Debian 12 also works, but you'll need to follow the instructions at [docker.com][debian-docker] to get Docker
+(don't use Debian's version).
- sudo ln -s /var/lib/docker /home/user/docker
+Create a new Fedora-38 AppVM (or reuse an existing one). In the Qube's Settings (Basic / Disk storage), increase the private storage max size from the default 2048 MiB to 8192 MiB. Open a terminal.
+
+Clone this Git repository and run the `build-with.sh` script with either `docker` or `podman` as argument (Note: The `chcon` call is mandatory on Fedora with new SELinux policies which do not allow to standardly keep the docker images in homedir):
+
+ mkdir /home/user/docker
+ sudo ln -s /home/user/docker /var/lib/docker
+ sudo chcon -Rt container_file_t /home/user/docker
sudo dnf install docker
sudo systemctl start docker
git clone https://github.com/mirage/qubes-mirage-firewall.git
cd qubes-mirage-firewall
- sudo ./build-with-docker.sh
+ sudo ./build-with.sh docker
-This took about 10 minutes on my laptop (it will be much quicker if you run it again).
-The symlink step at the start isn't needed if your build VM is standalone.
-It gives Docker more disk space and avoids losing the Docker image cache when you reboot the Qube.
+Or
+
+ sudo systemctl start podman
+ git clone https://github.com/mirage/qubes-mirage-firewall.git
+ cd qubes-mirage-firewall
+ ./build-with.sh podman
+
+This took about 15 minutes on my laptop (it will be much quicker if you run it again).
+The symlink step at the start isn't needed if your build VM is standalone. It gives Docker more disk space and avoids losing the Docker image cache when you reboot the Qube.
+It's not needed with Podman as the containers lives in your home directory by default.
Note: the object files are stored in the `_build` directory to speed up incremental builds.
If you change the dependencies, you will need to delete this directory before rebuilding.
-If you want to build on Debian, follow the instructions at [docker.com][debian-docker] to get Docker and then run `sudo ./build-with-docker.sh` as above.
-
-It's OK to install the Docker package in a template VM if you want it to remain
+It's OK to install the Docker or Podman package in a template VM if you want it to remain
after a reboot, but the build of the firewall itself should be done in a regular AppVM.
-You can also build without Docker, as for any normal Mirage unikernel;
-see [the Mirage installation instructions](https://mirage.io/wiki/install) for details.
+You can also build without that script, as for any normal Mirage unikernel;
+see [the Mirage installation instructions](https://mirageos.org/wiki/install) for details.
-The Docker build 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 Docker, it will build
+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
against the latest versions instead (and the hash will therefore probably not match).
However, it should still work fine.
## Deploy
-If you want to deploy manually, unpack `mirage-firewall.tar.bz2` in dom0, inside `/var/lib/qubes/vm-kernels/`. e.g. (if `dev` is the AppVM where you built it):
+### Manual deployment
+If you want to deploy manually, you just need to download `qubes-firewall.xen` and
+`qubes-firewall.sha256` in domU and check that the `.xen` file has a corresponding
+hashsum. `qubes-firewall.xen` is the unikernel itself and should be copied to
+`vmlinuz` in the `/var/lib/qubes/vm-kernels/mirage-firewall` directory in dom0, e.g.
+(if `dev` is the AppVM where you built it):
- [tal@dom0 ~]$ cd /var/lib/qubes/vm-kernels/
- [tal@dom0 vm-kernels]$ qvm-run -p dev 'cat qubes-mirage-firewall/mirage-firewall.tar.bz2' | tar xjf -
+ [tal@dom0 ~]$ mkdir -p /var/lib/qubes/vm-kernels/mirage-firewall/
+ [tal@dom0 ~]$ cd /var/lib/qubes/vm-kernels/mirage-firewall/
+ [tal@dom0 mirage-firewall]$ qvm-run -p dev 'cat mirage-firewall/qubes-firewall.xen' > vmlinuz
-The tarball contains `vmlinuz`, which is the unikernel itself, plus a couple of dummy files that Qubes requires.
-
-Run this command in dom0 to create a `mirage-firewall` VM using the `mirage-firewall` kernel you added above:
+Run this command in dom0 to create a `mirage-firewall` VM using the `mirage-firewall` kernel you added above
```
qvm-create \
--property kernel=mirage-firewall \
- --property kernelopts=None \
+ --property kernelopts='' \
--property memory=32 \
--property maxmem=32 \
--property netvm=sys-net \
--property provides_network=True \
--property vcpus=1 \
- --property virt_mode=pv \
+ --property virt_mode=pvh \
--label=green \
--class StandaloneVM \
mirage-firewall
+
+qvm-features mirage-firewall qubes-firewall 1
+qvm-features mirage-firewall no-default-kernelopts 1
```
+### Deployment using saltstack
+If you're familiar how to run salt states in Qubes, you can also use the script `SaltScriptToDownloadAndInstallMirageFirewallInQubes.sls` to automatically deploy the latest version of mirage firewall in your Qubes OS. An introduction can be found [here](https://forum.qubes-os.org/t/qubes-salt-beginners-guide/20126) and [here](https://www.qubes-os.org/doc/salt/). Following the instructions from the former link, you can run the script in dom0 with the command `sudo qubesctl --show-output state.apply SaltScriptToDownloadAndInstallMirageFirewallInQubes saltenv=user`. The script checks the checksum from the integration server and compares with the latest version provided in the github releases. It might be necessary to adjust the VM templates in the script which are used for downloading of the mirage unikernel, if your default templates do not have the tools `curl` and `tar` installed by default. Also don't forget to change the VMs in which the uni kernel should be used or adjust the "Qubes Global Settings".
+
+## Upgrading
+
To upgrade from an earlier release, just overwrite `/var/lib/qubes/vm-kernels/mirage-firewall/vmlinuz` with the new version and restart the firewall VM.
### Configure AppVMs to use it
@@ -86,6 +108,25 @@ qvm-prefs --set my-app-vm netvm mirage-firewall
Alternatively, you can configure `mirage-firewall` to be your default firewall VM.
+Note that by default dom0 uses sys-firewall as its "UpdateVM" (a proxy for downloading updates).
+mirage-firewall cannot be used for this, but any Linux VM should be fine.
+https://www.qubes-os.org/doc/software-update-dom0/ says:
+
+> The role of UpdateVM can be assigned to any VM in the Qubes VM Manager, and
+> there are no significant security implications in this choice. By default,
+> this role is assigned to the firewallvm.
+
+### Configure firewall with OpenBSD-like netvm
+
+OpenBSD is currently unable to be used as netvm, so if you want to use a BSD as your sys-net VM, you'll need to set its netvm to qubes-mirage-firewall (see https://github.com/mirage/qubes-mirage-firewall/issues/146 for more information).
+That means you'll have `AppVMs -> qubes-mirage-firewall <- OpenBSD` with the arrow standing for the netvm property setting.
+
+In that case you'll have to tell qubes-mirage-firewall which AppVM client should be used as uplink:
+```
+qvm-prefs --set mirage-firewall -- kernelopts '--ipv4=X.X.X.X --ipv4-gw=Y.Y.Y.Y'
+```
+with `X.X.X.X` the IP address for mirage-firewall and `Y.Y.Y.Y` the IP address of your OpenBSD HVM.
+
### Components
This diagram show the main components (each box corresponds to a source `.ml` file with the same name):
@@ -95,7 +136,7 @@ This diagram show the main components (each box corresponds to a source `.ml` fi
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.
+Internet (IP) packets are sent to `firewall`, which consults the NAT table and the rules from QubesDB 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.
@@ -111,48 +152,54 @@ The boot process:
### Easy deployment for developers
-For development, use the [test-mirage][] scripts to deploy the unikernel (`qubes_firewall.xen`) from your development AppVM.
+For development, use the [test-mirage][] scripts to deploy the unikernel (`qubes-firewall.xen`) from your development AppVM.
This takes a little more setting up the first time, but will be much quicker after that. e.g.
- $ test-mirage qubes_firewall.xen mirage-firewall
+ [user@dev ~]$ test-mirage dist/qubes-firewall.xen mirage-firewall
Waiting for 'Ready'... OK
- Uploading 'qubes_firewall.xen' (5901080 bytes) to "mirage-firewall"
+ Uploading 'dist/qubes-firewall.xen' (7454880 bytes) to "mirage-test"
Waiting for 'Booting'... OK
- --> Loading the VM (type = ProxyVM)...
- --> Starting Qubes DB...
- --> Setting Qubes DB info for the VM...
- --> Updating firewall rules...
- --> Starting the VM...
- --> Starting the qrexec daemon...
- Waiting for VM's qrexec agent.connected
- --> Starting Qubes GUId...
- Connecting to VM's GUI agent: .connected
- --> Sending monitor layout...
- --> Waiting for qubes-session...
- Connecting to mirage-firewall console...
- MirageOS booting...
- Initialising timer interface
- Initialising console ... done.
- gnttab_stubs.c: initialised mini-os gntmap
- 2017-03-18 11:32:37 -00:00: INF [qubes.rexec] waiting for client...
- 2017-03-18 11:32:37 -00:00: INF [qubes.gui] waiting for client...
- 2017-03-18 11:32:37 -00:00: INF [qubes.db] connecting to server...
- 2017-03-18 11:32:37 -00:00: INF [qubes.db] connected
- 2017-03-18 11:32:37 -00:00: INF [qubes.rexec] client connected, using protocol version 2
- 2017-03-18 11:32:37 -00:00: INF [qubes.db] got update: "/qubes-keyboard" = "xkb_keymap {\n\txkb_keycodes { include \"evdev+aliases(qwerty)\"\t};\n\txkb_types { include \"complete\"\t};\n\txkb_compat { include \"complete\"\t};\n\txkb_symbols { include \"pc+gb+inet(evdev)\"\t};\n\txkb_geometry { include \"pc(pc105)\"\t};\n};"
- 2017-03-18 11:32:37 -00:00: INF [qubes.gui] client connected (screen size: 6720x2160)
- 2017-03-18 11:32:37 -00:00: INF [unikernel] Qubes agents connected in 0.095 s (CPU time used since boot: 0.008 s)
- 2017-03-18 11:32:37 -00:00: INF [net-xen:frontend] connect 0
- 2017-03-18 11:32:37 -00:00: INF [memory_pressure] Writing meminfo: free 6584 / 17504 kB (37.61 %)
- Note: cannot write Xen 'control' directory
- 2017-03-18 11:32:37 -00:00: INF [net-xen:frontend] create: id=0 domid=1
- 2017-03-18 11:32:37 -00:00: INF [net-xen:frontend] sg:true gso_tcpv4:true rx_copy:true rx_flip:false smart_poll:false
- 2017-03-18 11:32:37 -00:00: INF [net-xen:frontend] MAC: 00:16:3e:5e:6c:11
- 2017-03-18 11:32:37 -00:00: WRN [command] << Unknown command "QUBESRPC qubes.SetMonitorLayout dom0"
- 2017-03-18 11:32:38 -00:00: INF [ethif] Connected Ethernet interface 00:16:3e:5e:6c:11
- 2017-03-18 11:32:38 -00:00: INF [arpv4] Connected arpv4 device on 00:16:3e:5e:6c:11
- 2017-03-18 11:32:38 -00:00: INF [dao] Watching backend/vif
- 2017-03-18 11:32:38 -00:00: INF [qubes.db] got update: "/qubes-netvm-domid" = "1"
+ Connecting to mirage-test console...
+ Solo5: Xen console: port 0x2, ring @0x00000000FEFFF000
+ | ___|
+ __| _ \ | _ \ __ \
+ \__ \ ( | | ( | ) |
+ ____/\___/ _|\___/____/
+ Solo5: Bindings version v0.7.3
+ Solo5: Memory map: 32 MB addressable:
+ Solo5: reserved @ (0x0 - 0xfffff)
+ Solo5: text @ (0x100000 - 0x319fff)
+ Solo5: rodata @ (0x31a000 - 0x384fff)
+ Solo5: data @ (0x385000 - 0x53ffff)
+ Solo5: heap >= 0x540000 < stack < 0x2000000
+ 2022-08-13 14:55:38 -00:00: INF [qubes.rexec] waiting for client...
+ 2022-08-13 14:55:38 -00:00: INF [qubes.db] connecting to server...
+ 2022-08-13 14:55:38 -00:00: INF [qubes.db] connected
+ 2022-08-13 14:55:38 -00:00: INF [qubes.db] got update: "/mapped-ip/10.137.0.20/visible-ip" = "10.137.0.20"
+ 2022-08-13 14:55:38 -00:00: INF [qubes.db] got update: "/mapped-ip/10.137.0.20/visible-gateway" = "10.137.0.23"
+ 2022-08-13 14:55:38 -00:00: INF [qubes.rexec] client connected, using protocol version 3
+ 2022-08-13 14:55:38 -00:00: INF [unikernel] QubesDB and qrexec agents connected in 0.041 s
+ 2022-08-13 14:55:38 -00:00: INF [dao] Got network configuration from QubesDB:
+ NetVM IP on uplink network: 10.137.0.4
+ Our IP on uplink network: 10.137.0.23
+ Our IP on client networks: 10.137.0.23
+ DNS resolver: 10.139.1.1
+ DNS secondary resolver: 10.139.1.2
+ 2022-08-13 14:55:38 -00:00: INF [net-xen frontend] connect 0
+ 2022-08-13 14:55:38 -00:00: INF [net-xen frontend] create: id=0 domid=1
+ 2022-08-13 14:55:38 -00:00: INF [net-xen frontend] sg:true gso_tcpv4:true rx_copy:true rx_flip:false smart_poll:false
+ 2022-08-13 14:55:38 -00:00: INF [net-xen frontend] MAC: 00:16:3e:5e:6c:00
+ 2022-08-13 14:55:38 -00:00: INF [ethernet] Connected Ethernet interface 00:16:3e:5e:6c:00
+ 2022-08-13 14:55:38 -00:00: INF [ARP] Sending gratuitous ARP for 10.137.0.23 (00:16:3e:5e:6c:00)
+ 2022-08-13 14:55:38 -00:00: INF [ARP] Sending gratuitous ARP for 10.137.0.23 (00:16:3e:5e:6c:00)
+ 2022-08-13 14:55:38 -00:00: INF [udp] UDP layer connected on 10.137.0.23
+ 2022-08-13 14:55:38 -00:00: INF [dao] Watching backend/vif
+ 2022-08-13 14:55:38 -00:00: INF [memory_pressure] Writing meminfo: free 20MiB / 27MiB (72.68 %)
+
+# Testing if the firewall works
+
+A unikernel which tests the firewall is available in the `test/` subdirectory.
+To use it, run `test.sh` and follow the instructions to set up the test environment.
# Security advisories
@@ -160,17 +207,7 @@ See [issues tagged "security"](https://github.com/mirage/qubes-mirage-firewall/i
# LICENSE
-Copyright (c) 2019, Thomas Leonard
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
-
-1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
-
-2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-gg
+See [LICENSE.md](https://github.com/mirage/qubes-mirage-firewall/blob/main/LICENSE.md)
[test-mirage]: https://github.com/talex5/qubes-test-mirage
[mirage-qubes]: https://github.com/mirage/mirage-qubes
diff --git a/SaltScriptToDownloadAndInstallMirageFirewallInQubes.sls b/SaltScriptToDownloadAndInstallMirageFirewallInQubes.sls
new file mode 100644
index 0000000..f9886b9
--- /dev/null
+++ b/SaltScriptToDownloadAndInstallMirageFirewallInQubes.sls
@@ -0,0 +1,104 @@
+# How to install the superlight mirage-firewall for Qubes OS by using saltstack
+# Tested on Qubes v4.1 and mirage v0.8.5
+# After the install, you have to switch your AppVMs to use the mirage firewall vm created by this script e.g. by using "Qubes Global Settings"
+# inspired by: https://github.com/one7two99/my-qubes/tree/master/mirage-firewall
+
+# default template + dispvm template are used. Possible optimization is to use min-dvms
+{% set DownloadVMTemplate = salt['cmd.shell']("qubes-prefs default_template") %}
+{% set DispVM = salt['cmd.shell']("qubes-prefs default_dispvm") %}
+
+{% set DownloadVM = "DownloadVmMirage" %}
+{% set MirageFW = "sys-mirage-fw" %}
+{% set GithubUrl = "https://github.com/mirage/qubes-mirage-firewall" %}
+{% set Kernel = "qubes-firewall.xen" %}
+{% set Shasum = "qubes-firewall-release.sha256" %}
+{% set MirageInstallDir = "/var/lib/qubes/vm-kernels/mirage-firewall" %}
+
+#download and install the latest version
+{% set Release = salt['cmd.shell']("qvm-run --dispvm " ~ DispVM ~ " --pass-io \"curl --silent --location -o /dev/null -w %{url_effective} " ~ GithubUrl ~ "/releases/latest | rev | cut -d \"/\" -f 1 | rev\"") %}
+
+{% if Release != salt['cmd.shell']("test -e " ~ MirageInstallDir ~ "/version.txt" ~ " || mkdir " ~ MirageInstallDir ~ " ; touch " ~ MirageInstallDir ~ "/version.txt" ~ " ; cat " ~ MirageInstallDir ~ "/version.txt") %}
+
+create-downloader-VM:
+ qvm.vm:
+ - name: {{ DownloadVM }}
+ - present:
+ - template: {{ DownloadVMTemplate }}
+ - label: red
+ - prefs:
+ - template: {{ DownloadVMTemplate }}
+ - include-in-backups: false
+
+{% set DownloadBinary = GithubUrl ~ "/releases/download/" ~ Release ~ "/" ~ Kernel %}
+{% set DownloadShasum = GithubUrl ~ "/releases/download/" ~ Release ~ "/" ~ Shasum %}
+
+download-and-unpack-in-DownloadVM4mirage:
+ cmd.run:
+ - names:
+ - qvm-run --pass-io {{ DownloadVM }} {{ "curl -L -O " ~ DownloadBinary }}
+ - qvm-run --pass-io {{ DownloadVM }} {{ "curl -L -O " ~ DownloadShasum }}
+ - require:
+ - create-downloader-VM
+
+
+check-checksum-in-DownloadVM:
+ cmd.run:
+ - names:
+ - qvm-run --pass-io {{ DownloadVM }} {{ "\"echo \\\"Checksum of release on github:\\\";cat " ~ Shasum ~ " | cut -d\' \' -f1\"" }}
+ - qvm-run --pass-io {{ DownloadVM }} {{ "\"echo \\\"Checksum of downloaded local file:\\\";sha256sum " ~ Kernel ~ " | cut -d\' \' -f1\"" }}
+ - qvm-run --pass-io {{ DownloadVM }} {{ "\"diff <(cat " ~ Shasum ~ " | cut -d\' \' -f1) <(sha256sum " ~ Kernel ~ " | cut -d\' \' -f1) && echo \\\"Checksums DO match.\\\" || (echo \\\"Checksums do NOT match.\\\";exit 101)\"" }}
+ - require:
+ - download-and-unpack-in-DownloadVM4mirage
+
+copy-mirage-kernel-to-dom0:
+ cmd.run:
+ - name: mkdir -p {{ MirageInstallDir }}; qvm-run --pass-io --no-gui {{ DownloadVM }} {{ "cat " ~ Kernel }} > {{ MirageInstallDir ~ "/vmlinuz" }}
+ - require:
+ - download-and-unpack-in-DownloadVM4mirage
+ - check-checksum-in-DownloadVM
+
+update-version:
+ cmd.run:
+ - names:
+ - echo {{ Release }} > {{ MirageInstallDir ~ "/version.txt" }}
+ - require:
+ - copy-mirage-kernel-to-dom0
+
+create-sys-mirage-fw:
+ qvm.vm:
+ - name: {{ MirageFW }}
+ - present:
+ - class: StandaloneVM
+ - label: black
+ - prefs:
+ - kernel: mirage-firewall
+ - kernelopts:
+ - include-in-backups: False
+ - memory: 32
+ - maxmem: 32
+ - netvm: sys-net
+ - provides-network: True
+ - vcpus: 1
+ - virt-mode: pvh
+ - features:
+ - enable:
+ - qubes-firewall
+ - no-default-kernelopts
+ - require:
+ - copy-mirage-kernel-to-dom0
+
+
+cleanup-in-DownloadVM:
+ cmd.run:
+ - names:
+ - qvm-run -a --pass-io --no-gui {{ DownloadVM }} "{{ "rm " ~ Kernel ~ " " ~ Shasum }}"
+ - require:
+ - update-version
+
+remove-DownloadVM4mirage:
+ qvm.absent:
+ - name: {{ DownloadVM }}
+ - require:
+ - cleanup-in-DownloadVM
+
+{% endif %}
diff --git a/_tags b/_tags
deleted file mode 100644
index 7441bd2..0000000
--- a/_tags
+++ /dev/null
@@ -1,2 +0,0 @@
-not : warn(A-4), strict_sequence
-: package(cstruct.syntax)
diff --git a/build-with-docker.sh b/build-with-docker.sh
deleted file mode 100755
index 701c686..0000000
--- a/build-with-docker.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/sh
-set -eu
-echo Building Docker image with dependencies..
-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: 5ee982b12fb3964e7d9e32ca74ce377ec068b3bbef2b6c86c131f8bb422a3134"
-echo "(hashes should match for released versions)"
diff --git a/build-with.sh b/build-with.sh
new file mode 100755
index 0000000..728ab1f
--- /dev/null
+++ b/build-with.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+set -eu
+
+if [[ $# -ne 1 ]] ; then
+ echo "Usage: build-with.sh { docker | podman }"
+ exit 1
+fi
+
+builder=$1
+case $builder in
+ docker|podman)
+ ;;
+ *)
+ echo "You should use either docker or podman for building"
+ exit 2
+esac
+
+echo Building $builder image with dependencies..
+$builder build -t qubes-mirage-firewall .
+echo Building Firewall...
+$builder run --rm -i -v `pwd`:/tmp/orb-build:Z qubes-mirage-firewall
+echo "SHA2 of build: $(sha256sum ./dist/qubes-firewall.xen | cut -d' ' -f1)"
+echo "SHA2 current head: $(cat qubes-firewall.sha256 | cut -d' ' -f1)"
+echo "SHA2 last release: $(cat qubes-firewall-release.sha256 | cut -d' ' -f1)"
+echo "(hashes should match for head versions)"
diff --git a/cleanup.ml b/cleanup.ml
index cbe9ebc..ecd3c78 100644
--- a/cleanup.ml
+++ b/cleanup.ml
@@ -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
diff --git a/cleanup.mli b/cleanup.mli
index d43661b..1358c07 100644
--- a/cleanup.mli
+++ b/cleanup.mli
@@ -1,8 +1,8 @@
(* Copyright (C) 2015, Thomas Leonard
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
diff --git a/client_eth.ml b/client_eth.ml
index 3aa3a8a..bd9d931 100644
--- a/client_eth.ml
+++ b/client_eth.ml
@@ -4,109 +4,117 @@
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 IpMap.t;
- changed : unit Lwt_condition.t; (* Fires when [iface_of_ip] changes. *)
- client_gw : Ipaddr.V4.t; (* The IP that clients are given as their default gateway. *)
+ 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. *)
}
-type host =
- [ `Client of client_link
- | `Firewall
- | `External of Ipaddr.t ]
+type host = [ `Client of client_link | `Firewall | `External of Ipaddr.t ]
-let create ~client_gw =
+let create config =
let changed = Lwt_condition.create () in
- { iface_of_ip = IpMap.empty; client_gw; changed }
+ let my_ip = config.Dao.our_ip in
+ Lwt.return { iface_of_ip = Ipaddr.V4.Map.empty; my_ip; changed }
-let client_gw t = t.client_gw
+let client_gw t = t.my_ip
let add_client t iface =
let ip = iface#other_ip in
let rec aux () =
- match IpMap.find ip t.iface_of_ip with
+ match Ipaddr.V4.Map.find_opt ip t.iface_of_ip with
| Some old ->
- (* Wait for old client to disappear before adding one with the same IP address.
+ (* 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);
- Lwt_condition.wait t.changed >>= aux
+ 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 |> IpMap.add ip iface;
- Lwt_condition.broadcast t.changed ();
- Lwt.return_unit
+ t.iface_of_ip <- t.iface_of_ip |> Ipaddr.V4.Map.add ip iface;
+ Lwt_condition.broadcast t.changed ();
+ Lwt.return_unit
in
aux ()
let remove_client t iface =
let ip = iface#other_ip in
- assert (IpMap.mem ip t.iface_of_ip);
- t.iface_of_ip <- t.iface_of_ip |> IpMap.remove ip;
+ assert (Ipaddr.V4.Map.mem ip t.iface_of_ip);
+ t.iface_of_ip <- t.iface_of_ip |> Ipaddr.V4.Map.remove ip;
Lwt_condition.broadcast t.changed ()
-let lookup t ip = IpMap.find ip t.iface_of_ip
+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 ->
- if ip4 = t.client_gw then `Firewall
- else match lookup t ip4 with
- | Some client_link -> `Client client_link
- | None -> `External ip
+ | Ipaddr.V4 ip4 -> (
+ if ip4 = t.my_ip then `Firewall
+ else
+ match lookup t ip4 with
+ | Some client_link -> `Client client_link
+ | None -> `External ip)
let resolve t : host -> Ipaddr.t = function
| `Client client_link -> Ipaddr.V4 client_link#other_ip
- | `Firewall -> Ipaddr.V4 t.client_gw
+ | `Firewall -> Ipaddr.V4 t.my_ip
| `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.client_gw then Some t.client_link#my_mac
- else if (Ipaddr.V4.to_bytes 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
+ 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
(* We're now treating client networks as point-to-point links,
so we no longer respond on behalf of other clients. *)
- (*
- else match IpMap.find ip t.net.iface_of_ip with
+ (*
+ else match Ipaddr.V4.Map.find_opt ip t.net.iface_of_ip with
| Some client_iface -> Some client_iface#other_mac
| None -> None
*)
- let create ~net client_link = {net; client_link}
+ let create ~net client_link = { net; client_link }
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
+ 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;
- (* 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;
- source_ip = req_ipv4;
- source_mac = req_mac;
- }
+ Log.info (fun f -> pf f "responding with %a" Macaddr.pp req_mac);
+ 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;
+ source_ip = req_ipv4;
+ source_mac = req_mac;
+ }
let input_gratuitous t arp =
let source_ip = arp.Arp_packet.source_ip in
@@ -114,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
diff --git a/client_eth.mli b/client_eth.mli
index 2bbb672..d7ecb55 100644
--- a/client_eth.mli
+++ b/client_eth.mli
@@ -1,34 +1,32 @@
(* Copyright (C) 2016, Thomas Leonard
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 : client_gw:Ipaddr.V4.t -> 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. *)
+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.
+*)
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
diff --git a/client_net.ml b/client_net.ml
deleted file mode 100644
index 68fe6d3..0000000
--- a/client_net.ml
+++ /dev/null
@@ -1,136 +0,0 @@
-(* Copyright (C) 2015, Thomas Leonard
- See the README file for details. *)
-
-open Lwt.Infix
-open Fw_utils
-
-module Netback = Netchannel.Backend.Make(Netchannel.Xenstore.Make(OS.Xs))
-module ClientEth = Ethernet.Make(Netback)
-
-let src = Logs.Src.create "client_net" ~doc:"Client networking"
-module Log = (val Logs.src_log src : Logs.LOG)
-
-let writev eth dst proto fillfn =
- Lwt.catch
- (fun () ->
- ClientEth.write eth dst proto fillfn >|= function
- | Ok () -> ()
- | Error e ->
- Log.err (fun f -> 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 -> f "uncaught exception trying to send to client: @[%s@]"
- (Printexc.to_string ex));
- Lwt.return ()
- )
-
-class client_iface eth ~domid ~gateway_ip ~client_ip client_mac : client_link =
- let log_header = Fmt.strf "dom%d:%a" domid Ipaddr.V4.pp client_ip in
- object
- val queue = FrameQ.create (Ipaddr.V4.to_string client_ip)
- method my_mac = ClientEth.mac eth
- method other_mac = client_mac
- method my_ip = gateway_ip
- method other_ip = client_ip
- method writev proto fillfn =
- FrameQ.send queue (fun () ->
- writev eth client_mac proto fillfn
- )
- method log_header = log_header
- end
-
-let clients : Cleanup.t Dao.VifMap.t ref = ref Dao.VifMap.empty
-
-(** Handle an ARP message from the client. *)
-let input_arp ~fixed_arp ~iface request =
- match Arp_packet.decode request with
- | Error e ->
- Log.warn (fun f -> f "Ignored unknown ARP message: %a" Arp_packet.pp_error e);
- Lwt.return ()
- | Ok arp ->
- match Client_eth.ARP.input fixed_arp arp with
- | None -> return ()
- | Some response ->
- iface#writev `ARP (fun b -> Arp_packet.encode_into response b; Arp_packet.size)
-
-(** Handle an IPv4 packet from the client. *)
-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);
- Lwt.return ()
- | Ok packet ->
- let `IPv4 (ip, _) = packet in
- let src = ip.Ipv4_packet.src in
- 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 iface#other_ip);
- return ()
- )
-
-(** Connect to a new client's interface and listen for incoming frames. *)
-let add_vif { Dao.ClientVif.domid; device_id } ~client_ip ~router ~cleanup_tasks =
- Netback.make ~domid ~device_id >>= fun backend ->
- Log.info (fun f -> f "Client %d (IP: %s) ready" domid (Ipaddr.V4.to_string client_ip));
- ClientEth.connect backend >>= fun eth ->
- let client_mac = Netback.frontend_mac backend in
- let client_eth = router.Router.client_eth in
- let gateway_ip = Client_eth.client_gw client_eth in
- let iface = new client_iface eth ~domid ~gateway_ip ~client_ip client_mac in
- Router.add_client router iface >>= fun () ->
- Cleanup.on_cleanup cleanup_tasks (fun () -> Router.remove_client router iface);
- let fixed_arp = Client_eth.ARP.create ~net:client_eth iface in
- Netback.listen backend ~header_size:Ethernet_wire.sizeof_ethernet (fun frame ->
- match Ethernet_packet.Unmarshal.of_cstruct frame with
- | exception ex ->
- Log.err (fun f -> f "Error unmarshalling ethernet frame from client: %s@.%a" (Printexc.to_string ex)
- Cstruct.hexdump_pp frame
- );
- Lwt.return_unit
- | Error err -> Log.warn (fun f -> f "Invalid Ethernet frame: %s" err); return ()
- | Ok (eth, payload) ->
- match eth.Ethernet_packet.ethertype with
- | `ARP -> input_arp ~fixed_arp ~iface payload
- | `IPv4 -> input_ipv4 ~iface ~router payload
- | `IPv6 -> return () (* TODO: oh no! *)
- )
- >|= or_raise "Listen on client interface" Netback.pp_error
-
-(** A new client VM has been found in XenStore. Find its interface and connect to it. *)
-let add_client ~router vif client_ip =
- let cleanup_tasks = Cleanup.create () in
- Log.info (fun f -> f "add client vif %a with IP %a" Dao.ClientVif.pp vif Ipaddr.V4.pp client_ip);
- Lwt.async (fun () ->
- Lwt.catch (fun () ->
- add_vif vif ~client_ip ~router ~cleanup_tasks
- )
- (fun ex ->
- Log.warn (fun f -> f "Error with client %a: %s"
- Dao.ClientVif.pp vif (Printexc.to_string ex));
- return ()
- )
- );
- cleanup_tasks
-
-(** Watch XenStore for notifications of new clients. *)
-let listen router =
- Dao.watch_clients (fun new_set ->
- (* Check for removed clients *)
- !clients |> Dao.VifMap.iter (fun key cleanup ->
- 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
- )
- );
- (* Check for added clients *)
- new_set |> Dao.VifMap.iter (fun key ip_addr ->
- if not (Dao.VifMap.mem key !clients) then (
- let cleanup = add_client ~router key ip_addr in
- clients := !clients |> Dao.VifMap.add key cleanup
- )
- )
- )
diff --git a/client_net.mli b/client_net.mli
deleted file mode 100644
index 7bc2660..0000000
--- a/client_net.mli
+++ /dev/null
@@ -1,10 +0,0 @@
-(* Copyright (C) 2015, Thomas Leonard
- See the README file for details. *)
-
-(** Handling client VMs. *)
-
-val listen : Router.t -> 'a Lwt.t
-(** [listen router] is a thread that watches for clients being added to and
- removed from XenStore. Clients are connected to the client network and
- packets are sent via [router]. We ensure the source IP address is correct
- before routing a packet. *)
diff --git a/command.ml b/command.ml
index da70727..0661bfc 100644
--- a/command.ml
+++ b/command.ml
@@ -4,24 +4,30 @@
(** 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 ->
- Log.warn (fun f -> f "<< %s" s);
- Flow.ewritef flow "%s [while processing %S]" s cmd >|= fun () -> 1 in
+ fmt
+ |> Printf.ksprintf @@ fun s ->
+ Log.warn (fun f -> f "<< %s" s);
+ 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! *)
+ | "QUBESRPC qubes.WaitForSession none" -> return 0 (* Always ready! *)
| cmd -> error "Unknown command %S" cmd
diff --git a/config.ml b/config.ml
index 4171927..b663813 100644
--- a/config.ml
+++ b/config.ml
@@ -1,3 +1,4 @@
+(* mirage >= 4.9.0 & < 4.10.0 *)
(* Copyright (C) 2017, Thomas Leonard
See the README file for details. *)
@@ -5,37 +6,25 @@
open Mirage
-let table_size =
- let open Functoria_key in
- let info = Arg.info
- ~doc:"The number of NAT entries to allocate."
- ~docv:"ENTRIES" ["nat-table-size"]
- in
- let key = Arg.opt ~stage:`Both Arg.int 5_000 info in
- create "nat_table_size" key
-
let main =
- foreign
- ~keys:[Functoria_key.abstract table_size]
- ~packages:[
- package "vchan" ~min:"4.0.2";
- package "cstruct";
- package "astring";
- package "tcpip" ~min:"3.7.0";
- package "arp";
- package "arp-mirage";
- package "ethernet";
- package "mirage-protocols";
- package "shared-memory-ring" ~min:"3.0.0";
- package "netchannel" ~min:"1.11.0" ~pin:"git+https://github.com/mirage/mirage-net-xen.git";
- package "mirage-net-xen";
- package "ipaddr" ~min:"3.0.0";
- package "mirage-qubes";
- package "mirage-nat" ~min:"1.2.0";
- package "mirage-logs";
- ]
- "Unikernel.Main" (mclock @-> job)
+ main
+ ~packages:
+ [
+ package "vchan" ~min:"4.0.2";
+ package "cstruct";
+ package "tcpip" ~min:"3.7.0";
+ package ~min:"2.3.0" ~sublibs:[ "mirage" ] "arp";
+ package ~min:"3.0.0" "ethernet";
+ package "shared-memory-ring" ~min:"3.0.0";
+ package "mirage-net-xen" ~min:"2.1.4";
+ package "ipaddr" ~min:"5.2.0";
+ package "mirage-qubes" ~min:"0.9.1";
+ package ~min:"3.0.1" "mirage-nat";
+ package "mirage-logs";
+ package "mirage-xen" ~min:"8.0.0";
+ package ~min:"6.4.0" "dns-client";
+ package "pf-qubes";
+ ]
+ "Unikernel" job
-let () =
- register "qubes-firewall" [main $ default_monotonic_clock]
- ~argv:no_argv
+let () = register "qubes-firewall" [ main ]
diff --git a/dao.ml b/dao.ml
index a68cc64..9219fa6 100644
--- a/dao.ml
+++ b/dao.ml
@@ -3,114 +3,179 @@
open Lwt.Infix
open Qubes
-open Fw_utils
-open Astring
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)
+ 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 =
- OS.Xs.directory handle dir >|= function
- | [""] -> [] (* XenStore client bug *)
+ Xen_os.Xs.directory handle dir >|= function
+ | [ "" ] -> [] (* XenStore client bug *)
| items -> items
-let vifs ~handle domid =
- match String.to_int domid with
- | None -> Log.err (fun f -> f "Invalid domid %S" domid); Lwt.return []
+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
+ let rec get_rule n l : (Pf_qubes.Parse_qubes.rule list, string) result =
+ let pattern = root ^ "/" ^ Printf.sprintf "%04d" n in
+ Log.debug (fun f -> f "reading %s" pattern);
+ match Qubes.DB.KeyMap.find_opt pattern rules with
+ | None ->
+ Log.debug (fun f -> f "rule %d does not exist; won't look for more" n);
+ Ok (List.rev l)
+ | 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
+ | Ok rule ->
+ 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;
+ proto = None;
+ specialtarget = None;
+ dst = `any;
+ dstports = None;
+ icmp_type = None;
+ 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 []
| Some domid ->
- let path = Printf.sprintf "backend/vif/%d" domid in
- directory ~handle path >>=
- Lwt_list.filter_map_p (fun device_id ->
- match String.to_int device_id with
- | None -> Log.err (fun f -> f "Invalid device ID %S for domid %d" device_id domid); Lwt.return_none
- | Some device_id ->
- let vif = { ClientVif.domid; device_id } in
- Lwt.try_bind
- (fun () -> OS.Xs.read handle (Printf.sprintf "%s/%d/ip" path device_id))
- (fun client_ip ->
- let client_ip = Ipaddr.V4.of_string_exn client_ip in
- Lwt.return (Some (vif, client_ip))
- )
- (function
- | Xs_protocol.Enoent _ -> Lwt.return None
- | ex ->
- Log.err (fun f -> f "Error getting IP address of %a: %s"
- ClientVif.pp vif (Printexc.to_string ex));
- Lwt.return None
- )
- )
+ 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
+ | None ->
+ Log.err (fun f ->
+ f "Invalid device ID %S for domid %d" device_id domid);
+ Lwt.return_none
+ | 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 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. *)
+ Lwt.return_some (vif, Ipaddr.V4.of_string_exn client_ip)
+ in
+ 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"
+ 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)
+ in
+ Lwt_list.filter_map_p ip_of_vif devices
+ in
+ Xen_os.Xs.immediate client vifs_of_domain
let watch_clients fn =
- OS.Xs.make () >>= fun xs ->
+ Xen_os.Xs.make () >>= fun xs ->
let backend_vifs = "backend/vif" in
Log.info (fun f -> f "Watching %s" backend_vifs);
- OS.Xs.wait xs (fun handle ->
- begin Lwt.catch
- (fun () -> directory ~handle backend_vifs)
- (function
- | Xs_protocol.Enoent _ -> return []
- | ex -> fail ex)
- end >>= fun items ->
- Lwt_list.map_p (vifs ~handle) items >>= fun items ->
- fn (List.concat items |> VifMap.of_list);
- (* Wait for further updates *)
- fail Xs_protocol.Eagain
- )
+ Xen_os.Xs.wait xs (fun handle ->
+ Lwt.catch
+ (fun () -> directory ~handle backend_vifs)
+ (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)
type network_config = {
- uplink_netvm_ip : Ipaddr.V4.t; (* The IP address of NetVM (our gateway) *)
- uplink_our_ip : Ipaddr.V4.t; (* The IP address of our interface to NetVM *)
-
- clients_our_ip : Ipaddr.V4.t; (* The IP address of our interface to our client VMs (their gateway) *)
+ 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;
+ dns2 : Ipaddr.V4.t;
}
exception Missing_key of string
-(* TODO: /qubes-secondary-dns *)
let try_read_network_config db =
let get name =
match DB.KeyMap.find_opt name db with
| None -> raise (Missing_key name)
- | Some value -> value in
- let uplink_our_ip = get "/qubes-ip" |> Ipaddr.V4.of_string_exn in
- let uplink_netvm_ip = get "/qubes-gateway" |> Ipaddr.V4.of_string_exn in
- let clients_our_ip = get "/qubes-netvm-gateway" |> Ipaddr.V4.of_string_exn in
- Log.info (fun f -> f "@[Got network configuration from QubesDB:@,\
- NetVM IP on uplink network: %a@,\
- Our IP on uplink network: %a@,\
- Our IP on client networks: %a@]"
- Ipaddr.V4.pp uplink_netvm_ip
- Ipaddr.V4.pp uplink_our_ip
- Ipaddr.V4.pp clients_our_ip);
- { uplink_netvm_ip; uplink_our_ip; clients_our_ip }
+ | 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 }
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
+ "@[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)
+
let set_iptables_error db = Qubes.DB.write db "/qubes-iptables-error"
diff --git a/dao.mli b/dao.mli
index b1f56b6..85f8912 100644
--- a/dao.mli
+++ b/dao.mli
@@ -4,30 +4,43 @@
(** 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) -> 'a Lwt.t
-(** [watch_clients fn] calls [fn clients] with the list of backend clients
- in XenStore, and again each time XenStore updates. *)
+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. *)
type network_config = {
- uplink_netvm_ip : Ipaddr.V4.t; (* The IP address of NetVM (our gateway) *)
- uplink_our_ip : Ipaddr.V4.t; (* The IP address of our interface to NetVM *)
-
- clients_our_ip : Ipaddr.V4.t; (* The IP address of our interface to our client VMs (their gateway) *)
+ 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;
+ dns2 : Ipaddr.V4.t;
}
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. *)
+
+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
diff --git a/diagrams/components.svg b/diagrams/components.svg
index 1e996b1..2d69f9d 100644
--- a/diagrams/components.svg
+++ b/diagrams/components.svg
@@ -1,149 +1,199 @@
-