use std::time::Duration;
use nlink::{
Result,
netlink::{
Connection, KobjectUevent, RtnetlinkGroup,
link::VethLink,
netdev::{NetdevEvent, NetdevInfo, NetdevLifecycle},
reflector::Store,
uevent_filter::UeventFilter,
},
};
use tokio_stream::StreamExt;
use crate::common::TestNamespace;
const SETTLE: Duration = Duration::from_secs(5);
#[tokio::test]
async fn netdev_lifecycle_joins_uevents_with_rtnetlink() -> Result<()> {
require_root!();
nlink::require_module!("veth");
let ns = TestNamespace::new("netdevlc")?;
let links = ns.connection()?;
links.subscribe(&[RtnetlinkGroup::Link])?;
let uevents = Connection::<KobjectUevent>::in_namespace(ns.name())?;
let compiled = uevents.attach_filter(&UeventFilter::new().subsystem("net").build())?;
assert!(
compiled.is_exact(),
"a subsystem-only filter should lower entirely into the kernel"
);
let store: Store<u32, NetdevInfo> = Store::new();
let mut lifecycle = NetdevLifecycle::new(links.events().await, uevents.events().await)
.with_store(store.clone());
let ctl = ns.connection()?;
ctl.add_link(VethLink::new("lcvet0", "lcvet1")).await?;
let mut added = Vec::new();
let mut attributed = Vec::new();
let deadline = tokio::time::Instant::now() + SETTLE;
while tokio::time::Instant::now() < deadline {
let Ok(Some(event)) =
tokio::time::timeout_at(deadline, lifecycle.next()).await
else {
break;
};
match event? {
NetdevEvent::Added(info) | NetdevEvent::Changed(info) => {
let Some(name) = info.name().map(str::to_string) else {
continue;
};
if !name.starts_with("lcvet") {
continue;
}
if !added.contains(&name) {
added.push(name.clone());
}
if info.is_fully_attributed() && !attributed.contains(&name) {
assert!(
info.devpath().is_some_and(|p| p.ends_with(&name)),
"devpath {:?} does not name {name}",
info.devpath()
);
assert!(info.ifindex() > 0);
attributed.push(name);
}
}
_ => {}
}
if attributed.len() == 2 {
break;
}
}
added.sort();
assert_eq!(
added,
vec!["lcvet0".to_string(), "lcvet1".to_string()],
"rtnetlink should announce both ends of the pair"
);
attributed.sort();
assert_eq!(
attributed,
vec!["lcvet0".to_string(), "lcvet1".to_string()],
"both ends should end up joined with their uevent annotation"
);
for (_, info) in store.snapshot() {
if info.name().is_some_and(|n| n.starts_with("lcvet")) {
assert!(info.annotation().is_some());
}
}
Ok(())
}
#[tokio::test]
async fn removal_evicts_the_device_from_the_store() -> Result<()> {
require_root!();
nlink::require_module!("veth");
let ns = TestNamespace::new("netdevrm")?;
let links = ns.connection()?;
links.subscribe(&[RtnetlinkGroup::Link])?;
let uevents = Connection::<KobjectUevent>::in_namespace(ns.name())?;
let store: Store<u32, NetdevInfo> = Store::new();
let mut lifecycle = NetdevLifecycle::new(links.events().await, uevents.events().await)
.with_store(store.clone());
let ctl = ns.connection()?;
ctl.add_link(VethLink::new("rmvet0", "rmvet1")).await?;
let deadline = tokio::time::Instant::now() + SETTLE;
let mut ifindex = None;
while ifindex.is_none() && tokio::time::Instant::now() < deadline {
let Ok(Some(event)) = tokio::time::timeout_at(deadline, lifecycle.next()).await else {
break;
};
if let NetdevEvent::Added(info) = event?
&& info.name() == Some("rmvet0")
{
ifindex = Some(info.ifindex());
}
}
let ifindex = ifindex.expect("rmvet0 should have been announced");
assert!(store.contains_key(&ifindex));
ctl.del_link("rmvet0").await?;
let deadline = tokio::time::Instant::now() + SETTLE;
let mut removed = false;
while !removed && tokio::time::Instant::now() < deadline {
let Ok(Some(event)) = tokio::time::timeout_at(deadline, lifecycle.next()).await else {
break;
};
if let NetdevEvent::Removed { ifindex: gone, .. } = event?
&& gone == ifindex
{
removed = true;
}
}
assert!(removed, "RTM_DELLINK for rmvet0 was never observed");
assert!(
!store.contains_key(&ifindex),
"the store still holds the removed device"
);
Ok(())
}