feat(rules): compile-time rule pipeline (PR A of phase 1) - #3
Merged
Conversation
Lands the host-buildable half of phase 1: a Cargo workspace, the runtime rule types, and a build.rs that turns examples/rules.yaml into a const RULES table at compile time. Per ADR 0005 and docs/architecture/03-rule-model.md. Layout: - Cargo.toml — single crate, lib + bin split so build.rs can share definitions with the dataplane (added in PR B/C). - rust-toolchain.toml — pin stable for now; PR B switches to nightly when it adds the x86_64-unknown-hermit target. - versions.lock — placeholder fields for Hermit/smoltcp/toolchain pins per ADR 0010; filled in by PR B/C. - build.rs — reads examples/rules.yaml, calls the compiler, writes $OUT_DIR/rules_table.rs. cargo:rerun-if-changed on YAML + sources. - src/rules/types.rs — Rule, Action, Direction, Protocol, PortSpec, CidrV4, StateSpec. All Copy + const-constructable so the generated RULES slice lives in .rodata. CidrV4::contains is the matcher used in phase 1. - src/rules/compiler.rs — YAML parse + validate + codegen. Rejects conflicting destination / destination_fqdn (schema-redundant; the CI schema-validate job is the upstream check). Errors stringify to point at the offending YAML. - src/main.rs — placeholder bin that prints the compiled rule count + default action. Replaced by PR C with the Hermit boot path. - src/lib.rs — re-exports the rule pipeline. - tests/rule_compiler.rs — integration tests against examples/rules.yaml (regression guard on rule count and IDs) plus fixtures for the per-field permutations: minimal, port range, default-allow flip, /32 host route, and the four reject cases (missing id, bad CIDR, bad action, destination+fqdn conflict, inverted port range). Plus unit tests for CidrV4::contains. Phase-1 scope (deferred to later PRs): - destination_fqdn is recorded but not matched (DNS proxy lands in phase 4, ADR 0004). Rule::needs_future_runtime() lets phase-1 callers skip such rules instead of treating them as always-true. - state is recorded but not matched (conntrack lands in phase 2). - defaults.* other than default_action and the whole nat block are parsed but emitted as comments in the generated module. Verified locally: - cargo test: 13/13 passing. - cargo clippy --all-targets -- -D warnings: clean. - cargo fmt --check: clean. - cargo run prints the canonical 3-rule + Deny output.
PR A of phase 1 ships a Cargo crate with a host-buildable rule pipeline; CI needs to keep it building and tested. The new `rust` job runs `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test --all-targets` against the toolchain pinned in `rust-toolchain.toml` (currently stable; PR B will move to nightly when it adds the x86_64-unknown-hermit target). Wired into the `all-checks` aggregator's `needs:` list so the branch-protection ruleset's single required status (`All Checks`) keeps gating on this job too without a ruleset edit.
3 tasks
LeTuR
added a commit
that referenced
this pull request
May 25, 2026
Phase-1 PR B research found that upstream `hermit-os/kernel` does not expose a public raw-frame API — every relevant layer is `pub(crate)` and the application-facing `hermit-abi` is POSIX sockets only. A firewall is a forwarding device; the socket layer terminates flows and cannot pass them through. The previous draft of ADR 0018 (closed PR #4) proposed forking hermit-os/kernel. That's been rejected on reconsideration: a fork papers over a substrate mismatch (Hermit's model is "app uses network stack"; ours is "app IS network stack") rather than fixing the underlying choice. This ADR pivots the substrate back to **Unikraft** (the original ADR 0001 choice) while keeping Rust as the application language and smoltcp::wire as the parser library. Both of those clauses from ADR 0017 stay live; only the Hermit substrate clause is superseded. The two reasons ADR 0017 cited for choosing Hermit over Unikraft don't survive contact with the implementation: - "Rust-native single toolchain" was true but the Rust we'd write has to fight Hermit's design intent. - "Unikraft's value-add (C library catalog) is mostly libs we no longer use" was wrong: we DO want one specific piece — `lib-uknetdev` — which is exactly the raw-frame device API the thurward data path needs per ADR 0013 and chapter 02. Four alternatives weighed and rejected: fork hermit-os/kernel, distroless Linux + Rust + AF_PACKET, wait for upstream Hermit PR, MirageOS. Each rejection cites the architecture requirement(s) it would violate or the asymmetric cost. Status: Proposed. The ADR ends with three confirmation questions for the deciders. On acceptance, a follow-up PR updates ADR 0017 (status note), ADR 0001 (status note), the chapter docs that name Hermit specifically, ADR 0010's supply-chain section, and the in-progress `versions.lock` (currently lists Hermit TBDs from PR #3).
LeTuR
added a commit
that referenced
this pull request
May 25, 2026
Phase-1 PR B research found that upstream `hermit-os/kernel` does not expose a public raw-frame API — every relevant layer is `pub(crate)` and the application-facing `hermit-abi` is POSIX sockets only. A firewall is a forwarding device; the socket layer terminates flows and cannot pass them through. The previous draft of ADR 0018 (closed PR #4) proposed forking hermit-os/kernel. That's been rejected on reconsideration: a fork papers over a substrate mismatch (Hermit's model is "app uses network stack"; ours is "app IS network stack") rather than fixing the underlying choice. This ADR pivots the substrate back to **Unikraft** (the original ADR 0001 choice) while keeping Rust as the application language and smoltcp::wire as the parser library. Both of those clauses from ADR 0017 stay live; only the Hermit substrate clause is superseded. The two reasons ADR 0017 cited for choosing Hermit over Unikraft don't survive contact with the implementation: - "Rust-native single toolchain" was true but the Rust we'd write has to fight Hermit's design intent. - "Unikraft's value-add (C library catalog) is mostly libs we no longer use" was wrong: we DO want one specific piece — `lib-uknetdev` — which is exactly the raw-frame device API the thurward data path needs per ADR 0013 and chapter 02. Four alternatives weighed and rejected: fork hermit-os/kernel, distroless Linux + Rust + AF_PACKET, wait for upstream Hermit PR, MirageOS. Each rejection cites the architecture requirement(s) it would violate or the asymmetric cost. Status: Proposed. The ADR ends with three confirmation questions for the deciders. On acceptance, a follow-up PR updates ADR 0017 (status note), ADR 0001 (status note), the chapter docs that name Hermit specifically, ADR 0010's supply-chain section, and the in-progress `versions.lock` (currently lists Hermit TBDs from PR #3).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First implementation slice of thurward (PR A of phase 1 per the implementation plan):
build.rscompilesexamples/rules.yaml→\$OUT_DIR/rules_table.rs(a constRULES: &[Rule]+DEFAULT_ACTION).Rule,Action,Direction,Protocol,PortSpec,CidrV4,StateSpec) insrc/rules/types.rs— allCopyand const-constructable so the table lives in.rodata.cargo test --all-targets).Rust (test + clippy + fmt); wired into theAll Checksaggregator'sneeds:list.What this does NOT do (deferred)
cargo runprints the rule table + exits. PR B adds the Hermit substrate; PR C adds RX/TX poll + filter loop.statefield is recorded but unused (phase 2).destination_fqdnis recorded but unused (phase 4). Rules that need it are flagged viaRule::needs_future_runtime().natblock is parsed but emitted as comments only (phase 3).Verified locally
cargo test: 13/13 passingcargo clippy --all-targets -- -D warnings: cleancargo fmt --check: cleancargo runagainstexamples/rules.yaml:Design references
versions.lockplaceholder)docs/architecture/03-rule-model.md— rule format + matching semantics