Skip to content

Commit

Permalink
test: add the possibility to run a test inside a network namespace
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Tuetuopay committed Jul 30, 2023
1 parent e95f76a commit c74813f
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 3 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 2 additions & 0 deletions aya/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 2 additions & 0 deletions aya/src/sys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down
32 changes: 29 additions & 3 deletions aya/src/sys/netlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions test/integration-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
2 changes: 2 additions & 0 deletions test/integration-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ pub const BPF_PROBE_READ: &[u8] =

#[cfg(test)]
mod tests;
#[cfg(test)]
mod utils;
6 changes: 6 additions & 0 deletions test/integration-test/src/tests/smoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand Down
72 changes: 72 additions & 0 deletions test/integration-test/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit c74813f

Please sign in to comment.