Skip to main content

tatara_ebpf/
lib.rs

1//! tatara-ebpf — author eBPF programs, maps, and policies in
2//! tatara-lisp; build hermetically through Rust + aya.
3//!
4//! ## The merger
5//!
6//! eBPF is the canonical case where Rust + tatara-lisp earn their
7//! keep. Three layers, each contributing what it does best:
8//!
9//! ```text
10//!   tatara-lisp authoring      (defbpf-program drop-syn :kind :xdp …)
11//!         ↓ typed surface
12//!   typed Rust structs         BpfProgramSpec { kind, attach, … }
13//!         ↓ codegen + build
14//!   aya-compatible Rust src    #[xdp] fn drop_syn(ctx) -> u32 { … }
15//!         ↓ rustc + libbpf
16//!   BPF bytecode object        drop-syn.bpf.o   (content-addressed)
17//!         ↓ runtime load
18//!   kernel verifier + JIT      attached to eth0 ingress
19//! ```
20//!
21//! This crate exposes the **typed surface** — three keyword forms
22//! authorable from tatara-lisp once `register()` is called:
23//!
24//! - `(defbpf-program …)` — one BPF program (kind, attach point, source).
25//! - `(defbpf-map …)` — one BPF map (kind, key/value, max-entries, pinning).
26//! - `(defbpf-policy …)` — high-level composition: maps + programs +
27//!   attach order — the IaC-style declaration that tools can apply.
28//!
29//! The runtime (`aya-runtime` feature) wires these to aya's loader,
30//! attaching programs and surfacing maps for read/write. The codegen
31//! tier (planned) lets you write the program **body** in tatara-lisp
32//! and emits the matching aya Rust source automatically — the
33//! "best merger of the two" the pleme-io theory points at.
34//!
35//! ## Why hand-written, not forge-generated
36//!
37//! BPF programs aren't CRDs. There's no upstream YAML schema we can
38//! ingest mechanically — the authoring surface is itself the design
39//! decision. Hand-written here, this crate is the canonical example
40//! of the **non-CRD domain pattern**: any future "no-schema" wrapper
41//! (HAProxy / nginx / iptables / WireGuard configs) follows the same
42//! shape — typed structs + register() + tests.
43
44pub mod bpf_fn;
45pub mod codegen;
46pub mod runtime;
47pub mod spec;
48
49pub use spec::{
50    BpfAttachPoint, BpfMapKind, BpfMapSpec, BpfPolicySpec, BpfProgramKind, BpfProgramSpec,
51};
52
53// ── Capability registrations beyond the typed compile path ────────
54//
55// Hand-written domains declare their non-render capability metadata
56// inline (the forge populates the trait impls for forge-generated
57// domains; we do it ourselves here).
58
59impl tatara_lisp::DocumentedDomain for BpfProgramSpec {
60    const DOCSTRING: &'static str =
61        "One BPF program — kind (XDP/TC/kprobe/...), attach point, source, license. \
62         Loaded via aya at runtime; built hermetically through substrate's ebpf.nix.";
63    const FIELD_DOCS: &'static [(&'static str, &'static str)] = &[
64        ("name", "Program name — the symbol exported in the BPF object."),
65        ("kind", "BPF program kind. Drives the aya `#[xdp]` etc. attribute."),
66        ("attach", "Where the program attaches (interface, kernel symbol, cgroup, ...)"),
67        ("source", "Path to the program body — `*.rs`, `*.bpf.o`, or `*.tlisp:fn`."),
68        ("license", "SPDX license string. GPL required for most helpers."),
69        ("pin_path", "Optional bpffs pin path so the program survives the loader."),
70        ("uses_maps", "BPF maps this program reads or writes."),
71    ];
72}
73
74impl tatara_lisp::DocumentedDomain for BpfMapSpec {
75    const DOCSTRING: &'static str =
76        "One BPF map — hash / array / per-cpu / ring-buf / etc. \
77         The kernel-↔-userspace data plane for BPF programs.";
78    const FIELD_DOCS: &'static [(&'static str, &'static str)] = &[
79        ("name", "Map name."),
80        ("kind", "Map kind — drives access pattern (hash/array/perf-event/...)"),
81        ("key_size", "Key size in bytes (0 for keyless maps like RingBuf)."),
82        ("value_size", "Value size in bytes."),
83        ("max_entries", "Capacity. For RingBuf, total bytes (page-rounded)."),
84        ("pin_path", "Optional bpffs pin path."),
85    ];
86}
87
88impl tatara_lisp::DocumentedDomain for BpfPolicySpec {
89    const DOCSTRING: &'static str =
90        "Composition of programs + maps applied as one unit. The IaC-shape \
91         arch-synthesizer + FluxCD consume.";
92    const FIELD_DOCS: &'static [(&'static str, &'static str)] = &[
93        ("name", "Policy name."),
94        ("description", "Human-readable description."),
95        ("programs", "Names of `defbpf-program`s composed in this policy."),
96        ("maps", "Names of `defbpf-map`s composed in this policy."),
97    ];
98}
99
100// Dependency layer — real edges this time. A policy is meaningful
101// only when its constituent programs + maps are already declared,
102// so it depends on both keywords. Programs depend on the maps they
103// reference (captured per-instance via `uses_maps`; type-level
104// they just depend on `defbpf-map`).
105impl tatara_lisp::DependentDomain for BpfMapSpec {
106    const DEPENDS_ON: &'static [&'static str] = &[];
107}
108impl tatara_lisp::DependentDomain for BpfProgramSpec {
109    const DEPENDS_ON: &'static [&'static str] = &["defbpf-map"];
110}
111impl tatara_lisp::DependentDomain for BpfPolicySpec {
112    const DEPENDS_ON: &'static [&'static str] = &["defbpf-program", "defbpf-map"];
113}
114
115// Attestation layer — same namespace for all three bpf domains.
116// The pleme.io group prefix prevents collision with the CNCF
117// k8s.io namespace tree even if a future CRD picks the same
118// keyword by accident.
119impl tatara_lisp::AttestableDomain for BpfMapSpec {
120    const ATTESTATION_NAMESPACE: &'static str = "pleme.io/ebpf";
121}
122impl tatara_lisp::AttestableDomain for BpfProgramSpec {
123    const ATTESTATION_NAMESPACE: &'static str = "pleme.io/ebpf";
124}
125impl tatara_lisp::AttestableDomain for BpfPolicySpec {
126    const ATTESTATION_NAMESPACE: &'static str = "pleme.io/ebpf";
127}
128
129// Validation layer — semantic checks the type system can't
130// catch. The kernel verifier rejects programs that call GPL-only
131// helpers without a GPL-compatible license; surfacing that at
132// compile time is far friendlier than a runtime ENOPKG. We're
133// conservative: any program that touches a map demands a
134// GPL-compatible license, since `bpf_map_lookup_elem` is GPL.
135impl tatara_lisp::ValidatedDomain for BpfProgramSpec {
136    fn validate_value(value: &serde_json::Value) -> std::result::Result<(), String> {
137        let obj = value
138            .as_object()
139            .ok_or_else(|| "expected JSON object".to_string())?;
140        let license = obj
141            .get("license")
142            .and_then(|v| v.as_str())
143            .unwrap_or("");
144        let uses_maps = obj
145            .get("uses_maps")
146            .and_then(|v| v.as_array())
147            .map(|a| !a.is_empty())
148            .unwrap_or(false);
149        if uses_maps && !is_gpl_compatible(license) {
150            return Err(format!(
151                "BPF program declares `:uses-maps` but `:license` `{license}` \
152                 is not GPL-compatible — kernel verifier will reject \
153                 calls to bpf_map_lookup_elem etc."
154            ));
155        }
156        // Per-kind sanity: XDP / TC need an interface in attach.target.
157        if let Some(kind) = obj.get("kind").and_then(|v| v.as_str()) {
158            let attach_target = obj
159                .get("attach")
160                .and_then(|a| a.get("target"))
161                .and_then(|t| t.as_str())
162                .unwrap_or("");
163            let needs_iface = matches!(kind, ":xdp" | ":tc");
164            if needs_iface && attach_target.is_empty() {
165                return Err(format!(
166                    "BPF program kind `{kind}` requires `:attach (:target \"<iface>\")` — got empty target"
167                ));
168            }
169        }
170        Ok(())
171    }
172}
173
174fn is_gpl_compatible(license: &str) -> bool {
175    matches!(license, "GPL" | "GPL v2" | "Dual MIT/GPL" | "Dual BSD/GPL")
176}
177
178impl tatara_lisp::ValidatedDomain for BpfMapSpec {}
179impl tatara_lisp::ValidatedDomain for BpfPolicySpec {}
180
181// Compliance layer — BPF programs at the kernel boundary
182// participate in NIST SC-7 (boundary protection) and CIS 5.1
183// (network controls) when they enforce L4 policy. Programs
184// alone don't satisfy a control; the policy DOES (it's the
185// auditable unit). Maps are pure data, claim no controls.
186impl tatara_lisp::CompliantDomain for BpfMapSpec {
187    const FRAMEWORKS: &'static [&'static str] = &[];
188    const CONTROLS: &'static [&'static str] = &[];
189}
190impl tatara_lisp::CompliantDomain for BpfProgramSpec {
191    const FRAMEWORKS: &'static [&'static str] = &[];
192    const CONTROLS: &'static [&'static str] = &[];
193}
194impl tatara_lisp::CompliantDomain for BpfPolicySpec {
195    const FRAMEWORKS: &'static [&'static str] = &["NIST 800-53", "CIS"];
196    const CONTROLS: &'static [&'static str] = &[
197        "NIST SC-7",   // boundary protection
198        "NIST SI-3",   // malicious code protection (when used as filter)
199        "CIS 5.1",     // network access controls
200    ];
201}
202
203// Observability — BPF programs emit per-CPU counter samples
204// scraped via the userspace exporter. Maps are storage, not
205// metric sources directly. Policies inherit from their programs.
206impl tatara_lisp::ObservableDomain for BpfMapSpec {
207    const METRIC_PREFIX: &'static str = "";
208    const LOG_LABELS: &'static [&'static str] = &[];
209}
210impl tatara_lisp::ObservableDomain for BpfProgramSpec {
211    const METRIC_PREFIX: &'static str = "tatara_ebpf_program";
212    const LOG_LABELS: &'static [&'static str] = &["program", "kind", "interface"];
213}
214impl tatara_lisp::ObservableDomain for BpfPolicySpec {
215    const METRIC_PREFIX: &'static str = "tatara_ebpf_policy";
216    const LOG_LABELS: &'static [&'static str] = &["policy"];
217}
218
219// Help — mnemonic + a working example each.
220impl tatara_lisp::HelpDomain for BpfMapSpec {
221    const MNEMONIC: &'static str = "kernel-↔-userspace data plane";
222    const EXAMPLES: &'static [&'static str] = &[concat!(
223        "(defbpf-map\n",
224        "  :name \"syn-counter\"\n",
225        "  :kind :per-cpu-array\n",
226        "  :key-size 4 :value-size 8 :max-entries 1)"
227    )];
228}
229impl tatara_lisp::HelpDomain for BpfProgramSpec {
230    const MNEMONIC: &'static str = "one BPF program (XDP/TC/kprobe/...)";
231    const EXAMPLES: &'static [&'static str] = &[concat!(
232        "(defbpf-program\n",
233        "  :name \"drop-syn-flood\"\n",
234        "  :kind :xdp\n",
235        "  :attach (:target \"eth0\")\n",
236        "  :source \"bpf/drop_syn.rs\"\n",
237        "  :license \"GPL\")"
238    )];
239}
240impl tatara_lisp::HelpDomain for BpfPolicySpec {
241    const MNEMONIC: &'static str = "composition of programs + maps as one IaC unit";
242    const EXAMPLES: &'static [&'static str] = &[concat!(
243        "(defbpf-policy\n",
244        "  :name \"edge-protection\"\n",
245        "  :description \"L4 SYN-flood mitigation\"\n",
246        "  :programs (\"drop_syn_flood\")\n",
247        "  :maps (\"syn_counter\"))"
248    )];
249}
250
251// Stability — the bpf surface is stable but young; programs +
252// policies are 0.2 (post-MVP), maps are 0.1 (just landed).
253impl tatara_lisp::StableDomain for BpfMapSpec {
254    const STABILITY: &'static str = "stable";
255    const SINCE_VERSION: &'static str = "0.1.0";
256}
257impl tatara_lisp::StableDomain for BpfProgramSpec {
258    const STABILITY: &'static str = "stable";
259    const SINCE_VERSION: &'static str = "0.2.0";
260}
261impl tatara_lisp::StableDomain for BpfPolicySpec {
262    const STABILITY: &'static str = "stable";
263    const SINCE_VERSION: &'static str = "0.2.0";
264}
265
266// Lifecycle layer — kernel-attached programs need BlueGreen.
267// The verifier rejects half-loaded state; the only safe shape
268// is "load new program in parallel, atomically replace the
269// attach point, unload the old one." Maps follow the same
270// pattern when their key/value sizes change. Policies compose
271// programs + maps so they inherit BlueGreen too.
272impl tatara_lisp::LifecycleProtocol for BpfProgramSpec {
273    const STRATEGY: tatara_lisp::RolloutStrategy = tatara_lisp::RolloutStrategy::BlueGreen;
274    const DRAIN_SECONDS: u32 = 5;
275}
276impl tatara_lisp::LifecycleProtocol for BpfMapSpec {
277    // Maps are state — recreating loses the contents. When the
278    // shape changes we still need Recreate (no in-place resize),
279    // but the drain is shorter since maps don't run code.
280    const STRATEGY: tatara_lisp::RolloutStrategy = tatara_lisp::RolloutStrategy::Recreate;
281    const DRAIN_SECONDS: u32 = 1;
282}
283impl tatara_lisp::LifecycleProtocol for BpfPolicySpec {
284    const STRATEGY: tatara_lisp::RolloutStrategy = tatara_lisp::RolloutStrategy::BlueGreen;
285    const DRAIN_SECONDS: u32 = 5;
286}
287
288/// Register every keyword form this domain exposes onto the host
289/// interpreter, plus its non-compile capability metadata. Embedders
290/// call this once during boot.
291pub fn register() {
292    tatara_lisp::domain::register::<BpfProgramSpec>();
293    tatara_lisp::domain::register::<BpfMapSpec>();
294    tatara_lisp::domain::register::<BpfPolicySpec>();
295    // Doc layer — markdown hover help / catalog browser.
296    tatara_lisp::domain::register_doc::<BpfProgramSpec>();
297    tatara_lisp::domain::register_doc::<BpfMapSpec>();
298    tatara_lisp::domain::register_doc::<BpfPolicySpec>();
299    // Deps layer — typed topo-sort over the rollout plan.
300    tatara_lisp::domain::register_deps::<BpfProgramSpec>();
301    tatara_lisp::domain::register_deps::<BpfMapSpec>();
302    tatara_lisp::domain::register_deps::<BpfPolicySpec>();
303    // Attestation layer — namespaced BLAKE3 for tameshi.
304    tatara_lisp::domain::register_attest::<BpfProgramSpec>();
305    tatara_lisp::domain::register_attest::<BpfMapSpec>();
306    tatara_lisp::domain::register_attest::<BpfPolicySpec>();
307    // Validation layer — semantic checks (license / attach target).
308    tatara_lisp::domain::register_validate::<BpfProgramSpec>();
309    tatara_lisp::domain::register_validate::<BpfMapSpec>();
310    tatara_lisp::domain::register_validate::<BpfPolicySpec>();
311    // Lifecycle layer — rollout strategy per resource kind.
312    tatara_lisp::domain::register_lifecycle::<BpfProgramSpec>();
313    tatara_lisp::domain::register_lifecycle::<BpfMapSpec>();
314    tatara_lisp::domain::register_lifecycle::<BpfPolicySpec>();
315    // Compliance layer — frameworks + controls per kind.
316    tatara_lisp::domain::register_compliance::<BpfProgramSpec>();
317    tatara_lisp::domain::register_compliance::<BpfMapSpec>();
318    tatara_lisp::domain::register_compliance::<BpfPolicySpec>();
319    // Observability — metric prefix + log labels.
320    tatara_lisp::domain::register_observability::<BpfProgramSpec>();
321    tatara_lisp::domain::register_observability::<BpfMapSpec>();
322    tatara_lisp::domain::register_observability::<BpfPolicySpec>();
323    // Help — examples + mnemonics.
324    tatara_lisp::domain::register_help::<BpfProgramSpec>();
325    tatara_lisp::domain::register_help::<BpfMapSpec>();
326    tatara_lisp::domain::register_help::<BpfPolicySpec>();
327    // Stability — since-version + posture.
328    tatara_lisp::domain::register_stability::<BpfProgramSpec>();
329    tatara_lisp::domain::register_stability::<BpfMapSpec>();
330    tatara_lisp::domain::register_stability::<BpfPolicySpec>();
331}