diff --git a/Cargo.toml b/Cargo.toml index 0db43a36..ce579306 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ integration-ebpf = { path = "test/integration-ebpf", default-features = false } lazy_static = { version = "1", default-features = false } libc = { version = "0.2.105", default-features = false } log = { version = "0.4", default-features = false } +netns-rs = { version = "0.1", default-features = false } num_enum = { version = "0.6", default-features = false } object = { version = "0.31", default-features = false } parking_lot = { version = "0.12.0", default-features = false } diff --git a/aya/src/lib.rs b/aya/src/lib.rs index 3f87e433..4ba70c2a 100644 --- a/aya/src/lib.rs +++ b/aya/src/lib.rs @@ -58,3 +58,5 @@ pub mod util; pub use bpf::*; pub use obj::btf::{Btf, BtfError}; pub use object::Endianness; +#[doc(hidden)] +pub use sys::netlink_set_link_up; diff --git a/aya/src/sys/mod.rs b/aya/src/sys/mod.rs index 448e7fee..642f8f20 100644 --- a/aya/src/sys/mod.rs +++ b/aya/src/sys/mod.rs @@ -12,6 +12,8 @@ use libc::{c_int, c_long, pid_t, SYS_bpf, SYS_perf_event_open}; pub(crate) use bpf::*; #[cfg(test)] pub(crate) use fake::*; +#[doc(hidden)] +pub use netlink::netlink_set_link_up; pub(crate) use netlink::*; pub(crate) use perf_event::*; diff --git a/aya/src/sys/netlink.rs b/aya/src/sys/netlink.rs index 91b9de16..1dfae70d 100644 --- a/aya/src/sys/netlink.rs +++ b/aya/src/sys/netlink.rs @@ -3,9 +3,9 @@ use thiserror::Error; use libc::{ close, getsockname, nlattr, nlmsgerr, nlmsghdr, recv, send, setsockopt, sockaddr_nl, socket, - AF_NETLINK, AF_UNSPEC, ETH_P_ALL, IFLA_XDP, NETLINK_EXT_ACK, NETLINK_ROUTE, NLA_ALIGNTO, - NLA_F_NESTED, NLA_TYPE_MASK, NLMSG_DONE, NLMSG_ERROR, NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, - NLM_F_ECHO, NLM_F_EXCL, NLM_F_MULTI, NLM_F_REQUEST, RTM_DELTFILTER, RTM_GETTFILTER, + AF_NETLINK, AF_UNSPEC, ETH_P_ALL, IFF_UP, IFLA_XDP, NETLINK_EXT_ACK, NETLINK_ROUTE, + NLA_ALIGNTO, NLA_F_NESTED, NLA_TYPE_MASK, NLMSG_DONE, NLMSG_ERROR, NLM_F_ACK, NLM_F_CREATE, + NLM_F_DUMP, NLM_F_ECHO, NLM_F_EXCL, NLM_F_MULTI, NLM_F_REQUEST, RTM_DELTFILTER, RTM_GETTFILTER, RTM_NEWQDISC, RTM_NEWTFILTER, RTM_SETLINK, SOCK_RAW, SOL_NETLINK, }; @@ -240,6 +240,32 @@ pub(crate) unsafe fn netlink_find_filter_with_name( Ok(filter_info) } +#[doc(hidden)] +pub unsafe fn netlink_set_link_up(if_index: i32) -> Result<(), io::Error> { + let sock = NetlinkSocket::open()?; + + // Safety: Request is POD so this is safe + let mut req = mem::zeroed::(); + + let nlmsg_len = mem::size_of::() + mem::size_of::(); + req.header = nlmsghdr { + nlmsg_len: nlmsg_len as u32, + nlmsg_flags: (NLM_F_REQUEST | NLM_F_ACK) as u16, + nlmsg_type: RTM_SETLINK, + nlmsg_pid: 0, + nlmsg_seq: 1, + }; + req.if_info.ifi_family = AF_UNSPEC as u8; + req.if_info.ifi_index = if_index; + req.if_info.ifi_flags = IFF_UP as u32; + req.if_info.ifi_change = IFF_UP as u32; + + sock.send(&bytes_of(&req)[..req.header.nlmsg_len as usize])?; + sock.recv()?; + + Ok(()) +} + #[repr(C)] struct Request { header: nlmsghdr, diff --git a/test/integration-test/Cargo.toml b/test/integration-test/Cargo.toml index df11ab76..e4e91647 100644 --- a/test/integration-test/Cargo.toml +++ b/test/integration-test/Cargo.toml @@ -12,6 +12,7 @@ aya-log = { workspace = true } aya-obj = { workspace = true } libc = { workspace = true } log = { workspace = true } +netns-rs = { workspace = true } object = { workspace = true } rbpf = { workspace = true } tokio = { workspace = true, default-features = false, features = [ diff --git a/test/integration-test/src/lib.rs b/test/integration-test/src/lib.rs index 7d571104..b8e5120d 100644 --- a/test/integration-test/src/lib.rs +++ b/test/integration-test/src/lib.rs @@ -18,3 +18,5 @@ pub const BPF_PROBE_READ: &[u8] = #[cfg(test)] mod tests; +#[cfg(test)] +mod utils; diff --git a/test/integration-test/src/tests/smoke.rs b/test/integration-test/src/tests/smoke.rs index 8b09ac92..30a97a39 100644 --- a/test/integration-test/src/tests/smoke.rs +++ b/test/integration-test/src/tests/smoke.rs @@ -4,8 +4,12 @@ use aya::{ Bpf, BpfLoader, }; +use crate::utils::NetNsGuard; + #[test] fn xdp() { + let _netns = NetNsGuard::new(); + let kernel_version = KernelVersion::current().unwrap(); if kernel_version < KernelVersion::new(5, 18, 0) { eprintln!("skipping test on kernel {kernel_version:?}, support for BPF_F_XDP_HAS_FRAGS was added in 5.18.0; see https://github.com/torvalds/linux/commit/c2f2cdb"); @@ -20,6 +24,8 @@ fn xdp() { #[test] fn extension() { + let _netns = NetNsGuard::new(); + let kernel_version = KernelVersion::current().unwrap(); if kernel_version < KernelVersion::new(5, 9, 0) { eprintln!("skipping test on kernel {kernel_version:?}, XDP uses netlink"); diff --git a/test/integration-test/src/utils.rs b/test/integration-test/src/utils.rs new file mode 100644 index 00000000..28b68d4a --- /dev/null +++ b/test/integration-test/src/utils.rs @@ -0,0 +1,72 @@ +//! Utilities to run tests + +use std::{ + ffi::CString, + io, process, + sync::atomic::{AtomicU64, Ordering}, +}; + +use aya::netlink_set_link_up; +use libc::if_nametoindex; +use netns_rs::{get_from_current_thread, NetNs}; + +pub struct NetNsGuard { + name: String, + old_ns: NetNs, + ns: Option, +} + +impl NetNsGuard { + pub fn new() -> Self { + let old_ns = get_from_current_thread().expect("Failed to get current netns"); + + static COUNTER: AtomicU64 = AtomicU64::new(0); + let pid = process::id(); + let name = format!("aya-test-{pid}-{}", COUNTER.fetch_add(1, Ordering::Relaxed)); + + // Create and enter netns + let ns = NetNs::new(&name).unwrap_or_else(|e| panic!("Failed to create netns {name}: {e}")); + let netns = Self { + old_ns, + ns: Some(ns), + name, + }; + + let ns = netns.ns.as_ref().unwrap(); + ns.enter() + .unwrap_or_else(|e| panic!("Failed to enter network namespace {}: {e}", netns.name)); + println!("Entered network namespace {}", netns.name); + + // By default, the loopback in a new netns is down. Set it up. + let lo = CString::new("lo").unwrap(); + unsafe { + let idx = if_nametoindex(lo.as_ptr()); + if idx == 0 { + panic!( + "Interface `lo` not found in netns {}: {}", + netns.name, + io::Error::last_os_error() + ); + } + netlink_set_link_up(idx as i32) + .unwrap_or_else(|e| panic!("Failed to set `lo` up in netns {}: {e}", netns.name)); + } + + netns + } +} + +impl Drop for NetNsGuard { + fn drop(&mut self) { + // Avoid panic in panic + if let Err(e) = self.old_ns.enter() { + eprintln!("Failed to return to original netns: {e}"); + } + if let Some(ns) = self.ns.take() { + if let Err(e) = ns.remove() { + eprintln!("Failed to remove netns {}: {e}", self.name); + } + } + println!("Exited network namespace {}", self.name); + } +}