Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,28 @@ jobs:
with:
configFile: .commitlintrc.json

rust:
name: Rust (test + clippy + fmt)
# Compiles the rule pipeline, runs the integration tests, and enforces
# clippy and rustfmt. The Hermit target lands in PR B/C; this job stays
# host-only.
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust toolchain (per rust-toolchain.toml)
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache Cargo registry + build
uses: Swatinem/rust-cache@v2
- name: cargo fmt --check
run: cargo fmt --all -- --check
- name: cargo clippy
run: cargo clippy --all-targets -- -D warnings
- name: cargo test
run: cargo test --all-targets

all-checks:
name: All Checks
# Aggregator job whose single status is what branch-protection rulesets
Expand All @@ -101,6 +123,7 @@ jobs:
- schema-validate
- link-check
- conventional-commits
- rust
runs-on: ubuntu-latest
steps:
- name: Verify all checks passed
Expand Down
153 changes: 153 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[package]
name = "thurward"
version = "0.0.0"
edition = "2024"
rust-version = "1.85"
description = "minimalistic unikernel firewall with FQDN filtering and first-class observability"
license = "TBD"
publish = false

[lib]
# The compile-time rule pipeline lives here so build.rs and integration tests
# can share definitions. PRs B/C add a separate dataplane module for the
# Hermit runtime.
path = "src/lib.rs"

# Single bin target; PR A only needs `cargo test` to be green, the bin compiles
# trivially against the host. PR B switches to the x86_64-unknown-hermit target.
[[bin]]
name = "thurward"
path = "src/main.rs"

# Runtime deps. Kept lean for PR A — smoltcp arrives in PR C with the dataplane.
[dependencies]
# serde + serde_yml: YAML deserialization for examples/rules.yaml.
# serde_yml is an actively-maintained fork; the original serde_yaml is in
# read-only maintenance and we want a maintained dep for the build pipeline.
serde = { version = "1", features = ["derive"] }
serde_yml = "0.0.12"

# Build-script-only deps. Same crates as runtime here; the split exists for
# when PR C/D add runtime-only deps (smoltcp etc.) that build.rs doesn't need.
[build-dependencies]
serde = { version = "1", features = ["derive"] }
serde_yml = "0.0.12"

[dev-dependencies]
# Integration tests stay on the host toolchain.

[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1
panic = "abort"
strip = true

[profile.dev]
panic = "abort"
71 changes: 71 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//! Compile `examples/rules.yaml` into `$OUT_DIR/rules_table.rs` at build
//! time. Per ADR 0005 / `docs/architecture/03-rule-model.md`.
//!
//! Re-runs when `examples/rules.yaml` changes (Cargo's `rerun-if-changed`)
//! AND when any rule-pipeline source changes (because they define the
//! types the generated code references).

#[path = "src/rules/compiler.rs"]
mod compiler;

#[path = "src/rules/types.rs"]
pub mod types;

use std::path::PathBuf;

fn main() {
let manifest_dir =
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let rules_yaml = manifest_dir.join("examples/rules.yaml");
let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR"));
let out_file = out_dir.join("rules_table.rs");

println!("cargo:rerun-if-changed={}", rules_yaml.display());
println!("cargo:rerun-if-changed=src/rules/compiler.rs");
println!("cargo:rerun-if-changed=src/rules/types.rs");
println!("cargo:rerun-if-changed=build.rs");

let yaml_text = std::fs::read_to_string(&rules_yaml).unwrap_or_else(|e| {
panic!(
"thurward build.rs: cannot read {}: {e}\n\
(this file is the source of truth for the compiled rule table;\n\
see docs/architecture/03-rule-model.md)",
rules_yaml.display()
);
});

let source = compiler::compile(&yaml_text).unwrap_or_else(|e| {
panic!(
"thurward build.rs: compiling {} failed: {e}\n\
(the YAML is also validated by CI's `schema-validate` job\n\
against schemas/rules.schema.json — check that first)",
rules_yaml.display()
);
});

std::fs::write(&out_file, &source).unwrap_or_else(|e| {
panic!(
"thurward build.rs: cannot write {}: {e}",
out_file.display()
);
});
}

// `compiler.rs` and `types.rs` are also `mod`-included from `src/lib.rs`,
// but `build.rs` runs before the main crate is built so it cannot depend
// on `crate::*` — we re-include both via `#[path = ...]` above. The
// `compiler::` -> `crate::rules::types::` references in the *emitted* code
// resolve against the main crate at compile time, not against this script.
//
// To keep that pun working, `compiler.rs` references the types via
// `crate::rules::types::*` paths (correct for src/) — we shadow those
// here with the `super::types` path so this script's own type-checking
// passes. This works because `compiler.rs` only uses the types in
// *function signatures*, never in literal codegen output.

// Re-export the script-local `types` module under the path
// `crate::rules::types::*` that compiler.rs expects. `build.rs` has no
// `crate::rules` namespace, so we provide one.
mod rules {
pub use super::types;
}
11 changes: 11 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Toolchain pin for the host-build pipeline.
#
# PR A targets stable: the rule compiler + build.rs run on host and only
# need edition 2024 (stabilised in 1.85).
#
# PR B switches this to nightly when it adds the x86_64-unknown-hermit target
# (Hermit currently requires nightly features). At that point this file is
# updated and `versions.lock` records the nightly date.
[toolchain]
channel = "stable"
components = ["rustfmt", "clippy"]
9 changes: 9 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! thurward — minimalistic unikernel firewall.
//!
//! This crate is split into the **rule pipeline** (host-buildable, used
//! by `build.rs`) and the **dataplane** (added in later PRs, target
//! `x86_64-unknown-hermit`). PR A lands only the rule pipeline.
//!
//! See `docs/architecture/` for the design that this code implements.

pub mod rules;
19 changes: 19 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//! thurward bin — placeholder until PR B/C land the dataplane.
//!
//! PR A's binary just demonstrates that the compiled rule table is
//! reachable from the bin entry. It prints the rule count and the
//! default action, then exits. PR B/C replace this `main` with the
//! Hermit boot path that spawns RX poll threads.

include!(concat!(env!("OUT_DIR"), "/rules_table.rs"));

fn main() {
println!(
"thurward: {} rule(s) compiled in; default action = {:?}",
RULES.len(),
DEFAULT_ACTION
);
for rule in RULES {
println!(" {} ({:?})", rule.id, rule.action);
}
}
Loading
Loading