ソースを参照

test: add the possibility to run a test inside a network namespace

For tests that do networking operations, this allows to have a
clean-state network namespace and interfaces for each test. Mainly, this
avoids "device or resource busy" errors when reusing the loopback
interface across tests.
Tuetuopay 2 年 前
コミット
c74813f8c5

+ 1 - 0
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 }

+ 2 - 0
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;

+ 2 - 0
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::*;
 

+ 29 - 3
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::<Request>();
+
+    let nlmsg_len = mem::size_of::<nlmsghdr>() + mem::size_of::<ifinfomsg>();
+    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,

+ 1 - 0
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 = [

+ 2 - 0
test/integration-test/src/lib.rs

@@ -18,3 +18,5 @@ pub const BPF_PROBE_READ: &[u8] =
 
 #[cfg(test)]
 mod tests;
+#[cfg(test)]
+mod utils;

+ 6 - 0
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");

+ 72 - 0
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<NetNs>,
+}
+
+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);
+    }
+}