Building Trishula

Trishula is built and verified with a small, reproducible toolchain. just is the entry runner: just alone lists every build and lab lane; the procedures stay in the scripts. The kernel-side eBPF sources compile CO-RE on a Linux kernel inside a virtual machine. The userspace Go services follow a test-first engineering standard. Every claim traces to a merge-verified pull request. This page documents that loop — the same loop that produces the evidence records cited across the roadmap.

The build environment

Layer Component Role
Entry runner just the task registry: just lists every lane; one recipe = one delegate to a script
Test target Linux VM on the workstation (Ubuntu 24.04, arm64) The actual eBPF test kernel: verifier, BTF, TC/XDP attach, netns/veth harnesses
Kernel toolchain clang -target bpf + bpftool CO-RE compile + vmlinux.h generation from the VM kernel’s own BTF
Object generation bpf2go Generated Go bindings + per-target objects, committed — a tagged release ships byte-identical objects (the hook inventory)
Loader cilium/ebpf Load/pin/attach from Go; no C loader on the host

The VM is the test kernel: eBPF is kernel work. XDP/TC programs, maps, BTF and the verifier exist only on Linux. The workstation orchestrates; the VM verifies. kind (Kubernetes-in-the-VM) extends the same pattern to the cluster-level labs (TR-06).

Verification standard

Every build item follows a hypothesis-first test-driven loop (the engineering standard, encoded in CONTRIBUTING):

  1. Spec — the spec section is the design source; the issue cites its sections (the spec index).
  2. RED — the failing test is written first and committed watched-failing. A kernel-side contract is frozen by test before the C implementation lands. The frozen facts: map names, types, sizes, wire layouts, action codes.
  3. GREEN — the minimal implementation makes the test pass; nothing more.
  4. PR — branch per change, GPG-signed commits, CI gates (build + vet + test, dependency review, pre-commit). Every PR carries the hypothesis and the RED/GREEN evidence in the body.
  5. Close on acceptance — an issue closes only when its own acceptance line is verified against the tree. The verification: run the check, paste the evidence.

This produces the property that matters: every roadmap artifact is proved by a runnable check, not asserted by prose.

The kernel-side loop in practice

A real example — the shield PoC (TR-03) and the ring producer (TR-04) — shows the loop end to end:

