mirror of
https://github.com/mirage/qubes-mirage-firewall.git
synced 2025-04-19 14:55:47 -04:00
Compare commits
374 Commits
flambda-te
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5257071810 | ||
![]() |
64d2b16c3a | ||
![]() |
0398036a14 | ||
![]() |
4d89b85892 | ||
![]() |
511ac0adfb | ||
![]() |
17941c7fbc | ||
![]() |
edba36b97b | ||
![]() |
4de45e2f67 | ||
![]() |
bc3fdaf3d5 | ||
![]() |
3138ef53ee | ||
![]() |
85c8b7a661 | ||
![]() |
a756effb14 | ||
![]() |
5d515c360d | ||
![]() |
592f53777e | ||
![]() |
56a823ab5e | ||
![]() |
5f5fe82b9b | ||
![]() |
f2fcae93d2 | ||
![]() |
cf181026a8 | ||
![]() |
2b2ac42ebc | ||
![]() |
d8871f68c0 | ||
![]() |
b3bc2afc58 | ||
![]() |
32394c79e1 | ||
![]() |
ecb043e669 | ||
![]() |
6d0cc1cf9d | ||
![]() |
812b99842f | ||
![]() |
85de608392 | ||
![]() |
763a3de57a | ||
![]() |
3bc01998a6 | ||
![]() |
d8a20eadc8 | ||
![]() |
a5d61cb034 | ||
![]() |
923719f306 | ||
![]() |
86ee78d301 | ||
![]() |
9fe27016ab | ||
![]() |
8817893c62 | ||
![]() |
64b45e8be6 | ||
![]() |
07f05f1408 | ||
![]() |
4936081112 | ||
![]() |
54a964e446 | ||
![]() |
e7eb1f2e3b | ||
![]() |
887f2d524c | ||
![]() |
de9a6ccc86 | ||
![]() |
c738753045 | ||
![]() |
fc75cce37c | ||
![]() |
74e39a6aa7 | ||
![]() |
56e66ca39a | ||
![]() |
e4e3e1ca36 | ||
![]() |
1406855a9e | ||
![]() |
3bb13f4c21 | ||
![]() |
e2a0b33352 | ||
![]() |
ceb712ec60 | ||
![]() |
9156d580df | ||
![]() |
12ed2b268d | ||
![]() |
a7cb153ee1 | ||
![]() |
3dc545681d | ||
![]() |
ad1afe99ee | ||
![]() |
e179ee36b3 | ||
![]() |
98506f5b1b | ||
![]() |
c7d8751b1c | ||
![]() |
8f739c610e | ||
![]() |
cf5cbc5e90 | ||
![]() |
b1886e308c | ||
![]() |
2acdd320ab | ||
![]() |
15dc3e20a7 | ||
![]() |
5690052db4 | ||
![]() |
6b0c18fd4e | ||
![]() |
9058d25dcc | ||
![]() |
332b118499 | ||
![]() |
958b84430a | ||
![]() |
8d67e9d47a | ||
![]() |
8e4c24bfba | ||
![]() |
a37584a720 | ||
![]() |
acac245840 | ||
![]() |
1cf2722954 | ||
![]() |
e36ffdb0a5 | ||
![]() |
a7830aa5a1 | ||
![]() |
a49c358a4c | ||
![]() |
ba2a8731ed | ||
![]() |
f1a333adce | ||
![]() |
a7a7ea4c38 | ||
![]() |
05c7a8d1d9 | ||
![]() |
46deafa650 | ||
![]() |
fc7f7f3544 | ||
![]() |
e18dbb602d | ||
![]() |
b318fabd43 | ||
![]() |
2ca22cad79 | ||
![]() |
58bc2a7a9f | ||
![]() |
16a50aad9b | ||
![]() |
d2b72f6a87 | ||
![]() |
b9c8674b52 | ||
![]() |
b944978bce | ||
![]() |
90de455fdb | ||
![]() |
2e86ea2ad3 | ||
![]() |
95f165a059 | ||
![]() |
173832e053 | ||
![]() |
700e03de85 | ||
![]() |
708040c3b4 | ||
![]() |
95c870b14e | ||
![]() |
27bf8c0cae | ||
![]() |
354c251701 | ||
![]() |
4dda3f513c | ||
![]() |
50306112ff | ||
![]() |
6df70c1b35 | ||
![]() |
3006c14453 | ||
![]() |
c87f2305ab | ||
![]() |
4fde2df804 | ||
![]() |
27236eafac | ||
![]() |
1ad5644553 | ||
![]() |
e6fd4e8646 | ||
![]() |
82d5a239fc | ||
![]() |
2d822302d8 | ||
![]() |
6f6eab5cd5 | ||
![]() |
f7bfa0299e | ||
![]() |
a62e81314e | ||
![]() |
6588871def | ||
![]() |
764e95e5be | ||
![]() |
a321287f2f | ||
![]() |
e4f4c3e958 | ||
![]() |
8e87f2e9e0 | ||
![]() |
a34aab52e9 | ||
![]() |
81a87fd526 | ||
![]() |
a33bb5ee7d | ||
![]() |
e055f810c7 | ||
![]() |
95812a7458 | ||
![]() |
ee2409dc61 | ||
![]() |
7f5729a12d | ||
![]() |
e99e80b150 | ||
![]() |
e5349c22a7 | ||
![]() |
fe99021dc0 | ||
![]() |
55b2f19196 | ||
![]() |
de9a1dbd1c | ||
![]() |
5a0711bb2d | ||
![]() |
9cabe7e303 | ||
![]() |
b288481d2f | ||
![]() |
d3e8e691fd | ||
![]() |
ffc8e95bc3 | ||
![]() |
cbf6c8c941 | ||
![]() |
609f5295c7 | ||
![]() |
09740b7e12 | ||
![]() |
916813b6ea | ||
![]() |
0c3959af04 | ||
![]() |
54dfd05ab5 | ||
![]() |
9239aa5277 | ||
![]() |
ba6629f4ca | ||
![]() |
ee45c7ba3d | ||
![]() |
b414230735 | ||
![]() |
2023cc4655 | ||
![]() |
20ce084a49 | ||
![]() |
e8e03fe6a6 | ||
![]() |
d094b20950 | ||
![]() |
0e0917f4fe | ||
![]() |
ddfb17c0b2 | ||
![]() |
33c7c24dfd | ||
![]() |
ecc5cbc409 | ||
![]() |
af60225671 | ||
![]() |
7370ba85f6 | ||
![]() |
bed0aa5cc4 | ||
![]() |
b09acdeec2 | ||
![]() |
2afa24536d | ||
![]() |
07da67c8cf | ||
![]() |
065c8bb69a | ||
![]() |
b958c10690 | ||
![]() |
c66d6a8727 | ||
![]() |
93b92c041b | ||
![]() |
f2d3faf1da | ||
![]() |
8187096bfa | ||
![]() |
06b9a88331 | ||
![]() |
eb4d0fc371 | ||
![]() |
abb508000e | ||
![]() |
721f552a3c | ||
![]() |
47562749b2 | ||
![]() |
6521b1474c | ||
![]() |
9b1b30aa2b | ||
![]() |
c643f97700 | ||
![]() |
5fdcaae7e8 | ||
![]() |
050c4706e3 | ||
![]() |
29ddbea03d | ||
![]() |
2af63f1f45 | ||
![]() |
147fe18e74 | ||
![]() |
699088bbde | ||
![]() |
b0205f7dab | ||
![]() |
61767ef0d5 | ||
![]() |
df4f7bf811 | ||
![]() |
deac2f6c8a | ||
![]() |
008b5b3b2f | ||
![]() |
ba1b04432d | ||
![]() |
e73c160cd4 | ||
![]() |
68ab4f37c1 | ||
![]() |
7718c95f20 | ||
![]() |
f33db2b42a | ||
![]() |
6f257c5b7b | ||
![]() |
dbe068c0fe | ||
![]() |
3cce2a5629 | ||
![]() |
a99d7f8792 | ||
![]() |
ef2419bf6f | ||
![]() |
ed0f7667e4 | ||
![]() |
1d0aaf2666 | ||
![]() |
d36676a630 | ||
![]() |
748f803ca0 | ||
![]() |
07c2d456ea | ||
![]() |
6e76ab299b | ||
![]() |
c4f9142376 | ||
![]() |
6835072104 | ||
![]() |
d4e365a499 | ||
![]() |
7e3303a8d6 | ||
![]() |
65ff2a9203 | ||
![]() |
ba8dbc3f57 | ||
![]() |
4cb5cfa036 | ||
![]() |
6080e6db30 | ||
![]() |
a368b12648 | ||
![]() |
cfe122592d | ||
![]() |
26b5b59b56 | ||
![]() |
089f349a05 | ||
![]() |
d8ae7f749c | ||
![]() |
be7461a20a | ||
![]() |
3dbb9ecb27 | ||
![]() |
997446af6c | ||
![]() |
c173bf1cb0 | ||
![]() |
006801c03e | ||
![]() |
aebaa2cafc | ||
![]() |
de0eb9d970 | ||
![]() |
094637b2de | ||
![]() |
f9842e8b18 | ||
![]() |
3ee01b5243 | ||
![]() |
620bbb5b35 | ||
![]() |
6dc7de26e3 | ||
![]() |
b5ec221e2a | ||
![]() |
60ebd61b72 | ||
![]() |
49da96d5d9 | ||
![]() |
53bf4f960c | ||
![]() |
cc534d9618 | ||
![]() |
17ace89ed8 | ||
![]() |
470160dcb2 | ||
![]() |
152202b0de | ||
![]() |
6a1b012527 | ||
![]() |
d34842e31a | ||
![]() |
8927a45f43 | ||
![]() |
2d78d47591 | ||
![]() |
87df5bdcc0 | ||
![]() |
02e515d27c | ||
![]() |
65324b4197 | ||
![]() |
88fec9fa49 | ||
![]() |
554e73a46d | ||
![]() |
0ced0ee901 | ||
![]() |
16581b1e2e | ||
![]() |
e68962ac48 | ||
![]() |
8e714c7712 | ||
![]() |
ab3508a936 | ||
![]() |
48b38fa992 | ||
![]() |
e851565823 | ||
![]() |
a734bcd2d3 | ||
![]() |
730957d19b | ||
![]() |
28bda78d20 | ||
![]() |
3fc418e80c | ||
![]() |
0f476c4d7b | ||
![]() |
c66ee54a9f | ||
![]() |
e8f62b8532 | ||
![]() |
43656be181 | ||
![]() |
dab790cb68 | ||
![]() |
dad1f6a723 | ||
![]() |
315fe4681e | ||
![]() |
706be3d823 | ||
![]() |
930d209cdb | ||
![]() |
32e4b8a31a | ||
![]() |
49195ed5e1 | ||
![]() |
bc7706cc97 | ||
![]() |
3fefba21a7 | ||
![]() |
b8a310dfa6 | ||
![]() |
cac3e53be1 | ||
![]() |
ce29c09f0f | ||
![]() |
8b411db751 | ||
![]() |
16231e2e52 | ||
![]() |
cb6d03d83d | ||
![]() |
aeaab0f078 | ||
![]() |
f9856a3605 | ||
![]() |
e7eb4412ed | ||
![]() |
d36ecf96af | ||
![]() |
448ba654fb | ||
![]() |
0a4b01a841 | ||
![]() |
7d22eafa59 | ||
![]() |
0c571a0601 | ||
![]() |
3ab7284a64 | ||
![]() |
de7d05ebfa | ||
![]() |
adb451e7e3 | ||
![]() |
ee97d67c84 | ||
![]() |
c55819ffdf | ||
![]() |
672c82c43c | ||
![]() |
a93bb954d7 | ||
![]() |
691c4ae745 | ||
![]() |
e15fc8c219 | ||
![]() |
eec1e985e5 | ||
![]() |
b60d098e96 | ||
![]() |
189a736368 | ||
![]() |
acf46b4231 | ||
![]() |
433f3e8f01 | ||
![]() |
d7b376d373 | ||
![]() |
8b4cc6f5a9 | ||
![]() |
0a4dd7413c | ||
![]() |
65b79208a1 | ||
![]() |
321a93aa5d | ||
![]() |
9d2723a08a | ||
![]() |
c7fc54af02 | ||
![]() |
eb14f7e777 | ||
![]() |
5e1588f861 | ||
![]() |
45eef49c95 | ||
![]() |
debd34cc3a | ||
![]() |
7000d9a010 | ||
![]() |
5958cfed97 | ||
![]() |
06511e076f | ||
![]() |
14461c3960 | ||
![]() |
74479c792e | ||
![]() |
88b55acaed | ||
![]() |
bd7babeda0 | ||
![]() |
3fc9790203 | ||
![]() |
cb7078633e | ||
![]() |
7f10c24232 | ||
![]() |
aa405530b4 | ||
![]() |
3553a7aa93 | ||
![]() |
7f99973a02 | ||
![]() |
f1a946af4e | ||
![]() |
0852aa0f43 | ||
![]() |
d7cd4e2961 | ||
![]() |
04bea6e9ba | ||
![]() |
455149249f | ||
![]() |
ab88d413c4 | ||
![]() |
2edb088650 | ||
![]() |
4526375a19 | ||
![]() |
ef09eb50ac | ||
![]() |
791342d508 | ||
![]() |
d849a09a25 | ||
![]() |
b123abb1d3 | ||
![]() |
184d320a8f | ||
![]() |
8ed4289b2a | ||
![]() |
0d0159b56f | ||
![]() |
d6b4dc6a52 | ||
![]() |
78e219da8c | ||
![]() |
2fd9e6a136 | ||
![]() |
b77d91cb20 | ||
![]() |
6e6ff755eb | ||
![]() |
aca156f21b | ||
![]() |
6fafa2f65a | ||
![]() |
f4a978b13c | ||
![]() |
b114e569f2 | ||
![]() |
ef0eb56fb8 | ||
![]() |
997d538a93 | ||
![]() |
42fcbdf1ad | ||
![]() |
d61c2312c1 | ||
![]() |
794ca35d23 | ||
![]() |
e55c304160 | ||
![]() |
445b1711cb | ||
![]() |
d8eb7ff387 | ||
![]() |
f4df389713 | ||
![]() |
78f25ea2c5 | ||
![]() |
583366b22b | ||
![]() |
5158853c30 | ||
![]() |
55972cca30 | ||
![]() |
630304500f | ||
![]() |
75dd8503c5 | ||
![]() |
0ef60ae767 | ||
![]() |
6f8d83f828 | ||
![]() |
ac711f4eee | ||
![]() |
15fb063137 | ||
![]() |
e070044fef | ||
![]() |
b4079ac861 | ||
![]() |
bb78a726e4 | ||
![]() |
150208fc72 | ||
![]() |
036d92b0ff | ||
![]() |
07ff3d6147 | ||
![]() |
d6074f2271 | ||
![]() |
312627e078 | ||
![]() |
79092e1463 | ||
![]() |
9c33da3bfd | ||
![]() |
63cbb4bed0 | ||
![]() |
a7001a70d2 | ||
![]() |
c11f245d64 |
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@ -0,0 +1,7 @@
|
||||
.git
|
||||
_build
|
||||
*.xen
|
||||
*.bz2
|
||||
*.tar.bz2
|
||||
*.tgz
|
||||
mirage-firewall-bin*
|
32
.github/workflows/docker.yml
vendored
Normal file
32
.github/workflows/docker.yml
vendored
Normal file
@ -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
|
42
.github/workflows/format.yml
vendored
Normal file
42
.github/workflows/format.yml
vendored
Normal 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
|
32
.github/workflows/podman.yml
vendored
Normal file
32
.github/workflows/podman.yml
vendored
Normal file
@ -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
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
Makefile
|
||||
/Makefile
|
||||
_build/
|
||||
log
|
||||
key_gen.ml
|
||||
@ -7,3 +7,4 @@ main.native
|
||||
mir-qubes-test
|
||||
qubes-firewall.xl.in
|
||||
qubes-firewall_libvirt.xml
|
||||
.merlin
|
||||
|
3
.merlin
3
.merlin
@ -1,3 +0,0 @@
|
||||
S .
|
||||
B _build
|
||||
PKG vchan.xen lwt mirage mirage-net-xen tcpip mirage-nat
|
3
.ocamlformat
Normal file
3
.ocamlformat
Normal file
@ -0,0 +1,3 @@
|
||||
version = 0.27.0
|
||||
profile = conventional
|
||||
parse-docstrings = true
|
23
.travis.yml
23
.travis.yml
@ -1,23 +0,0 @@
|
||||
language: c
|
||||
install: wget https://raw.githubusercontent.com/ocaml/ocaml-travisci-skeleton/master/.travis-mirage.sh
|
||||
script: bash -ex .travis-mirage.sh
|
||||
sudo: required
|
||||
dist: trusty
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- avsm
|
||||
packages:
|
||||
- ocaml
|
||||
- ocaml-base
|
||||
- ocaml-native-compilers
|
||||
- ocaml-compiler-libs
|
||||
- ocaml-interp
|
||||
- ocaml-base-nox
|
||||
- ocaml-nox
|
||||
- camlp4
|
||||
- camlp4-extra
|
||||
- time
|
||||
- libxen-dev
|
||||
env:
|
||||
- FORK_USER=talex5 FORK_BRANCH=unikernel OCAML_VERSION=4.02 MIRAGE_BACKEND=xen PINS="mirage-nat:https://github.com/talex5/mirage-nat.git#simplify-checksum"
|
378
CHANGES.md
Normal file
378
CHANGES.md
Normal file
@ -0,0 +1,378 @@
|
||||
### 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:
|
||||
|
||||
- Allow naming hosts (@talex5, #54).
|
||||
Previously, we passed in the interface, from which it was possible (but a
|
||||
little difficult) to extract the IP address and compare with some predefined
|
||||
ones. Now, we allow the user to list IP addresses and named tags for them,
|
||||
which can be matched on easily.
|
||||
|
||||
- Add some types to the rules (@talex5, #54).
|
||||
Before, we inferred the types from `rules.ml` and then the compiler checked that
|
||||
it was consistent with what `firewall.ml` expected. If it wasn't then it
|
||||
reported the problem as being with `firewall.ml`, which could be confusing to
|
||||
users.
|
||||
|
||||
- Give exact types for `Packet.src` (@talex5, #54).
|
||||
Before, the packet passed to `rules.ml` could have any host as its `src`.
|
||||
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, #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,
|
||||
and there doesn't seem to be any advantage to keeping them separate anyway.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Upgrade to latest mirage-nat to fix ICMP (@yomimono, @linse, #55).
|
||||
Now ping and traceroute should work. Reported by @xaki23.
|
||||
|
||||
- Respond to ARP requests for `*.*.*.1` (@talex5, #61).
|
||||
This is a work-around to get DHCP working with HVM domains.
|
||||
Reported by @cgchinicz.
|
||||
See: https://github.com/QubesOS/qubes-issues/issues/5022
|
||||
|
||||
- Force backend MAC to `fe:ff:ff:ff:ff:ff` to fix HVM clients (@talex5, #61).
|
||||
Xen appears to configure the same MAC address for both the frontend and
|
||||
backend in XenStore. This works if the client uses just a simple ethernet
|
||||
device, but fails if it connects via a bridge. HVM domains have an associated
|
||||
stub domain running qemu, which provides an emulated network device. The stub
|
||||
domain uses a bridge to connect qemu's interface with eth0, and this didn't
|
||||
work. Force the use of the fixed version of mirage-net-xen, which no longer
|
||||
uses XenStore to get the backend MAC, and provides a new function to get the
|
||||
frontend one.
|
||||
|
||||
- Wait if dom0 is slow to set the network configuration (@talex5, #60).
|
||||
Sometimes we boot before dom0 has put the network settings in QubesDB.
|
||||
If that happens, log a message, wait until the database changes, and retry.
|
||||
|
||||
Reproducible builds:
|
||||
|
||||
- Add patch to cmdliner for reproducible build (@talex5, #52).
|
||||
See https://github.com/dbuenzli/cmdliner/pull/106
|
||||
|
||||
- Use source date in .tar.bz2 archive (@talex5, #49).
|
||||
All files are now added using the date the `build-with-docker` script was last changed.
|
||||
Since this includes the hash of the result, it should be up-to-date.
|
||||
This ensures that rebuilding the archive doesn't change it in any way.
|
||||
Reported by Holger Levsen.
|
||||
|
||||
Documentation changes:
|
||||
|
||||
- Added example rules showing how to block access to an external service or
|
||||
allow SSH between AppVMs (@talex5, #54). Requested at
|
||||
https://groups.google.com/d/msg/qubes-users/BnL0nZGpJOE/61HOBg1rCgAJ.
|
||||
|
||||
- Add overview of the main components of the firewall in the README (@talex5, #54).
|
||||
|
||||
- Link to security advisories from README (@talex5, #58).
|
||||
|
||||
- Clarify how to build from source (@talex5, #51).
|
||||
|
||||
- Remove Qubes 3 instructions (@talex5, #48).
|
||||
See https://www.qubes-os.org/news/2019/03/28/qubes-3-2-has-reached-eol/
|
||||
|
||||
### 0.5
|
||||
|
||||
- Update to the latest mirage-net-xen, mirage-nat and tcpip libraries (@yomimono, @talex5, #45, #47).
|
||||
In iperf benchmarks between a client VM and sys-net, this more than doubled the reported bandwidth!
|
||||
|
||||
- Don't wait for the Qubes GUI daemon to connect before attaching client VMs (@talex5, #38).
|
||||
If the firewall is restarted while AppVMs are connected, qubesd tries to
|
||||
reconnect them before starting the GUI agent. However, the firewall was
|
||||
waiting for the GUI agent to connect before handling the connections. This
|
||||
led to a 10s delay on restart for each client VM. Reported by @xaki23.
|
||||
|
||||
- Add stub makefile for qubes-builder (@xaki23, #37).
|
||||
|
||||
- Update build instructions for latest Fedora (@talex5, #36). `yum` no longer exists.
|
||||
Also, show how to create a symlink for `/var/lib/docker` on build VMs that aren't standalone.
|
||||
Reported by @xaki23.
|
||||
|
||||
- Add installation instructions for Qubes 4 (@yomimono, @reynir, @talex5, #27).
|
||||
|
||||
- Use `Ethernet_wire.sizeof_ethernet` instead of a magic `14` (@hannesm, #46).
|
||||
|
||||
### 0.4
|
||||
|
||||
- Add support for HVM guests (needed for Qubes 4).
|
||||
|
||||
- Add support for disposable VMs.
|
||||
|
||||
- Drop frames if an interface's queue gets too long.
|
||||
|
||||
- Show the packet when failing to add a NAT rule. The previous message was
|
||||
just: `WRN [firewall] Failed to add NAT rewrite rule: Cannot NAT this packet`
|
||||
|
||||
### 0.3
|
||||
|
||||
- Add support for NAT of ICMP queries (e.g. pings) and errors (e.g. "Host unreachable").
|
||||
Before, these packets would be dropped.
|
||||
|
||||
- Use an LRU cache to avoid running out of memory and needing to reset the table.
|
||||
Should avoid any more out-of-memory bugs.
|
||||
|
||||
- Pass around parsed packets rather than raw ethernet frames.
|
||||
|
||||
- Pin Docker base image to a specific hash. Requested by Joanna Rutkowska.
|
||||
|
||||
- Update for Mirage 3.
|
||||
|
||||
- Remove non-Docker build instructions. Fedora 24 doesn't work with opam
|
||||
(because the current binary release of aspcud's clasp binary segfaults, which
|
||||
opam reports as `External solver failed with inconsistent return value.`).
|
||||
|
||||
### 0.2
|
||||
|
||||
Build:
|
||||
|
||||
- Add option to build with Docker. This fixes opam-repository to a known commit
|
||||
for reproducible builds. It also displays the actual and expected SHA hashes
|
||||
after building.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Updated README: the build also requires "patch". Reported by William Waites.
|
||||
- Monitor set of client interfaces, not client domains. Qubes does not remove
|
||||
the client directory itself when the domain exits. This prevented clients
|
||||
from reconnecting. This may also make it possible to connect clients to the
|
||||
firewall via multiple interfaces, although this doesn't seem useful.
|
||||
- Handle errors writing to client. mirage-net-xen would report `Netback_shutdown`
|
||||
if we tried to write to a client after it had disconnected. Now we just log
|
||||
this and continue.
|
||||
- Ensure that old client has quit before adding new one. Not sure if this can
|
||||
happen, but it removes a TODO from the code.
|
||||
- Allow clients to have any IP address. We previously assumed that Qubes would
|
||||
always give clients IP addresses on a particular network. However, it is not
|
||||
required to do this and in fact uses a different network for disposable VMs.
|
||||
With this change:
|
||||
- We no longer reject clients with unknown IP addresses.
|
||||
- The `Unknown_client` classification is gone; we have no way to tell the
|
||||
difference between a client that isn't connected and an external address.
|
||||
- We now consider every client to be on a point-to-point link and do not
|
||||
answer ARP requests on behalf of other clients. Clients should assume their
|
||||
netmask is `255.255.255.255` (and ignore `/qubes-netmask`). This allows
|
||||
disposable VMs to connect to the firewall but for some reason they don't
|
||||
process any frames we send them (we get their ARP requests but they don't
|
||||
get our replies). Taking eth0 down in the disp VM, then bringing it back up
|
||||
(and re-adding the routes) allows it to work.
|
||||
- Cope with writing a frame failing. If a client disconnects suddenly then we
|
||||
may get an error trying to map its grant to send the frame.
|
||||
- Survive death of our GUId connection to dom0. We don't need the GUI anyway.
|
||||
- Handle `Out_of_memory` adding NAT entries. Because hash tables resize in big
|
||||
steps, this can happen even if we have a fair chunk of free memory.
|
||||
- Calculate checksums even for `Accept` action. If packet has been NAT'd then we
|
||||
certainly need to recalculate the checksum, but even for direct pass-through
|
||||
it might have been received with an invalid checksum due to checksum offload.
|
||||
For now, recalculate full checksum in all cases.
|
||||
- Log correct destination for redirected packets. Before, we always said it was
|
||||
going to "NetVM".
|
||||
- If we can't find a free port, reset the NAT table.
|
||||
- Reset NAT table if memory gets low.
|
||||
|
||||
Other changes:
|
||||
|
||||
- Report current memory use to XenStore.
|
||||
- Reduce logging verbosity.
|
||||
- Avoid using `Lwt.join` on listening threads.
|
||||
`Lwt.join` only reports an error if _both_ threads fail.
|
||||
- Keep track of transmit queue lengths. Log if we have to wait to send a frame.
|
||||
- Use mirage-logs library for log reporter.
|
||||
- Respond to `WaitForSession` commands (we're always ready!).
|
||||
- Log `SetDateTime` messages from dom0 (we still don't actually update our clock,
|
||||
though).
|
||||
|
||||
Updates for upstream library changes:
|
||||
|
||||
- Updates for mirage 2.9.0.
|
||||
- Use new name for uplink device (`0`, not `tap0`).
|
||||
- Don't configure logging - mirage does that for us now.
|
||||
- Remove tcpip pin. The 2.7.0 release has the checksum feature we need.
|
||||
- Remove mirage-xen pin. mirage-xen 2.4.0 has been released with the required
|
||||
features (also fixes indentation problem reported by @cfcs).
|
||||
- Add ncurses-dev to required yum packages. The ocamlfind package has started
|
||||
listing this as a required dependency for some reason, although it appears
|
||||
not to need it. Reported by cyrinux.
|
||||
- Add work-around for Qubes passing Linux kernel arguments. With the new
|
||||
Functoria release of Mirage, these unrecognised arguments prevented the
|
||||
unikernel from booting. See: https://github.com/mirage/mirage/issues/493
|
||||
- Remove mirage-logs pin. Now available from the main repository.
|
||||
- Remove mirage-qubes pin.
|
||||
mirage-qubes 0.2 has been released, and supports the latests Logs API.
|
||||
- Remove mirage-net-xen pin.
|
||||
Version 1.5 has now been released, and includes netback support.
|
||||
- Update to new Logs API.
|
||||
- Remove pin for mirage-clock-xen. New version has been released now.
|
||||
|
||||
### 0.1
|
||||
|
||||
Initial release.
|
35
Dockerfile
Normal file
35
Dockerfile
Normal file
@ -0,0 +1,35 @@
|
||||
# Pin the base image to a specific hash for maximum reproducibility.
|
||||
# It will probably still work on newer images, though, unless an update
|
||||
# changes some compiler optimisations (unlikely).
|
||||
# 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.
|
||||
# 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'
|
23
LICENSE.md
Normal file
23
LICENSE.md
Normal file
@ -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.
|
7
Makefile.builder
Normal file
7
Makefile.builder
Normal file
@ -0,0 +1,7 @@
|
||||
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
|
||||
|
@ -1,7 +1,10 @@
|
||||
tar: build
|
||||
rm -rf _build/mirage-firewall
|
||||
mkdir _build/mirage-firewall
|
||||
cp mir-qubes-firewall.xen _build/mirage-firewall/vmlinuz
|
||||
touch _build/mirage-firewall/modules.img
|
||||
cat /dev/null | gzip > _build/mirage-firewall/initramfs
|
||||
tar cjf mirage-firewall.tar.bz2 -C _build 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
|
||||
|
265
README.md
265
README.md
@ -3,103 +3,214 @@
|
||||
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.
|
||||
|
||||
To build (tested by creating a fresh Fedora 23 AppVM in Qubes):
|
||||
|
||||
1. Install build tools:
|
||||
## Binary releases
|
||||
|
||||
sudo yum install git gcc m4 0install patch ncurses-devel
|
||||
mkdir ~/bin
|
||||
0install add opam http://tools.ocaml.org/opam.xml
|
||||
opam init --comp=4.02.3
|
||||
eval `opam config env`
|
||||
Pre-built binaries are available from the [releases page][].
|
||||
See the [Deploy](#deploy) section below for installation instructions.
|
||||
|
||||
2. Install mirage, pinning a few unreleased features we need:
|
||||
## Build from source
|
||||
|
||||
opam pin add -y mirage-nat 'https://github.com/talex5/mirage-nat.git#simplify-checksum'
|
||||
opam install mirage
|
||||
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).
|
||||
|
||||
3. Build mirage-firewall:
|
||||
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.
|
||||
|
||||
git clone https://github.com/talex5/qubes-mirage-firewall.git
|
||||
cd qubes-mirage-firewall
|
||||
mirage configure --xen
|
||||
make
|
||||
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):
|
||||
|
||||
If you want to deploy manually, use `make tar` to create `mirage-firewall.tar.bz2` and unpack this in dom0, inside `/var/lib/qubes/vm-kernels/`. e.g. (if `dev` is the AppVM where you built it):
|
||||
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.sh docker
|
||||
|
||||
[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 -
|
||||
Or
|
||||
|
||||
The tarball contains `vmlinuz`, which is the unikernel itself, plus a couple of dummy files that Qubes requires.
|
||||
sudo systemctl start podman
|
||||
git clone https://github.com/mirage/qubes-mirage-firewall.git
|
||||
cd qubes-mirage-firewall
|
||||
./build-with.sh podman
|
||||
|
||||
For development, use the [test-mirage][] scripts to deploy the unikernel (`mir-qubes-firewall.xen`) from your development AppVM. e.g.
|
||||
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.
|
||||
|
||||
$ test-mirage mir-firewall.xen mirage-firewall
|
||||
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.
|
||||
|
||||
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 that script, as for any normal Mirage unikernel;
|
||||
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
|
||||
against the latest versions instead (and the hash will therefore probably not match).
|
||||
However, it should still work fine.
|
||||
|
||||
## Deploy
|
||||
|
||||
### 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 ~]$ 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
|
||||
|
||||
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='' \
|
||||
--property memory=32 \
|
||||
--property maxmem=32 \
|
||||
--property netvm=sys-net \
|
||||
--property provides_network=True \
|
||||
--property vcpus=1 \
|
||||
--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
|
||||
|
||||
You can run `mirage-firewall` alongside your existing `sys-firewall` and you can choose which AppVMs use which firewall using the GUI.
|
||||
To configure an AppVM to use it, go to the app VM's settings in the GUI and change its `NetVM` from `default (sys-firewall)` to `mirage-firewall`.
|
||||
|
||||
You can also configure it by running this command in dom0 (replace `my-app-vm` with the AppVM's name):
|
||||
|
||||
```
|
||||
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):
|
||||
|
||||
<p align='center'>
|
||||
<img src="./diagrams/components.svg"/>
|
||||
</p>
|
||||
|
||||
Ethernet frames arrives from client qubes (such as `work` or `personal`) or from `sys-net`.
|
||||
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.
|
||||
|
||||
The boot process:
|
||||
|
||||
- `config.ml` describes the libraries used and static configuration settings (NAT table size).
|
||||
The `mirage` tool uses this to generate `main.ml`.
|
||||
- `main.ml` initialises the drivers selected by `config.ml`
|
||||
and calls the `start` function in `unikernel.ml`.
|
||||
- `unikernel.ml` connects the Qubes agents, sets up the networking components,
|
||||
and then waits for a shutdown request.
|
||||
|
||||
### Easy deployment for developers
|
||||
|
||||
For development, use the [test-mirage][] scripts to deploy the unikernel (`qubes-firewall.xen`) from your development AppVM.
|
||||
This takes a little more setting up the first time, but will be much quicker after that. e.g.
|
||||
|
||||
[user@dev ~]$ test-mirage dist/qubes-firewall.xen mirage-firewall
|
||||
Waiting for 'Ready'... OK
|
||||
Uploading 'mir-qubes-firewall.xen' (4843304 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.
|
||||
Netif: add resume hook
|
||||
gnttab_stubs.c: initialised mini-os gntmap
|
||||
2015-12-30 10:04.42: INF [qubes.rexec] waiting for client...
|
||||
2015-12-30 10:04.42: INF [qubes.gui] waiting for client...
|
||||
2015-12-30 10:04.42: INF [qubes.db] connecting to server...
|
||||
2015-12-30 10:04.42: INF [qubes.db] connected
|
||||
2015-12-30 10:04.42: INF [qubes.rexec] client connected, using protocol version 2
|
||||
2015-12-30 10:04.42: 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(pc104)\"\t};\n};"
|
||||
2015-12-30 10:04.42: INF [qubes.gui] client connected (screen size: 6720x2160)
|
||||
2015-12-30 10:04.42: INF [unikernel] agents connected in 0.052 s (CPU time used since boot: 0.007 s)
|
||||
Netif.connect 0
|
||||
Netfront.create: id=0 domid=1
|
||||
sg:true gso_tcpv4:true rx_copy:true rx_flip:false smart_poll:false
|
||||
MAC: 00:16:3e:5e:6c:0b
|
||||
ARP: sending gratuitous from 10.137.1.13
|
||||
2015-12-30 10:04.42: INF [application] Client (internal) network is 10.137.3.0/24
|
||||
ARP: transmitting probe -> 10.137.1.1
|
||||
2015-12-30 10:04.42: INF [net] Watching backend/vif
|
||||
2015-12-30 10:04.42: INF [qubes.rexec] Execute "user:QUBESRPC qubes.SetMonitorLayout dom0\000"
|
||||
2015-12-30 10:04.42: WRN [command] << Unknown command "QUBESRPC qubes.SetMonitorLayout dom0"
|
||||
2015-12-30 10:04.42: INF [qubes.rexec] Execute "root:QUBESRPC qubes.WaitForSession none\000"
|
||||
2015-12-30 10:04.42: WRN [command] << Unknown command "QUBESRPC qubes.WaitForSession none"
|
||||
2015-12-30 10:04.42: INF [qubes.db] got update: "/qubes-netvm-domid" = "1"
|
||||
ARP: retrying 10.137.1.1 (n=1)
|
||||
ARP: transmitting probe -> 10.137.1.1
|
||||
ARP: updating 10.137.1.1 -> fe:ff:ff:ff:ff:ff
|
||||
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
|
||||
|
||||
See [issues tagged "security"](https://github.com/mirage/qubes-mirage-firewall/issues?utf8=%E2%9C%93&q=label%3Asecurity+) for security advisories affecting the firewall.
|
||||
|
||||
# LICENSE
|
||||
|
||||
Copyright (c) 2015, 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/talex5/mirage-qubes
|
||||
[mirage-qubes]: https://github.com/mirage/mirage-qubes
|
||||
[A Unikernel Firewall for QubesOS]: http://roscidus.com/blog/blog/2016/01/01/a-unikernel-firewall-for-qubesos/
|
||||
[releases page]: https://github.com/mirage/qubes-mirage-firewall/releases
|
||||
[debian-docker]: https://docs.docker.com/install/linux/docker-ce/debian/#install-using-the-repository
|
||||
|
104
SaltScriptToDownloadAndInstallMirageFirewallInQubes.sls
Normal file
104
SaltScriptToDownloadAndInstallMirageFirewallInQubes.sls
Normal file
@ -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 %}
|
3
_tags
3
_tags
@ -1,3 +0,0 @@
|
||||
not <main.*>: warn(A-4), strict_sequence
|
||||
<qubes_protocol.*>: package(cstruct.syntax)
|
||||
true: -syntax(camlp4o)
|
25
build-with.sh
Executable file
25
build-with.sh
Executable file
@ -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)"
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
220
client_eth.ml
220
client_eth.ml
@ -1,151 +1,149 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
(* Copyright (C) 2016, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
open Utils
|
||||
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 for NetVM clients"
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
type t = {
|
||||
mutable iface_of_ip : client_link IpMap.t;
|
||||
prefix : Ipaddr.V4.Prefix.t;
|
||||
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
|
||||
| `Unknown_client of Ipaddr.t
|
||||
| `Client_gateway
|
||||
| `External of Ipaddr.t ]
|
||||
type host = [ `Client of client_link | `Firewall | `External of Ipaddr.t ]
|
||||
|
||||
let create ~prefix ~client_gw =
|
||||
{ iface_of_ip = IpMap.empty; client_gw; prefix }
|
||||
let create config =
|
||||
let changed = Lwt_condition.create () in
|
||||
let my_ip = config.Dao.our_ip in
|
||||
Lwt.return { iface_of_ip = Ipaddr.V4.Map.empty; my_ip; changed }
|
||||
|
||||
let prefix t = t.prefix
|
||||
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
|
||||
assert (Ipaddr.V4.Prefix.mem ip t.prefix);
|
||||
(* TODO: Should probably wait for the previous client to disappear. *)
|
||||
(* assert (not (IpMap.mem ip t.iface_of_ip)); *)
|
||||
t.iface_of_ip <- t.iface_of_ip |> IpMap.add ip iface
|
||||
let rec aux () =
|
||||
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.
|
||||
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
|
||||
| None ->
|
||||
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 `Client_gateway
|
||||
else match lookup t ip4 with
|
||||
| Some client_link -> `Client client_link
|
||||
| None when Ipaddr.V4.Prefix.mem ip4 t.prefix -> `Unknown_client ip
|
||||
| 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
|
||||
| `Client_gateway -> Ipaddr.V4 t.client_gw
|
||||
| `Unknown_client addr
|
||||
| `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 match IpMap.find ip t.net.iface_of_ip with
|
||||
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 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 }
|
||||
|
||||
type arp_msg = {
|
||||
op: [ `Request |`Reply |`Unknown of int ];
|
||||
sha: Macaddr.t;
|
||||
spa: Ipaddr.V4.t;
|
||||
tha: Macaddr.t;
|
||||
tpa: Ipaddr.V4.t;
|
||||
}
|
||||
|
||||
let to_wire arp =
|
||||
let open Arpv4_wire in
|
||||
(* Obtain a buffer to write into *)
|
||||
let buf = Cstruct.create (Wire_structs.sizeof_ethernet + sizeof_arp) in
|
||||
(* Write the ARP packet *)
|
||||
let dmac = Macaddr.to_bytes arp.tha in
|
||||
let smac = Macaddr.to_bytes arp.sha in
|
||||
let spa = Ipaddr.V4.to_int32 arp.spa in
|
||||
let tpa = Ipaddr.V4.to_int32 arp.tpa in
|
||||
let op =
|
||||
match arp.op with
|
||||
|`Request -> 1
|
||||
|`Reply -> 2
|
||||
|`Unknown n -> n
|
||||
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
|
||||
in
|
||||
Wire_structs.set_ethernet_dst dmac 0 buf;
|
||||
Wire_structs.set_ethernet_src smac 0 buf;
|
||||
Wire_structs.set_ethernet_ethertype buf 0x0806; (* ARP *)
|
||||
let arpbuf = Cstruct.shift buf 14 in
|
||||
set_arp_htype arpbuf 1;
|
||||
set_arp_ptype arpbuf 0x0800; (* IPv4 *)
|
||||
set_arp_hlen arpbuf 6; (* ethernet mac size *)
|
||||
set_arp_plen arpbuf 4; (* ipv4 size *)
|
||||
set_arp_op arpbuf op;
|
||||
set_arp_sha smac 0 arpbuf;
|
||||
set_arp_spa arpbuf spa;
|
||||
set_arp_tha dmac 0 arpbuf;
|
||||
set_arp_tpa arpbuf tpa;
|
||||
buf
|
||||
|
||||
let input_query t frame =
|
||||
let open Arpv4_wire in
|
||||
let req_ipv4 = Ipaddr.V4.of_int32 (get_arp_tpa frame) in
|
||||
Log.info (fun f -> f "who-has %s?" (Ipaddr.V4.to_string req_ipv4));
|
||||
if req_ipv4 = t.client_link#other_ip then (
|
||||
Log.info (fun f -> f "ignoring request for client's own IP");
|
||||
None
|
||||
) else match lookup t req_ipv4 with
|
||||
| None ->
|
||||
Log.info (fun f -> f "unknown address; not responding");
|
||||
None
|
||||
| Some req_mac ->
|
||||
Log.info (fun f -> f "responding to: who-has %s?" (Ipaddr.V4.to_string req_ipv4));
|
||||
Some (to_wire {
|
||||
op = `Reply;
|
||||
(* The Target Hardware Address and IP are copied from the request *)
|
||||
tha = Macaddr.of_bytes_exn (copy_arp_sha frame);
|
||||
tpa = Ipaddr.V4.of_int32 (get_arp_spa frame);
|
||||
sha = req_mac;
|
||||
spa = req_ipv4;
|
||||
})
|
||||
Log.info (fun f -> pf f "ignoring request for client's own IP");
|
||||
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;
|
||||
(* 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 frame =
|
||||
let open Arpv4_wire in
|
||||
let spa = Ipaddr.V4.of_int32 (get_arp_spa frame) in
|
||||
let sha = Macaddr.of_bytes_exn (copy_arp_sha frame) in
|
||||
match lookup t spa with
|
||||
| Some real_mac when Macaddr.compare sha real_mac = 0 ->
|
||||
Log.info (fun f -> f "client suggests updating %s -> %s (as expected)"
|
||||
(Ipaddr.V4.to_string spa) (Macaddr.to_string sha));
|
||||
let input_gratuitous t arp =
|
||||
let source_ip = arp.Arp_packet.source_ip in
|
||||
let source_mac = arp.Arp_packet.source_mac in
|
||||
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))
|
||||
| Some other_mac ->
|
||||
Log.warn (fun f -> f "client suggests incorrect update %s -> %s (should be %s)"
|
||||
(Ipaddr.V4.to_string spa) (Macaddr.to_string sha) (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 "client suggests incorrect update %s -> %s (unexpected IP)"
|
||||
(Ipaddr.V4.to_string spa) (Macaddr.to_string sha))
|
||||
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 frame =
|
||||
match Arpv4_wire.get_arp_op frame with
|
||||
|1 -> input_query t frame
|
||||
|2 -> input_gratuitous t frame; None
|
||||
|n -> Log.warn (fun f -> f "unknown message %d - ignored" n); None
|
||||
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
|
||||
end
|
||||
|
@ -1,50 +1,51 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
(* Copyright (C) 2016, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
(** The ethernet network our client AppVMs are on. *)
|
||||
(** 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 Utils
|
||||
open Fw_utils
|
||||
|
||||
type t
|
||||
(** A network for client AppVMs to join. *)
|
||||
(** A collection of clients. *)
|
||||
|
||||
type host =
|
||||
[ `Client of client_link
|
||||
| `Unknown_client of Ipaddr.t
|
||||
| `Client_gateway
|
||||
| `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 : prefix:Ipaddr.V4.Prefix.t -> client_gw:Ipaddr.V4.t -> t
|
||||
(** [create ~prefix ~client_gw] is a network of client machines.
|
||||
Their IP addresses all start with [prefix] and they are configured 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. *)
|
||||
|
||||
val add_client : t -> client_link -> unit
|
||||
val remove_client : t -> client_link -> unit
|
||||
|
||||
val prefix : t -> Ipaddr.V4.Prefix.t
|
||||
val client_gw : t -> Ipaddr.V4.t
|
||||
|
||||
val classify : t -> Ipaddr.t -> host
|
||||
val resolve : t -> host -> Ipaddr.t
|
||||
|
||||
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 on behalf of other clients in [net] (but not for the client
|
||||
itself, since the client might be trying to check that its own address is
|
||||
free). It also answers 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 -> Cstruct.t -> Cstruct.t option
|
||||
(** Process one ethernet frame containing an ARP message.
|
||||
Returns a response frame, if one is needed. *)
|
||||
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. *)
|
||||
end
|
||||
|
109
client_net.ml
109
client_net.ml
@ -1,109 +0,0 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
open Lwt.Infix
|
||||
open Utils
|
||||
|
||||
module Netback = Netchannel.Backend.Make(Netchannel.Xenstore.Make(OS.Xs))
|
||||
module ClientEth = Ethif.Make(Netback)
|
||||
|
||||
let src = Logs.Src.create "net" ~doc:"Client networking"
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
class client_iface eth ~gateway_ip ~client_ip client_mac : client_link = 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 ip =
|
||||
FrameQ.send queue (fun () ->
|
||||
let eth_hdr = eth_header_ipv4 ~src:(ClientEth.mac eth) ~dst:client_mac in
|
||||
ClientEth.writev eth (fixup_checksums (Cstruct.concat (eth_hdr :: ip)))
|
||||
)
|
||||
end
|
||||
|
||||
let clients : Cleanup.t IntMap.t ref = ref IntMap.empty
|
||||
|
||||
(** Handle an ARP message from the client. *)
|
||||
let input_arp ~fixed_arp ~eth request =
|
||||
match Client_eth.ARP.input fixed_arp request with
|
||||
| None -> return ()
|
||||
| Some response -> ClientEth.write eth response
|
||||
|
||||
(** Handle an IPv4 packet from the client. *)
|
||||
let input_ipv4 ~client_ip ~router frame packet =
|
||||
let src = Wire_structs.Ipv4_wire.get_ipv4_src packet |> Ipaddr.V4.of_int32 in
|
||||
if src = client_ip then Firewall.ipv4_from_client router frame
|
||||
else (
|
||||
Log.warn (fun f -> f "Incorrect source IP %a in IP packet from %a (dropping)"
|
||||
Ipaddr.V4.pp_hum src Ipaddr.V4.pp_hum client_ip);
|
||||
return ()
|
||||
)
|
||||
|
||||
(** Connect to a new client's interface and listen for incoming frames. *)
|
||||
let add_vif { Dao.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 >>= or_fail "Can't make Ethernet device" >>= fun eth ->
|
||||
let client_mac = Netback.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 ~gateway_ip ~client_ip client_mac in
|
||||
Router.add_client router iface;
|
||||
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 (fun frame ->
|
||||
match Wire_structs.parse_ethernet_frame frame with
|
||||
| None -> Log.warn (fun f -> f "Invalid Ethernet frame"); return ()
|
||||
| Some (typ, _destination, payload) ->
|
||||
match typ with
|
||||
| Some Wire_structs.ARP -> input_arp ~fixed_arp ~eth payload
|
||||
| Some Wire_structs.IPv4 -> input_ipv4 ~client_ip ~router frame payload
|
||||
| Some Wire_structs.IPv6 -> return ()
|
||||
| None -> Logs.warn (fun f -> f "Unknown Ethernet type"); Lwt.return_unit
|
||||
)
|
||||
|
||||
(** A new client VM has been found in XenStore. Find its interface and connect to it. *)
|
||||
let add_client ~router domid =
|
||||
let cleanup_tasks = Cleanup.create () in
|
||||
Log.info (fun f -> f "add client domain %d" domid);
|
||||
Lwt.async (fun () ->
|
||||
Lwt.catch (fun () ->
|
||||
Dao.client_vifs domid >>= function
|
||||
| [] ->
|
||||
Log.warn (fun f -> f "Client has no interfaces");
|
||||
return ()
|
||||
| vif :: others ->
|
||||
if others <> [] then Log.warn (fun f -> f "Client has multiple interfaces; using first");
|
||||
add_vif vif ~router ~cleanup_tasks
|
||||
)
|
||||
(fun ex ->
|
||||
Log.warn (fun f -> f "Error connecting client domain %d: %s"
|
||||
domid (Printexc.to_string ex));
|
||||
return ()
|
||||
)
|
||||
);
|
||||
cleanup_tasks
|
||||
|
||||
(** Watch XenStore for notifications of new clients. *)
|
||||
let listen router =
|
||||
let backend_vifs = "backend/vif" in
|
||||
Log.info (fun f -> f "Watching %s" backend_vifs);
|
||||
Dao.watch_clients (fun new_set ->
|
||||
(* Check for removed clients *)
|
||||
!clients |> IntMap.iter (fun key cleanup ->
|
||||
if not (IntSet.mem key new_set) then (
|
||||
clients := !clients |> IntMap.remove key;
|
||||
Log.info (fun f -> f "client %d has gone" key);
|
||||
Cleanup.cleanup cleanup
|
||||
)
|
||||
);
|
||||
(* Check for added clients *)
|
||||
new_set |> IntSet.iter (fun key ->
|
||||
if not (IntMap.mem key !clients) then (
|
||||
let cleanup = add_client ~router key in
|
||||
clients := !clients |> IntMap.add key cleanup
|
||||
)
|
||||
)
|
||||
)
|
@ -1,10 +0,0 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
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. *)
|
20
command.ml
20
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
|
||||
|
30
config.ml
30
config.ml
@ -1,4 +1,5 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
(* mirage >= 4.9.0 & < 4.10.0 *)
|
||||
(* Copyright (C) 2017, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
(** Configuration for the "mirage" tool. *)
|
||||
@ -6,11 +7,24 @@
|
||||
open Mirage
|
||||
|
||||
let main =
|
||||
foreign
|
||||
~libraries:["mirage-net-xen"; "tcpip.stack-direct"; "tcpip.xen"; "mirage-qubes"; "mirage-nat"; "mirage-logs"]
|
||||
~packages:["vchan"; "cstruct"; "tcpip"; "mirage-net-xen"; "mirage-qubes"; "mirage-nat"; "mirage-logs"]
|
||||
"Unikernel.Main" (clock @-> 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_clock]
|
||||
~argv:no_argv
|
||||
let () = register "qubes-firewall" [ main ]
|
||||
|
214
dao.ml
214
dao.ml
@ -2,66 +2,180 @@
|
||||
See the README file for details. *)
|
||||
|
||||
open Lwt.Infix
|
||||
open Utils
|
||||
open Qubes
|
||||
|
||||
type client_vif = {
|
||||
domid : int;
|
||||
device_id : int;
|
||||
client_ip : Ipaddr.V4.t;
|
||||
}
|
||||
let src = Logs.Src.create "dao" ~doc:"QubesDB data access"
|
||||
|
||||
let client_vifs domid =
|
||||
let path = Printf.sprintf "backend/vif/%d" domid in
|
||||
OS.Xs.make () >>= fun xs ->
|
||||
OS.Xs.immediate xs (fun h ->
|
||||
OS.Xs.directory h path >>=
|
||||
Lwt_list.map_p (fun device_id ->
|
||||
let device_id = int_of_string device_id in
|
||||
OS.Xs.read h (Printf.sprintf "%s/%d/ip" path device_id) >|= fun client_ip ->
|
||||
let client_ip = Ipaddr.V4.of_string_exn client_ip in
|
||||
{ domid; device_id; client_ip }
|
||||
)
|
||||
)
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
module ClientVif = struct
|
||||
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 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
|
||||
end
|
||||
|
||||
let directory ~handle dir =
|
||||
Xen_os.Xs.directory handle dir >|= function
|
||||
| [ "" ] -> [] (* XenStore client bug *)
|
||||
| items -> items
|
||||
|
||||
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 = 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
|
||||
OS.Xs.wait xs (fun handle ->
|
||||
begin Lwt.catch
|
||||
(fun () -> OS.Xs.directory handle backend_vifs)
|
||||
(function
|
||||
| Xs_protocol.Enoent _ -> return []
|
||||
| ex -> fail ex)
|
||||
end >>= fun items ->
|
||||
let items = items |> List.fold_left (fun acc key -> IntSet.add (int_of_string key) acc) IntSet.empty in
|
||||
fn items;
|
||||
(* Wait for further updates *)
|
||||
fail Xs_protocol.Eagain
|
||||
)
|
||||
Log.info (fun f -> f "Watching %s" backend_vifs);
|
||||
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_prefix : Ipaddr.V4.Prefix.t; (* The network connecting our client VMs to us *)
|
||||
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;
|
||||
}
|
||||
|
||||
(* TODO: /qubes-secondary-dns *)
|
||||
let read_network_config qubesDB =
|
||||
exception Missing_key of string
|
||||
|
||||
let try_read_network_config db =
|
||||
let get name =
|
||||
match DB.read qubesDB name with
|
||||
| None -> raise (error "QubesDB key %S not present" 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_prefix =
|
||||
(* This is oddly named: seems to be the network we provide to our clients *)
|
||||
let client_network = get "/qubes-netvm-network" |> Ipaddr.V4.of_string_exn in
|
||||
let client_netmask = get "/qubes-netvm-netmask" |> Ipaddr.V4.of_string_exn in
|
||||
Ipaddr.V4.Prefix.of_netmask client_netmask client_network in
|
||||
let clients_our_ip = get "/qubes-netvm-gateway" |> Ipaddr.V4.of_string_exn in
|
||||
{ uplink_netvm_ip; uplink_our_ip; clients_prefix; clients_our_ip }
|
||||
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) *)
|
||||
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);
|
||||
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):@,\
|
||||
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"
|
||||
|
49
dao.mli
49
dao.mli
@ -3,29 +3,44 @@
|
||||
|
||||
(** Wrapper for XenStore and QubesDB databases. *)
|
||||
|
||||
open Utils
|
||||
module ClientVif : sig
|
||||
type t = { domid : int; device_id : int }
|
||||
|
||||
type client_vif = {
|
||||
domid : int;
|
||||
device_id : int;
|
||||
client_ip : Ipaddr.V4.t;
|
||||
}
|
||||
val pp : t Fmt.t
|
||||
end
|
||||
|
||||
val watch_clients : (IntSet.t -> unit) -> 'a Lwt.t
|
||||
(** [watch_clients fn] calls [fn clients] with the current set of backend client domain IDs
|
||||
in XenStore, and again each time the set changes. *)
|
||||
module VifMap : sig
|
||||
include Map.S with type key = ClientVif.t
|
||||
|
||||
val client_vifs : int -> client_vif list Lwt.t
|
||||
(** [client_vif domid] is the list of network interfaces to the client VM [domid]. *)
|
||||
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. *)
|
||||
|
||||
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_prefix : Ipaddr.V4.Prefix.t; (* The network connecting our client VMs to us *)
|
||||
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
|
||||
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. *)
|
||||
|
||||
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
|
||||
|
6
diagrams/Makefile
Normal file
6
diagrams/Makefile
Normal file
@ -0,0 +1,6 @@
|
||||
# Requires https://github.com/blampe/goat
|
||||
|
||||
all: components.svg
|
||||
|
||||
%.svg: %.txt
|
||||
goat $^ > $@
|
199
diagrams/components.svg
Normal file
199
diagrams/components.svg
Normal file
@ -0,0 +1,199 @@
|
||||
<svg class='diagram' xmlns='http://www.w3.org/2000/svg' version='1.1' height='425' width='600'>
|
||||
<g transform='translate(8,16)'>
|
||||
<path d='M 240,0 L 408,0' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 240,32 L 408,32' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 272,96 L 368,96' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 272,128 L 368,128' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 120,192 L 224,192' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 272,192 L 320,192' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 320,192 L 360,192' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 72,208 L 112,208' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 224,208 L 264,208' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 368,208 L 440,208' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 272,224 L 320,224' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 320,224 L 360,224' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 400,240 L 440,240' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 440,240 L 472,240' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 72,256 L 112,256' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 480,256 L 520,256' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 400,272 L 472,272' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 272,288 L 360,288' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 72,304 L 112,304' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 232,304 L 272,304' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 360,304 L 440,304' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 120,320 L 176,320' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 176,320 L 224,320' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 272,320 L 360,320' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 120,192 L 120,320' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 176,320 L 176,368' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 224,192 L 224,208' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 224,208 L 224,320' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 240,0 L 240,32' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 272,96 L 272,128' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 272,192 L 272,224' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 272,288 L 272,304' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 272,304 L 272,320' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 320,48 L 320,80' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 320,144 L 320,192' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 320,224 L 320,272' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 360,192 L 360,224' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 360,288 L 360,304' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 360,304 L 360,320' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 368,96 L 368,128' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 400,240 L 400,272' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 408,0 L 408,32' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 440,208 L 440,240' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 440,288 L 440,304' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 472,240 L 472,272' style='fill:none;stroke:#000;'></path>
|
||||
<path d='M 320,80 L 320,88' style='fill:none;stroke:#000;'></path>
|
||||
<polygon points='80.000000,208.000000 68.000000,202.399994 68.000000,213.600006' style='fill:#000' transform='rotate(180.000000, 72.000000, 208.000000)'></polygon>
|
||||
<polygon points='80.000000,256.000000 68.000000,250.399994 68.000000,261.600006' style='fill:#000' transform='rotate(180.000000, 72.000000, 256.000000)'></polygon>
|
||||
<polygon points='80.000000,304.000000 68.000000,298.399994 68.000000,309.600006' style='fill:#000' transform='rotate(180.000000, 72.000000, 304.000000)'></polygon>
|
||||
<polygon points='120.000000,208.000000 108.000000,202.399994 108.000000,213.600006' style='fill:#000' transform='rotate(0.000000, 112.000000, 208.000000)'></polygon>
|
||||
<polygon points='120.000000,256.000000 108.000000,250.399994 108.000000,261.600006' style='fill:#000' transform='rotate(0.000000, 112.000000, 256.000000)'></polygon>
|
||||
<polygon points='120.000000,304.000000 108.000000,298.399994 108.000000,309.600006' style='fill:#000' transform='rotate(0.000000, 112.000000, 304.000000)'></polygon>
|
||||
<polygon points='184.000000,368.000000 172.000000,362.399994 172.000000,373.600006' style='fill:#000' transform='rotate(90.000000, 176.000000, 368.000000)'></polygon>
|
||||
<polygon points='240.000000,304.000000 228.000000,298.399994 228.000000,309.600006' style='fill:#000' transform='rotate(180.000000, 232.000000, 304.000000)'></polygon>
|
||||
<polygon points='272.000000,208.000000 260.000000,202.399994 260.000000,213.600006' style='fill:#000' transform='rotate(0.000000, 264.000000, 208.000000)'></polygon>
|
||||
<path d='M 320,40 L 320,48' style='fill:none;stroke:#000;'></path>
|
||||
<polygon points='336.000000,48.000000 324.000000,42.400002 324.000000,53.599998' style='fill:#000' transform='rotate(270.000000, 320.000000, 48.000000)'></polygon>
|
||||
<path d='M 320,136 L 320,144' style='fill:none;stroke:#000;'></path>
|
||||
<polygon points='336.000000,144.000000 324.000000,138.399994 324.000000,149.600006' style='fill:#000' transform='rotate(270.000000, 320.000000, 144.000000)'></polygon>
|
||||
<path d='M 320,272 L 320,280' style='fill:none;stroke:#000;'></path>
|
||||
<polygon points='336.000000,272.000000 324.000000,266.399994 324.000000,277.600006' style='fill:#000' transform='rotate(90.000000, 320.000000, 272.000000)'></polygon>
|
||||
<polygon points='376.000000,208.000000 364.000000,202.399994 364.000000,213.600006' style='fill:#000' transform='rotate(180.000000, 368.000000, 208.000000)'></polygon>
|
||||
<path d='M 440,280 L 440,288' style='fill:none;stroke:#000;'></path>
|
||||
<polygon points='456.000000,288.000000 444.000000,282.399994 444.000000,293.600006' style='fill:#000' transform='rotate(270.000000, 440.000000, 288.000000)'></polygon>
|
||||
<polygon points='488.000000,256.000000 476.000000,250.399994 476.000000,261.600006' style='fill:#000' transform='rotate(180.000000, 480.000000, 256.000000)'></polygon>
|
||||
<polygon points='528.000000,256.000000 516.000000,250.399994 516.000000,261.600006' style='fill:#000' transform='rotate(0.000000, 520.000000, 256.000000)'></polygon>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='312' y='20' style='fill:#000;font-size:1em'>r</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='352' y='68' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='288' y='116' style='fill:#000;font-size:1em'>n</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='360' y='164' style='fill:#000;font-size:1em'>k</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='200' y='260' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='216' y='356' style='fill:#000;font-size:1em'>t</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='248' y='68' style='fill:#000;font-size:1em'>t</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='32' y='212' style='fill:#000;font-size:1em'>w</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='40' y='212' style='fill:#000;font-size:1em'>o</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='320' y='212' style='fill:#000;font-size:1em'>w</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='280' y='20' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='136' y='260' style='fill:#000;font-size:1em'>c</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='168' y='260' style='fill:#000;font-size:1em'>n</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='40' y='308' style='fill:#000;font-size:1em'>n</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='192' y='356' style='fill:#000;font-size:1em'>o</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='176' y='388' style='fill:#000;font-size:1em'>S</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='160' y='404' style='fill:#000;font-size:1em'>(</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='272' y='68' style='fill:#000;font-size:1em'>n</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='176' y='260' style='fill:#000;font-size:1em'>t</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='192' y='404' style='fill:#000;font-size:1em'>0</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='56' y='260' style='fill:#000;font-size:1em'>]</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='192' y='260' style='fill:#000;font-size:1em'>n</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='184' y='356' style='fill:#000;font-size:1em'>m</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='48' y='260' style='fill:#000;font-size:1em'>.</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='392' y='20' style='fill:#000;font-size:1em'>B</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='408' y='68' style='fill:#000;font-size:1em'>k</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='304' y='116' style='fill:#000;font-size:1em'>t</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='344' y='116' style='fill:#000;font-size:1em'>l</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='56' y='212' style='fill:#000;font-size:1em'>k</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='296' y='212' style='fill:#000;font-size:1em'>i</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='312' y='212' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='16' y='308' style='fill:#000;font-size:1em'>r</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='352' y='164' style='fill:#000;font-size:1em'>c</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='536' y='260' style='fill:#000;font-size:1em'>s</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='360' y='20' style='fill:#000;font-size:1em'>b</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='264' y='68' style='fill:#000;font-size:1em'>i</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='168' y='404' style='fill:#000;font-size:1em'>d</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='8' y='308' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='232' y='68' style='fill:#000;font-size:1em'>n</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='336' y='68' style='fill:#000;font-size:1em'>t</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='344' y='68' style='fill:#000;font-size:1em'>h</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='336' y='116' style='fill:#000;font-size:1em'>b</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='336' y='212' style='fill:#000;font-size:1em'>l</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='456' y='260' style='fill:#000;font-size:1em'>k</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='560' y='260' style='fill:#000;font-size:1em'>-</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='304' y='20' style='fill:#000;font-size:1em'>f</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='296' y='116' style='fill:#000;font-size:1em'>a</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='344' y='164' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='200' y='356' style='fill:#000;font-size:1em'>n</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='376' y='20' style='fill:#000;font-size:1em'>s</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='208' y='68' style='fill:#000;font-size:1em'>i</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='368' y='164' style='fill:#000;font-size:1em'>s</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='48' y='212' style='fill:#000;font-size:1em'>r</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='32' y='260' style='fill:#000;font-size:1em'>.</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='328' y='308' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='320' y='20' style='fill:#000;font-size:1em'>o</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='240' y='68' style='fill:#000;font-size:1em'>o</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='352' y='20' style='fill:#000;font-size:1em'>u</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='360' y='68' style='fill:#000;font-size:1em'>n</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='400' y='68' style='fill:#000;font-size:1em'>c</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='328' y='212' style='fill:#000;font-size:1em'>a</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='144' y='260' style='fill:#000;font-size:1em'>l</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='192' y='388' style='fill:#000;font-size:1em'>o</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='200' y='404' style='fill:#000;font-size:1em'>)</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='256' y='68' style='fill:#000;font-size:1em'>-</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='440' y='260' style='fill:#000;font-size:1em'>i</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='432' y='260' style='fill:#000;font-size:1em'>l</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='232' y='356' style='fill:#000;font-size:1em'>r</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='208' y='388' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='328' y='20' style='fill:#000;font-size:1em'>m</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='152' y='260' style='fill:#000;font-size:1em'>i</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='24' y='308' style='fill:#000;font-size:1em'>s</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='336' y='308' style='fill:#000;font-size:1em'>r</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='160' y='388' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='272' y='20' style='fill:#000;font-size:1em'>l</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='384' y='20' style='fill:#000;font-size:1em'>D</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='328' y='164' style='fill:#000;font-size:1em'>c</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='24' y='260' style='fill:#000;font-size:1em'>[</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='568' y='260' style='fill:#000;font-size:1em'>n</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='240' y='356' style='fill:#000;font-size:1em'>s</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='176' y='404' style='fill:#000;font-size:1em'>o</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='216' y='68' style='fill:#000;font-size:1em'>f</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='280' y='68' style='fill:#000;font-size:1em'>-</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='312' y='116' style='fill:#000;font-size:1em'>-</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='344' y='212' style='fill:#000;font-size:1em'>l</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='224' y='356' style='fill:#000;font-size:1em'>o</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='296' y='308' style='fill:#000;font-size:1em'>r</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='320' y='308' style='fill:#000;font-size:1em'>t</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='376' y='68' style='fill:#000;font-size:1em'>c</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='184' y='260' style='fill:#000;font-size:1em'>_</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='208' y='356' style='fill:#000;font-size:1em'>i</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='184' y='404' style='fill:#000;font-size:1em'>m</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='264' y='20' style='fill:#000;font-size:1em'>u</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='344' y='20' style='fill:#000;font-size:1em'>Q</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='208' y='260' style='fill:#000;font-size:1em'>t</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='576' y='260' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='48' y='308' style='fill:#000;font-size:1em'>a</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='384' y='68' style='fill:#000;font-size:1em'>h</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='40' y='260' style='fill:#000;font-size:1em'>.</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='584' y='260' style='fill:#000;font-size:1em'>t</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='0' y='308' style='fill:#000;font-size:1em'>p</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='56' y='308' style='fill:#000;font-size:1em'>l</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='168' y='388' style='fill:#000;font-size:1em'>n</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='200' y='388' style='fill:#000;font-size:1em'>r</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='368' y='20' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='424' y='260' style='fill:#000;font-size:1em'>p</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='288' y='20' style='fill:#000;font-size:1em'>s</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='288' y='68' style='fill:#000;font-size:1em'>n</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='448' y='260' style='fill:#000;font-size:1em'>n</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='544' y='260' style='fill:#000;font-size:1em'>y</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='152' y='388' style='fill:#000;font-size:1em'>X</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='160' y='260' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='416' y='260' style='fill:#000;font-size:1em'>u</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='552' y='260' style='fill:#000;font-size:1em'>s</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='392' y='68' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='320' y='116' style='fill:#000;font-size:1em'>t</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='336' y='164' style='fill:#000;font-size:1em'>h</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='32' y='308' style='fill:#000;font-size:1em'>o</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='312' y='308' style='fill:#000;font-size:1em'>u</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='296' y='68' style='fill:#000;font-size:1em'>a</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='304' y='68' style='fill:#000;font-size:1em'>t</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='304' y='212' style='fill:#000;font-size:1em'>r</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='256' y='20' style='fill:#000;font-size:1em'>r</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='328' y='116' style='fill:#000;font-size:1em'>a</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='304' y='308' style='fill:#000;font-size:1em'>o</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='184' y='388' style='fill:#000;font-size:1em'>t</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='224' y='68' style='fill:#000;font-size:1em'>-</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='352' y='116' style='fill:#000;font-size:1em'>e</text>
|
||||
<text text-anchor='middle' font-family='Menlo,Lucida Console,monospace' x='288' y='212' style='fill:#000;font-size:1em'>f</text>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 22 KiB |
26
diagrams/components.txt
Normal file
26
diagrams/components.txt
Normal file
@ -0,0 +1,26 @@
|
||||
+--------------------+
|
||||
| rules from QubesDB |
|
||||
+--------------------+
|
||||
^
|
||||
if-not-in-nat | then check
|
||||
|
|
||||
+-----------+
|
||||
| nat-table |
|
||||
+-----------+
|
||||
^
|
||||
|checks
|
||||
|
|
||||
+------------+ +-----+----+
|
||||
work <---->| +---->| firewall |<--------.
|
||||
| | +-----+----+ |
|
||||
| | | +----+---+
|
||||
[...] <---->| client_net | | | uplink |<----> sys-net
|
||||
| | v +--------+
|
||||
| | +----------+ ^
|
||||
personal <---->| |<----+ router +---------'
|
||||
+------+-----+ +----------+
|
||||
|
|
||||
|monitors
|
||||
v
|
||||
XenStore
|
||||
(dom0)
|
635
dispatcher.ml
Normal file
635
dispatcher.ml
Normal file
@ -0,0 +1,635 @@
|
||||
open Lwt.Infix
|
||||
open Fw_utils
|
||||
module Netback = Backend.Make (Xenstore.Make (Xen_os.Xs))
|
||||
module ClientEth = Ethernet.Make (Netback)
|
||||
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)
|
||||
|
||||
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 = []
|
||||
method get_rules = rules
|
||||
method set_rules new_db = rules <- Dao.read_rules new_db 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 =
|
||||
Lwt.catch
|
||||
(fun () ->
|
||||
ClientEth.write eth client_mac 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_unit)
|
||||
|
||||
method log_header = log_header
|
||||
end
|
||||
|
||||
class netvm_iface eth mac ~my_ip ~other_ip : interface =
|
||||
object
|
||||
method my_mac = UplinkEth.mac eth
|
||||
method my_ip = my_ip
|
||||
method other_ip = other_ip
|
||||
|
||||
method writev ethertype fillfn =
|
||||
Lwt.catch
|
||||
(fun () ->
|
||||
mac >>= fun dst ->
|
||||
UplinkEth.write eth dst ethertype fillfn
|
||||
>|= or_raise "Write to uplink" UplinkEth.pp_error)
|
||||
(fun ex ->
|
||||
Log.err (fun f ->
|
||||
f "uncaught exception trying to send to uplink: @[%s@]"
|
||||
(Printexc.to_string ex));
|
||||
Lwt.return_unit)
|
||||
end
|
||||
|
||||
type uplink = {
|
||||
net : Netif.t;
|
||||
eth : UplinkEth.t;
|
||||
arp : Arp.t;
|
||||
interface : interface;
|
||||
mutable fragments : Fragments.Cache.t;
|
||||
ip : I.t;
|
||||
udp : U.t;
|
||||
}
|
||||
|
||||
type t = {
|
||||
uplink_connected : unit Lwt_condition.t;
|
||||
uplink_disconnect : unit Lwt_condition.t;
|
||||
uplink_disconnected : unit Lwt_condition.t;
|
||||
mutable config : Dao.network_config;
|
||||
clients : Client_eth.t;
|
||||
nat : My_nat.t;
|
||||
mutable uplink : uplink option;
|
||||
}
|
||||
|
||||
let create ~config ~clients ~nat ~uplink =
|
||||
{
|
||||
uplink_connected = Lwt_condition.create ();
|
||||
uplink_disconnect = Lwt_condition.create ();
|
||||
uplink_disconnected = Lwt_condition.create ();
|
||||
config;
|
||||
clients;
|
||||
nat;
|
||||
uplink;
|
||||
}
|
||||
|
||||
let update t ~config ~uplink =
|
||||
t.config <- config;
|
||||
t.uplink <- uplink;
|
||||
Lwt.return_unit
|
||||
|
||||
let target t buf =
|
||||
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 *)
|
||||
match t.uplink with
|
||||
| None -> (
|
||||
match Client_eth.lookup t.clients t.config.netvm_ip with
|
||||
| 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);
|
||||
None)
|
||||
| Some uplink -> Some uplink.interface)
|
||||
|
||||
let add_client t = Client_eth.add_client t.clients
|
||||
let remove_client t = Client_eth.remove_client t.clients
|
||||
|
||||
let classify t ip =
|
||||
if ip = Ipaddr.V4 t.config.our_ip then `Firewall
|
||||
else if ip = Ipaddr.V4 t.config.netvm_ip then `NetVM
|
||||
else (Client_eth.classify t.clients ip :> Packet.host)
|
||||
|
||||
let resolve t = function
|
||||
| `Firewall -> Ipaddr.V4 t.config.our_ip
|
||||
| `NetVM -> Ipaddr.V4 t.config.netvm_ip
|
||||
| #Client_eth.host as host -> Client_eth.resolve t.clients host
|
||||
|
||||
(* Transmission *)
|
||||
|
||||
let transmit_ipv4 packet iface =
|
||||
Lwt.catch
|
||||
(fun () ->
|
||||
let fragments = ref [] in
|
||||
iface#writev `IPv4 (fun b ->
|
||||
match Nat_packet.into_cstruct packet b with
|
||||
| Error e ->
|
||||
Log.warn (fun f ->
|
||||
f "Failed to write packet to %a: %a" Ipaddr.V4.pp
|
||||
iface#other_ip Nat_packet.pp_error e);
|
||||
0
|
||||
| Ok (n, frags) ->
|
||||
fragments := frags;
|
||||
n)
|
||||
>>= fun () ->
|
||||
Lwt_list.iter_s
|
||||
(fun f ->
|
||||
let size = Cstruct.length f in
|
||||
iface#writev `IPv4 (fun b ->
|
||||
Cstruct.blit f 0 b 0 size;
|
||||
size))
|
||||
!fragments)
|
||||
(fun ex ->
|
||||
Log.warn (fun f ->
|
||||
f "Failed to write packet to %a: %s" Ipaddr.V4.pp iface#other_ip
|
||||
(Printexc.to_string ex));
|
||||
Lwt.return_unit)
|
||||
|
||||
let forward_ipv4 t packet =
|
||||
let (`IPv4 (ip, _)) = packet in
|
||||
Lwt.catch
|
||||
(fun () ->
|
||||
match target t ip with
|
||||
| Some iface -> transmit_ipv4 packet iface
|
||||
| None -> Lwt.return_unit)
|
||||
(fun ex ->
|
||||
let dst_ip = ip.Ipv4_packet.dst in
|
||||
Log.warn (fun f ->
|
||||
f "Failed to lookup for target %a: %s" Ipaddr.V4.pp dst_ip
|
||||
(Printexc.to_string ex));
|
||||
Lwt.return_unit)
|
||||
|
||||
(* NAT *)
|
||||
|
||||
let translate t packet = My_nat.translate t.nat packet
|
||||
|
||||
(* Add a NAT rule for the endpoints in this frame, via a random port on the firewall. *)
|
||||
let add_nat_and_forward_ipv4 t packet =
|
||||
let xl_host = t.config.our_ip in
|
||||
match My_nat.add_nat_rule_and_translate t.nat ~xl_host `NAT packet with
|
||||
| Ok packet -> forward_ipv4 t packet
|
||||
| Error e ->
|
||||
Log.warn (fun f ->
|
||||
f "Failed to add NAT rewrite rule: %s (%a)" e Nat_packet.pp packet);
|
||||
Lwt.return_unit
|
||||
|
||||
(* Add a NAT rule to redirect this conversation to [host:port] instead of us. *)
|
||||
let nat_to t ~host ~port packet =
|
||||
match resolve t host with
|
||||
| Ipaddr.V6 _ ->
|
||||
Log.warn (fun f -> f "Cannot NAT with IPv6");
|
||||
Lwt.return_unit
|
||||
| Ipaddr.V4 target -> (
|
||||
let xl_host = t.config.our_ip in
|
||||
match
|
||||
My_nat.add_nat_rule_and_translate t.nat ~xl_host
|
||||
(`Redirect (target, port))
|
||||
packet
|
||||
with
|
||||
| Ok packet -> forward_ipv4 t packet
|
||||
| Error e ->
|
||||
Log.warn (fun f ->
|
||||
f "Failed to add NAT redirect rule: %s (%a)" e Nat_packet.pp
|
||||
packet);
|
||||
Lwt.return_unit)
|
||||
|
||||
let apply_rules t (rules : ('a, 'b) Packet.t -> Packet.action Lwt.t) ~dst
|
||||
(annotated_packet : ('a, 'b) Packet.t) : unit Lwt.t =
|
||||
let packet = Packet.to_mirage_nat_packet annotated_packet in
|
||||
rules annotated_packet >>= fun action ->
|
||||
match (action, dst) with
|
||||
| `Accept, `Client client_link -> transmit_ipv4 packet client_link
|
||||
| `Accept, (`External _ | `NetVM) -> (
|
||||
match t.uplink with
|
||||
| Some uplink -> transmit_ipv4 packet uplink.interface
|
||||
| None -> (
|
||||
match Client_eth.lookup t.clients t.config.netvm_ip with
|
||||
| Some iface -> transmit_ipv4 packet iface
|
||||
| None ->
|
||||
Log.warn (fun f ->
|
||||
f "No output interface for %a : drop" Nat_packet.pp packet);
|
||||
Lwt.return_unit))
|
||||
| `Accept, `Firewall ->
|
||||
Log.warn (fun f ->
|
||||
f "Bad rule: firewall can't accept packets %a" Nat_packet.pp packet);
|
||||
Lwt.return_unit
|
||||
| `NAT, _ ->
|
||||
Log.debug (fun f -> f "adding NAT rule for %a" Nat_packet.pp packet);
|
||||
add_nat_and_forward_ipv4 t packet
|
||||
| `NAT_to (host, port), _ -> nat_to t packet ~host ~port
|
||||
| `Drop reason, _ ->
|
||||
Log.debug (fun f ->
|
||||
f "Dropped packet (%s) %a" reason Nat_packet.pp packet);
|
||||
Lwt.return_unit
|
||||
|
||||
let ipv4_from_netvm t packet =
|
||||
match Memory_pressure.status () with
|
||||
| `Memory_critical -> Lwt.return_unit
|
||||
| `Ok -> (
|
||||
let (`IPv4 (ip, _transport)) = packet in
|
||||
let src = classify t (Ipaddr.V4 ip.Ipv4_packet.src) in
|
||||
let dst = classify t (Ipaddr.V4 ip.Ipv4_packet.dst) in
|
||||
match Packet.of_mirage_nat_packet ~src ~dst packet with
|
||||
| None -> Lwt.return_unit
|
||||
| Some _ -> (
|
||||
match src with
|
||||
| `Client _ | `Firewall ->
|
||||
Log.warn (fun f ->
|
||||
f "Frame from NetVM has internal source IP address! %a"
|
||||
Nat_packet.pp packet);
|
||||
Lwt.return_unit
|
||||
| (`External _ | `NetVM) as src -> (
|
||||
match translate t packet with
|
||||
| Some frame -> forward_ipv4 t frame
|
||||
| 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)))
|
||||
)
|
||||
|
||||
let ipv4_from_client resolver dns_servers t ~src packet =
|
||||
match Memory_pressure.status () with
|
||||
| `Memory_critical -> Lwt.return_unit
|
||||
| `Ok -> (
|
||||
(* Check for existing NAT entry for this packet *)
|
||||
match translate t packet with
|
||||
| Some frame ->
|
||||
forward_ipv4 t frame (* Some existing connection or redirect *)
|
||||
| None -> (
|
||||
(* No existing NAT entry. Check the firewall rules. *)
|
||||
let (`IPv4 (ip, _transport)) = packet in
|
||||
match classify t (Ipaddr.V4 ip.Ipv4_packet.src) with
|
||||
| `Client _ | `Firewall -> (
|
||||
let dst = classify t (Ipaddr.V4 ip.Ipv4_packet.dst) in
|
||||
match
|
||||
Packet.of_mirage_nat_packet ~src:(`Client src) ~dst packet
|
||||
with
|
||||
| None -> Lwt.return_unit
|
||||
| Some firewall_packet ->
|
||||
apply_rules t
|
||||
(Rules.from_client resolver dns_servers)
|
||||
~dst firewall_packet)
|
||||
| `NetVM -> ipv4_from_netvm t packet
|
||||
| `External _ ->
|
||||
Log.warn (fun f ->
|
||||
f "Frame from Inside has external source IP address! %a"
|
||||
Nat_packet.pp packet);
|
||||
Lwt.return_unit))
|
||||
|
||||
(** Handle an ARP message from the client. *)
|
||||
let client_handle_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_unit
|
||||
| Ok arp -> (
|
||||
match Client_eth.ARP.input fixed_arp arp with
|
||||
| None -> Lwt.return_unit
|
||||
| Some response ->
|
||||
Lwt.catch
|
||||
(fun () ->
|
||||
iface#writev `ARP (fun b ->
|
||||
Arp_packet.encode_into response b;
|
||||
Arp_packet.size))
|
||||
(fun ex ->
|
||||
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))
|
||||
|
||||
(** Handle an IPv4 packet from the client. *)
|
||||
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
|
||||
| Error e ->
|
||||
Log.warn (fun f ->
|
||||
f "Ignored unknown IPv4 message: %a" Nat_packet.pp_error e);
|
||||
Lwt.return_unit
|
||||
| Ok None -> Lwt.return_unit
|
||||
| Ok (Some packet) ->
|
||||
let (`IPv4 (ip, _)) = packet in
|
||||
let src = ip.Ipv4_packet.src in
|
||||
if src = iface#other_ip then
|
||||
ipv4_from_client dns_client dns_servers router ~src:iface packet
|
||||
else if iface#other_ip = router.config.netvm_ip then
|
||||
(* This can occurs when used with *BSD as netvm (and a gateway is set) *)
|
||||
ipv4_from_netvm router 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);
|
||||
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 () =
|
||||
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));
|
||||
|
||||
(* update the rules whenever QubesDB notices a change for this IP *)
|
||||
let qubesdb_updater =
|
||||
Lwt.catch
|
||||
(fun () ->
|
||||
let rec update current_db current_rules =
|
||||
Qubes.DB.got_new_commit qubesDB (Dao.db_root client_ip) current_db
|
||||
>>= fun new_db ->
|
||||
iface#set_rules new_db;
|
||||
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))
|
||||
else (
|
||||
Log.info (fun m ->
|
||||
m "New firewall rules for %s@.%a"
|
||||
(Ipaddr.V4.to_string client_ip)
|
||||
Fmt.(list ~sep:(any "@.") Pf_qubes.Parse_qubes.pp_rule)
|
||||
new_rules);
|
||||
(* empty NAT table if rules are updated: they might deny old connections *)
|
||||
My_nat.remove_connections router.nat client_ip);
|
||||
update new_db new_rules
|
||||
in
|
||||
update Qubes.DB.KeyMap.empty [])
|
||||
(function Lwt.Canceled -> Lwt.return_unit | e -> Lwt.fail e)
|
||||
in
|
||||
Cleanup.on_cleanup cleanup_tasks (fun () -> Lwt.cancel qubesdb_updater);
|
||||
|
||||
let fixed_arp = Client_eth.ARP.create ~net:client_eth iface in
|
||||
let fragment_cache = ref (Fragments.Cache.empty (256 * 1024)) in
|
||||
let listener =
|
||||
Lwt.catch
|
||||
(fun () ->
|
||||
Netback.listen backend ~header_size:Ethernet.Packet.sizeof_ethernet
|
||||
(fun frame ->
|
||||
match Ethernet.Packet.of_cstruct frame with
|
||||
| Error err ->
|
||||
Log.warn (fun f -> f "Invalid Ethernet frame: %s" err);
|
||||
Lwt.return_unit
|
||||
| Ok (eth, payload) -> (
|
||||
match eth.Ethernet.Packet.ethertype with
|
||||
| `ARP -> client_handle_arp ~fixed_arp ~iface payload
|
||||
| `IPv4 ->
|
||||
client_handle_ipv4 get_ts fragment_cache ~iface ~router
|
||||
dns_client dns_servers payload
|
||||
| `IPv6 -> Lwt.return_unit (* TODO: oh no! *)))
|
||||
>|= or_raise "Listen on client interface" Netback.pp_error)
|
||||
(function Lwt.Canceled -> Lwt.return_unit | e -> Lwt.fail e)
|
||||
in
|
||||
Cleanup.on_cleanup cleanup_tasks (fun () -> Lwt.cancel listener);
|
||||
(* NOTE(dinosaure): [qubes_updater] and [listener] can be forgotten, our [cleanup_task]
|
||||
will cancel them if the client is disconnected. *)
|
||||
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. *)
|
||||
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
|
||||
Log.info (fun f ->
|
||||
f "add client vif %a with IP %a" Dao.ClientVif.pp vif Ipaddr.V4.pp
|
||||
client_ip);
|
||||
let { Dao.ClientVif.domid; device_id } = vif in
|
||||
let* backend = Netback.make ~domid ~device_id in
|
||||
let* eth = ClientEth.connect backend in
|
||||
let client_mac = Netback.frontend_mac backend in
|
||||
let client_eth = router.clients 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
|
||||
|
||||
Cleanup.on_cleanup cleanup_tasks (fun () -> remove_client router iface);
|
||||
Lwt.async (fun () ->
|
||||
Lwt.catch
|
||||
(fun () -> add_client router iface)
|
||||
(fun ex ->
|
||||
Log.warn (fun f ->
|
||||
f "Error with client %a: %s" Dao.ClientVif.pp vif
|
||||
(Printexc.to_string ex));
|
||||
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)
|
||||
@@ fun exn ->
|
||||
Log.warn (fun f ->
|
||||
f "Error with client %a: %s" Dao.ClientVif.pp vif
|
||||
(Printexc.to_string exn));
|
||||
Lwt.return_unit
|
||||
in
|
||||
Lwt.return cleanup_tasks
|
||||
|
||||
(** Watch XenStore for notifications of new clients. *)
|
||||
let wait_clients get_ts dns_client dns_servers qubesDB router =
|
||||
let open Lwt.Syntax in
|
||||
let clients : Cleanup.t Dao.VifMap.t ref = ref Dao.VifMap.empty in
|
||||
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 (
|
||||
clients := !clients |> Dao.VifMap.remove key;
|
||||
Log.info (fun f -> f "client %a has gone" Dao.ClientVif.pp key);
|
||||
Cleanup.cleanup cleanup)
|
||||
in
|
||||
Dao.VifMap.iter clean_up_clients !clients;
|
||||
(* Check for added clients *)
|
||||
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
|
||||
Log.debug (fun f -> f "client %a arrived" Dao.ClientVif.pp key);
|
||||
clients := Dao.VifMap.add key cleanup !clients;
|
||||
go seq
|
||||
| Some (_, seq) -> go seq
|
||||
in
|
||||
go (Dao.VifMap.to_seq new_set)
|
||||
|
||||
let send_dns_client_query t ~src_port ~dst ~dst_port buf =
|
||||
match t.uplink with
|
||||
| None ->
|
||||
Log.err (fun f -> f "No uplink interface");
|
||||
Lwt.return (Error (`Msg "failure"))
|
||||
| Some uplink ->
|
||||
Lwt.catch
|
||||
(fun () ->
|
||||
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@]"
|
||||
(Printexc.to_string ex));
|
||||
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 =
|
||||
Lwt_condition.wait router.uplink_connected >>= fun () ->
|
||||
match router.uplink with
|
||||
| None ->
|
||||
Log.err (fun f ->
|
||||
f "Uplink is connected but not found in the router, retrying...%!");
|
||||
uplink_listen get_ts dns_responses router
|
||||
| Some uplink ->
|
||||
let listen =
|
||||
Lwt.catch
|
||||
(fun () ->
|
||||
Netif.listen uplink.net ~header_size:Ethernet.Packet.sizeof_ethernet
|
||||
(fun frame ->
|
||||
(* Handle one Ethernet frame from NetVM *)
|
||||
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
|
||||
in
|
||||
uplink.fragments <- cache;
|
||||
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))) -> (
|
||||
let open Udp_packet in
|
||||
Log.debug (fun f ->
|
||||
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"
|
||||
header.dst_port);
|
||||
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 ->
|
||||
(* 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");
|
||||
U.disconnect uplink.udp >>= fun () ->
|
||||
I.disconnect uplink.ip >>= fun () ->
|
||||
(* mutable fragments : Fragments.Cache.t; *)
|
||||
(* interface : interface; *)
|
||||
Arp.disconnect uplink.arp >>= fun () ->
|
||||
UplinkEth.disconnect uplink.eth >>= fun () ->
|
||||
Netif.disconnect uplink.net >>= fun () ->
|
||||
Lwt_condition.broadcast router.uplink_disconnected ();
|
||||
Lwt.return_unit
|
||||
| e -> Lwt.fail e)
|
||||
in
|
||||
let reconnect_uplink =
|
||||
Lwt_condition.wait router.uplink_disconnect >>= fun () ->
|
||||
Log.info (fun f -> f "we need to reconnect to the new uplink");
|
||||
Lwt.return_unit
|
||||
in
|
||||
Lwt.pick [ listen; reconnect_uplink ] >>= fun () ->
|
||||
uplink_listen get_ts dns_responses router
|
||||
|
||||
(** Connect to our uplink backend (we must have an uplink here...). *)
|
||||
let connect config =
|
||||
let my_ip = config.Dao.our_ip in
|
||||
let gateway = config.Dao.netvm_ip in
|
||||
Netif.connect "0" >>= fun net ->
|
||||
UplinkEth.connect net >>= fun eth ->
|
||||
Arp.connect eth >>= fun arp ->
|
||||
Arp.add_ip arp my_ip >>= fun () ->
|
||||
let cidr = Ipaddr.V4.Prefix.make 0 my_ip in
|
||||
I.connect ~cidr ~gateway eth arp >>= fun ip ->
|
||||
U.connect ip >>= fun udp ->
|
||||
let netvm_mac =
|
||||
Arp.query arp gateway >>= function
|
||||
| Error e ->
|
||||
Log.err (fun f -> f "Getting MAC of our NetVM: %a" Arp.pp_error e);
|
||||
(* This mac address is a special address used by Qubes when the device
|
||||
is not managed by Qubes itself. This can occurs inside a service
|
||||
AppVM (e.g. VPN) when the service creates a new interface. *)
|
||||
Lwt.return (Macaddr.of_string_exn "fe:ff:ff:ff:ff:ff")
|
||||
| Ok mac -> Lwt.return mac
|
||||
in
|
||||
let interface =
|
||||
new netvm_iface eth netvm_mac ~my_ip ~other_ip:config.Dao.netvm_ip
|
||||
in
|
||||
let fragments = Fragments.Cache.empty (256 * 1024) in
|
||||
Lwt.return { net; eth; arp; interface; fragments; ip; udp }
|
||||
|
||||
(** Wait Xenstore for our uplink changes (we must have an uplink here...). *)
|
||||
let uplink_wait_update qubesDB router =
|
||||
let rec aux current_db =
|
||||
let netvm = "/qubes-gateway" in
|
||||
Log.info (fun f -> f "Waiting for netvm changes to %S...%!" netvm);
|
||||
Qubes.DB.after qubesDB current_db >>= fun new_db ->
|
||||
(match (router.uplink, Qubes.DB.KeyMap.find_opt netvm new_db) with
|
||||
| Some uplink, Some netvm
|
||||
when not
|
||||
(String.equal netvm
|
||||
(Ipaddr.V4.to_string uplink.interface#other_ip)) ->
|
||||
Log.info (fun f ->
|
||||
f "Our netvm IP has changed, before it was %s, now it's: %s%!"
|
||||
(Ipaddr.V4.to_string uplink.interface#other_ip)
|
||||
netvm);
|
||||
Lwt_condition.broadcast router.uplink_disconnect ();
|
||||
(* wait for uplink disconnexion *)
|
||||
Lwt_condition.wait router.uplink_disconnected >>= fun () ->
|
||||
Dao.read_network_config qubesDB >>= fun config ->
|
||||
Dao.print_network_config config;
|
||||
connect config >>= fun uplink ->
|
||||
update router ~config ~uplink:(Some uplink) >>= fun () ->
|
||||
Lwt_condition.broadcast router.uplink_connected ();
|
||||
Lwt.return_unit
|
||||
| None, Some _ ->
|
||||
(* a new interface is attributed to qubes-mirage-firewall *)
|
||||
Log.info (fun f -> f "Going from netvm not connected to %s%!" netvm);
|
||||
Dao.read_network_config qubesDB >>= fun config ->
|
||||
Dao.print_network_config config;
|
||||
connect config >>= fun uplink ->
|
||||
update router ~config ~uplink:(Some uplink) >>= fun () ->
|
||||
Lwt_condition.broadcast router.uplink_connected ();
|
||||
Lwt.return_unit
|
||||
| Some _, None ->
|
||||
(* This currently is never triggered :( *)
|
||||
Log.info (fun f ->
|
||||
f "TODO: Our netvm disapeared, troubles are coming!%!");
|
||||
Lwt.return_unit
|
||||
| Some _, Some _ (* The new netvm IP is unchanged (it's our old netvm IP) *)
|
||||
| None, None ->
|
||||
Log.info (fun f ->
|
||||
f "QubesDB has changed but not the situation of our netvm!%!");
|
||||
Lwt.return_unit)
|
||||
>>= fun () -> aux new_db
|
||||
in
|
||||
aux Qubes.DB.KeyMap.empty
|
199
firewall.ml
199
firewall.ml
@ -1,199 +0,0 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
open Utils
|
||||
open Packet
|
||||
|
||||
let src = Logs.Src.create "firewall" ~doc:"Packet handler"
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
(* Transmission *)
|
||||
|
||||
let transmit ~frame iface =
|
||||
(* If packet has been NAT'd then we certainly need to recalculate the checksum,
|
||||
but even for direct pass-through it might have been received with an invalid
|
||||
checksum due to checksum offload. For now, recalculate full checksum in all
|
||||
cases. *)
|
||||
let frame = fixup_checksums frame |> Cstruct.concat in
|
||||
let packet = Cstruct.shift frame Wire_structs.sizeof_ethernet in
|
||||
iface#writev [packet]
|
||||
|
||||
let forward_ipv4 t frame =
|
||||
let packet = Cstruct.shift frame Wire_structs.sizeof_ethernet in
|
||||
match Router.target t packet with
|
||||
| Some iface -> transmit ~frame iface
|
||||
| None -> return ()
|
||||
|
||||
(* Packet classification *)
|
||||
|
||||
let ports transport =
|
||||
let sport, dport = Nat_rewrite.ports_of_transport transport in
|
||||
{ sport; dport }
|
||||
|
||||
let classify t frame =
|
||||
match Nat_rewrite.layers frame with
|
||||
| None ->
|
||||
Log.warn (fun f -> f "Failed to parse frame");
|
||||
None
|
||||
| Some (_eth, ip, transport) ->
|
||||
let src, dst = Nat_rewrite.addresses_of_ip ip in
|
||||
let proto =
|
||||
match Nat_rewrite.proto_of_ip ip with
|
||||
| 1 -> `ICMP
|
||||
| 6 -> `TCP (ports transport)
|
||||
| 17 -> `UDP (ports transport)
|
||||
| _ -> `Unknown in
|
||||
Some {
|
||||
frame;
|
||||
src = Router.classify t src;
|
||||
dst = Router.classify t dst;
|
||||
proto;
|
||||
}
|
||||
|
||||
let pp_ports fmt {sport; dport} =
|
||||
Format.fprintf fmt "sport=%d dport=%d" sport dport
|
||||
|
||||
let pp_host fmt = function
|
||||
| `Client c -> Ipaddr.V4.pp_hum fmt (c#other_ip)
|
||||
| `Unknown_client ip -> Format.fprintf fmt "unknown-client(%a)" Ipaddr.pp_hum ip
|
||||
| `NetVM -> Format.pp_print_string fmt "net-vm"
|
||||
| `External ip -> Format.fprintf fmt "external(%a)" Ipaddr.pp_hum ip
|
||||
| `Firewall_uplink -> Format.pp_print_string fmt "firewall(uplink)"
|
||||
| `Client_gateway -> Format.pp_print_string fmt "firewall(client-gw)"
|
||||
|
||||
let pp_proto fmt = function
|
||||
| `UDP ports -> Format.fprintf fmt "UDP(%a)" pp_ports ports
|
||||
| `TCP ports -> Format.fprintf fmt "TCP(%a)" pp_ports ports
|
||||
| `ICMP -> Format.pp_print_string fmt "ICMP"
|
||||
| `Unknown -> Format.pp_print_string fmt "UnknownProtocol"
|
||||
|
||||
let pp_packet fmt {src; dst; proto; frame = _} =
|
||||
Format.fprintf fmt "[src=%a dst=%a proto=%a]"
|
||||
pp_host src
|
||||
pp_host dst
|
||||
pp_proto proto
|
||||
|
||||
(* NAT *)
|
||||
|
||||
let translate t frame =
|
||||
Nat_rewrite.translate t.Router.nat frame
|
||||
|
||||
let random_user_port () =
|
||||
1024 + Random.int (0xffff - 1024)
|
||||
|
||||
let rec add_nat_rule_and_transmit ?(retries=100) t frame fn logf =
|
||||
let xl_port = random_user_port () in
|
||||
match fn xl_port with
|
||||
| exception Out_of_memory ->
|
||||
(* Because hash tables resize in big steps, this can happen even if we have a fair
|
||||
chunk of free memory. *)
|
||||
Log.warn (fun f -> f "Out_of_memory adding NAT rule. Dropping NAT table...");
|
||||
Router.reset t;
|
||||
add_nat_rule_and_transmit ~retries:(retries - 1) t frame fn logf
|
||||
| Nat_rewrite.Overlap when retries < 0 -> return ()
|
||||
| Nat_rewrite.Overlap ->
|
||||
if retries = 0 then (
|
||||
Log.warn (fun f -> f "Failed to find a free port; resetting NAT table");
|
||||
Router.reset t;
|
||||
);
|
||||
add_nat_rule_and_transmit ~retries:(retries - 1) t frame fn logf (* Try a different port *)
|
||||
| Nat_rewrite.Unparseable ->
|
||||
Log.warn (fun f -> f "Failed to add NAT rule: Unparseable");
|
||||
return ()
|
||||
| Nat_rewrite.Ok _ ->
|
||||
Log.debug (logf xl_port);
|
||||
match translate t frame with
|
||||
| Some frame -> forward_ipv4 t frame
|
||||
| None ->
|
||||
Log.warn (fun f -> f "No NAT entry, even after adding one!");
|
||||
return ()
|
||||
|
||||
(* Add a NAT rule for the endpoints in this frame, via a random port on the firewall. *)
|
||||
let add_nat_and_forward_ipv4 t ~frame =
|
||||
let xl_host = Ipaddr.V4 t.Router.uplink#my_ip in
|
||||
add_nat_rule_and_transmit t frame
|
||||
(* Note: DO NOT partially apply; [t.nat] may change between calls *)
|
||||
(fun xl_port -> Nat_rewrite.make_nat_entry t.Router.nat frame xl_host xl_port)
|
||||
(fun xl_port f ->
|
||||
match Nat_rewrite.layers frame with
|
||||
| None -> assert false
|
||||
| Some (_eth, ip, transport) ->
|
||||
let src, dst = Nat_rewrite.addresses_of_ip ip in
|
||||
let sport, dport = Nat_rewrite.ports_of_transport transport in
|
||||
f "added NAT entry: %s:%d -> firewall:%d -> %d:%s" (Ipaddr.to_string src) sport xl_port dport (Ipaddr.to_string dst)
|
||||
)
|
||||
|
||||
(* Add a NAT rule to redirect this conversation to [host:port] instead of us. *)
|
||||
let nat_to t ~frame ~host ~port =
|
||||
let target = Router.resolve t host in
|
||||
let xl_host = Ipaddr.V4 t.Router.uplink#my_ip in
|
||||
add_nat_rule_and_transmit t frame
|
||||
(fun xl_port ->
|
||||
Nat_rewrite.make_redirect_entry t.Router.nat frame (xl_host, xl_port) (target, port)
|
||||
)
|
||||
(fun xl_port f ->
|
||||
match Nat_rewrite.layers frame with
|
||||
| None -> assert false
|
||||
| Some (_eth, ip, transport) ->
|
||||
let src, _dst = Nat_rewrite.addresses_of_ip ip in
|
||||
let sport, dport = Nat_rewrite.ports_of_transport transport in
|
||||
f "added NAT redirect %s:%d -> %d:firewall:%d -> %d:%a"
|
||||
(Ipaddr.to_string src) sport dport xl_port port pp_host host
|
||||
)
|
||||
|
||||
(* Handle incoming packets *)
|
||||
|
||||
let apply_rules t rules info =
|
||||
let frame = info.frame in
|
||||
match rules info, info.dst with
|
||||
| `Accept, `Client client_link -> transmit ~frame client_link
|
||||
| `Accept, (`External _ | `NetVM) -> transmit ~frame t.Router.uplink
|
||||
| `Accept, `Unknown_client _ ->
|
||||
Log.warn (fun f -> f "Dropping packet to unknown client %a" pp_packet info);
|
||||
return ()
|
||||
| `Accept, (`Firewall_uplink | `Client_gateway) ->
|
||||
Log.warn (fun f -> f "Bad rule: firewall can't accept packets %a" pp_packet info);
|
||||
return ()
|
||||
| `NAT, _ -> add_nat_and_forward_ipv4 t ~frame
|
||||
| `NAT_to (host, port), _ -> nat_to t ~frame ~host ~port
|
||||
| `Drop reason, _ ->
|
||||
Log.info (fun f -> f "Dropped packet (%s) %a" reason pp_packet info);
|
||||
return ()
|
||||
|
||||
let handle_low_memory t =
|
||||
match Memory_pressure.status () with
|
||||
| `Memory_critical -> (* TODO: should happen before copying and async *)
|
||||
Log.warn (fun f -> f "Memory low - dropping packet and resetting NAT table");
|
||||
Router.reset t;
|
||||
`Memory_critical
|
||||
| `Ok -> `Ok
|
||||
|
||||
let ipv4_from_client t frame =
|
||||
match handle_low_memory t with
|
||||
| `Memory_critical -> return ()
|
||||
| `Ok ->
|
||||
(* Check for existing NAT entry for this packet *)
|
||||
match translate t frame with
|
||||
| Some frame -> forward_ipv4 t frame (* Some existing connection or redirect *)
|
||||
| None ->
|
||||
(* No existing NAT entry. Check the firewall rules. *)
|
||||
match classify t frame with
|
||||
| None -> return ()
|
||||
| Some info -> apply_rules t Rules.from_client info
|
||||
|
||||
let ipv4_from_netvm t frame =
|
||||
match handle_low_memory t with
|
||||
| `Memory_critical -> return ()
|
||||
| `Ok ->
|
||||
match classify t frame with
|
||||
| None -> return ()
|
||||
| Some info ->
|
||||
match info.src with
|
||||
| `Client _ | `Unknown_client _ | `Firewall_uplink | `Client_gateway ->
|
||||
Log.warn (fun f -> f "Frame from NetVM has internal source IP address! %a" pp_packet info);
|
||||
return ()
|
||||
| `External _ | `NetVM ->
|
||||
match translate t frame with
|
||||
| Some frame -> forward_ipv4 t frame
|
||||
| None ->
|
||||
apply_rules t Rules.from_netvm info
|
11
firewall.mli
11
firewall.mli
@ -1,11 +0,0 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
(** Classify IP packets, apply rules and send as appropriate. *)
|
||||
|
||||
val ipv4_from_netvm : Router.t -> Cstruct.t -> unit Lwt.t
|
||||
(** Handle a frame from the outside world (this module will validate the source IP). *)
|
||||
|
||||
val ipv4_from_client : Router.t -> Cstruct.t -> unit Lwt.t
|
||||
(** Handle a frame from a client. Caller must check the source IP matches the client's
|
||||
before calling this. *)
|
25
frameQ.ml
25
frameQ.ml
@ -1,25 +0,0 @@
|
||||
(* Copyright (C) 2016, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
let src = Logs.Src.create "frameQ" ~doc:"Interface output queue"
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
type t = {
|
||||
name : string;
|
||||
mutable items : int;
|
||||
}
|
||||
|
||||
let create name = { name; items = 0 }
|
||||
|
||||
let send q fn =
|
||||
(* TODO: drop if queue too long *)
|
||||
let sent = fn () in
|
||||
if Lwt.state sent = Lwt.Sleep then (
|
||||
q.items <- q.items + 1;
|
||||
Log.info (fun f -> f "Queue length for %s: incr to %d" q.name q.items);
|
||||
Lwt.on_termination sent (fun () ->
|
||||
q.items <- q.items - 1;
|
||||
Log.info (fun f -> f "Queue length for %s: decr to %d" q.name q.items);
|
||||
)
|
||||
);
|
||||
sent
|
15
frameQ.mli
15
frameQ.mli
@ -1,15 +0,0 @@
|
||||
(* Copyright (C) 2016, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
(** Keep track of the queue length for output buffers. *)
|
||||
|
||||
type t
|
||||
|
||||
val create : string -> t
|
||||
(** [create name] is a new empty queue. [name] is used in log messages. *)
|
||||
|
||||
val send : t -> (unit -> 'a Lwt.t) -> 'a Lwt.t
|
||||
(** [send t fn] checks that the queue isn't overloaded and calls [fn ()] if it's OK.
|
||||
The item is considered to be queued until the result of [fn] has resolved.
|
||||
In the case of mirage-net-xen's [writev], this happens when the frame has been
|
||||
added to the ring (not when it is consumed), which is fine for us. *)
|
35
fw_utils.ml
Normal file
35
fw_utils.ml
Normal file
@ -0,0 +1,35 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
(** General utility functions. *)
|
||||
|
||||
(** An Ethernet interface. *)
|
||||
class type interface = object
|
||||
method my_mac : Macaddr.t
|
||||
method writev : Ethernet.Packet.proto -> (Cstruct.t -> int) -> unit Lwt.t
|
||||
method my_ip : Ipaddr.V4.t
|
||||
method other_ip : Ipaddr.V4.t
|
||||
end
|
||||
|
||||
(** An Ethernet interface connected to a clientVM. *)
|
||||
class type client_link = object
|
||||
inherit interface
|
||||
method other_mac : Macaddr.t
|
||||
method log_header : string (* For log messages *)
|
||||
method get_rules : Pf_qubes.Parse_qubes.rule list
|
||||
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.
|
||||
*)
|
||||
let eth_header ethertype ~src ~dst =
|
||||
Ethernet.Packet.make_cstruct
|
||||
{ Ethernet.Packet.source = src; destination = dst; ethertype }
|
||||
|
||||
let error fmt =
|
||||
let err s = Failure s in
|
||||
Printf.ksprintf err fmt
|
||||
|
||||
let or_raise msg pp = function
|
||||
| Ok x -> x
|
||||
| Error e -> failwith (Fmt.str "%s: %a" msg pp e)
|
@ -1,49 +1,21 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
open Lwt
|
||||
|
||||
let src = Logs.Src.create "memory_pressure" ~doc:"Memory pressure monitor"
|
||||
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
let total_pages = OS.MM.Heap_pages.total ()
|
||||
let pagesize_kb = Io_page.page_size / 1024
|
||||
let fraction_free stats =
|
||||
let { Xen_os.Memory.free_words; heap_words; _ } = stats in
|
||||
float free_words /. float heap_words
|
||||
|
||||
let meminfo ~used =
|
||||
let mem_total = total_pages * pagesize_kb in
|
||||
let mem_free = (total_pages - used) * pagesize_kb in
|
||||
Log.info (fun f -> f "Writing meminfo: free %d / %d kB (%.2f %%)"
|
||||
mem_free mem_total (float_of_int mem_free /. float_of_int mem_total *. 100.0));
|
||||
Printf.sprintf "MemTotal: %d kB\n\
|
||||
MemFree: %d kB\n\
|
||||
Buffers: 0 kB\n\
|
||||
Cached: 0 kB\n\
|
||||
SwapTotal: 0 kB\n\
|
||||
SwapFree: 0 kB\n" mem_total mem_free
|
||||
|
||||
let report_mem_usage used =
|
||||
Lwt.async (fun () ->
|
||||
let open OS in
|
||||
Xs.make () >>= fun xs ->
|
||||
Xs.immediate xs (fun h ->
|
||||
Xs.write h "memory/meminfo" (meminfo ~used)
|
||||
)
|
||||
)
|
||||
|
||||
let init () =
|
||||
Gc.full_major ();
|
||||
let used = OS.MM.Heap_pages.used () in
|
||||
report_mem_usage used
|
||||
let init () = Gc.full_major ()
|
||||
|
||||
let status () =
|
||||
let used = OS.MM.Heap_pages.used () |> float_of_int in
|
||||
let frac = used /. float_of_int total_pages in
|
||||
if frac < 0.9 then `Ok
|
||||
let stats = Xen_os.Memory.quick_stat () in
|
||||
if fraction_free stats > 0.5 then `Ok
|
||||
else (
|
||||
Gc.full_major ();
|
||||
let used = OS.MM.Heap_pages.used () in
|
||||
report_mem_usage used;
|
||||
let frac = float_of_int used /. float_of_int total_pages in
|
||||
if frac > 0.9 then `Memory_critical
|
||||
else `Ok
|
||||
)
|
||||
Xen_os.Memory.trim ();
|
||||
let stats = Xen_os.Memory.quick_stat () in
|
||||
if fraction_free stats < 0.6 then `Memory_critical else `Ok)
|
||||
|
@ -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. *)
|
||||
|
81
my_dns.ml
Normal file
81
my_dns.ml
Normal file
@ -0,0 +1,81 @@
|
||||
open Lwt.Infix
|
||||
|
||||
type +'a io = 'a Lwt.t
|
||||
type io_addr = Ipaddr.V4.t * int
|
||||
|
||||
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)
|
||||
|
||||
type t = {
|
||||
protocol : Dns.proto;
|
||||
nameserver : io_addr;
|
||||
stack : stack;
|
||||
timeout_ns : int64;
|
||||
mutable requests : string Lwt_condition.t IM.t;
|
||||
}
|
||||
|
||||
type context = t
|
||||
|
||||
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
|
||||
match IM.find_opt (String.get_uint16_be data 0) t.requests with
|
||||
| Some cond -> Lwt_condition.broadcast cond data
|
||||
| None -> ());
|
||||
read t
|
||||
|
||||
let create ?nameservers ~timeout stack =
|
||||
let protocol, nameserver =
|
||||
match nameservers with
|
||||
| None | Some (_, []) -> invalid_arg "no nameserver found"
|
||||
| Some (proto, ns :: _) -> (proto, ns)
|
||||
in
|
||||
let t =
|
||||
{ protocol; nameserver; stack; timeout_ns = timeout; requests = IM.empty }
|
||||
in
|
||||
Lwt.async (fun () -> read t);
|
||||
t
|
||||
|
||||
let with_timeout timeout_ns f =
|
||||
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))
|
||||
|
||||
let send_recv (ctx : context) buf : (string, [> `Msg of string ]) result Lwt.t =
|
||||
let dst, dst_port = ctx.nameserver in
|
||||
let router, send_udp, _ = ctx.stack in
|
||||
let src_port, evict =
|
||||
My_nat.free_udp_port router.nat ~src:router.config.our_ip ~dst ~dst_port:53
|
||||
in
|
||||
let id = String.get_uint16_be buf 0 in
|
||||
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
|
||||
| Ok () -> Lwt_condition.wait cond >|= fun dns_response -> Ok dns_response
|
||||
| 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
|
86
my_nat.ml
Normal file
86
my_nat.ml
Normal file
@ -0,0 +1,86 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
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 ]
|
||||
|
||||
module Nat = Mirage_nat_lru
|
||||
|
||||
module S = Set.Make (struct
|
||||
type t = int
|
||||
|
||||
let compare (a : int) (b : int) = compare a b
|
||||
end)
|
||||
|
||||
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
|
||||
let udp_size = max_entries - tcp_size in
|
||||
let table = Nat.empty ~tcp_size ~udp_size ~icmp_size:100 in
|
||||
let last_resort_port = pick_port () in
|
||||
{ table; udp_dns = S.empty; last_resort_port }
|
||||
|
||||
let pick_free_port t proto =
|
||||
let rec go retries =
|
||||
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)
|
||||
| _ -> 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)
|
||||
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
|
||||
let remove =
|
||||
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)
|
||||
else Fun.id
|
||||
in
|
||||
(src_port, remove)
|
||||
else go (retries - 1)
|
||||
in
|
||||
go 10
|
||||
|
||||
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);
|
||||
None
|
||||
| Ok packet -> Some packet
|
||||
|
||||
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
|
||||
| `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
|
||||
| Error `Overlap -> Error "Too many retries"
|
||||
| Error `Cannot_NAT -> Error "Cannot NAT this packet"
|
||||
| Ok () ->
|
||||
Log.debug (fun f -> f "Updated NAT table: %a" Nat.pp_summary t.table);
|
||||
Option.to_result ~none:"No NAT entry, even after adding one!"
|
||||
(translate t packet)
|
26
my_nat.mli
Normal file
26
my_nat.mli
Normal file
@ -0,0 +1,26 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
(* Abstract over NAT interface (todo: remove this) *)
|
||||
|
||||
type t
|
||||
type action = [ `NAT | `Redirect of Mirage_nat.endpoint ]
|
||||
|
||||
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
|
63
packet.ml
63
packet.ml
@ -1,21 +1,60 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
open Utils
|
||||
open Fw_utils
|
||||
|
||||
type port = int
|
||||
|
||||
type ports = {
|
||||
sport : port; (* Source port *)
|
||||
dport : port; (* Destination *)
|
||||
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 ('src, 'dst) t = {
|
||||
ipv4_header : Ipv4_packet.t;
|
||||
transport_header : transport_header;
|
||||
transport_payload : Cstruct.t;
|
||||
src : 'src;
|
||||
dst : 'dst;
|
||||
}
|
||||
|
||||
type host =
|
||||
[ `Client of client_link | `Unknown_client of Ipaddr.t | `Client_gateway | `Firewall_uplink | `NetVM | `External of Ipaddr.t ]
|
||||
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
|
||||
|
||||
type info = {
|
||||
frame : Cstruct.t;
|
||||
src : host;
|
||||
dst : host;
|
||||
proto : [ `UDP of ports | `TCP of ports | `ICMP | `Unknown ];
|
||||
}
|
||||
let pp_host fmt = function
|
||||
| `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
|
||||
| `Firewall -> Format.pp_print_string fmt "firewall(client-gw)"
|
||||
|
||||
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))
|
||||
|
||||
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)
|
||||
in
|
||||
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.
|
||||
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
|
||||
destination fields so it will be sent to [host:port]. *)
|
||||
| `Drop of string (* Drop packet for this reason. *) ]
|
||||
|
35
packet.mli
Normal file
35
packet.mli
Normal file
@ -0,0 +1,35 @@
|
||||
type port = int
|
||||
|
||||
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 *) ]
|
||||
|
||||
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;
|
||||
transport_header : transport_header;
|
||||
transport_payload : Cstruct.t;
|
||||
src : 'src;
|
||||
dst : 'dst;
|
||||
}
|
||||
|
||||
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.
|
||||
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
|
||||
destination fields so it will be sent to [host:port]. *)
|
||||
| `Drop of string (* Drop packet for this reason. *) ]
|
1
qubes-firewall-release.sha256
Normal file
1
qubes-firewall-release.sha256
Normal file
@ -0,0 +1 @@
|
||||
0c3c2c0e62a834112c69d7cddc5dd6f70ecb93afa988768fb860ed26e423b1f8 dist/qubes-firewall.xen
|
1
qubes-firewall.sha256
Normal file
1
qubes-firewall.sha256
Normal file
@ -0,0 +1 @@
|
||||
ac049069b35f786fa11b18a2261d7dbecd588301af0363ef6888ec9d924dc989 dist/qubes-firewall.xen
|
53
router.ml
53
router.ml
@ -1,53 +0,0 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
open Utils
|
||||
|
||||
let src = Logs.Src.create "router" ~doc:"Router"
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
(* The routing table *)
|
||||
|
||||
type t = {
|
||||
client_eth : Client_eth.t;
|
||||
mutable nat : Nat_lookup.t;
|
||||
uplink : interface;
|
||||
}
|
||||
|
||||
let create ~client_eth ~uplink =
|
||||
let nat = Nat_lookup.empty () in
|
||||
{ client_eth; nat; uplink }
|
||||
|
||||
let target t buf =
|
||||
let open Wire_structs.Ipv4_wire in
|
||||
let dst_ip = get_ipv4_dst buf |> Ipaddr.V4.of_int32 in
|
||||
if Ipaddr.V4.Prefix.mem dst_ip (Client_eth.prefix t.client_eth) then (
|
||||
match Client_eth.lookup t.client_eth dst_ip with
|
||||
| Some client_link -> Some (client_link :> interface)
|
||||
| None ->
|
||||
Log.warn (fun f -> f "Packet to unknown internal client %a - dropping"
|
||||
Ipaddr.V4.pp_hum dst_ip);
|
||||
None
|
||||
) else Some t.uplink
|
||||
|
||||
let add_client t = Client_eth.add_client t.client_eth
|
||||
let remove_client t = Client_eth.remove_client t.client_eth
|
||||
|
||||
let classify t ip =
|
||||
if ip = Ipaddr.V4 t.uplink#my_ip then `Firewall_uplink
|
||||
else if ip = Ipaddr.V4 t.uplink#other_ip then `NetVM
|
||||
else (Client_eth.classify t.client_eth ip :> Packet.host)
|
||||
|
||||
let resolve t = function
|
||||
| `Firewall_uplink -> Ipaddr.V4 t.uplink#my_ip
|
||||
| `NetVM -> Ipaddr.V4 t.uplink#other_ip
|
||||
| #Client_eth.host as host -> Client_eth.resolve t.client_eth host
|
||||
|
||||
(* To avoid needing to allocate a new NAT table when we've run out of
|
||||
memory, pre-allocate the new one ahead of time. *)
|
||||
let next_nat = ref (Nat_lookup.empty ())
|
||||
let reset t =
|
||||
t.nat <- !next_nat;
|
||||
(* (at this point, the big old NAT table can be GC'd, so allocating
|
||||
a new one should be OK) *)
|
||||
next_nat := Nat_lookup.empty ()
|
35
router.mli
35
router.mli
@ -1,35 +0,0 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
(** Routing packets to the right network interface. *)
|
||||
|
||||
open Utils
|
||||
|
||||
type t = private {
|
||||
client_eth : Client_eth.t;
|
||||
mutable nat : Nat_lookup.t;
|
||||
uplink : interface;
|
||||
}
|
||||
(** A routing table. *)
|
||||
|
||||
val create :
|
||||
client_eth:Client_eth.t ->
|
||||
uplink:interface ->
|
||||
t
|
||||
(** [create ~client_eth ~uplink] is a new routing table
|
||||
that routes packets outside of [client_eth] via [uplink]. *)
|
||||
|
||||
val target : t -> Cstruct.t -> interface option
|
||||
(** [target t packet] is the interface to which [packet] (an IP packet) should be routed. *)
|
||||
|
||||
val add_client : t -> client_link -> unit
|
||||
(** [add_client t iface] adds a rule for routing packets addressed to [iface].
|
||||
The client's IP address must be within the [client_eth] passed to [create]. *)
|
||||
|
||||
val remove_client : t -> client_link -> unit
|
||||
|
||||
val classify : t -> Ipaddr.t -> Packet.host
|
||||
val resolve : t -> Packet.host -> Ipaddr.t
|
||||
|
||||
val reset : t -> unit
|
||||
(** Clear the NAT table (to free memory). *)
|
134
rules.ml
134
rules.ml
@ -1,40 +1,122 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
(** Put your firewall rules here. *)
|
||||
(** This module applies firewall rules from QubesDB. *)
|
||||
|
||||
open Packet
|
||||
open Lwt.Infix
|
||||
module Q = Pf_qubes.Parse_qubes
|
||||
|
||||
(* OCaml normally warns if you don't match all fields, but that's OK here. *)
|
||||
[@@@ocaml.warning "-9"]
|
||||
let src = Logs.Src.create "rules" ~doc:"Firewall rules"
|
||||
|
||||
(** {2 Actions}
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
The possible actions are:
|
||||
let dns_port = 53
|
||||
|
||||
- [`Accept] : Send the packet to its destination.
|
||||
module Classifier = struct
|
||||
let matches_port dstports (port : int) =
|
||||
match dstports with
|
||||
| None -> true
|
||||
| Some (Q.Range_inclusive (min, max)) -> min <= port && port <= max
|
||||
|
||||
- [`NAT] : Rewrite the packet's source field so packet appears to
|
||||
have come from the firewall, via an unused port.
|
||||
Also, add NAT rules so related packets will be translated accordingly.
|
||||
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 -> (
|
||||
(* 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)
|
||||
(* 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 -> (
|
||||
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)
|
||||
| _, _ -> false)
|
||||
|
||||
- [`NAT_to (host, port)] :
|
||||
As for [`NAT], but also rewrite the packet's destination fields so it
|
||||
will be sent to [host:port].
|
||||
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 -> (
|
||||
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
|
||||
| Error (`Msg m) ->
|
||||
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 *))
|
||||
end
|
||||
|
||||
- [`Drop reason] drop the packet and log the reason.
|
||||
*)
|
||||
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
|
||||
else Lwt.return `No_match
|
||||
| q -> Lwt.return q
|
||||
|
||||
(** Decide what to do with a packet from a client VM.
|
||||
Note: If the packet matched an existing NAT rule then this isn't called. *)
|
||||
let from_client = function
|
||||
| { dst = (`External _ | `NetVM) } -> `NAT
|
||||
| { dst = `Client_gateway; proto = `UDP { dport = 53 } } -> `NAT_to (`NetVM, 53)
|
||||
| { dst = (`Client_gateway | `Firewall_uplink) } -> `Drop "packet addressed to firewall itself"
|
||||
| { dst = `Client _ } -> `Drop "prevent communication between client VMs"
|
||||
| { dst = `Unknown_client _ } -> `Drop "target client not running"
|
||||
(* Does the packet match our rules? *)
|
||||
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
|
||||
| `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)
|
||||
|
||||
(** Decide what to do with a packet received from the outside world.
|
||||
Note: If the packet matched an existing NAT rule then this isn't called. *)
|
||||
let from_netvm = function
|
||||
| _ -> `Drop "drop by default"
|
||||
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 =
|
||||
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
|
||||
| _ -> 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 =
|
||||
Lwt.return @@ `Drop "drop by default"
|
||||
|
33
test/config.ml
Normal file
33
test/config.ml
Normal file
@ -0,0 +1,33 @@
|
||||
open Mirage
|
||||
|
||||
let pin = "git+https://github.com/roburio/alcotest.git#mirage"
|
||||
|
||||
let packages =
|
||||
[
|
||||
package "ethernet";
|
||||
package "arp";
|
||||
package "arp-mirage";
|
||||
package "ipaddr";
|
||||
package "tcpip" ~sublibs:[ "stack-direct"; "icmpv4"; "ipv4"; "udp"; "tcp" ];
|
||||
package "mirage-qubes";
|
||||
package "mirage-qubes-ipv4";
|
||||
package "dns-client" ~sublibs:[ "mirage" ];
|
||||
package ~pin "alcotest";
|
||||
package ~pin "alcotest-mirage";
|
||||
]
|
||||
|
||||
let client =
|
||||
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
|
||||
register "http-fetch" job
|
138
test/test.sh
Executable file
138
test/test.sh
Executable file
@ -0,0 +1,138 @@
|
||||
#!/bin/bash
|
||||
function explain_commands {
|
||||
echo "1) Set up test qubes:"
|
||||
echo "First, set up the test-mirage script from https://github.com/talex5/qubes-test-mirage.git"
|
||||
|
||||
echo "Then, use `qubes-manager` to create two new AppVMs called `mirage-fw-test` and `fetchmotron`.
|
||||
You can make it standalone or not and use any template (it doesn't matter
|
||||
because unikernels already contain all their code and don't need to use a disk
|
||||
to boot)."
|
||||
|
||||
echo "Next, still in dom0, create a new `mirage-fw-test` and `fetchmotron` kernels, with an empty `modules.img` and `vmlinuz` and a compressed empty file for the initramfs, and then set that as the kernel for the new VMs:
|
||||
|
||||
mkdir /var/lib/qubes/vm-kernels/mirage-fw-test
|
||||
cd /var/lib/qubes/vm-kernels/mirage-fw-test
|
||||
touch modules.img vmlinuz test-mirage-ok
|
||||
cat /dev/null | gzip > initramfs
|
||||
qvm-prefs -s mirage-fw-test kernel mirage-fw-test
|
||||
|
||||
mkdir /var/lib/qubes/vm-kernels/fetchmotron
|
||||
cd /var/lib/qubes/vm-kernels/fetchmotron
|
||||
touch modules.img vmlinuz test-mirage-ok
|
||||
cat /dev/null | gzip > initramfs
|
||||
qvm-prefs -s fetchmotron kernel fetchmotron
|
||||
"
|
||||
}
|
||||
|
||||
function explain_service {
|
||||
echo "2) Set up rule update service:"
|
||||
echo "In dom0, make a new service:
|
||||
|
||||
sudo bash
|
||||
echo /usr/local/bin/update-firewall > /etc/qubes-rpc/yomimono.updateFirewall
|
||||
|
||||
Make a policy file for this service, YOUR_DEV_VM being the qube from which you build (e.g. ocamldev):
|
||||
|
||||
cd /etc/qubes-rpc/policy
|
||||
cat << EOF >> yomimono.updateFirewall
|
||||
YOUR_DEV_VM dom0 allow
|
||||
|
||||
copy the update-firewall script:
|
||||
|
||||
cd /usr/local/bin
|
||||
qvm-run -p YOUR_DEV_VM 'cat /path/to/qubes-mirage-firewall/test/update-firewall.sh' > update-firewall
|
||||
chmod +x update-firewall
|
||||
|
||||
Now, back to YOUR_DEV_VM. Let's test to change fetchmotron's firewall rules:
|
||||
|
||||
qrexec-client-vm dom0 yomimono.updateFirewall"
|
||||
}
|
||||
|
||||
function explain_upstream {
|
||||
echo "Also, start the test services on the upstream NetVM (which is available at 10.137.0.5 from the test unikernel).
|
||||
For the UDP and TCP reply services:
|
||||
Install nmap-ncat (to persist this package, install it in your sys-net template VM):
|
||||
|
||||
sudo dnf install nmap-ncat
|
||||
|
||||
Allow incoming traffic from local virtual interfaces on the appropriate ports,
|
||||
then run the services:
|
||||
|
||||
sudo iptables -I INPUT -i vif+ -p udp --dport $udp_echo_port -j ACCEPT
|
||||
sudo iptables -I INPUT -i vif+ -p tcp --dport $tcp_echo_port_lower -j ACCEPT
|
||||
sudo iptables -I INPUT -i vif+ -p tcp --dport $tcp_echo_port_upper -j ACCEPT
|
||||
ncat -e /bin/cat -k -u -l $udp_echo_port &
|
||||
ncat -e /bin/cat -k -l $tcp_echo_port_lower &
|
||||
ncat -e /bin/cat -k -l $tcp_echo_port_upper &
|
||||
"
|
||||
}
|
||||
|
||||
if ! [ -x "$(command -v test-mirage)" ]; then
|
||||
echo 'Error: test-mirage is not installed.' >&2
|
||||
explain_commands >&2
|
||||
exit 1
|
||||
fi
|
||||
qrexec-client-vm dom0 yomimono.updateFirewall
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: can't update firewall rules." >&2
|
||||
explain_service >&2
|
||||
exit 1
|
||||
fi
|
||||
echo_host=10.137.0.5
|
||||
udp_echo_port=1235
|
||||
tcp_echo_port_lower=6668
|
||||
tcp_echo_port_upper=6670
|
||||
|
||||
# Pretest that checks if our echo servers work.
|
||||
# NOTE: we assume the dev qube has the same netvm as fetchmotron.
|
||||
# If yours is different, this test will fail (comment it out)
|
||||
function pretest {
|
||||
protocol=$1
|
||||
port=$2
|
||||
if [ "$protocol" = "udp" ]; then
|
||||
udp_arg="-u"
|
||||
else
|
||||
udp_arg=""
|
||||
fi
|
||||
reply=$(echo hi | nc $udp_arg $echo_host -w 1 $port)
|
||||
if [ "$reply" != "hi" ]; then
|
||||
echo "echo hi | nc $udp_arg $echo_host -w 1 $port"
|
||||
echo "echo services not reachable at $protocol $echo_host:$port" >&2
|
||||
explain_upstream >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
pretest "udp" "$udp_echo_port"
|
||||
pretest "tcp" "$tcp_echo_port_lower"
|
||||
pretest "tcp" "$tcp_echo_port_upper"
|
||||
|
||||
echo "We're gonna set up a unikernel for the mirage-fw-test qube"
|
||||
cd ..
|
||||
make clean && \
|
||||
#mirage configure -t xen -l "application:error,net-xen xenstore:error,firewall:debug,frameQ:debug,uplink:debug,rules:debug,udp:debug,ipv4:debug,fw-resolver:debug" && \
|
||||
mirage configure -t xen -l "net-xen xenstore:error,application:warning,qubes.db:warning" && \
|
||||
#mirage configure -t xen -l "*:debug" && \
|
||||
make depend && \
|
||||
make
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Could not build unikernel for mirage-fw-test qube" >&2
|
||||
exit 1
|
||||
fi
|
||||
cd test
|
||||
|
||||
echo "We're gonna set up a unikernel for fetchmotron qube"
|
||||
make clean && \
|
||||
mirage configure -t qubes -l "net-xen frontend:error,firewall test:debug" && \
|
||||
#mirage configure -t qubes -l "*:error" && \
|
||||
make depend && \
|
||||
make
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Could not build unikernel for fetchmotron qube" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
test-mirage qubes_firewall.xen mirage-fw-test &
|
||||
cd test
|
||||
test-mirage http_fetch.xen fetchmotron
|
467
test/unikernel.ml
Normal file
467
test/unikernel.ml
Normal file
@ -0,0 +1,467 @@
|
||||
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
|
||||
* things we can have in rule
|
||||
* - action:
|
||||
x accept (UDP fetch test)
|
||||
x drop (TCP connect denied test)
|
||||
* - proto:
|
||||
x None (TCP connect denied test)
|
||||
x TCP (TCP connect test)
|
||||
x UDP (UDP fetch test)
|
||||
x ICMP (ping test)
|
||||
* - specialtarget:
|
||||
x None (UDP fetch test, TCP connect denied test)
|
||||
x DNS (TCP connect test, TCP connect denied test)
|
||||
* - destination:
|
||||
x Any (TCP connect denied test)
|
||||
x Some ipv4 host (UDP fetch test)
|
||||
Some ipv6 host (we can't do this right now)
|
||||
Some hostname (need a bunch of DNS stuff for that)
|
||||
* - destination ports:
|
||||
x none (TCP connect denied test)
|
||||
x range is one port (UDP fetch test)
|
||||
x range has different ports in pair
|
||||
* - icmp type:
|
||||
x None (TCP connect denied, UDP fetch test)
|
||||
x query type (ping test)
|
||||
error type
|
||||
x - errors related to allowed traffic (does it have a host waiting for it?)
|
||||
x - directly allowed outbound icmp errors (e.g. for forwarding)
|
||||
* - number (ordering over rules, to resolve conflicts by precedence)
|
||||
no overlap between rules, i.e. ordering unimportant
|
||||
error case: multiple rules with same number?
|
||||
x conflicting rules (specific accept rules with low numbers, drop all with high number)
|
||||
*)
|
||||
|
||||
(* 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
|
||||
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
|
||||
(* A Mirage_stack.V4 implementation which diverts DHCP messages to a DHCP
|
||||
server. The DHCP server needs to get the entire Ethernet frame, because
|
||||
the Ethernet source address is the address to send replies to, its IPv4
|
||||
addresses (source, destination) do not matter (since the DHCP client that
|
||||
sent this request does not have an IP address yet). ARP cannot be used
|
||||
by DHCP, because the client does not have an IP address (and thus no ARP
|
||||
replies). *)
|
||||
|
||||
module UDPV4 = U
|
||||
module TCPV4 = T
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
let listen_udpv4 { udp_listeners; _ } ~port cb =
|
||||
Hashtbl.replace udp_listeners port cb
|
||||
|
||||
let stop_listen_udpv4 { udp_listeners; _ } ~port =
|
||||
Hashtbl.remove udp_listeners port
|
||||
|
||||
let listen_tcpv4 ?keepalive { tcp_listeners; _ } ~port cb =
|
||||
Hashtbl.replace tcp_listeners port { T.process = cb; T.keepalive }
|
||||
|
||||
let stop_listen_tcpv4 { tcp_listeners; _ } ~port =
|
||||
Hashtbl.remove tcp_listeners port
|
||||
|
||||
let listen_icmp t cb = t.icmp_listener <- cb
|
||||
|
||||
let listen t =
|
||||
let ethif_listener =
|
||||
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 -> (
|
||||
match t.icmp_listener with
|
||||
| None -> Icmp.input t.icmp ~src ~dst buf
|
||||
| Some cb -> cb ~src ~dst buf)
|
||||
| _ -> Lwt.return_unit)
|
||||
t.ip)
|
||||
~ipv6:(fun _ -> Lwt.return_unit)
|
||||
t.eth
|
||||
in
|
||||
NET.listen t.net ~header_size:Ethernet_wire.sizeof_ethernet ethif_listener
|
||||
>>= function
|
||||
| Error e ->
|
||||
Logs.warn (fun p -> p "%a" NET.pp_error e);
|
||||
Lwt.return_unit
|
||||
| Ok _res -> Lwt.return_unit
|
||||
|
||||
let connect 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;
|
||||
}
|
||||
|
||||
let disconnect _ =
|
||||
Logs.warn (fun m -> m "ignoring disconnect");
|
||||
Lwt.return_unit
|
||||
end
|
||||
|
||||
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 *)
|
||||
ty = Icmpv4_wire.Echo_request;
|
||||
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))
|
||||
|
||||
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);
|
||||
match Icmpv4_packet.Unmarshal.of_cstruct buf with
|
||||
| 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);
|
||||
if is_ping_reply src server packet then resp_received := true;
|
||||
Lwt.return_unit
|
||||
in
|
||||
Stack.listen_icmp stack (Some icmp_listener)
|
||||
|
||||
let ping_expect_failure server stack () =
|
||||
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
|
||||
| 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 :)");
|
||||
Stack.listen_icmp stack None;
|
||||
Lwt.return_unit
|
||||
|
||||
let icmp_error_type stack () =
|
||||
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
|
||||
(* 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 *)
|
||||
then resp_correct := true
|
||||
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 *)
|
||||
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);
|
||||
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"
|
||||
1338 U.pp_error e);
|
||||
Lwt.return_unit
|
||||
|
||||
let tcp_connect msg server port tcp () =
|
||||
Log.info (fun f -> f "Entering tcp connect test: %s:%d" server port);
|
||||
let ip = Ipaddr.V4.of_string_exn server in
|
||||
let msg' = Printf.sprintf "TCP connect test %s to %s:%d" msg server port in
|
||||
T.create_connection tcp (ip, port) >>= function
|
||||
| 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);
|
||||
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
|
||||
in
|
||||
let timeout =
|
||||
Time.sleep_ns 1_000_000_000L >>= fun () ->
|
||||
Log.info (fun f -> f "%s passed :)" msg');
|
||||
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);
|
||||
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 (
|
||||
match Cstruct.equal buf content with
|
||||
| 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 (
|
||||
(* 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)"
|
||||
src_port echo_server_port);
|
||||
(* don't cancel the listener, since we want to keep listening *)
|
||||
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 *)
|
||||
Time.sleep_ns 1_000_000_000L >>= fun () ->
|
||||
Stack.stop_listen_udpv4 stack ~port:src_port;
|
||||
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"
|
||||
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 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);
|
||||
Lwt.return_unit
|
||||
| Error (`Msg 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);
|
||||
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 "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
|
||||
| Ok 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
|
||||
|
||||
let start _random _time _clock network db =
|
||||
E.connect network >>= fun ethernet ->
|
||||
A.connect ethernet >>= fun arp ->
|
||||
I.connect db ethernet arp >>= fun ipv4 ->
|
||||
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 );
|
||||
("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
|
||||
Alcotest.run ~and_exit:false "name" [ general_tests ] >>= fun () ->
|
||||
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 "" netvm 8082 tcp);
|
||||
] )
|
||||
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
|
||||
);
|
||||
(* 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
|
||||
Alcotest.run "name" [ stack_tests ]
|
||||
end
|
54
test/update-firewall.sh
Normal file
54
test/update-firewall.sh
Normal file
@ -0,0 +1,54 @@
|
||||
#!/bin/sh
|
||||
|
||||
# this script sets a deny-all rule for a particular VM, set here as TEST_VM.
|
||||
# it is intended to be used as part of a test suite which analyzes whether
|
||||
# an upstream FirewallVM correctly applies rule changes when they occur.
|
||||
|
||||
# Copy this script into dom0 at /usr/local/bin/update-firewall.sh so it can be
|
||||
# remotely triggered by your development VM as part of the firewall testing
|
||||
# script.
|
||||
|
||||
TEST_VM=fetchmotron
|
||||
|
||||
#echo "Current $TEST_VM firewall rules:"
|
||||
#qvm-firewall $TEST_VM list
|
||||
|
||||
echo "Removing $TEST_VM rules..."
|
||||
rc=0
|
||||
while [ "$rc" = "0" ]; do
|
||||
qvm-firewall $TEST_VM del --rule-no 0
|
||||
rc=$?
|
||||
done
|
||||
|
||||
#echo "$TEST_VM firewall rules are now:"
|
||||
#qvm-firewall $TEST_VM list
|
||||
|
||||
#echo "Setting $TEST_VM specialtarget=dns rule:"
|
||||
qvm-firewall $TEST_VM add accept specialtarget=dns
|
||||
|
||||
#echo "Setting $TEST_VM allow rule for UDP port 1235 to 10.137.0.5:"
|
||||
qvm-firewall $TEST_VM add accept 10.137.0.5 udp 1235
|
||||
|
||||
#echo "Setting $TEST_VM allow rule for UDP port 1338 to 10.137.0.5:"
|
||||
qvm-firewall $TEST_VM add accept 10.137.0.5 udp 1338
|
||||
|
||||
#echo "Setting $TEST_VM allow rule for TCP port 6668-6670 to 10.137.0.5:"
|
||||
qvm-firewall $TEST_VM add accept 10.137.0.5 tcp 6668-6670
|
||||
|
||||
#echo "Setting $TEST_VM allow rule for ICMP type 8 (ping) to 10.137.0.5:"
|
||||
qvm-firewall $TEST_VM add accept 10.137.0.5 icmp icmptype=8
|
||||
|
||||
#echo "Setting $TEST_VM allow rule for bogus.linse.me:"
|
||||
qvm-firewall $TEST_VM add accept dsthost=bogus.linse.me
|
||||
|
||||
#echo "Setting deny rule to host google.com:"
|
||||
qvm-firewall $TEST_VM add drop dsthost=google.com
|
||||
|
||||
#echo "Setting allow-all on port 443 rule:"
|
||||
qvm-firewall $TEST_VM add accept proto=tcp dstports=443-443
|
||||
|
||||
#echo "Setting $TEST_VM deny-all rule:"
|
||||
qvm-firewall $TEST_VM add drop
|
||||
|
||||
echo "$TEST_VM firewall rules are now:"
|
||||
qvm-firewall $TEST_VM list
|
177
unikernel.ml
177
unikernel.ml
@ -3,75 +3,124 @@
|
||||
|
||||
open Lwt
|
||||
open Qubes
|
||||
open Cmdliner
|
||||
|
||||
let src = Logs.Src.create "unikernel" ~doc:"Main unikernel code"
|
||||
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
module Main (Clock : V1.CLOCK) = struct
|
||||
module Uplink = Uplink.Make(Clock)
|
||||
let nat_table_size =
|
||||
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)
|
||||
|
||||
(* Set up networking and listen for incoming packets. *)
|
||||
let network qubesDB =
|
||||
(* Read configuration from QubesDB *)
|
||||
let config = Dao.read_network_config qubesDB in
|
||||
Logs.info (fun f -> f "Client (internal) network is %a"
|
||||
Ipaddr.V4.Prefix.pp_hum config.Dao.clients_prefix);
|
||||
(* Initialise connection to NetVM *)
|
||||
Uplink.connect config >>= fun uplink ->
|
||||
(* Report success *)
|
||||
Dao.set_iptables_error qubesDB "" >>= fun () ->
|
||||
(* Set up client-side networking *)
|
||||
let client_eth = Client_eth.create
|
||||
~client_gw:config.Dao.clients_our_ip
|
||||
~prefix:config.Dao.clients_prefix in
|
||||
(* Set up routing between networks and hosts *)
|
||||
let router = Router.create
|
||||
~client_eth
|
||||
~uplink:(Uplink.interface uplink) in
|
||||
(* Handle packets from both networks *)
|
||||
Lwt.join [
|
||||
Client_net.listen router;
|
||||
Uplink.listen uplink router
|
||||
let ipv4 =
|
||||
let doc = Arg.info ~doc:"Manual IP setting." [ "ipv4" ] in
|
||||
Mirage_runtime.register_arg Arg.(value & opt string "0.0.0.0" doc)
|
||||
|
||||
let ipv4_gw =
|
||||
let doc = Arg.info ~doc:"Manual Gateway IP setting." [ "ipv4-gw" ] in
|
||||
Mirage_runtime.register_arg Arg.(value & opt string "0.0.0.0" doc)
|
||||
|
||||
let ipv4_dns =
|
||||
let doc = Arg.info ~doc:"Manual DNS IP setting." [ "ipv4-dns" ] in
|
||||
Mirage_runtime.register_arg Arg.(value & opt string "10.139.1.1" doc)
|
||||
|
||||
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 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 Mirage_mtime.elapsed_ns dns_client dns_servers
|
||||
qubesDB router;
|
||||
Dispatcher.uplink_wait_update qubesDB router;
|
||||
Dispatcher.uplink_listen Mirage_mtime.elapsed_ns dns_responses router;
|
||||
]
|
||||
|
||||
(* We don't use the GUI, but it's interesting to keep an eye on it.
|
||||
If the other end dies, don't let it take us with it (can happen on log out). *)
|
||||
let watch_gui gui =
|
||||
Lwt.async (fun () ->
|
||||
Lwt.try_bind
|
||||
(fun () -> GUI.listen gui)
|
||||
(fun `Cant_happen -> assert false)
|
||||
(fun ex ->
|
||||
Log.warn (fun f -> f "GUI thread failed: %s" (Printexc.to_string ex));
|
||||
return ()
|
||||
)
|
||||
)
|
||||
(* Main unikernel entry point (called from auto-generated main.ml). *)
|
||||
let start () =
|
||||
let open Lwt.Syntax 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 = 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);
|
||||
(* Watch for shutdown requests from Qubes *)
|
||||
let shutdown_rq =
|
||||
Xen_os.Lifecycle.await_shutdown_request () >>= fun (`Poweroff | `Reboot) ->
|
||||
Lwt.return_unit
|
||||
in
|
||||
(* Set up networking *)
|
||||
let nat = My_nat.create ~max_entries:(nat_table_size ()) in
|
||||
|
||||
(* Main unikernel entry point (called from auto-generated main.ml). *)
|
||||
let start () =
|
||||
let start_time = Clock.time () in
|
||||
(* Start qrexec agent, GUI agent and QubesDB agent in parallel *)
|
||||
let qrexec = RExec.connect ~domid:0 () in
|
||||
let gui = GUI.connect ~domid:0 () in
|
||||
let qubesDB = DB.connect ~domid:0 () in
|
||||
(* Wait for clients to connect *)
|
||||
qrexec >>= fun qrexec ->
|
||||
let agent_listener = RExec.listen qrexec Command.handler in
|
||||
gui >>= fun gui ->
|
||||
watch_gui gui;
|
||||
qubesDB >>= fun qubesDB ->
|
||||
Log.info (fun f -> f "agents connected in %.3f s (CPU time used since boot: %.3f s)"
|
||||
(Clock.time () -. start_time) (Sys.time ()));
|
||||
(* Watch for shutdown requests from Qubes *)
|
||||
let shutdown_rq =
|
||||
OS.Lifecycle.await_shutdown_request () >>= fun (`Poweroff | `Reboot) ->
|
||||
return () in
|
||||
(* Set up networking *)
|
||||
let net_listener = network qubesDB 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. *)
|
||||
OS.Time.sleep 1.0
|
||||
end
|
||||
let netvm_ip = Ipaddr.V4.of_string_exn (ipv4_gw ()) in
|
||||
let our_ip = Ipaddr.V4.of_string_exn (ipv4 ()) in
|
||||
let dns = Ipaddr.V4.of_string_exn (ipv4_dns ()) in
|
||||
let dns2 = Ipaddr.V4.of_string_exn (ipv4_dns2 ()) in
|
||||
|
||||
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 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...");
|
||||
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
|
||||
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;
|
||||
|
||||
(* Set up client-side networking *)
|
||||
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 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 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
|
||||
|
||||
(* 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. *)
|
||||
Mirage_sleep.ns (1.0 *. 1e9 |> Int64.of_float)
|
||||
|
61
uplink.ml
61
uplink.ml
@ -1,61 +0,0 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
open Lwt.Infix
|
||||
open Utils
|
||||
|
||||
module Eth = Ethif.Make(Netif)
|
||||
|
||||
let src = Logs.Src.create "uplink" ~doc:"Network connection to NetVM"
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
module Make(Clock : V1.CLOCK) = struct
|
||||
module Arp = Arpv4.Make(Eth)(Clock)(OS.Time)
|
||||
module IPv4 = Ipv4.Make(Eth)(Arp)
|
||||
|
||||
type t = {
|
||||
net : Netif.t;
|
||||
eth : Eth.t;
|
||||
arp : Arp.t;
|
||||
interface : interface;
|
||||
}
|
||||
|
||||
class netvm_iface eth mac ~my_ip ~other_ip : interface = object
|
||||
val queue = FrameQ.create (Ipaddr.V4.to_string other_ip)
|
||||
method my_mac = Eth.mac eth
|
||||
method my_ip = my_ip
|
||||
method other_ip = other_ip
|
||||
method writev ip =
|
||||
FrameQ.send queue (fun () ->
|
||||
mac >>= fun dst ->
|
||||
let eth_hdr = eth_header_ipv4 ~src:(Eth.mac eth) ~dst in
|
||||
Eth.writev eth (eth_hdr :: ip)
|
||||
)
|
||||
end
|
||||
|
||||
let listen t router =
|
||||
Netif.listen t.net (fun frame ->
|
||||
(* Handle one Ethernet frame from NetVM *)
|
||||
Eth.input t.eth
|
||||
~arpv4:(Arp.input t.arp)
|
||||
~ipv4:(fun _ip -> Firewall.ipv4_from_netvm router frame)
|
||||
~ipv6:(fun _ip -> return ())
|
||||
frame
|
||||
)
|
||||
|
||||
let interface t = t.interface
|
||||
|
||||
let connect config =
|
||||
let ip = config.Dao.uplink_our_ip in
|
||||
Netif.connect "0" >>= or_fail "Can't connect uplink device" >>= fun net ->
|
||||
Eth.connect net >>= or_fail "Can't make Ethernet device for tap" >>= fun eth ->
|
||||
Arp.connect eth >>= or_fail "Can't add ARP" >>= fun arp ->
|
||||
Arp.add_ip arp ip >>= fun () ->
|
||||
let netvm_mac = Arp.query arp config.Dao.uplink_netvm_ip >|= function
|
||||
| `Timeout -> failwith "ARP timeout getting MAC of our NetVM"
|
||||
| `Ok netvm_mac -> netvm_mac in
|
||||
let interface = new netvm_iface eth netvm_mac
|
||||
~my_ip:ip
|
||||
~other_ip:config.Dao.uplink_netvm_ip in
|
||||
return { net; eth; arp; interface }
|
||||
end
|
19
uplink.mli
19
uplink.mli
@ -1,19 +0,0 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
(** The link from us to NetVM (and, through that, to the outside world). *)
|
||||
|
||||
open Utils
|
||||
|
||||
module Make(Clock : V1.CLOCK) : sig
|
||||
type t
|
||||
|
||||
val connect : Dao.network_config -> t Lwt.t
|
||||
(** Connect to our NetVM (gateway). *)
|
||||
|
||||
val interface : t -> interface
|
||||
(** The network interface to NetVM. *)
|
||||
|
||||
val listen : t -> Router.t -> unit Lwt.t
|
||||
(** Handle incoming frames from NetVM. *)
|
||||
end
|
65
utils.ml
65
utils.ml
@ -1,65 +0,0 @@
|
||||
(* Copyright (C) 2015, Thomas Leonard <thomas.leonard@unikernel.com>
|
||||
See the README file for details. *)
|
||||
|
||||
(** General utility functions. *)
|
||||
|
||||
module IpMap = struct
|
||||
include Map.Make(Ipaddr.V4)
|
||||
let find x map =
|
||||
try Some (find x map)
|
||||
with Not_found -> None
|
||||
end
|
||||
|
||||
module Int = struct
|
||||
type t = int
|
||||
let compare (a:t) (b:t) = compare a b
|
||||
end
|
||||
|
||||
module IntSet = Set.Make(Int)
|
||||
module IntMap = Map.Make(Int)
|
||||
|
||||
(** An Ethernet interface. *)
|
||||
class type interface = object
|
||||
method my_mac : Macaddr.t
|
||||
method writev : Cstruct.t list -> unit Lwt.t
|
||||
method my_ip : Ipaddr.V4.t
|
||||
method other_ip : Ipaddr.V4.t
|
||||
end
|
||||
|
||||
(** An Ethernet interface connected to a clientVM. *)
|
||||
class type client_link = object
|
||||
inherit interface
|
||||
method other_mac : Macaddr.t
|
||||
end
|
||||
|
||||
(** An Ethernet header from [src]'s MAC address to [dst]'s with an IPv4 payload. *)
|
||||
let eth_header_ipv4 ~src ~dst =
|
||||
let open Wire_structs in
|
||||
let frame = Cstruct.create sizeof_ethernet in
|
||||
frame |> set_ethernet_src (Macaddr.to_bytes src) 0;
|
||||
frame |> set_ethernet_dst (Macaddr.to_bytes dst) 0;
|
||||
set_ethernet_ethertype frame (ethertype_to_int IPv4);
|
||||
frame
|
||||
|
||||
(** Recalculate checksums after modifying packets.
|
||||
Note that frames often arrive with invalid checksums due to checksum offload.
|
||||
For now, we always calculate valid checksums for out-bound frames. *)
|
||||
let fixup_checksums frame =
|
||||
match Nat_rewrite.layers frame with
|
||||
| None -> raise (Invalid_argument "NAT transformation rendered packet unparseable")
|
||||
| Some (ether, ip, tx) ->
|
||||
let (just_headers, higherlevel_data) =
|
||||
Nat_rewrite.recalculate_transport_checksum (ether, ip, tx)
|
||||
in
|
||||
[just_headers; higherlevel_data]
|
||||
|
||||
let error fmt =
|
||||
let err s = Failure s in
|
||||
Printf.ksprintf err fmt
|
||||
|
||||
let return = Lwt.return
|
||||
let fail = Lwt.fail
|
||||
|
||||
let or_fail msg = function
|
||||
| `Ok x -> return x
|
||||
| `Error _ -> fail (Failure msg)
|
Loading…
x
Reference in New Issue
Block a user