#![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(all(target_os = "linux", feature = "youki-runtime"))]
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;
use libcontainer::network::link::LinkClient;
use libcontainer::network::wrapper::create_network_client;
let client = create_network_client();
let mut link_client = LinkClient::new(client)
.map_err(|e| NetlinkError::Netlink(format!("failed to create LinkClient: {e}")))?;
let link = link_client.get_by_name(link_name).map_err(|e| {
let msg = e.to_string();
if msg.contains("No such device") || msg.contains("not found") {
NetlinkError::NotFound(link_name.to_string())
} else {
NetlinkError::Netlink(format!("get_by_name({link_name}) failed: {msg}"))
}
})?;
let index = link.header.index;
link_client
.set_ns_fd(index, new_name, ns_fd.as_raw_fd())
.map_err(|e| {
NetlinkError::Netlink(format!(
"set_ns_fd(index={index}, new_name={new_name}) failed: {e}"
))
})?;
Ok(())
}
#[cfg(any(
all(not(target_os = "linux"), unix),
all(target_os = "linux", not(feature = "youki-runtime")),
))]
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 requires Linux with the 'youki-runtime' feature"
.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 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(),
))
}