Skip to content

Commit ba3b017

Browse files
committed
docs(architecture): apply adr 0018 migration (hermit → unikraft)
Mechanical follow-up to ADR 0018 (substrate pivot to Unikraft). Updates the status notes on ADR 0017 + ADR 0001, the supply-chain text in ADR 0010, the Hermit-specific references in the chapter docs (00-overview, 02-packet-path, 03-rule-model, 06-deployment, 08-security-model, 09-limitations), the two canonical mermaid sources (packet-path.mmd, rule-compilation.mmd), and retargets versions.lock from Hermit TBDs to Unikraft TBDs. Specifically: - ADR 0017: Status changed to "Accepted (substrate clause superseded by ADR 0018; language clause + smoltcp::wire clause stay live)"; added forward-pointer; supersedes-line clarified to be language clause only of ADR 0001. - ADR 0001: Status changed from "Superseded by ADR 0017" to "Accepted (substrate clause restored by ADR 0018; language clause superseded by ADR 0017)"; banner rewritten to explain the restoration arc. - ADR 0010 supply-chain section: pinned-deps list updated from "Hermit framework revision" to "Unikraft revision plus each selected lib-* component revision". - Chapter docs: Hermit-specific descriptions softened to "the substrate" or rewritten to reference Unikraft's `lib-uknetdev`. The "Hermit framework maturity" subsection of 09-limitations becomes "`lib-rust` on Unikraft is early-adopter territory" — same risk surfaced for the new substrate. The "No bare-metal direct boot" section now cites both ADR 0017 and ADR 0018 (both keep bare-metal out of v1 scope; ADR 0017's "Hermit isn't ready" framing is replaced with the substrate-agnostic "v1 hypervisor only" framing). - versions.lock: hermit_revision/hermit_rust_target replaced with unikraft_revision/unikraft_libs/unikraft_lib_revisions. lib-* selection inlined per ADR 0018. ADRs 0003, 0008, 0009, 0013, 0014, 0016 still mention Hermit in their historical text. Those are intentionally NOT edited — they record the state at the time of writing and the supersession chain (ADR 0001 → 0017 → 0018) makes the current state findable. `cargo test` clean on the PR A rule pipeline; no source changes in this PR.
1 parent 93a305b commit ba3b017

12 files changed

Lines changed: 162 additions & 111 deletions

docs/architecture/00-overview.md

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ the [decision records](decisions/) in order; then the topical chapters.*
55

66
## What thurward is
77

8-
A minimalistic open-source firewall packaged as a Hermit unikernel
8+
A minimalistic open-source firewall packaged as a Unikraft unikernel
99
written in Rust. It sits **inline between two networks**, filters
1010
traffic on **source CIDR, destination port, and FQDN**, performs
1111
**SNAT/masquerade and static DNAT**, and is configured by editing
@@ -16,14 +16,16 @@ collector — no in-band telemetry traffic. v1 is **single-VM**; HA
1616
and bare-metal direct boot are both deferred to v2.
1717

1818
It is **not** a Linux firewall with extra steps. The OS substrate is
19-
[Hermit](https://hermit-os.org/); the entire data path (parser,
20-
conntrack, filter, NAT, TX) is Rust, using `smoltcp::wire` for typed
21-
packet parsing and writing every other layer ourselves — no in-image
22-
TCP/IP socket layer between the driver and the filter. The image is
23-
a single signed unikernel that boots in milliseconds and exposes no
24-
shell, no mgmt port, no userspace utilities. See
25-
[ADR 0017](decisions/0017-hermit-rust-substrate.md) and
26-
[ADR 0013](decisions/0013-zig-fast-path-on-uknetdev.md).
19+
[Unikraft](https://unikraft.org/) (via `lib-uknetdev` for raw frame
20+
access); the entire data path (parser, conntrack, filter, NAT, TX)
21+
is Rust, using `smoltcp::wire` for typed packet parsing and writing
22+
every other layer ourselves — no in-image TCP/IP socket layer
23+
between the driver and the filter. The image is a single signed
24+
unikernel that boots in milliseconds and exposes no shell, no mgmt
25+
port, no userspace utilities. See
26+
[ADR 0018](decisions/0018-substrate-pivot-unikraft.md) (substrate),
27+
[ADR 0017](decisions/0017-hermit-rust-substrate.md) (language +
28+
parser), and [ADR 0013](decisions/0013-zig-fast-path-on-uknetdev.md).
2729

