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()); } }