1. contract test (RED)        map names/types/sizes pinned from [the stage sketches](https://github.com/trishula-dev/trishula/blob/main/docs/spec.md#191-stage-sketches-from-the-code-v0-poc)
2. wire-format test (RED)     ban_key/ban_val byte layouts frozen for C + Go
3. C implementation (GREEN)   bpf/shield_xdp.c + bpf/flow_tc.c compiled
                              -target bpf, verifier-passed
4. loader (GREEN)             cilium/ebpf attach; XDP on the interface,
                              TC clsact on veth pairs
5. E2E (RED → GREEN)          netns + veth + synthetic flow: a banned
                              source drops in-kernel; a real flow
                              decodes with full header fields
6. gate script                lab/verify-xdp-chain.sh — the environment
                              gate every kernel-side PR pastes as evidence

Facts the loop surfaced (and the tests now pin): the ring event is 200 bytes (C natural alignment). TCP ports travel network-order. The L7 payload offset follows tcp->doff. Payload bytes can sit in paged fragments; linear-only reads never see them. The peek uses bpf_skb_load_bytes. Each was caught by a test during the build and recorded in the PR trail.

Environment gates

Two gates run before kernel-side work merges:

  • lab/verify-xdp-chain.sh — proves the test host end to end. The proof: BTF present; vmlinux.h dumped from the live kernel; a CO-RE probe compiled, verifier-loaded, pinned; XDP attached; clean detach. Exit 0 is pasted into the PR.
  • In-VM E2E tags — kernel-attach tests run behind build tags (ingest4d, attach) and are root-gated. The split: CI runs the portable suites; the VM runs the kernel suite.

What is built, what is needed (the GA view)

The build surface as of 2026-10-25:

  • Built and verified: the entry surface (justfile + lab/prereq.sh). The CI gates run: the build/vet/test with the CRS rx profile, the cue goldens and the route verifier load. The gates also check the residue teardown and the fastpath diff. The attestation chain ships per release: the SLSA provenance + the SPDX SBOM, Rekor-backed. The in-VM E2E families: the ban enforce, the ring, the DoS runner (root-gated build tags).
  • Needed before GA: the release notes carry the consumer gh attestation verify block; the cut recipe requires it. The registry leg for the remote clusters: the lab images on EKS/DOKS = push + pull, #261’s follow-up. The NGF 2.x dataplane agent on OrbStack: #259’s probe matrix = P1 the process-visibility shim, P2 the agent config surface. TR-203 = the TLS-termination lane in this cut. The lane covers the QUIC/H3 ingress, the eBPF UDP class, the PQC key exchange and the JA4 impact. The 2026.02/03 trains carry the PQC signature migration, kTLS and the DPU lanes; re-encryption = the named post-GA boundary. The upstream drafts stay parked — nothing files upstream.

Engineering records

Work is tracked as issues (TR-01 …) on the roadmap board. Large items are sliced into sub-issues (~1 hour each) with native blocked-by edges. The board shows the dependency order. Kernel-quirk discoveries are filed as defects (#80 — platform fast-path bypasses a TC hook), not absorbed silently. The spec index maps every item to its design sections. The delivery-gate ledger tracks the verified capability at each stage.

The DX1 lab quickstart — one command from clone to the full demo

From TR-14 on, the whole exit-gate demo is one command. The prereqs = two layers (issue #261):

  1. just — the entry runner (brew install just; just alone lists the lanes).
  2. docker OR kubernetes — lab/prereq.sh detects the target and prints what it found. The target: a local kind or OrbStack, a hypervisor VM’s node or a cloud Kubernetes (EKS, DOKS). Point your kubeconfig at it and run.
git clone https://github.com/trishula-dev/trishula && cd trishula
just demo        # ~3 stages, each pass/fail loudly; ends on the hop evidence

The direct form (bash lab/demo.sh) stays equivalent. The lab needs go, kubectl, helm and one of the two target surfaces. A remote cluster needs its own registry for the lab images (the follow-up leaf on #261). The local targets load the images directly.

The stages it composes (each also = a just recipe):

just setup                    # stage 1 — cluster + NGF + engine + echo (idempotent)
bash lab/dx1/apply-effect.sh  # stage 2 — WAFPolicy apply → operator compile →
                              #           engine hot-reload → 403 on traffic, timed (M9 ≤ 5 s)
bash lab/dx1/verify.sh        # stage 3 — hop evidence (NGF → engine → echo), printed LAST

Verify the composition yourself — the demo’s stage markers, from the recorded run:

| stage 1 PASS — cluster + NGF + engine + echo up (lab left installed)
| stage 2 PASS — CRD apply → operator compile → engine hot-reload → 403, within budget
| stage 3 PASS — NGF → engine → echo pod hop-by-hop evidenced
== DEMO-OK — lab/demo.sh complete: setup + M9 apply→effect + hop evidence all PASS (lab left running, no teardown)

The stage-3 hop transcript (X-Trishula-Hop: engine, echo pod body, engine-local /healthz) is the demo’s closing evidence. The transcript sits verbatim in the repo README “Proof” section and on #6.

Reproduce it locally

git clone https://github.com/trishula-dev/trishula && cd trishula
just build && just vet                   # the portable lanes
just test                                # the full suite (the CI shape)
go build ./... && go test ./...          # the direct form (any OS)
# kernel suites need Linux (root):
sudo go test -tags attach -run TestBanDropE2E ./internal/shield/ -v
sudo go test -tags ingest4d -run TestFlowTCRingE2E ./internal/engine/ingest/ -v

Any Linux host with BTF (/sys/kernel/btf/vmlinux) and root works; the gate script above checks the details.