2830
## Why
2931

@@ -104,9 +106,9 @@ LAN clients
104106
│ │ ├─ DNS proxy + fqdn_set │ │
105107
│ │ └─ observability emitter (vsock) │ │
106108
│ ├──────────────────────────────────────────┤ │
107-
│ │ Hermit virtio-net + virtio-vsock │ │
109+
│ │ Unikraft lib-uknetdev + virtio-vsock │ │
108110
│ ├──────────────────────────────────────────┤ │
109-
│ │ Hermit unikernel core │ │
111+
│ │ Unikraft unikernel core │ │
110112
│ └──────────────────────────────────────────┘ │
111113
└────────┬──────────────────────────┬──────────────┘
112114
│ NIC 0 (LAN) │ NIC 1 (WAN)
@@ -119,9 +121,10 @@ LAN clients
119121

120122
Note: there is no TCP/IP socket layer in the image. `smoltcp` is
121123
linked only for its `wire` / parser modules; the Rust data plane
122-
owns the forwarding loop directly
124+
owns the forwarding loop directly via Unikraft's `lib-uknetdev`
123125
([ADR 0013](decisions/0013-zig-fast-path-on-uknetdev.md),
124-
[ADR 0017](decisions/0017-hermit-rust-substrate.md)).
126+
[ADR 0017](decisions/0017-hermit-rust-substrate.md),
127+
[ADR 0018](decisions/0018-substrate-pivot-unikraft.md)).
125128

126129
(System context, packet path, deployment topology, and the rule/IaC
127130
flow are documented in chapters 01, 02, 06, and 10 respectively, with
@@ -132,9 +135,10 @@ proper diagrams.)
132135
- **Unikernel** — a single-purpose operating system image where the
133136
application and the kernel libraries are linked into one binary.
134137
Boots on a hypervisor; no shell, no users, no multi-process model.
135-
- **Hermit** — a Rust-native unikernel framework. Application runs in
136-
kernel space; `cargo build --target x86_64-unknown-hermit` produces
137-
the image.
138+
- **Unikraft** — a modular unikernel framework. Application runs in
139+
kernel space; the build (`Kraftfile` + `lib-rust` + selected
140+
`lib-*` components) produces a single image. Per
141+
[ADR 0018](decisions/0018-substrate-pivot-unikraft.md).
138142
- **smoltcp** — a `no_std` Rust TCP/IP stack. thurward links only its
139143
`wire` (parser/builder) modules — not its socket or interface layers
140144
— so the application keeps direct control over the forwarding loop.

docs/architecture/02-packet-path.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ the filter sits, where NAT happens, and what the fast path looks like.*
55

66
The decisions framing this chapter:
77

