From d8303ef8cbdefa22acbb0f4d18148059e5642301 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Thu, 12 Nov 2026 11:29:35 -0500 Subject: [PATCH] netns: namespace handle for Linux netns files and FreeBSD jails Introduce network::netns with a Netns type and a NetnsRef borrow. On Linux they are File and BorrowedFd, so the Linux code keeps its types and setns behavior. On FreeBSD a namespace is the VNET jail named by the netns path: Netns::open looks the jid up with jail_get, and because a process cannot leave a jail, work inside it runs in a forked child. A netlink socket for the jail is created in such a child and handed back over SCM_RIGHTS; the kernel serves netlink requests in the vnet the socket was created in. DriverInfo, the netlink CreateLinkOptions and set_link_ns take NetnsRef. The exec_netns macro keeps its Linux definition and gets a FreeBSD one that runs the block in the jail. Moving a link into a jail and creating a link inside one return "not supported yet" on FreeBSD; that is the SIOCSIFVNET ioctl, which comes with the bridge driver. The connection tester and the netlink unit tests need Linux namespaces and are gated to Linux. The netns validation looks the jail up on FreeBSD instead of opening a file, and the FreeBSD socket accepts EISCONN from connect: the kernel marks a netlink socket connected as soon as it is bound. Signed-off-by: Michael Johnson --- Cargo.toml | 2 +- examples/host-device-plugin.rs | 10 +- src/commands/setup.rs | 6 +- src/commands/teardown.rs | 6 +- src/dhcp_proxy/lib.rs | 5 + src/network/bridge.rs | 14 ++- src/network/core_utils.rs | 61 ++++++++- src/network/driver.rs | 7 +- src/network/mod.rs | 1 + src/network/netlink.rs | 7 +- src/network/netlink_route.rs | 31 ++++- src/network/netlink_socket.rs | 13 +- src/network/netns.rs | 222 +++++++++++++++++++++++++++++++++ src/network/validation.rs | 13 +- src/network/vlan.rs | 9 +- src/test/main.rs | 13 ++ src/test/mod.rs | 1 + src/test/test.rs | 1 + 18 files changed, 384 insertions(+), 38 deletions(-) create mode 100644 src/network/netns.rs diff --git a/Cargo.toml b/Cargo.toml index 7fdc1a81c..6e44a4faf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ log = "0.4.30" serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.146" zbus = { version = "5.19.0" } -nix = { version = "0.31.3", features = ["net", "sched", "signal", "socket", "user"] } +nix = { version = "0.31.3", features = ["net", "sched", "signal", "socket", "uio", "user"] } rand = "0.10.3" sha2 = "0.11.1" base16ct = "1.0.0" diff --git a/examples/host-device-plugin.rs b/examples/host-device-plugin.rs index ff569276a..3fa93f4e2 100644 --- a/examples/host-device-plugin.rs +++ b/examples/host-device-plugin.rs @@ -1,11 +1,13 @@ //! This is just an example plugin, do not use it in production! -use std::{collections::HashMap, os::fd::AsFd}; +use std::collections::HashMap; use netavark::{ network::{ core_utils::{open_netlink_sockets, CoreUtils}, - netlink_route, types, + netlink_route, + netns::NetnsExt, + types, }, new_error, plugin::{Info, Plugin, PluginExec, API_VERSION}, @@ -69,7 +71,7 @@ impl Plugin for Exec { } host.netlink - .set_link_ns(link.header.index, netns.file.as_fd())?; + .set_link_ns(link.header.index, netns.file.netns_ref())?; // interfaces map, but we only ever expect one, for response let mut interfaces: HashMap = HashMap::new(); @@ -104,7 +106,7 @@ impl Plugin for Exec { netns .netlink - .set_link_ns(link.header.index, host.file.as_fd())?; + .set_link_ns(link.header.index, host.file.netns_ref())?; Ok(()) } diff --git a/src/commands/setup.rs b/src/commands/setup.rs index 9f93aa8f8..037147a29 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -6,6 +6,7 @@ use crate::firewall; use crate::network::driver::{get_network_driver, DriverInfo, NetworkDriver}; use crate::network::netlink::Socket; use crate::network::netlink_route::{LinkID, NetlinkRoute}; +use crate::network::netns::NetnsExt; use crate::network::{self}; use crate::network::{core_utils, types}; @@ -15,7 +16,6 @@ use log::{debug, error, info}; use std::collections::HashMap; use std::ffi::OsString; use std::fs::{self}; -use std::os::fd::AsFd; use std::path::Path; #[derive(Parser, Debug)] @@ -85,8 +85,8 @@ impl Setup { container_name: &network_options.container_name, container_hostname: &network_options.container_hostname, container_dns_servers: &network_options.dns_servers, - netns_host: hostns.file.as_fd(), - netns_container: Some(netns.file.as_fd()), + netns_host: hostns.file.netns_ref(), + netns_container: Some(netns.file.netns_ref()), netns_path: &self.network_namespace_path, network, per_network_opts, diff --git a/src/commands/teardown.rs b/src/commands/teardown.rs index 1bd64e8b8..b1ee145f9 100644 --- a/src/commands/teardown.rs +++ b/src/commands/teardown.rs @@ -4,6 +4,7 @@ use crate::error::{NetavarkError, NetavarkErrorList, NetavarkResult}; use crate::network::constants::DRIVER_BRIDGE; use crate::network::core_utils; use crate::network::driver::{get_network_driver, DriverInfo}; +use crate::network::netns::NetnsExt; use crate::{firewall, network}; use clap::builder::NonEmptyStringValueParser; @@ -11,7 +12,6 @@ use clap::Parser; use log::debug; use nix::sys::signal; use std::ffi::OsString; -use std::os::fd::AsFd; use std::path::Path; #[derive(Parser, Debug)] @@ -131,8 +131,8 @@ impl Teardown { container_name: &network_options.container_name, container_hostname: &network_options.container_hostname, container_dns_servers: &network_options.dns_servers, - netns_host: hostns.file.as_fd(), - netns_container: netns_file.as_ref().map(|f| f.as_fd()), // None when no netns + netns_host: hostns.file.netns_ref(), + netns_container: netns_file.as_ref().map(|f| f.netns_ref()), // None when no netns netns_path: &self.network_namespace_path, network, per_network_opts, diff --git a/src/dhcp_proxy/lib.rs b/src/dhcp_proxy/lib.rs index c15592906..d720621b6 100644 --- a/src/dhcp_proxy/lib.rs +++ b/src/dhcp_proxy/lib.rs @@ -9,8 +9,11 @@ use g_rpc::netavark_proxy_client::NetavarkProxyClient; use hyper_util::rt::TokioIo; use log::debug; use std::fs::File; +#[cfg(target_os = "linux")] use std::net::AddrParseError; +#[cfg(target_os = "linux")] use std::net::Ipv4Addr; +#[cfg(target_os = "linux")] use std::str::FromStr; use tokio::net::UnixStream; use tonic::transport::{Channel, Endpoint}; @@ -286,10 +289,12 @@ impl NetworkConfig { } } +#[cfg(target_os = "linux")] trait VectorConv { fn to_v4_addrs(&self) -> Result>, AddrParseError>; } +#[cfg(target_os = "linux")] impl VectorConv for Vec { fn to_v4_addrs(&self) -> Result>, AddrParseError> { if self.is_empty() { diff --git a/src/network/bridge.rs b/src/network/bridge.rs index b6f596fdd..4ec628b8c 100644 --- a/src/network/bridge.rs +++ b/src/network/bridge.rs @@ -2,7 +2,6 @@ use std::{ collections::HashMap, fs, net::{IpAddr, Ipv4Addr, Ipv6Addr}, - os::fd::BorrowedFd, }; use crate::dns::aardvark::SafeString; @@ -41,7 +40,7 @@ use super::{ OPTION_MODE, OPTION_MTU, OPTION_NO_DEFAULT_ROUTE, OPTION_OUTBOUND_ADDR4, OPTION_OUTBOUND_ADDR6, OPTION_VLAN, OPTION_VRF, VALID_BRIDGE_OPTS, }, - core_utils::{self, get_ipam_addresses, is_using_systemd, join_netns, parse_option, CoreUtils}, + core_utils::{self, get_ipam_addresses, is_using_systemd, parse_option, CoreUtils}, driver::{self, DriverInfo}, internal_types::{ IPAMAddresses, IsolateOption, PortForwardConfig, SetupNetwork, TearDownNetwork, @@ -50,6 +49,9 @@ use super::{ sysctl, types::StatusBlock, }; +#[cfg(target_os = "linux")] +use crate::network::core_utils::join_netns; +use crate::network::netns::NetnsRef; const NO_BRIDGE_NAME_ERROR: &str = "no bridge interface name given"; @@ -737,8 +739,8 @@ fn create_interfaces( data: &InternalData, internal: bool, rootless: bool, - hostns_fd: BorrowedFd<'_>, - netns_fd: BorrowedFd<'_>, + hostns_fd: NetnsRef<'_>, + netns_fd: NetnsRef<'_>, ) -> NetavarkResult { let mut sysctl_writer = None; let (bridge_index, mtu, mac) = match host @@ -938,8 +940,8 @@ fn create_veth_pair<'fd>( primary_index: u32, bridge_mac: Option>, internal: bool, - hostns_fd: BorrowedFd<'fd>, - netns_fd: BorrowedFd<'fd>, + hostns_fd: NetnsRef<'fd>, + netns_fd: NetnsRef<'fd>, mtu: u32, ) -> NetavarkResult { let mut peer_opts = diff --git a/src/network/core_utils.rs b/src/network/core_utils.rs index 276148df5..79a8635f1 100644 --- a/src/network/core_utils.rs +++ b/src/network/core_utils.rs @@ -5,16 +5,18 @@ use base16ct::HexDisplay; use ipnet::IpNet; use netlink_packet_route::link::{IpVlanMode, LinkMessage, MacVlanMode}; use netlink_packet_route::route::RouteType; +#[cfg(target_os = "linux")] use nix::sched; use sha2::{Digest, Sha512}; use std::collections::HashMap; use std::env; use std::fmt::Display; -use std::fs::File; +#[cfg(target_os = "linux")] use std::io; use std::net::IpAddr; use std::net::Ipv4Addr; use std::net::Ipv6Addr; +#[cfg(target_os = "linux")] use std::os::unix::prelude::*; use std::path::Path; use std::str::FromStr; @@ -22,6 +24,7 @@ use std::str::FromStr; use super::netlink; use crate::network::netlink::Socket; use crate::network::netlink_route::{LinkID, NetlinkRoute, Route}; +use crate::network::netns::Netns; use netlink_packet_route::link::LinkAttribute; @@ -292,6 +295,7 @@ impl CoreUtils { } } +#[cfg(target_os = "linux")] pub fn join_netns(fd: Fd) -> NetavarkResult<()> { match sched::setns(fd, sched::CloneFlags::CLONE_NEWNET) { Ok(_) => Ok(()), @@ -305,6 +309,7 @@ pub fn join_netns(fd: Fd) -> NetavarkResult<()> { /// safe way to join the namespace and join back to the host after the task is done /// This first arg should be the hostns fd, the second is the container ns fd. /// The third and last the closure that should be executed in the ns. +#[cfg(target_os = "linux")] #[macro_export] macro_rules! exec_netns { ($host:expr, $netns:expr, $exec:expr) => {{ @@ -315,13 +320,25 @@ macro_rules! exec_netns { }}; } +/// Run the closure inside the jail in a forked child, see [`Netns::run`]. +/// The closure must return `NetavarkResult<()>`. +#[cfg(target_os = "freebsd")] +#[macro_export] +macro_rules! exec_netns { + ($host:expr, $netns:expr, $exec:expr) => {{ + let _ = &$host; + $netns.run(|| $exec) + }}; +} + pub struct NamespaceOptions { /// Note we have to return the File object since the fd is only valid /// as long as the File object is valid - pub file: File, + pub file: Netns, pub netlink: Socket, } +#[cfg(target_os = "linux")] pub fn open_netlink_sockets( netns_path: &str, ) -> NetavarkResult<(NamespaceOptions, NamespaceOptions)> { @@ -349,6 +366,7 @@ pub fn open_netlink_sockets( /// Open the host netlink socket. Used during teardown when the /// container network namespace is no longer available. +#[cfg(target_os = "linux")] pub fn open_host_netlink_socket() -> NetavarkResult { let hostns = open_netlink_socket("/proc/self/ns/net").wrap("open host netns")?; let host_socket = netlink::Socket::::new().wrap("host netlink socket")?; @@ -359,6 +377,7 @@ pub fn open_host_netlink_socket() -> NetavarkResult { } /// Open the container netns netlink socket, given an already-opened host ns. +#[cfg(target_os = "linux")] pub fn open_netns_netlink_socket( netns_path: &str, hostns: &NamespaceOptions, @@ -375,8 +394,42 @@ pub fn open_netns_netlink_socket( }) } -fn open_netlink_socket(netns_path: &str) -> NetavarkResult { - wrap!(File::open(netns_path), format!("open {netns_path}")) +#[cfg(target_os = "linux")] +fn open_netlink_socket(netns_path: &str) -> NetavarkResult { + wrap!( + std::fs::File::open(netns_path), + format!("open {netns_path}") + ) +} + +#[cfg(target_os = "freebsd")] +pub fn open_netlink_sockets( + netns_path: &str, +) -> NetavarkResult<(NamespaceOptions, NamespaceOptions)> { + let hostns = open_host_netlink_socket()?; + let netns = open_netns_netlink_socket(netns_path, &hostns)?; + Ok((hostns, netns)) +} + +#[cfg(target_os = "freebsd")] +pub fn open_host_netlink_socket() -> NetavarkResult { + Ok(NamespaceOptions { + file: Netns::host(), + netlink: netlink::Socket::::new().wrap("host netlink socket")?, + }) +} + +/// Open the netlink socket for the jail named `netns_path`. +#[cfg(target_os = "freebsd")] +pub fn open_netns_netlink_socket( + netns_path: &str, + _hostns: &NamespaceOptions, +) -> NetavarkResult { + let jail = wrap!(Netns::open(netns_path), format!("open jail {netns_path}"))?; + Ok(NamespaceOptions { + netlink: jail.netlink_socket().wrap("netns netlink socket")?, + file: jail, + }) } pub fn add_default_routes( diff --git a/src/network/driver.rs b/src/network/driver.rs index 7116fc7fb..05ec7affe 100644 --- a/src/network/driver.rs +++ b/src/network/driver.rs @@ -4,7 +4,7 @@ use crate::{ firewall::FirewallDriver, }; -use std::{ffi::OsString, net::IpAddr, os::fd::BorrowedFd, path::Path}; +use std::{ffi::OsString, net::IpAddr, path::Path}; use super::{ bridge::Bridge, @@ -15,6 +15,7 @@ use super::{ }; use crate::network::netlink::Socket; use crate::network::netlink_route::NetlinkRoute; +use crate::network::netns::NetnsRef; use std::os::unix::fs::PermissionsExt; pub struct DriverInfo<'a> { @@ -22,8 +23,8 @@ pub struct DriverInfo<'a> { pub container_id: &'a String, pub container_name: &'a String, pub container_dns_servers: &'a Option>, - pub netns_host: BorrowedFd<'a>, - pub netns_container: Option>, + pub netns_host: NetnsRef<'a>, + pub netns_container: Option>, pub netns_path: &'a str, pub network: &'a Network, pub per_network_opts: &'a PerNetworkOptions, diff --git a/src/network/mod.rs b/src/network/mod.rs index d23792cea..42513c920 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -21,6 +21,7 @@ pub mod internal_types; pub mod netlink; pub mod netlink_route; mod netlink_socket; +pub mod netns; pub mod plugin; pub mod sysctl; diff --git a/src/network/netlink.rs b/src/network/netlink.rs index 82e3c0585..fde2bee29 100644 --- a/src/network/netlink.rs +++ b/src/network/netlink.rs @@ -65,7 +65,12 @@ where P: NetlinkFamily, { pub fn new() -> NetavarkResult> { - let mut socket = wrap!(RawSocket::new(P::PROTOCOL), "open")?; + Self::from_raw(wrap!(RawSocket::new(P::PROTOCOL), "open")?) + } + + /// Finish setting up a socket that was opened elsewhere, for example + /// inside a FreeBSD jail. + pub(crate) fn from_raw(mut socket: RawSocket) -> NetavarkResult> { let addr = &SocketAddr::new(0, 0); // Needs to be enabled for dump filtering to work socket.set_netlink_get_strict_chk(true)?; diff --git a/src/network/netlink_route.rs b/src/network/netlink_route.rs index 71b09bd5d..4f770586d 100644 --- a/src/network/netlink_route.rs +++ b/src/network/netlink_route.rs @@ -1,9 +1,7 @@ -use std::{ - net::{Ipv4Addr, Ipv6Addr}, - os::fd::{AsFd, AsRawFd, BorrowedFd}, -}; +use std::net::{Ipv4Addr, Ipv6Addr}; use super::netlink_socket::NETLINK_ROUTE; +use super::netns::NetnsRef; use crate::{ error::{NetavarkError, NetavarkResult}, network::{ @@ -24,6 +22,8 @@ use netlink_packet_route::{ route::{RouteAddress, RouteMessage, RouteProtocol, RouteScope, RouteType}, AddressFamily, RouteNetlinkMessage, }; +#[cfg(target_os = "linux")] +use std::os::fd::{AsFd, AsRawFd}; // Route table ids. Linux keeps its routes in the main table (254). On FreeBSD // a table is a FIB: the default FIB is 0 and the kernel takes the FIB from @@ -46,7 +46,7 @@ pub struct CreateLinkOptions<'fd> { pub primary_index: u32, pub link: u32, pub mac: Vec, - pub netns: Option>, + pub netns: Option>, } pub enum LinkID { @@ -131,6 +131,14 @@ impl Socket { } pub fn create_link(&mut self, options: CreateLinkOptions) -> NetavarkResult<()> { + // FreeBSD has no netlink attribute for this; an interface is moved + // into a jail after creation with the SIOCSIFVNET ioctl. + #[cfg(not(target_os = "linux"))] + if options.netns.is_some() { + return Err(NetavarkError::msg( + "creating a link inside a jail is not supported yet", + )); + } let mut msg = LinkMessage::default(); parse_create_link_options(&mut msg, options); let result = self.make_netlink_request( @@ -175,7 +183,8 @@ impl Socket { Ok(()) } - pub fn set_link_ns(&mut self, link_id: u32, netns: Fd) -> NetavarkResult<()> { + #[cfg(target_os = "linux")] + pub fn set_link_ns(&mut self, link_id: u32, netns: NetnsRef<'_>) -> NetavarkResult<()> { let mut msg = LinkMessage::default(); msg.header.index = link_id; msg.attributes @@ -186,6 +195,15 @@ impl Socket { Ok(()) } + /// Moving a link into a jail is an ioctl on FreeBSD (SIOCSIFVNET), not + /// a netlink attribute. + #[cfg(not(target_os = "linux"))] + pub fn set_link_ns(&mut self, _link_id: u32, _netns: NetnsRef<'_>) -> NetavarkResult<()> { + Err(NetavarkError::msg( + "moving a link into a jail is not supported yet", + )) + } + /// set the vlan_filtering attribute on a bridge pub fn set_vlan_filtering(&mut self, link_id: u32, vlan_filtering: bool) -> NetavarkResult<()> { let mut msg = LinkMessage::default(); @@ -556,6 +574,7 @@ pub fn parse_create_link_options(msg: &mut LinkMessage, options: CreateLinkOptio } // add netnsfd + #[cfg(target_os = "linux")] if let Some(netns) = options.netns { msg.attributes .push(LinkAttribute::NetNsFd(netns.as_raw_fd())); diff --git a/src/network/netlink_socket.rs b/src/network/netlink_socket.rs index 25b6495a2..f0fa9f608 100644 --- a/src/network/netlink_socket.rs +++ b/src/network/netlink_socket.rs @@ -100,6 +100,8 @@ mod freebsd { check_int(res) } + /// Connect to the kernel. FreeBSD marks a netlink socket connected + /// as soon as it is bound, so EISCONN here means it already is. pub fn connect(&self, addr: &SocketAddr) -> io::Result<()> { let raw = addr.as_raw(); let res = unsafe { @@ -109,7 +111,10 @@ mod freebsd { mem::size_of::() as libc::socklen_t, ) }; - check_int(res) + match check_int(res) { + Err(e) if e.raw_os_error() == Some(libc::EISCONN) => Ok(()), + other => other, + } } pub fn send(&self, buf: &[u8], flags: libc::c_int) -> io::Result { @@ -154,6 +159,12 @@ mod freebsd { } } + impl From for OwnedFd { + fn from(s: Socket) -> OwnedFd { + s.0 + } + } + impl AsRawFd for Socket { fn as_raw_fd(&self) -> RawFd { self.0.as_raw_fd() diff --git a/src/network/netns.rs b/src/network/netns.rs new file mode 100644 index 000000000..ed8ea148e --- /dev/null +++ b/src/network/netns.rs @@ -0,0 +1,222 @@ +//! Container network namespaces. +//! +//! On Linux a namespace is a file (`/proc//ns/net` or a bind mount) and +//! the process enters it with setns and comes back the same way. +//! +//! On FreeBSD it is a VNET jail named by the netns path. A process cannot +//! leave a jail, so work inside it runs in a forked child, and a netlink +//! socket for the jail is created in such a child and passed back over a +//! socketpair: the kernel handles netlink requests in the vnet the socket +//! was created in. + +#[cfg(target_os = "freebsd")] +pub use self::freebsd::*; +#[cfg(target_os = "linux")] +pub use self::linux::*; + +/// Borrow a namespace handle for [`super::driver::DriverInfo`]. +pub trait NetnsExt { + fn netns_ref(&self) -> NetnsRef<'_>; +} + +#[cfg(target_os = "linux")] +mod linux { + use super::NetnsExt; + use std::fs::File; + use std::os::fd::{AsFd, BorrowedFd}; + + /// An open netns file. + pub type Netns = File; + pub type NetnsRef<'a> = BorrowedFd<'a>; + + impl NetnsExt for File { + fn netns_ref(&self) -> NetnsRef<'_> { + self.as_fd() + } + } +} + +#[cfg(target_os = "freebsd")] +mod freebsd { + use super::NetnsExt; + use crate::error::{ErrorWrap, NetavarkError, NetavarkResult}; + use crate::network::netlink::{NetlinkFamily, Socket}; + use crate::network::netlink_socket::Socket as RawSocket; + use nix::sys::socket::{ + recvmsg, sendmsg, socketpair, AddressFamily, ControlMessage, ControlMessageOwned, MsgFlags, + SockFlag, SockType, UnixAddr, + }; + use nix::sys::wait::{waitpid, WaitStatus}; + use nix::unistd::{fork, ForkResult}; + use std::ffi::CString; + use std::io::{self, IoSlice, IoSliceMut}; + use std::mem; + use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd}; + + /// A VNET jail. A negative jid stands for the jail netavark itself runs + /// in, which is the host side of every network. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct Netns { + jid: libc::c_int, + } + + pub type NetnsRef<'a> = &'a Netns; + + impl NetnsExt for Netns { + fn netns_ref(&self) -> NetnsRef<'_> { + self + } + } + + impl Netns { + pub fn host() -> Self { + Self { jid: -1 } + } + + /// Look a jail up by name. A missing jail is `NotFound`, like a + /// missing netns file on Linux. + pub fn open(name: &str) -> io::Result { + let key = CString::new("name")?; + let value = CString::new(name)?; + let jid_key = CString::new("jid")?; + let mut jid: libc::c_int = 0; + let mut iov = [ + iovec(key.as_ptr() as *mut _, key.as_bytes_with_nul().len()), + iovec(value.as_ptr() as *mut _, value.as_bytes_with_nul().len()), + iovec( + jid_key.as_ptr() as *mut _, + jid_key.as_bytes_with_nul().len(), + ), + iovec(&mut jid as *mut _ as *mut _, mem::size_of::()), + ]; + let res = unsafe { libc::jail_get(iov.as_mut_ptr(), iov.len() as libc::c_uint, 0) }; + if res < 0 { + return Err(io::Error::last_os_error()); + } + Ok(Self { jid: res }) + } + + /// Run `f` inside the jail. It runs in a forked child, so it can only + /// report success or an error message back. + pub fn run(&self, f: impl FnOnce() -> NetavarkResult<()>) -> NetavarkResult<()> { + in_jail(self.jid, || f().map(|()| None).map_err(|e| e.to_string())).map(drop) + } + + /// Open a netlink socket that acts in the jail's vnet. + pub fn netlink_socket(&self) -> NetavarkResult> { + if self.jid < 0 { + return Socket::new(); + } + let fd = in_jail(self.jid, || { + RawSocket::new(P::PROTOCOL) + .map(|s| Some(OwnedFd::from(s))) + .map_err(|e| format!("open netlink socket: {e}")) + })? + .ok_or_else(|| NetavarkError::msg("no netlink socket received from the jail"))?; + let raw = unsafe { RawSocket::from_raw_fd(fd.into_raw_fd()) }; + Socket::from_raw(raw).wrap("init netlink socket") + } + } + + fn iovec(base: *mut libc::c_void, len: usize) -> libc::iovec { + libc::iovec { + iov_base: base, + iov_len: len, + } + } + + /// Fork, attach the child to the jail and run `child` there. The child + /// sends back one datagram: a status byte, then either the error text + /// or an SCM_RIGHTS fd. netavark is single threaded when this runs. + fn in_jail( + jid: libc::c_int, + child: impl FnOnce() -> Result, String>, + ) -> NetavarkResult> { + let (parent_sock, child_sock) = socketpair( + AddressFamily::Unix, + SockType::Datagram, + None, + SockFlag::SOCK_CLOEXEC, + ) + .map_err(|e| NetavarkError::msg(format!("socketpair: {e}")))?; + + match unsafe { fork() }.map_err(|e| NetavarkError::msg(format!("fork: {e}")))? { + ForkResult::Child => { + drop(parent_sock); + let outcome = if jid >= 0 && unsafe { libc::jail_attach(jid) } != 0 { + Err(format!( + "jail_attach({jid}): {}", + io::Error::last_os_error() + )) + } else { + child() + }; + let (payload, fd) = match outcome { + Ok(fd) => (vec![b'0'], fd), + Err(msg) => { + let mut payload = vec![b'1']; + payload.extend_from_slice(msg.as_bytes()); + (payload, None) + } + }; + let fds: Vec = fd.iter().map(|f| f.as_raw_fd()).collect(); + let cmsgs = if fds.is_empty() { + Vec::new() + } else { + vec![ControlMessage::ScmRights(&fds)] + }; + let _ = sendmsg::( + child_sock.as_raw_fd(), + &[IoSlice::new(&payload)], + &cmsgs, + MsgFlags::empty(), + None, + ); + unsafe { libc::_exit(0) } + } + ForkResult::Parent { child: pid } => { + drop(child_sock); + let mut buf = [0u8; 4096]; + let mut cmsg = nix::cmsg_space!([RawFd; 1]); + let (len, fd) = { + let mut iov = [IoSliceMut::new(&mut buf)]; + let msg = recvmsg::( + parent_sock.as_raw_fd(), + &mut iov, + Some(&mut cmsg), + MsgFlags::empty(), + ); + let status = waitpid(pid, None); + let msg = msg.map_err(|e| NetavarkError::msg(format!("recvmsg: {e}")))?; + let mut fd = None; + for c in msg + .cmsgs() + .map_err(|e| NetavarkError::msg(format!("cmsgs: {e}")))? + { + if let ControlMessageOwned::ScmRights(fds) = c { + fd = fds.first().map(|&f| unsafe { OwnedFd::from_raw_fd(f) }); + } + } + if msg.bytes == 0 { + return Err(NetavarkError::msg(format!( + "jail child exited without a reply: {status:?}" + ))); + } + if let Ok(WaitStatus::Signaled(_, sig, _)) = status { + return Err(NetavarkError::msg(format!( + "jail child killed by signal {sig}" + ))); + } + (msg.bytes, fd) + }; + if buf[0] == b'0' { + Ok(fd) + } else { + Err(NetavarkError::msg( + String::from_utf8_lossy(&buf[1..len]).into_owned(), + )) + } + } + } + } +} diff --git a/src/network/validation.rs b/src/network/validation.rs index e23db5f28..e9956b15d 100644 --- a/src/network/validation.rs +++ b/src/network/validation.rs @@ -1,10 +1,19 @@ use crate::error::NetavarkResult; use log::debug; -use std::fs::File; +/// Check that the netns argument names something we can use: a netns file +/// on Linux, a jail on FreeBSD. +#[cfg(target_os = "linux")] pub fn ns_checks(file: &str) -> NetavarkResult<()> { debug!("Validating network namespace..."); // TODO check for FS_MAGIC - let _ = File::open(file)?.metadata()?; + let _ = std::fs::File::open(file)?.metadata()?; + Ok(()) +} + +#[cfg(target_os = "freebsd")] +pub fn ns_checks(name: &str) -> NetavarkResult<()> { + debug!("Validating network namespace..."); + let _ = crate::network::netns::Netns::open(name)?; Ok(()) } diff --git a/src/network/vlan.rs b/src/network/vlan.rs index 961f8daee..91bc6336c 100644 --- a/src/network/vlan.rs +++ b/src/network/vlan.rs @@ -1,5 +1,5 @@ +use crate::network::netns::NetnsRef; use log::{debug, error}; -use std::os::fd::BorrowedFd; use std::{collections::HashMap, net::IpAddr}; use crate::network::netlink::Socket; @@ -10,12 +10,13 @@ use netlink_packet_route::link::{ use rand::distr::{Alphanumeric, SampleString}; use crate::network::core_utils::get_default_route_interface; +#[cfg(target_os = "linux")] +use crate::network::core_utils::join_netns; use crate::network::dhcp::{dhcp_teardown, get_dhcp_lease}; use crate::{ dns::aardvark::AardvarkEntry, error::{ErrorWrap, NetavarkError, NetavarkResult}, exec_netns, - network::core_utils::join_netns, network::sysctl::disable_ipv6_autoconf, }; @@ -246,8 +247,8 @@ fn setup( netns: &mut Socket, if_name: &str, data: &InternalData, - hostns_fd: BorrowedFd<'_>, - netns_fd: BorrowedFd<'_>, + hostns_fd: NetnsRef<'_>, + netns_fd: NetnsRef<'_>, kind_data: &KindData, ) -> NetavarkResult { let link = match data.host_interface_name.as_ref() { diff --git a/src/test/main.rs b/src/test/main.rs index 9b4d6d227..3795b3d61 100644 --- a/src/test/main.rs +++ b/src/test/main.rs @@ -1,13 +1,24 @@ //! Test program only, unsupported outside of our test suite. +#![cfg_attr(not(target_os = "linux"), allow(dead_code, unused_imports))] + +#[cfg(not(target_os = "linux"))] +fn main() { + eprintln!("netavark-connection-tester needs Linux network namespaces"); + std::process::exit(1); +} use std::{ io::Read, os::fd::{AsRawFd, OwnedFd}, }; +#[cfg(target_os = "linux")] use netavark::exec_netns; +#[cfg(target_os = "linux")] use netavark::network::core_utils::join_netns; +#[cfg(target_os = "linux")] use nix::sys::socket; +#[cfg(target_os = "linux")] fn main() -> Result<(), Box> { let mut args = std::env::args(); _ = args.next(); // skip argv0 @@ -119,6 +130,7 @@ fn main() -> Result<(), Box> { Ok(()) } +#[cfg(target_os = "linux")] fn stream_test( listen_sock: OwnedFd, connect_sock: OwnedFd, @@ -151,6 +163,7 @@ fn stream_test( ); } +#[cfg(target_os = "linux")] fn datagram_test( listen_sock: OwnedFd, connect_sock: OwnedFd, diff --git a/src/test/mod.rs b/src/test/mod.rs index e603fd9a3..2d38fb3b8 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,2 +1,3 @@ +#[cfg(target_os = "linux")] pub mod netlink; pub mod test; diff --git a/src/test/test.rs b/src/test/test.rs index 85df4b2dd..27c8acc08 100644 --- a/src/test/test.rs +++ b/src/test/test.rs @@ -78,6 +78,7 @@ mod tests { // Test commands::setup::ns_checks works correctly #[test] fn test_ns_checks() { + #[cfg(target_os = "linux")] assert!(network::validation::ns_checks("src/test/config/setupopts.test.json").is_ok()); } }