#![cfg_attr(
not(target_os = "linux"),
allow(clippy::missing_errors_doc, clippy::unused_async)
)]
use thiserror::Error;
#[derive(Debug, Error)]
pub enum NetlinkError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("link '{0}' not found in current netns")]
NotFound(String),
#[error("netlink operation failed: {0}")]
Netlink(String),
}
#[cfg(target_os = "linux")]
pub fn move_link_into_netns_fd_and_rename(
link_name: &str,
ns_fd: std::os::fd::BorrowedFd<'_>,
new_name: &str,
) -> Result<(), NetlinkError> {
use std::os::fd::AsRawFd;
let raw_fd = ns_fd.as_raw_fd();
let link_name = link_name.to_string();
let new_name = new_name.to_string();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| NetlinkError::Netlink(format!("local runtime build failed: {e}")))?;
rt.block_on(async move {
use futures_util::stream::TryStreamExt;
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| NetlinkError::Netlink(format!("new_connection failed: {e}")))?;
tokio::spawn(connection);
let link = handle
.link()
.get()
.match_name(link_name.clone())
.execute()
.try_next()
.await
.map_err(|e| {
let msg = e.to_string();
if msg.contains("No such device") {
NetlinkError::NotFound(link_name.clone())
} else {
NetlinkError::Netlink(format!("link lookup failed for {link_name}: {msg}"))
}
})?
.ok_or_else(|| NetlinkError::NotFound(link_name.clone()))?;
let index = link.header.index;
handle
.link()
.set(index)
.setns_by_fd(raw_fd)
.name(new_name.clone())
.execute()
.await
.map_err(|e| {
NetlinkError::Netlink(format!(
"setns_by_fd(index={index}, new_name={new_name}) failed: {e}"
))
})
})
}
#[cfg(all(not(target_os = "linux"), unix))]
pub fn move_link_into_netns_fd_and_rename(
_link_name: &str,
_ns_fd: std::os::fd::BorrowedFd<'_>,
_new_name: &str,
) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"move_link_into_netns_fd_and_rename is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub fn move_link_into_netns_and_rename(
link_name: &str,
target_pid: u32,
new_name: &str,
) -> Result<(), NetlinkError> {
use std::os::fd::{AsFd, OwnedFd};
let ns_file = std::fs::File::open(format!("/proc/{target_pid}/ns/net"))?;
let ns_fd: OwnedFd = OwnedFd::from(ns_file);
move_link_into_netns_fd_and_rename(link_name, ns_fd.as_fd(), new_name)
}
#[cfg(not(target_os = "linux"))]
pub fn move_link_into_netns_and_rename(
_link_name: &str,
_target_pid: u32,
_new_name: &str,
) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"move_link_into_netns_and_rename is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub async fn create_veth_pair(host_name: &str, peer_name: &str) -> Result<(), NetlinkError> {
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| NetlinkError::Netlink(format!("new_connection failed: {e}")))?;
tokio::spawn(connection);
handle
.link()
.add()
.veth(host_name.to_string(), peer_name.to_string())
.execute()
.await
.map_err(|e| {
let msg = e.to_string();
if msg.contains("File exists") || msg.contains("EEXIST") {
NetlinkError::Netlink(format!(
"veth pair already exists: host={host_name} peer={peer_name}: {msg}"
))
} else {
NetlinkError::Netlink(format!(
"veth create failed (host={host_name}, peer={peer_name}): {msg}"
))
}
})
}
#[cfg(not(target_os = "linux"))]
pub async fn create_veth_pair(_host_name: &str, _peer_name: &str) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"create_veth_pair is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub async fn delete_link_by_name(name: &str) -> Result<(), NetlinkError> {
use futures_util::stream::TryStreamExt;
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| NetlinkError::Netlink(format!("new_connection failed: {e}")))?;
tokio::spawn(connection);
let lookup = handle
.link()
.get()
.match_name(name.to_string())
.execute()
.try_next()
.await;
let link = match lookup {
Ok(Some(link)) => link,
Ok(None) => return Ok(()),
Err(rtnetlink::Error::NetlinkError(err)) => {
let msg = err.to_string();
let is_enodev = err
.code
.is_some_and(|c| c.get().unsigned_abs() == libc::ENODEV as u32);
if is_enodev || msg.contains("No such device") {
return Ok(());
}
return Err(NetlinkError::Netlink(format!(
"link lookup failed for {name}: {msg}"
)));
}
Err(e) => {
let msg = e.to_string();
if msg.contains("No such device") {
return Ok(());
}
return Err(NetlinkError::Netlink(format!(
"link lookup failed for {name}: {msg}"
)));
}
};
let index = link.header.index;
handle
.link()
.del(index)
.execute()
.await
.map_err(|e| NetlinkError::Netlink(format!("link delete failed for {name}: {e}")))
}
#[cfg(not(target_os = "linux"))]
pub async fn delete_link_by_name(_name: &str) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"delete_link_by_name is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub async fn list_all_links() -> Result<Vec<(u32, String)>, NetlinkError> {
use futures_util::stream::TryStreamExt;
use netlink_packet_route::link::LinkAttribute;
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| NetlinkError::Netlink(format!("new_connection failed: {e}")))?;
tokio::spawn(connection);
let mut stream = handle.link().get().execute();
let mut links = Vec::new();
while let Some(msg) = stream
.try_next()
.await
.map_err(|e| NetlinkError::Netlink(format!("link dump failed: {e}")))?
{
let index = msg.header.index;
let Some(name) = msg.attributes.iter().find_map(|a| match a {
LinkAttribute::IfName(n) => Some(n.clone()),
_ => None,
}) else {
continue;
};
links.push((index, name));
}
Ok(links)
}
#[cfg(not(target_os = "linux"))]
pub async fn list_all_links() -> Result<Vec<(u32, String)>, NetlinkError> {
Err(NetlinkError::Netlink(
"list_all_links is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub async fn set_link_up_by_name(name: &str) -> Result<(), NetlinkError> {
use futures_util::stream::TryStreamExt;
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| NetlinkError::Netlink(format!("new_connection failed: {e}")))?;
tokio::spawn(connection);
let link = handle
.link()
.get()
.match_name(name.to_string())
.execute()
.try_next()
.await
.map_err(|e| {
let msg = e.to_string();
if msg.contains("No such device") {
NetlinkError::NotFound(name.to_string())
} else {
NetlinkError::Netlink(format!("link lookup failed for {name}: {msg}"))
}
})?
.ok_or_else(|| NetlinkError::NotFound(name.to_string()))?;
let index = link.header.index;
handle
.link()
.set(index)
.up()
.execute()
.await
.map_err(|e| NetlinkError::Netlink(format!("link set up failed for {name}: {e}")))
}
#[cfg(not(target_os = "linux"))]
pub async fn set_link_up_by_name(_name: &str) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"set_link_up_by_name is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub async fn add_address_to_link_by_name(
name: &str,
addr: std::net::IpAddr,
prefix_len: u8,
) -> Result<(), NetlinkError> {
use futures_util::stream::TryStreamExt;
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| NetlinkError::Netlink(format!("new_connection failed: {e}")))?;
tokio::spawn(connection);
let link = handle
.link()
.get()
.match_name(name.to_string())
.execute()
.try_next()
.await
.map_err(|e| {
let msg = e.to_string();
if msg.contains("No such device") {
NetlinkError::NotFound(name.to_string())
} else {
NetlinkError::Netlink(format!("link lookup failed for {name}: {msg}"))
}
})?
.ok_or_else(|| NetlinkError::NotFound(name.to_string()))?;
let index = link.header.index;
handle
.address()
.add(index, addr, prefix_len)
.execute()
.await
.map_err(|e| {
NetlinkError::Netlink(format!(
"address add failed for {name} ({addr}/{prefix_len}): {e}"
))
})
}
#[cfg(not(target_os = "linux"))]
pub async fn add_address_to_link_by_name(
_name: &str,
_addr: std::net::IpAddr,
_prefix_len: u8,
) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"add_address_to_link_by_name is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub async fn add_default_route_via_dev(dev_name: &str, is_v6: bool) -> Result<(), NetlinkError> {
use futures_util::stream::TryStreamExt;
use netlink_packet_route::route::RouteScope;
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| NetlinkError::Netlink(format!("new_connection failed: {e}")))?;
tokio::spawn(connection);
let link = handle
.link()
.get()
.match_name(dev_name.to_string())
.execute()
.try_next()
.await
.map_err(|e| {
let msg = e.to_string();
if msg.contains("No such device") {
NetlinkError::NotFound(dev_name.to_string())
} else {
NetlinkError::Netlink(format!("link lookup failed for {dev_name}: {msg}"))
}
})?
.ok_or_else(|| NetlinkError::NotFound(dev_name.to_string()))?;
let oif_idx = link.header.index;
if is_v6 {
handle
.route()
.add()
.v6()
.destination_prefix(std::net::Ipv6Addr::UNSPECIFIED, 0)
.output_interface(oif_idx)
.scope(RouteScope::Link)
.execute()
.await
.map_err(|e| {
NetlinkError::Netlink(format!("default route add v6 via {dev_name} failed: {e}"))
})
} else {
handle
.route()
.add()
.v4()
.destination_prefix(std::net::Ipv4Addr::UNSPECIFIED, 0)
.output_interface(oif_idx)
.scope(RouteScope::Link)
.execute()
.await
.map_err(|e| {
NetlinkError::Netlink(format!("default route add v4 via {dev_name} failed: {e}"))
})
}
}
#[cfg(not(target_os = "linux"))]
pub async fn add_default_route_via_dev(_dev_name: &str, _is_v6: bool) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"add_default_route_via_dev is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub async fn add_default_route_via_gateway(gateway: std::net::IpAddr) -> Result<(), NetlinkError> {
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| NetlinkError::Netlink(format!("new_connection failed: {e}")))?;
tokio::spawn(connection);
match gateway {
std::net::IpAddr::V4(gw) => handle
.route()
.add()
.v4()
.destination_prefix(std::net::Ipv4Addr::UNSPECIFIED, 0)
.gateway(gw)
.execute()
.await
.map_err(|e| {
NetlinkError::Netlink(format!("default route add v4 via gateway {gw} failed: {e}"))
}),
std::net::IpAddr::V6(gw) => handle
.route()
.add()
.v6()
.destination_prefix(std::net::Ipv6Addr::UNSPECIFIED, 0)
.gateway(gw)
.execute()
.await
.map_err(|e| {
NetlinkError::Netlink(format!("default route add v6 via gateway {gw} failed: {e}"))
}),
}
}
#[cfg(not(target_os = "linux"))]
pub async fn add_default_route_via_gateway(_gateway: std::net::IpAddr) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"add_default_route_via_gateway is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub async fn replace_route_via_dev(
dest: std::net::IpAddr,
prefix_len: u8,
dev_name: &str,
src: Option<std::net::IpAddr>,
) -> Result<(), NetlinkError> {
use std::net::IpAddr;
use futures_util::stream::TryStreamExt;
use netlink_packet_route::route::RouteScope;
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| NetlinkError::Netlink(format!("new_connection failed: {e}")))?;
tokio::spawn(connection);
let link = handle
.link()
.get()
.match_name(dev_name.to_string())
.execute()
.try_next()
.await
.map_err(|e| {
let msg = e.to_string();
if msg.contains("No such device") {
NetlinkError::NotFound(dev_name.to_string())
} else {
NetlinkError::Netlink(format!("link lookup failed for {dev_name}: {msg}"))
}
})?
.ok_or_else(|| NetlinkError::NotFound(dev_name.to_string()))?;
let oif_idx = link.header.index;
match (dest, src) {
(IpAddr::V4(d), Some(IpAddr::V4(s))) => handle
.route()
.add()
.v4()
.destination_prefix(d, prefix_len)
.output_interface(oif_idx)
.scope(RouteScope::Link)
.pref_source(s)
.replace()
.execute()
.await
.map_err(|e| {
NetlinkError::Netlink(format!(
"route replace v4 {d}/{prefix_len} dev {dev_name} src {s} failed: {e}"
))
}),
(IpAddr::V4(d), None) => handle
.route()
.add()
.v4()
.destination_prefix(d, prefix_len)
.output_interface(oif_idx)
.scope(RouteScope::Link)
.replace()
.execute()
.await
.map_err(|e| {
NetlinkError::Netlink(format!(
"route replace v4 {d}/{prefix_len} dev {dev_name} failed: {e}"
))
}),
(IpAddr::V6(d), Some(IpAddr::V6(s))) => handle
.route()
.add()
.v6()
.destination_prefix(d, prefix_len)
.output_interface(oif_idx)
.scope(RouteScope::Link)
.pref_source(s)
.replace()
.execute()
.await
.map_err(|e| {
NetlinkError::Netlink(format!(
"route replace v6 {d}/{prefix_len} dev {dev_name} src {s} failed: {e}"
))
}),
(IpAddr::V6(d), None) => handle
.route()
.add()
.v6()
.destination_prefix(d, prefix_len)
.output_interface(oif_idx)
.scope(RouteScope::Link)
.replace()
.execute()
.await
.map_err(|e| {
NetlinkError::Netlink(format!(
"route replace v6 {d}/{prefix_len} dev {dev_name} failed: {e}"
))
}),
(IpAddr::V4(_), Some(IpAddr::V6(_))) | (IpAddr::V6(_), Some(IpAddr::V4(_))) => Err(
NetlinkError::Netlink(format!("address family mismatch: dest={dest} src={src:?}")),
),
}
}
#[cfg(not(target_os = "linux"))]
pub async fn replace_route_via_dev(
_dest: std::net::IpAddr,
_prefix_len: u8,
_dev_name: &str,
_src: Option<std::net::IpAddr>,
) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"replace_route_via_dev is only supported on Linux".to_string(),
))
}
pub fn set_sysctl(key: &str, value: &str) -> Result<(), NetlinkError> {
let path = format!("/proc/sys/{}", key.replace('.', "/"));
std::fs::write(&path, value)?;
Ok(())
}
#[cfg(target_os = "linux")]
pub fn with_netns_fd<F, T>(ns_fd: std::os::fd::OwnedFd, f: F) -> Result<T, NetlinkError>
where
F: FnOnce() -> Result<T, NetlinkError> + Send + 'static,
T: Send + 'static,
{
let join_handle = std::thread::spawn(move || -> Result<T, NetlinkError> {
nix::sched::setns(&ns_fd, nix::sched::CloneFlags::CLONE_NEWNET)
.map_err(|e| NetlinkError::Netlink(format!("setns(ns_fd) failed: {e}")))?;
let result = f();
drop(ns_fd);
result
});
join_handle
.join()
.map_err(|_| NetlinkError::Netlink("with_netns_fd thread panicked".to_string()))?
}
#[cfg(all(not(target_os = "linux"), unix))]
pub fn with_netns_fd<F, T>(_ns_fd: std::os::fd::OwnedFd, _f: F) -> Result<T, NetlinkError>
where
F: FnOnce() -> Result<T, NetlinkError> + Send + 'static,
T: Send + 'static,
{
Err(NetlinkError::Netlink(
"with_netns_fd is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub fn with_netns<F, T>(target_pid: u32, f: F) -> Result<T, NetlinkError>
where
F: FnOnce() -> Result<T, NetlinkError> + Send + 'static,
T: Send + 'static,
{
use std::os::fd::OwnedFd;
let ns_file = std::fs::File::open(format!("/proc/{target_pid}/ns/net"))?;
let ns_fd: OwnedFd = OwnedFd::from(ns_file);
with_netns_fd(ns_fd, f)
}
#[cfg(not(target_os = "linux"))]
pub fn with_netns<F, T>(_target_pid: u32, _f: F) -> Result<T, NetlinkError>
where
F: FnOnce() -> Result<T, NetlinkError> + Send + 'static,
T: Send + 'static,
{
Err(NetlinkError::Netlink(
"with_netns is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub fn with_netns_fd_async<F, Fut, T>(ns_fd: std::os::fd::OwnedFd, f: F) -> Result<T, NetlinkError>
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<T, NetlinkError>>,
T: Send + 'static,
{
with_netns_fd(ns_fd, move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| NetlinkError::Netlink(format!("local runtime build failed: {e}")))?;
rt.block_on(f())
})
}
#[cfg(all(not(target_os = "linux"), unix))]
pub fn with_netns_fd_async<F, Fut, T>(
_ns_fd: std::os::fd::OwnedFd,
_f: F,
) -> Result<T, NetlinkError>
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<T, NetlinkError>>,
T: Send + 'static,
{
Err(NetlinkError::Netlink(
"with_netns_fd_async is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub fn with_netns_async<F, Fut, T>(target_pid: u32, f: F) -> Result<T, NetlinkError>
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<T, NetlinkError>>,
T: Send + 'static,
{
use std::os::fd::OwnedFd;
let ns_file = std::fs::File::open(format!("/proc/{target_pid}/ns/net"))?;
let ns_fd: OwnedFd = OwnedFd::from(ns_file);
with_netns_fd_async(ns_fd, f)
}
#[cfg(not(target_os = "linux"))]
pub fn with_netns_async<F, Fut, T>(_target_pid: u32, _f: F) -> Result<T, NetlinkError>
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<T, NetlinkError>>,
T: Send + 'static,
{
Err(NetlinkError::Netlink(
"with_netns_async is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub async fn create_bridge(name: &str) -> Result<(), NetlinkError> {
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| NetlinkError::Netlink(format!("new_connection failed: {e}")))?;
tokio::spawn(connection);
match handle.link().add().bridge(name.to_string()).execute().await {
Ok(()) => Ok(()),
Err(rtnetlink::Error::NetlinkError(err)) => {
let is_eexist = err
.code
.is_some_and(|c| c.get().unsigned_abs() == libc::EEXIST as u32);
let msg = err.to_string();
if is_eexist || msg.contains("File exists") {
Ok(())
} else {
Err(NetlinkError::Netlink(format!(
"bridge create failed for {name}: {msg}"
)))
}
}
Err(e) => {
let msg = e.to_string();
if msg.contains("File exists") {
Ok(())
} else {
Err(NetlinkError::Netlink(format!(
"bridge create failed for {name}: {msg}"
)))
}
}
}
}
#[cfg(not(target_os = "linux"))]
pub async fn create_bridge(_name: &str) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"create_bridge is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub async fn delete_bridge(name: &str) -> Result<(), NetlinkError> {
delete_link_by_name(name).await
}
#[cfg(not(target_os = "linux"))]
pub async fn delete_bridge(_name: &str) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"delete_bridge is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub async fn add_link_to_bridge(link: &str, bridge: &str) -> Result<(), NetlinkError> {
use futures_util::stream::TryStreamExt;
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| NetlinkError::Netlink(format!("new_connection failed: {e}")))?;
tokio::spawn(connection);
let bridge_link = handle
.link()
.get()
.match_name(bridge.to_string())
.execute()
.try_next()
.await
.map_err(|e| {
let msg = e.to_string();
if msg.contains("No such device") {
NetlinkError::NotFound(bridge.to_string())
} else {
NetlinkError::Netlink(format!("link lookup failed for {bridge}: {msg}"))
}
})?
.ok_or_else(|| NetlinkError::NotFound(bridge.to_string()))?;
let bridge_idx = bridge_link.header.index;
let member_link = handle
.link()
.get()
.match_name(link.to_string())
.execute()
.try_next()
.await
.map_err(|e| {
let msg = e.to_string();
if msg.contains("No such device") {
NetlinkError::NotFound(link.to_string())
} else {
NetlinkError::Netlink(format!("link lookup failed for {link}: {msg}"))
}
})?
.ok_or_else(|| NetlinkError::NotFound(link.to_string()))?;
let member_idx = member_link.header.index;
handle
.link()
.set(member_idx)
.controller(bridge_idx)
.execute()
.await
.map_err(|e| {
NetlinkError::Netlink(format!(
"set master failed: link={link} bridge={bridge}: {e}"
))
})
}
#[cfg(not(target_os = "linux"))]
pub async fn add_link_to_bridge(_link: &str, _bridge: &str) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"add_link_to_bridge is only supported on Linux".to_string(),
))
}
#[cfg(target_os = "linux")]
pub fn set_bridge_stp(name: &str, stp_on: bool) -> Result<(), NetlinkError> {
let bridge_dir = format!("/sys/class/net/{name}/bridge");
if !std::path::Path::new(&bridge_dir).exists() {
return Err(NetlinkError::NotFound(name.to_string()));
}
let path = format!("{bridge_dir}/stp_state");
let value = if stp_on { "1" } else { "0" };
std::fs::write(&path, value)?;
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn set_bridge_stp(_name: &str, _stp_on: bool) -> Result<(), NetlinkError> {
Err(NetlinkError::Netlink(
"set_bridge_stp is only supported on Linux".to_string(),
))
}
#[cfg(test)]
mod tests {
#[cfg(target_os = "linux")]
use super::*;
#[cfg(target_os = "linux")]
fn rand_suffix() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
const CHARS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let mut n = u64::from(nanos);
let mut out = String::new();
let base = CHARS.len() as u64;
for _ in 0..6 {
let idx = usize::try_from(n % base).unwrap_or(0);
out.push(CHARS[idx] as char);
n /= base;
}
out
}
#[cfg(target_os = "linux")]
async fn create_dummy(name: &str) -> Result<(), NetlinkError> {
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| NetlinkError::Netlink(format!("new_connection failed: {e}")))?;
tokio::spawn(connection);
handle
.link()
.add()
.dummy(name.to_string())
.execute()
.await
.map_err(|e| NetlinkError::Netlink(format!("dummy create failed for {name}: {e}")))
}
#[cfg(target_os = "linux")]
#[tokio::test]
#[ignore = "requires CAP_NET_ADMIN; run manually or in privileged CI"]
async fn bridge_create_idempotent() {
let name = format!("zlb-{}", rand_suffix());
assert!(name.len() <= 15, "interface name exceeds IFNAMSIZ: {name}");
create_bridge(&name).await.expect("first create_bridge");
assert!(
std::path::Path::new(&format!("/sys/class/net/{name}")).exists(),
"bridge {name} should exist after create"
);
create_bridge(&name)
.await
.expect("second create_bridge should be idempotent");
delete_bridge(&name).await.expect("delete_bridge");
assert!(
!std::path::Path::new(&format!("/sys/class/net/{name}")).exists(),
"bridge {name} should be gone after delete"
);
delete_bridge(&name)
.await
.expect("second delete_bridge should be idempotent");
}
#[cfg(target_os = "linux")]
#[tokio::test]
#[ignore = "requires CAP_NET_ADMIN; run manually or in privileged CI"]
async fn bridge_add_link_membership() {
let suffix = rand_suffix();
let bridge = format!("zlb-{suffix}");
let dummy = format!("zld-{suffix}");
assert!(bridge.len() <= 15);
assert!(dummy.len() <= 15);
create_bridge(&bridge).await.expect("create_bridge");
create_dummy(&dummy).await.expect("create_dummy");
add_link_to_bridge(&dummy, &bridge)
.await
.expect("add_link_to_bridge");
let master_ifindex_path = format!("/sys/class/net/{dummy}/master/ifindex");
let dummy_master_ifindex = std::fs::read_to_string(&master_ifindex_path)
.expect("read dummy master ifindex")
.trim()
.parse::<u32>()
.expect("parse dummy master ifindex");
let bridge_ifindex = std::fs::read_to_string(format!("/sys/class/net/{bridge}/ifindex"))
.expect("read bridge ifindex")
.trim()
.parse::<u32>()
.expect("parse bridge ifindex");
assert_eq!(
dummy_master_ifindex, bridge_ifindex,
"dummy's master ifindex should equal bridge's ifindex"
);
delete_link_by_name(&dummy).await.expect("delete dummy");
delete_bridge(&bridge).await.expect("delete bridge");
}
#[cfg(target_os = "linux")]
#[tokio::test]
#[ignore = "requires CAP_NET_ADMIN; run manually or in privileged CI"]
async fn bridge_stp_off() {
let name = format!("zlb-{}", rand_suffix());
assert!(name.len() <= 15);
create_bridge(&name).await.expect("create_bridge");
set_bridge_stp(&name, false).expect("set_bridge_stp off");
let stp_state = std::fs::read_to_string(format!("/sys/class/net/{name}/bridge/stp_state"))
.expect("read stp_state")
.trim()
.to_string();
assert_eq!(
stp_state, "0",
"stp_state should be 0 after set_bridge_stp(false)"
);
delete_bridge(&name).await.expect("delete_bridge");
}
}