8-
- [ADR 0017](decisions/0017-hermit-rust-substrate.md) — substrate is
9-
Hermit + Rust + smoltcp wire types (no socket layer).
8+
- [ADR 0018](decisions/0018-substrate-pivot-unikraft.md) — substrate
9+
is Unikraft + Rust + smoltcp wire types (no socket layer);
10+
language clause via [ADR 0017](decisions/0017-hermit-rust-substrate.md).
1011
- [ADR 0013](decisions/0013-zig-fast-path-on-uknetdev.md) — the
1112
application owns the data path; no in-image TCP/IP socket layer
1213
between the driver and the filter.
@@ -25,7 +26,7 @@ WAN interface traverses the following sequence:
2526
sequenceDiagram
2627
autonumber
2728
participant NIC0 as virtio-net (LAN)
28-
participant POLL as Hermit RX poll
29+
participant POLL as uknetdev RX poll
2930
participant PARSE as Rust parse (smoltcp::wire)
3031
participant CT as conntrack
3132
participant FILT as filter
@@ -65,10 +66,10 @@ destination IPs (see [ADR 0014](decisions/0014-stateful-nat.md) §
6566

6667
## Where the code lives
6768

68-
Hermit exposes the virtio-net device through a low-level RX/TX
69-
descriptor API; thurward's Rust crate calls into it directly. The
70-
`main` function spawns one **RX poll thread per interface**, each
71-
running:
69+
Unikraft's `lib-uknetdev` exposes each virtio-net device through a
70+
low-level RX/TX descriptor API; thurward's Rust crate calls into it
71+
directly. The `main` function spawns one **RX poll thread per
72+
interface**, each running:
7273

7374
```rust
7475
// src/dataplane.rs — sketch, not the implementation
@@ -121,7 +122,7 @@ The filter does NOT do payload inspection. No DPI, no L7 — just the
121122
per-thread conntrack shards (the 5–10 Gbps stretch in
122123
[ADR 0013](decisions/0013-zig-fast-path-on-uknetdev.md)) is additive,
123124
not a redesign.
124-
- **Hot-loop discipline:** no allocator calls, no syscalls (Hermit's
125+
- **Hot-loop discipline:** no allocator calls, no syscalls (the
125126
unikernel model — application runs in kernel space — helps here),
126127
batched RX/TX descriptors, slice-based smoltcp parsers that don't
127128
copy, branch-prediction-friendly common-case parsing.

docs/architecture/03-rule-model.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Cargo `build.rs` script compiles `rules.yaml` into a packed Rust
9999
```mermaid
100100
flowchart LR
101101
A[rules.yaml] -->|build.rs| B[OUT_DIR/rules_table.rs]
102-
B --> C[cargo build --target x86_64-unknown-hermit]
102+
B --> C[Unikraft build with lib-rust]
103103
C --> F[thurward image]
104104
G[schemas/rules.schema.json] -.validates.-> A
105105
style A fill:#dae8fc,stroke:#6c8ebf

docs/architecture/06-deployment.md

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ and the [decision records](decisions/).*
66

77
The deployment model is dictated by:
88

9-
- [ADR 0017](decisions/0017-hermit-rust-substrate.md) — Hermit
10-
unikernel + Rust; the same image runs on two v1 targets (QEMU/KVM
11-
and Firecracker). Bare-metal direct boot is deferred to v2 — see
12-
[09 — Limitations](09-limitations.md).
9+
- [ADR 0018](decisions/0018-substrate-pivot-unikraft.md) — Unikraft
10+
unikernel substrate; [ADR 0017](decisions/0017-hermit-rust-substrate.md)
11+
— Rust language clause. The same image runs on two v1 targets
12+
(QEMU/KVM and Firecracker). Bare-metal direct boot is deferred to
13+
v2 — see [09 — Limitations](09-limitations.md).
1314
- [ADR 0013](decisions/0013-zig-fast-path-on-uknetdev.md) — the app
1415
owns the data path; no in-image TCP/IP socket layer (smoltcp's
1516
wire/parser modules only).
@@ -33,8 +34,8 @@ The deployment model is dictated by:
3334

3435
Steps 1 and 2 happen on a developer/operator workstation (or in CI if
3536
preferred). Step 3 happens on the target host. The artifact crossing
36-
the boundary is **one signed Hermit unikernel image plus its cosign
37-
signature and SLSA-3 attestation**
37+
the boundary is **one signed Unikraft unikernel image plus its
38+
cosign signature and SLSA-3 attestation**
3839
([ADR 0010](decisions/0010-supply-chain-hardening.md)).
3940

4041
Both v1 deployment paths consume the same image; only the launcher
@@ -56,7 +57,7 @@ cd thurward
5657
# edit rules.yaml — VS Code / Helix will pull schemas/rules.schema.json
5758
# automatically for autocomplete + inline validation
5859
$EDITOR rules.yaml
59-
make build # wraps `cargo build --release --target x86_64-unknown-hermit`
60+
make build # wraps the Unikraft + Rust build flow (Kraftfile + lib-rust)
6061
make run # wraps the QEMU invocation below
6162
```
6263

@@ -65,7 +66,7 @@ make run # wraps the QEMU invocation below
6566
```bash
6667
qemu-system-x86_64 \
6768
-enable-kvm -cpu host -smp 1 -m 128M \
68-
-kernel target/x86_64-unknown-hermit/release/thurward \
69+
-kernel build/thurward_kvm-x86_64 \
6970
-netdev bridge,id=lan,br=br-lan -device virtio-net-pci,netdev=lan \
7071
-netdev bridge,id=wan,br=br-wan -device virtio-net-pci,netdev=wan \
7172
-device vhost-vsock-pci,guest-cid=3
@@ -109,16 +110,18 @@ A reference `thurward.service` systemd unit is shipped in `contrib/`:
109110
it runs `firecracker` with two virtio NICs, one virtio-vsock device
110111
(CID 3, host port 9000 for observability), and the current image; it
111112
restarts on failure. On rule changes, replace `current.image` and
112-
`systemctl restart thurward` — the restart is fast (Firecracker
113-
sub-10ms boot + Hermit sub-10ms boot), but **flows in progress drop**
113+
`systemctl restart thurward` — the restart is fast (Firecracker and
114+
unikernel boot are both sub-10ms), but **flows in progress drop**
114115
because v1 has no HA ([09 — Limitations](09-limitations.md)).
115116

116117
## Bare-metal — deferred to v2
117118

118119
A previous revision of this chapter described a third path (bare-metal
119-
x86_64 EFI direct boot). [ADR 0017](decisions/0017-hermit-rust-substrate.md)
120-
walks that back: Hermit's bare-metal story isn't ready for the v1
121-
promise. Bare-metal is a known v2 direction; see
120+
x86_64 EFI direct boot). Per
121+
[ADR 0017](decisions/0017-hermit-rust-substrate.md) and
122+
[ADR 0018](decisions/0018-substrate-pivot-unikraft.md), bare-metal is
123+
out of scope for v1 regardless of substrate; v1 ships hypervisor
124+
targets only. Bare-metal is a known v2 direction; see
122125
[09 — Limitations](09-limitations.md) for the trade-off and the v2
123126
options under consideration.
124127

