1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
name: CI
on:
push:
branches:
pull_request:
branches:
env:
CARGO_TERM_COLOR: always
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: cargo check
run: cargo check --all-targets
- name: cargo test
run: cargo test --lib
- name: cargo fmt --check
run: cargo fmt --all -- --check
continue-on-error: true
- name: cargo clippy
run: cargo clippy --all-targets -- -D warnings
continue-on-error: true
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v2
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
# Single instrumented run; subsequent `report` invocations reuse the data
# so we don't pay for re-compiling tests just to change the output format.
# Threshold (--fail-under-lines) is set just below the current baseline so
# a regression fails CI but day-to-day improvements don't trip an alert.
- name: Run coverage
run: cargo llvm-cov --lib --no-report
- name: Coverage summary
run: cargo llvm-cov report --summary-only --fail-under-lines 83
- name: Generate LCOV
run: cargo llvm-cov report --lcov --output-path lcov.info
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-lcov
path: lcov.info
ebpf-build:
# Builds the BPF programs with the pinned nightly + bpf-linker, stages
# the artifact via scripts/build-ebpf.sh, then rebuilds the SDK with
# `--features ebpf` so the embedded include_bytes! resolves to the
# real object. This proves Phase 1 wires up end-to-end without needing
# a privileged container or actually loading the program in the kernel
# (that's the future ebpf-integration job, not this one).
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Stable toolchain for the userspace SDK build.
- uses: dtolnay/rust-toolchain@stable
# Nightly toolchain for the BPF crate. The channel here MUST match
# crates/ebpf-programs/rust-toolchain.toml. When you bump that file,
# bump this `toolchain:` value in lockstep.
#
# Note: bpfel-unknown-none is a tier-3 target — there is no
# rustup-installable rust-std for it. We use `-Z build-std=core`
# (configured in crates/ebpf-programs/.cargo/config.toml) which
# builds `core` from `rust-src`, so we install rust-src here but
# NOT bpfel-unknown-none as a `target`.
- name: Install nightly toolchain for BPF crate
uses: dtolnay/rust-toolchain@nightly
with:
toolchain: nightly-2026-01-15
components: rust-src,rustc-dev,llvm-tools-preview
- uses: Swatinem/rust-cache@v2
with:
# Separate cache from the test job so the nightly artifacts don't
# poison the stable build's cache.
shared-key: ebpf-build
- name: Install bpf-linker
# Builds against the host LLVM (`apt install` provides LLVM 18 on
# ubuntu-24.04 / 14 on ubuntu-22.04). bpf-linker is published on
# crates.io.
run: cargo install bpf-linker --locked
- name: Build BPF programs
run: scripts/build-ebpf.sh
- name: Confirm BPF object was produced
run: |
test -s target/bpf/netwatch_sdk_ebpf.o
file target/bpf/netwatch_sdk_ebpf.o
ls -la target/bpf/
- name: Build SDK with --features ebpf
run: cargo build --features ebpf
- name: Run SDK tests with --features ebpf
run: cargo test --features ebpf --lib
ebpf-integration:
# Loads the compiled BPF program in the host kernel and asserts an
# end-to-end round-trip: a userspace TCP connect triggers the kprobe,
# the kernel writes an event into the ring buffer, and userspace
# decodes it via EventSource.
#
# GitHub-hosted ubuntu-latest runners allow sudo and have modern
# kernels with BTF, so this works without a self-hosted runner or
# `--privileged` Docker gymnastics. The build-and-run-as-root dance
# is:
# 1. Build the BPF object with the pinned nightly + bpf-linker.
# 2. Build the integration test binary as the runner user so
# cargo's paths resolve normally.
# 3. Locate the emitted test binary, run it under sudo.
needs: ebpf-build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@nightly
with:
toolchain: nightly-2026-01-15
components: rust-src,rustc-dev,llvm-tools-preview
# No rust-cache here — the build.rs staging of target/bpf/…
# into OUT_DIR doesn't play well with cache restoration, which
# can resurrect a stale (empty or malformed) embedded BPF object.
# The BPF build is cheap; the clean build is worth the
# determinism.
- name: Install bpf-linker
run: cargo install bpf-linker --locked
- name: Build BPF programs
run: scripts/build-ebpf.sh
- name: Inspect the staged BPF object
run: |
ls -la target/bpf/
file target/bpf/netwatch_sdk_ebpf.o
sha256sum target/bpf/netwatch_sdk_ebpf.o
head -c 64 target/bpf/netwatch_sdk_ebpf.o | xxd
echo "size=$(stat -c%s target/bpf/netwatch_sdk_ebpf.o) bytes"
echo
echo "=== readelf -h ==="
readelf -h target/bpf/netwatch_sdk_ebpf.o
echo "=== readelf -SW ==="
readelf -SW target/bpf/netwatch_sdk_ebpf.o
echo "=== bpf-linker version ==="
bpf-linker --version || true
echo "=== aya crate version ==="
grep '^name = "aya"' -A 1 Cargo.lock | head -4
- name: Build integration test binary (unprivileged)
run: cargo test --features ebpf --test ebpf_integration --no-run
- name: Inspect what build.rs embedded into OUT_DIR
run: |
embedded=$(find target -name netwatch_sdk_ebpf.o -path '*/build/*/out/*' | head -1)
if [ -n "$embedded" ]; then
ls -la "$embedded"
sha256sum "$embedded"
head -c 16 "$embedded" | xxd
else
echo "!! no embedded BPF object found in build/out/"
find target -name netwatch_sdk_ebpf.o -ls
fi
- name: Locate the test binary
id: testbin
run: |
bin=$(cargo test --features ebpf --test ebpf_integration --no-run --message-format=json 2>/dev/null \
| jq -rs '.[] | select(.profile.test == true and .target.name == "ebpf_integration") | .executable' \
| tail -n1)
test -n "$bin" || { echo "could not locate ebpf_integration test binary"; exit 1; }
echo "bin=$bin" >> "$GITHUB_OUTPUT"
echo "locating $bin"
- name: Run integration test as root
# sudo preserves the existing PATH so ldconfig, etc. still work.
# --nocapture so the skip messages or assertion output show up.
# `timeout 60` is belt-and-suspenders — if the test binary hangs
# (e.g. reader-thread Drop deadlock) we want to know within a
# minute, not after the whole job's 10-minute budget elapses.
timeout-minutes: 3
run: sudo timeout 60 ${{ steps.testbin.outputs.bin }} --nocapture