Skip to content

Commit 4e0e1e4

Browse files
lunarthegreyclaude
andcommitted
Add veth attach/detach integration test (sudo in CI)
Creates a veth pair, loads the fast-path ELF, attaches to one end with native→generic fallback, detaches, cleans up. Tests the aya attach/detach round-trip against a real ifindex — which the existing verifier test can't do since `Xdp::load` only exercises the verifier, not the attach path. Drop-guard cleanup ensures the veth pair is removed even if the test panics. CI's sudo step now runs all ignored fast-path integration tests (`--tests -- --ignored`), so future cap-requiring tests land without workflow edits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 802036a commit 4e0e1e4

2 files changed

Lines changed: 94 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ jobs:
6868
- name: cargo test
6969
run: cargo test --workspace
7070

71-
- name: cargo test (BPF verifier, sudo)
72-
# The verifier-pass integration test loads the fast-path ELF
73-
# into the kernel, which requires CAP_BPF + CAP_NET_ADMIN. Run
74-
# under sudo so the kernel accepts the `bpf(BPF_PROG_LOAD)`
75-
# call. `-E` preserves cargo's environment (CARGO_TARGET_DIR
76-
# etc.) so this test reuses the prior step's build.
77-
run: sudo -E $(which cargo) test -p packetframe-fast-path --test verifier -- --ignored --nocapture
71+
- name: cargo test (BPF verifier + attach, sudo)
72+
# Integration tests that load the BPF ELF into the kernel or
73+
# attach it to an interface need CAP_BPF + CAP_NET_ADMIN. Run
74+
# them all under sudo; `-E` preserves the cargo env so they
75+
# reuse the prior step's build. All such tests are marked
76+
# `#[ignore]` so the non-sudo `cargo test` above skips them.
77+
run: sudo -E $(which cargo) test -p packetframe-fast-path --tests -- --ignored --nocapture
7878

7979
cross-build:
8080
name: cross-build ${{ matrix.target }}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// aya is Linux-only.
2+
#![cfg(target_os = "linux")]
3+
4+
//! Integration test: load the fast-path BPF ELF, create a veth pair,
5+
//! attach to one end (native XDP, falling back to generic — veth
6+
//! supports native on modern kernels), detach, and clean up.
7+
//!
8+
//! This is the lightweight version of a full netns routing test — the
9+
//! point is to exercise the aya attach/detach round-trip against a real
10+
//! ifindex, which `bpf_prog_test_run` can't do. Full end-to-end
11+
//! forwarding (packet in → redirect → packet out) lands in a later PR
12+
//! with a netns harness and synthetic traffic.
13+
//!
14+
//! Requires CAP_NET_ADMIN + CAP_BPF; CI runs this under `sudo`.
15+
16+
use std::process::Command;
17+
18+
use packetframe_fast_path::{FAST_PATH_BPF, FAST_PATH_BPF_AVAILABLE};
19+
20+
const PEER_A: &str = "pf-veth0";
21+
const PEER_B: &str = "pf-veth1";
22+
23+
struct Cleanup;
24+
25+
impl Drop for Cleanup {
26+
fn drop(&mut self) {
27+
// `ip link del <peer>` removes the whole pair.
28+
let _ = Command::new("ip").args(["link", "del", PEER_A]).status();
29+
}
30+
}
31+
32+
fn run(cmd: &[&str]) {
33+
let status = Command::new(cmd[0])
34+
.args(&cmd[1..])
35+
.status()
36+
.unwrap_or_else(|e| panic!("spawning `{}`: {e}", cmd.join(" ")));
37+
assert!(status.success(), "`{}` failed: {status}", cmd.join(" "));
38+
}
39+
40+
#[test]
41+
#[ignore = "needs CAP_NET_ADMIN + BPF build; run via `sudo -E cargo test ... -- --ignored`"]
42+
fn attach_detach_roundtrip_on_veth() {
43+
if !FAST_PATH_BPF_AVAILABLE {
44+
eprintln!("BPF stub in effect (no rustup); skipping attach test.");
45+
return;
46+
}
47+
48+
// Clean any leftover from a prior aborted run. Idempotent.
49+
let _ = Command::new("ip").args(["link", "del", PEER_A]).status();
50+
51+
run(&[
52+
"ip", "link", "add", PEER_A, "type", "veth", "peer", "name", PEER_B,
53+
]);
54+
run(&["ip", "link", "set", PEER_A, "up"]);
55+
run(&["ip", "link", "set", PEER_B, "up"]);
56+
57+
// Ensure cleanup even on panic.
58+
let _cleanup = Cleanup;
59+
60+
let ifindex = {
61+
let c = std::ffi::CString::new(PEER_A).unwrap();
62+
let idx = unsafe { libc::if_nametoindex(c.as_ptr()) };
63+
assert!(idx > 0, "if_nametoindex({PEER_A}) failed");
64+
idx
65+
};
66+
67+
let mut bpf = aya::Ebpf::load(FAST_PATH_BPF).expect("aya::Ebpf::load");
68+
let prog: &mut aya::programs::Xdp = bpf
69+
.program_mut("fast_path")
70+
.expect("fast_path program present")
71+
.try_into()
72+
.expect("fast_path is XDP");
73+
prog.load().expect("verifier");
74+
75+
// Try native first; fall back to generic. veth *should* support
76+
// native XDP on any remotely modern kernel, but we don't gate on it.
77+
use aya::programs::xdp::XdpFlags;
78+
let link_id = prog
79+
.attach_to_if_index(ifindex, XdpFlags::DRV_MODE)
80+
.or_else(|native_err| {
81+
eprintln!("veth native XDP attach failed ({native_err}); trying generic");
82+
prog.attach_to_if_index(ifindex, XdpFlags::SKB_MODE)
83+
})
84+
.expect("attach XDP to veth");
85+
86+
prog.detach(link_id).expect("detach");
87+
}

0 commit comments

Comments
 (0)