@@ -142,8 +145,8 @@ operational story are the same.
142145

143146
| Lives on the host | Lives in the image |
144147
| -------------------------- | -------------------------------------------------------------------------------------- |
145-
| Linux bridges | Hermit unikernel core |
146-
| TAP devices | Hermit virtio-net + virtio-vsock drivers |
148+
| Linux bridges | Unikraft unikernel core (`lib-ukboot`, `lib-ukalloc`, `lib-uksched`) |
149+
| TAP devices | `lib-uknetdev` + `lib-ukbus-virtio` (virtio-net + virtio-vsock drivers) |
147150
| `vhost_vsock` module | smoltcp (wire/parser layer only) |
148151
| `cosign` + `slsa-verifier` | Rust data plane (parse, conntrack, filter, NAT, TX) |
149152
| Collector stack | Compiled rule + DNAT table (Rust module generated by `build.rs` from rules.yaml) |

docs/architecture/08-security-model.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,12 @@ In words:
5858
2. **CI build environment** is pinned (Nix flake or container SHA).
5959
Network-isolated; deps fetched against a content-addressed proxy.
6060
3. **Build inputs** (Rust toolchain channel + components, every
61-
transitive crate in `Cargo.lock`, the Hermit framework revision,
62-
the smoltcp crate version) are pinned in `versions.lock`. CI
63-
fails on drift. See
64-
[ADR 0017](decisions/0017-hermit-rust-substrate.md) and
61+
transitive crate in `Cargo.lock`, the Unikraft revision plus each
62+
selected `lib-*` component revision, the smoltcp crate version)
63+
are pinned in `versions.lock`. CI fails on drift. See
64+
[ADR 0018](decisions/0018-substrate-pivot-unikraft.md) (substrate),
65+
[ADR 0017](decisions/0017-hermit-rust-substrate.md) (language +
66+
parser), and
6567
[ADR 0010](decisions/0010-supply-chain-hardening.md).
6668
4. **Build output** is reproducible — byte-identical given the same
6769
inputs.

