Skip to content

feat(rules): compile-time rule pipeline (PR A of phase 1) - #3

Merged
LeTuR merged 2 commits into
mainfrom
feat/rule-compiler
May 25, 2026
Merged

feat(rules): compile-time rule pipeline (PR A of phase 1)#3
LeTuR merged 2 commits into
mainfrom
feat/rule-compiler

Conversation

@LeTuR

@LeTuR LeTuR commented May 25, 2026

Copy link
Copy Markdown
Owner

Summary

First implementation slice of thurward (PR A of phase 1 per the implementation plan):

  • Cargo workspace at repo root, single crate, lib + bin split.
  • build.rs compiles examples/rules.yaml\$OUT_DIR/rules_table.rs (a const RULES: &[Rule] + DEFAULT_ACTION).
  • Runtime types (Rule, Action, Direction, Protocol, PortSpec, CidrV4, StateSpec) in src/rules/types.rs — all Copy and const-constructable so the table lives in .rodata.
  • 13 integration + unit tests (cargo test --all-targets).
  • New CI job Rust (test + clippy + fmt); wired into the All Checks aggregator's needs: list.

What this does NOT do (deferred)

  • No data path. cargo run prints the rule table + exits. PR B adds the Hermit substrate; PR C adds RX/TX poll + filter loop.
  • No conntrack — state field is recorded but unused (phase 2).
  • No DNS proxy / FQDN matching — destination_fqdn is recorded but unused (phase 4). Rules that need it are flagged via Rule::needs_future_runtime().
  • No SNAT/DNAT — nat block is parsed but emitted as comments only (phase 3).

Verified locally

  • cargo test: 13/13 passing
  • cargo clippy --all-targets -- -D warnings: clean
  • cargo fmt --check: clean
  • cargo run against examples/rules.yaml:
    thurward: 3 rule(s) compiled in; default action = Deny
      allow-dns-out  (Accept)
      allow-github-https  (Accept)
      allow-established  (Accept)
    

Design references

  • ADR 0005 — build-time rule compilation
  • ADR 0010 — supply-chain hardening (the versions.lock placeholder)
  • ADR 0017 — Hermit + Rust substrate (PR B switches the toolchain pin)
  • docs/architecture/03-rule-model.md — rule format + matching semantics

LeTuR added 2 commits May 25, 2026 21:16
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.
@LeTuR
LeTuR merged commit 3cdc351 into main May 25, 2026
6 checks passed
@LeTuR
LeTuR deleted the feat/rule-compiler branch May 25, 2026 19:23
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant