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.
pull/696/head
Tuetuopay 2 years ago
parent e95f76a5b3
commit c74813f8c5

@ -71,6 +71,7 @@ integration-ebpf = { path = "test/integration-ebpf", default-features = false }
lazy_static = { version = "1", default-features = false } lazy_static = { version = "1", default-features = false }
libc = { version = "0.2.105", default-features = false } libc = { version = "0.2.105", default-features = false }
log = { version = "0.4", 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 } num_enum = { version = "0.6", default-features = false }
object = { version = "0.31", default-features = false } object = { version = "0.31", default-features = false }
parking_lot = { version = "0.12.0", default-features = false } parking_lot = { version = "0.12.0", default-features = false }

@ -58,3 +58,5 @@ pub mod util;
pub use bpf::*; pub use bpf::*;
pub use obj::btf::{Btf, BtfError}; pub use obj::btf::{Btf, BtfError};
pub use object::Endianness; pub use object::Endianness;
#[doc(hidden)]
pub use sys::netlink_set_link_up;

@ -12,6 +12,8 @@ use libc::{c_int, c_long, pid_t, SYS_bpf, SYS_perf_event_open};
pub(crate) use bpf::*; pub(crate) use bpf::*;
#[cfg(test)] #[cfg(test)]
pub(crate) use fake::*; pub(crate) use fake::*;
#[doc(hidden)]
pub use netlink::netlink_set_link_up;
pub(crate) use netlink::*; pub(crate) use netlink::*;
pub(crate) use perf_event::*; pub(crate) use perf_event::*;

@ -3,9 +3,9 @@ use thiserror::Error;
use libc::{ use libc::{
close, getsockname, nlattr, nlmsgerr, nlmsghdr, recv, send, setsockopt, sockaddr_nl, socket, 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, AF_NETLINK, AF_UNSPEC, ETH_P_ALL, IFF_UP, IFLA_XDP, NETLINK_EXT_ACK, NETLINK_ROUTE,
NLA_F_NESTED, NLA_TYPE_MASK, NLMSG_DONE, NLMSG_ERROR, NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, NLA_ALIGNTO, NLA_F_NESTED, NLA_TYPE_MASK, NLMSG_DONE, NLMSG_ERROR, NLM_F_ACK, NLM_F_CREATE,
NLM_F_ECHO, NLM_F_EXCL, NLM_F_MULTI, NLM_F_REQUEST, RTM_DELTFILTER, RTM_GETTFILTER, 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, 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) 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)] #[repr(C)]
struct Request { struct Request {
header: nlmsghdr, header: nlmsghdr,

@ -12,6 +12,7 @@ aya-log = { workspace = true }
aya-obj = { workspace = true } aya-obj = { workspace = true }
libc = { workspace = true } libc = { workspace = true }
log = { workspace = true } log = { workspace = true }
netns-rs = { workspace = true }
object = { workspace = true } object = { workspace = true }
rbpf = { workspace = true } rbpf = { workspace = true }
tokio = { workspace = true, default-features = false, features = [ tokio = { workspace = true, default-features = false, features = [

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

@ -4,8 +4,12 @@ use aya::{
Bpf, BpfLoader, Bpf, BpfLoader,
}; };
use crate::utils::NetNsGuard;
#[test] #[test]
fn xdp() { fn xdp() {
let _netns = NetNsGuard::new();
let kernel_version = KernelVersion::current().unwrap(); let kernel_version = KernelVersion::current().unwrap();
if kernel_version < KernelVersion::new(5, 18, 0) { 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"); 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] #[test]
fn extension() { fn extension() {
let _netns = NetNsGuard::new();
let kernel_version = KernelVersion::current().unwrap(); let kernel_version = KernelVersion::current().unwrap();
if kernel_version < KernelVersion::new(5, 9, 0) { if kernel_version < KernelVersion::new(5, 9, 0) {
eprintln!("skipping test on kernel {kernel_version:?}, XDP uses netlink"); eprintln!("skipping test on kernel {kernel_version:?}, XDP uses netlink");

@ -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);
}
}
Loading…
Cancel
Save