docs/architecture/09-limitations.md

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -65,23 +65,23 @@ environment, thurward v1 is not for you.
6565

6666
## No bare-metal direct boot (v1)
6767

68-
A prior revision of this architecture lifted bare-metal x86_64 EFI
69-
direct boot to a v1 first-class target on Unikraft. The substrate
70-
switch to Hermit ([ADR 0017](decisions/0017-hermit-rust-substrate.md))
71-
walks that back: Hermit's bare-metal story is less mature than
72-
Unikraft's, and rather than weaken either the substrate decision or
73-
the bare-metal promise, v1 commits to Hermit and defers bare-metal
74-
to v2.
75-
7668
v1 targets are **QEMU/KVM** and **Firecracker** only
77-
([06 — Deployment](06-deployment.md)). If you need a dedicated-hardware
78-
deployment today, run a small Linux+KVM appliance image on the box and
79-
launch Firecracker (Path B) on top.
80-
81-
The v2 ADR for bare-metal will revisit the trade-off: Hermit may have
82-
matured, or a Linux+KVM appliance image may be the pragmatic answer,
83-
or a different unikernel's bare-metal story may have caught up
84-
(Unikraft on ARM, MirageOS, etc.).
69+
([06 — Deployment](06-deployment.md)). Bare-metal x86_64 EFI direct
70+
boot is deferred to v2 — per
71+
[ADR 0017](decisions/0017-hermit-rust-substrate.md) and
72+
[ADR 0018](decisions/0018-substrate-pivot-unikraft.md), both
73+
substrate-related decisions explicitly keep bare-metal out of v1
74+
scope. Unikraft does have a credible bare-metal direct-boot story,
75+
but v1 does not exercise it.
76+
77+
If you need a dedicated-hardware deployment today, run a small
78+
Linux+KVM appliance image on the box and launch Firecracker (Path B)
79+
on top.
80+
81+
The v2 ADR for bare-metal will revisit the trade-off: Unikraft's
82+
bare-metal target may be ready to lift, or a Linux+KVM appliance
83+
image may remain the pragmatic answer, or a different unikernel's
84+
bare-metal story may have caught up (MirageOS, etc.).
8585

8686
ARM in general is also not in v1.
8787

@@ -139,24 +139,28 @@ For Path A (QEMU) and Path B (Firecracker), the host kernel must have
139139
`vhost_vsock` loaded (`modprobe vhost_vsock`). Without it, the
140140
observability channel doesn't open.
141141

142-
## Hermit framework maturity
142+
## `lib-rust` on Unikraft is early-adopter territory
143143

144-
Hermit ([ADR 0017](decisions/0017-hermit-rust-substrate.md)) is
145-
actively developed but has a smaller user base than Unikraft. Expect
146-
a narrower set of example projects and Stack Overflow answers when
147-
diagnosing build or runtime issues. Upstream patches for bugs we
148-
hit may take longer to land than they would on a larger framework.
144+
Unikraft's Rust integration (`lib-rust`) is less mature than its C
145+
support. Some Rust crates that assume a hosted runtime (`std::*`) may
146+
not build cleanly inside the image — the hot loop is already
147+
`no_std`-friendly per [ADR 0013](decisions/0013-zig-fast-path-on-uknetdev.md),
148+
but adding new runtime dependencies needs a `no_std` check. Build-
149+
time crates run on the host and aren't subject to this constraint.
150+
Upstream patches we contribute to `lib-rust` may take longer to land
151+
than they would in Unikraft's C ecosystem.
149152

150153
## Toolchain pinning is heavier than the prior Zig stack
151154

152-
The Rust + Hermit + smoltcp stack pins more moving parts than the
153-
prior Zig + Unikraft stack did: `rust-toolchain.toml` (rustup channel
154-
+ components), `Cargo.lock` (every transitive crate), the Hermit
155-
framework revision, and the smoltcp crate version all live in
156-
`versions.lock` (see
157-
[ADR 0010](decisions/0010-supply-chain-hardening.md)). Upgrades are
158-
deliberate, reviewed actions with reproducibility re-validation —
159-
not passive `cargo update` runs.
155+
The Rust + Unikraft + smoltcp stack pins more moving parts than the
156+
prior Zig stack did: `rust-toolchain.toml` (rustup channel +
157+
components), `Cargo.lock` (every transitive crate), the Unikraft
158+
revision plus each selected `lib-*` component revision, and the
159+
smoltcp crate version all live in `versions.lock` (see
160+
[ADR 0010](decisions/0010-supply-chain-hardening.md) and
161+
[ADR 0018](decisions/0018-substrate-pivot-unikraft.md)). Upgrades
162+
are deliberate, reviewed actions with reproducibility re-validation
163+
— not passive `cargo update` runs.
160164

161165
## Out of scope explicitly
162166

docs/architecture/decisions/0001-unikraft-as-os-substrate.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1-
# ADR 0001 — Unikraft as OS substrate (superseded)
1+
# ADR 0001 — Unikraft as OS substrate
22

3-
**Status:** Superseded by [ADR 0017](0017-hermit-rust-substrate.md)
3+
**Status:** Accepted (substrate clause restored by
4+
[ADR 0018](0018-substrate-pivot-unikraft.md); language clause
5+
superseded by [ADR 0017](0017-hermit-rust-substrate.md) — original
6+
Zig choice replaced with Rust)
47
**Date:** 2026-05-23
5-
**Superseded:** 2026-05-25
8+
**Superseded:** 2026-05-25 (substrate clause originally) —
9+
2026-05-25 (substrate clause restored)
610
**Deciders:** magicletur
711

8-
> **Superseded.** thurward does not ship on Unikraft. The substrate
9-
> chosen for v1 is **Hermit** — see
10-
> [ADR 0017](0017-hermit-rust-substrate.md). This ADR is preserved
11-
> because the *non*-Unikraft alternatives analysis below (why not
12-
> stripped Linux, why not Alpine + Rust, etc.) still applies and is
13-
> referenced by ADR 0017 as the canonical "why a unikernel at all"
14-
> argument.
12+
> **Substrate clause restored.** thurward ships on **Unikraft** for
13+
> v1, with Rust as the application language (per
14+
> [ADR 0017](0017-hermit-rust-substrate.md)'s language clause) and
15+
> `lib-uknetdev` for raw frame access (per
16+
> [ADR 0018](0018-substrate-pivot-unikraft.md)). The original
17+
> language clause of this ADR (Zig) is superseded by ADR 0017; the
18+
> substrate clause was briefly superseded by ADR 0017 and restored
19+
> by ADR 0018 after phase-1 implementation found Hermit was the
20+
> wrong substrate shape for a forwarding device. The original
21+
> alternatives analysis below (why not stripped Linux, why not
22+
> Alpine + Rust, etc.) still applies.
1523
1624
## What survives from this ADR
1725

0 commit comments

Comments
 (0)