#![cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub(crate) const MAX_NAMESPACE_SOCKETS: usize = crate::egress_proxy::MAX_EGRESS_TCP_PORTS + 1;
pub(crate) const MAX_SESSION_NETNS_FDS: usize = MAX_NAMESPACE_SOCKETS + 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EgressNamespaceBlocker {
CapabilityGrantMissing,
KernelSupportMissing,
}
impl EgressNamespaceBlocker {
#[must_use]
pub fn reason(self) -> String {
match self {
EgressNamespaceBlocker::CapabilityGrantMissing => format!(
"this host refused unshare(CLONE_NEWUSER | CLONE_NEWNET) to the mur binary. On an \
AppArmor host (Ubuntu 23.10+ and derivatives) this is the unprivileged-userns \
restriction: install and load the profile shipped with mur, `sudo install -m 644 \
packaging/apparmor/{name} {path} && sudo apparmor_parser -r {path}` (or re-run \
the mur installer as root), then re-run. Building out of a checkout, where the \
binary sits at ./target/{{debug,release}}/mur and no shipped profile attaches to \
it: run `scripts/install-dev-apparmor.sh`, which generates and loads the same \
grant for those two paths. Inside a container it is a missing capability \
instead: add `--cap-add SYS_ADMIN` to the container invocation, or create the \
network namespace outside the container and run mur inside it. LAST RESORT, only \
where a profile genuinely cannot be loaded: `sudo sysctl -w \
kernel.apparmor_restrict_unprivileged_userns=0` — this removes \
unprivileged-userns hardening from every program on the machine, not just from \
mur, and is not the configuration murmur ships.",
name = crate::sealed::SEALED_APPARMOR_PROFILE_NAME,
path = crate::sealed::SEALED_APPARMOR_PROFILE_PATH,
),
EgressNamespaceBlocker::KernelSupportMissing => {
"this kernel does not provide unprivileged user namespaces, which a capsule's \
network namespace has to be created inside (CONFIG_USER_NS=n, or \
user.max_user_namespaces=0). Raise `sudo sysctl -w \
user.max_user_namespaces=10000` if the sysctl is merely zeroed, otherwise run on \
a kernel built with CONFIG_USER_NS=y."
.to_string()
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum EgressNamespaceProbe {
Ok,
Denied,
MapDenied,
ConfigDenied,
#[default]
Unsupported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct EgressNamespaceSupport {
pub(crate) userns_grant: crate::sealed::UsernsGrant,
pub(crate) namespace: EgressNamespaceProbe,
}
pub(crate) fn egress_namespace_blocker(
is_linux: bool,
support: EgressNamespaceSupport,
) -> Option<EgressNamespaceBlocker> {
if !is_linux {
return None;
}
if !support.userns_grant.permits_userns() {
return Some(EgressNamespaceBlocker::CapabilityGrantMissing);
}
match support.namespace {
EgressNamespaceProbe::Ok => None,
EgressNamespaceProbe::Denied
| EgressNamespaceProbe::MapDenied
| EgressNamespaceProbe::ConfigDenied => {
Some(EgressNamespaceBlocker::CapabilityGrantMissing)
}
EgressNamespaceProbe::Unsupported => Some(EgressNamespaceBlocker::KernelSupportMissing),
}
}
#[cfg(target_os = "linux")]
#[must_use]
pub fn detect_egress_namespace_blocker() -> Option<EgressNamespaceBlocker> {
static SUPPORT: std::sync::OnceLock<EgressNamespaceSupport> = std::sync::OnceLock::new();
let support = *SUPPORT.get_or_init(linux::probe_egress_namespace);
egress_namespace_blocker(true, support)
}
#[cfg(not(target_os = "linux"))]
#[must_use]
pub fn detect_egress_namespace_blocker() -> Option<EgressNamespaceBlocker> {
egress_namespace_blocker(false, EgressNamespaceSupport::default())
}
pub fn skip_without_egress_namespace(test_name: &str) -> bool {
match detect_egress_namespace_blocker() {
Some(blocker) => {
crate::runtime_err!("[SKIP-HOST] {test_name}: {}", blocker.reason());
true
}
None => false,
}
}
pub fn check_egress_namespace(
can_spawn_subprocess: bool,
blocker: Option<EgressNamespaceBlocker>,
) -> Result<(), crate::errors::RuntimeError> {
if !can_spawn_subprocess {
return Ok(());
}
match blocker {
None => Ok(()),
Some(blocker) => Err(crate::errors::RuntimeError::EgressNamespaceUnavailable {
blocker,
reason: blocker.reason(),
}),
}
}
#[derive(Debug, Clone)]
pub(crate) struct CapsuleNetnsPlan {
pub(crate) tcp_ports: Vec<u16>,
pub(crate) uid: u32,
pub(crate) gid: u32,
}
impl CapsuleNetnsPlan {
#[cfg(target_os = "linux")]
#[allow(unsafe_code)]
pub(crate) fn resolve(tcp_ports: Vec<u16>) -> Self {
let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
Self {
tcp_ports,
uid,
gid,
}
}
pub(crate) fn socket_count(&self) -> usize {
self.tcp_ports.len() + 1
}
}
#[derive(Debug, Default)]
pub(crate) struct SessionNetns {
state: std::sync::Mutex<SessionNetnsState>,
#[cfg(test)]
creations: std::sync::atomic::AtomicUsize,
}
#[derive(Debug, Default)]
enum SessionNetnsState {
#[default]
NotYetCreated,
#[cfg(target_os = "linux")]
Established(linux::EstablishedNetns),
Ended,
}
#[cfg(target_os = "linux")]
#[derive(Debug, Clone)]
pub(crate) struct SessionNetnsJoin {
user_ns: std::sync::Arc<std::os::fd::OwnedFd>,
net_ns: std::sync::Arc<std::os::fd::OwnedFd>,
}
#[cfg(target_os = "linux")]
impl SessionNetnsJoin {
pub(crate) fn raw_fds(&self) -> (std::os::fd::RawFd, std::os::fd::RawFd) {
use std::os::fd::AsRawFd;
(self.user_ns.as_raw_fd(), self.net_ns.as_raw_fd())
}
}
impl SessionNetns {
pub(crate) fn new() -> Self {
Self::default()
}
#[cfg(target_os = "linux")]
pub(crate) fn establish(
&self,
tcp_ports: &[u16],
policy: crate::egress_proxy::EgressPolicy,
) -> Result<SessionNetnsJoin, String> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match &*state {
SessionNetnsState::Established(established) => return Ok(established.join()),
SessionNetnsState::Ended => {
return Err(
"sandbox: this session has ended, and its network namespace was released \
with it; no further command runs in it"
.to_string(),
)
}
SessionNetnsState::NotYetCreated => {}
}
let plan = CapsuleNetnsPlan::resolve(tcp_ports.to_vec());
let established = linux::create_session_netns(&plan)
.and_then(|created| created.serve(policy))
.map_err(|detail| {
format!("sandbox: could not create this session's network namespace: {detail}")
})?;
let join = established.join();
*state = SessionNetnsState::Established(established);
#[cfg(test)]
self.creations
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(join)
}
pub(crate) fn end(&self) {
let previous = std::mem::replace(
&mut *self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
SessionNetnsState::Ended,
);
#[cfg(target_os = "linux")]
if let SessionNetnsState::Established(established) = previous {
established.release();
}
#[cfg(not(target_os = "linux"))]
drop(previous);
}
#[cfg(test)]
pub(crate) fn creations(&self) -> usize {
self.creations.load(std::sync::atomic::Ordering::SeqCst)
}
}
impl Drop for SessionNetns {
fn drop(&mut self) {
self.end();
}
}
#[cfg(target_os = "linux")]
pub(crate) use linux::join_session_netns;
#[cfg(target_os = "linux")]
#[allow(unsafe_code)]
mod linux {
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::sync::Arc;
use super::{
CapsuleNetnsPlan, EgressNamespaceProbe, EgressNamespaceSupport, SessionNetnsJoin,
MAX_NAMESPACE_SOCKETS, MAX_SESSION_NETNS_FDS,
};
const RTM_NEWROUTE: u16 = 24;
const NLM_F_REQUEST: u16 = 0x001;
const NLM_F_ACK: u16 = 0x004;
const NLM_F_EXCL: u16 = 0x200;
const NLM_F_CREATE: u16 = 0x400;
const NLMSG_ERROR: u16 = 2;
const RT_TABLE_LOCAL: u8 = 255;
const RTPROT_BOOT: u8 = 3;
const RT_SCOPE_HOST: u8 = 254;
const RTN_LOCAL: u8 = 2;
const RTA_OIF: u16 = 4;
const LOOPBACK_IFINDEX: u32 = 1;
const NLMSGHDR_LEN: usize = 16;
const RTMSG_LEN: usize = 12;
const RTATTR_OIF_LEN: usize = 8;
const ROUTE_REQUEST_LEN: usize = NLMSGHDR_LEN + RTMSG_LEN + RTATTR_OIF_LEN;
pub(super) fn probe_egress_namespace() -> EgressNamespaceSupport {
EgressNamespaceSupport {
userns_grant: crate::sealed::userns_grant(),
namespace: probe_namespace(),
}
}
const PROBE_OK: i32 = 0;
const PROBE_UNSHARE_DENIED: i32 = 1;
const PROBE_UNSHARE_UNSUPPORTED: i32 = 2;
const PROBE_MAP_DENIED: i32 = 3;
const PROBE_CONFIG_DENIED: i32 = 4;
fn probe_namespace() -> EgressNamespaceProbe {
let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
let pid = unsafe { libc::fork() };
if pid < 0 {
return EgressNamespaceProbe::Unsupported;
}
if pid == 0 {
unsafe {
if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNET) != 0 {
let errno = *libc::__errno_location();
libc::_exit(match errno {
libc::EPERM => PROBE_UNSHARE_DENIED,
_ => PROBE_UNSHARE_UNSUPPORTED,
});
}
if write_identity_maps(uid, gid).is_err() {
libc::_exit(PROBE_MAP_DENIED);
}
if bring_loopback_up().is_err() || add_local_default_route().is_err() {
libc::_exit(PROBE_CONFIG_DENIED);
}
libc::_exit(PROBE_OK);
}
}
let mut status: libc::c_int = 0;
let waited = unsafe { libc::waitpid(pid, &mut status, 0) };
if waited != pid || !libc::WIFEXITED(status) {
return EgressNamespaceProbe::Unsupported;
}
match libc::WEXITSTATUS(status) {
PROBE_OK => EgressNamespaceProbe::Ok,
PROBE_UNSHARE_DENIED => EgressNamespaceProbe::Denied,
PROBE_MAP_DENIED => EgressNamespaceProbe::MapDenied,
PROBE_CONFIG_DENIED => EgressNamespaceProbe::ConfigDenied,
_ => EgressNamespaceProbe::Unsupported,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum CreationStep {
Unshare = 1,
IdentityMaps = 2,
Loopback = 3,
LocalRoute = 4,
TcpListener = 5,
Resolver = 6,
NamespaceHandles = 7,
HandOff = 8,
}
impl CreationStep {
fn from_code(code: u32) -> Option<Self> {
Some(match code {
1 => Self::Unshare,
2 => Self::IdentityMaps,
3 => Self::Loopback,
4 => Self::LocalRoute,
5 => Self::TcpListener,
6 => Self::Resolver,
7 => Self::NamespaceHandles,
8 => Self::HandOff,
_ => return None,
})
}
fn describe(self) -> &'static str {
match self {
Self::Unshare => "unshare(CLONE_NEWUSER | CLONE_NEWNET)",
Self::IdentityMaps => "writing uid_map/gid_map",
Self::Loopback => "bringing lo up",
Self::LocalRoute => "installing the local default route",
Self::TcpListener => "binding a TCP listener inside the namespace",
Self::Resolver => "binding UDP 53 inside the namespace",
Self::NamespaceHandles => "opening /proc/self/ns/user and /proc/self/ns/net",
Self::HandOff => "handing the namespace to the runtime",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct CreationFailure {
step: CreationStep,
errno: i32,
}
const FAILURE_RECORD_LEN: usize = 8;
impl CreationFailure {
unsafe fn at(step: CreationStep) -> Self {
let errno = unsafe { *libc::__errno_location() };
Self { step, errno }
}
fn encode(self) -> [u8; FAILURE_RECORD_LEN] {
let mut record = [0u8; FAILURE_RECORD_LEN];
record[..4].copy_from_slice(&(self.step as u32).to_ne_bytes());
record[4..].copy_from_slice(&self.errno.to_ne_bytes());
record
}
fn decode(record: &[u8; FAILURE_RECORD_LEN]) -> Option<Self> {
let step = CreationStep::from_code(u32::from_ne_bytes([
record[0], record[1], record[2], record[3],
]))?;
let errno = i32::from_ne_bytes([record[4], record[5], record[6], record[7]]);
Some(Self { step, errno })
}
}
impl std::fmt::Display for CreationFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} failed: {}",
self.step.describe(),
io::Error::from_raw_os_error(self.errno)
)
}
}
unsafe fn create_capsule_netns(
plan: &CapsuleNetnsPlan,
sock_fd: RawFd,
) -> Result<(), CreationFailure> {
unsafe {
if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNET) != 0 {
return Err(CreationFailure::at(CreationStep::Unshare));
}
if write_identity_maps(plan.uid, plan.gid).is_err() {
return Err(CreationFailure::at(CreationStep::IdentityMaps));
}
if bring_loopback_up().is_err() {
return Err(CreationFailure::at(CreationStep::Loopback));
}
if add_local_default_route().is_err() {
return Err(CreationFailure::at(CreationStep::LocalRoute));
}
let mut fds = [-1 as RawFd; MAX_SESSION_NETNS_FDS];
let mut count = 0usize;
for port in &plan.tcp_ports {
let fd = bind_tcp_listener(*port);
if fd < 0 {
let failure = CreationFailure::at(CreationStep::TcpListener);
close_all(&fds[..count]);
return Err(failure);
}
fds[count] = fd;
count += 1;
}
let resolver = bind_dns_socket();
if resolver < 0 {
let failure = CreationFailure::at(CreationStep::Resolver);
close_all(&fds[..count]);
return Err(failure);
}
fds[count] = resolver;
count += 1;
for namespace in [c"/proc/self/ns/user", c"/proc/self/ns/net"] {
let fd = libc::open(namespace.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC);
if fd < 0 {
let failure = CreationFailure::at(CreationStep::NamespaceHandles);
close_all(&fds[..count]);
return Err(failure);
}
fds[count] = fd;
count += 1;
}
let sent = send_fds(sock_fd, &fds[..count]);
let failure = CreationFailure::at(CreationStep::HandOff);
close_all(&fds[..count]);
sent.map_err(|()| failure)
}
}
pub(crate) unsafe fn join_session_netns(user_fd: RawFd, net_fd: RawFd) -> io::Result<()> {
unsafe {
if libc::setns(user_fd, libc::CLONE_NEWUSER) != 0 {
return Err(io::Error::last_os_error());
}
if libc::setns(net_fd, libc::CLONE_NEWNET) != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
pub(super) struct CreatedNetns {
sockets: Vec<OwnedFd>,
user_ns: OwnedFd,
net_ns: OwnedFd,
}
impl CreatedNetns {
pub(super) fn serve(
self,
policy: crate::egress_proxy::EgressPolicy,
) -> Result<EstablishedNetns, String> {
let proxy = crate::egress_proxy::start_egress_proxy(self.sockets, policy)
.map_err(|error| format!("starting the session's egress proxy failed: {error}"))?;
Ok(EstablishedNetns {
user_ns: Arc::new(self.user_ns),
net_ns: Arc::new(self.net_ns),
proxy,
})
}
}
#[derive(Debug)]
pub(super) struct EstablishedNetns {
user_ns: Arc<OwnedFd>,
net_ns: Arc<OwnedFd>,
proxy: crate::egress_proxy::EgressProxyHandle,
}
impl EstablishedNetns {
pub(super) fn join(&self) -> SessionNetnsJoin {
SessionNetnsJoin {
user_ns: Arc::clone(&self.user_ns),
net_ns: Arc::clone(&self.net_ns),
}
}
pub(super) fn release(self) {
self.proxy.shutdown();
}
}
pub(super) fn create_session_netns(plan: &CapsuleNetnsPlan) -> Result<CreatedNetns, String> {
if plan.socket_count() > MAX_NAMESPACE_SOCKETS {
return Err(format!(
"{} namespace sockets requested, more than the {MAX_NAMESPACE_SOCKETS} one \
hand-off carries",
plan.socket_count()
));
}
let mut pair = [0i32; 2];
let rc = unsafe {
libc::socketpair(
libc::AF_UNIX,
libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
0,
pair.as_mut_ptr(),
)
};
if rc != 0 {
return Err(format!(
"socketpair() for the namespace hand-off failed: {}",
io::Error::last_os_error()
));
}
let (parent_end, creator_end) =
unsafe { (OwnedFd::from_raw_fd(pair[0]), OwnedFd::from_raw_fd(pair[1])) };
let creator_fd = creator_end.as_raw_fd();
let pid = unsafe { libc::fork() };
if pid < 0 {
return Err(format!(
"fork() for the namespace creator failed: {}",
io::Error::last_os_error()
));
}
if pid == 0 {
unsafe {
match create_capsule_netns(plan, creator_fd) {
Ok(()) => libc::_exit(0),
Err(failure) => {
let record = failure.encode();
libc::send(creator_fd, record.as_ptr().cast(), record.len(), 0);
libc::_exit(1);
}
}
}
}
drop(creator_end);
let status = wait_for_creator(pid)?;
match receive_session_netns(parent_end.as_raw_fd(), plan.socket_count())? {
Received::Handles(mut fds) => {
let net_ns = fds.pop().expect("the count was checked on receipt");
let user_ns = fds.pop().expect("the count was checked on receipt");
Ok(CreatedNetns {
sockets: fds,
user_ns,
net_ns,
})
}
Received::Failure(failure) => Err(failure.to_string()),
Received::Nothing => Err(format!(
"the namespace creator exited without handing anything over ({status})"
)),
}
}
fn wait_for_creator(pid: libc::pid_t) -> Result<String, String> {
let mut status: libc::c_int = 0;
loop {
let waited = unsafe { libc::waitpid(pid, &mut status, 0) };
if waited == pid {
break;
}
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted {
return Err(format!("waiting for the namespace creator failed: {error}"));
}
}
Ok(if libc::WIFEXITED(status) {
format!("exit status {}", libc::WEXITSTATUS(status))
} else if libc::WIFSIGNALED(status) {
format!("killed by signal {}", libc::WTERMSIG(status))
} else {
format!("wait status {status}")
})
}
#[cfg(test)]
pub(super) fn describe_failure_record(step: u32, errno: i32) -> Option<String> {
let mut record = [0u8; FAILURE_RECORD_LEN];
record[..4].copy_from_slice(&step.to_ne_bytes());
record[4..].copy_from_slice(&errno.to_ne_bytes());
CreationFailure::decode(&record).map(|failure| {
assert_eq!(failure.encode(), record);
failure.to_string()
})
}
enum Received {
Handles(Vec<OwnedFd>),
Failure(CreationFailure),
Nothing,
}
unsafe fn write_identity_maps(uid: u32, gid: u32) -> Result<(), ()> {
unsafe {
let previous = crate::sealed::make_dumpable_for_map_writes();
let _ = crate::sealed::write_decimal_map(c"/proc/self/setgroups", None, 0);
let mapped = crate::sealed::write_decimal_map(c"/proc/self/uid_map", Some(uid), uid)
.is_ok()
&& crate::sealed::write_decimal_map(c"/proc/self/gid_map", Some(gid), gid).is_ok();
crate::sealed::restore_dumpable(previous);
if mapped {
Ok(())
} else {
Err(())
}
}
}
unsafe fn bring_loopback_up() -> Result<(), ()> {
unsafe {
let sock = libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0);
if sock < 0 {
return Err(());
}
let mut ifr: libc::ifreq = std::mem::zeroed();
ifr.ifr_name[0] = b'l' as libc::c_char;
ifr.ifr_name[1] = b'o' as libc::c_char;
if libc::ioctl(sock, libc::SIOCGIFFLAGS, &mut ifr) < 0 {
libc::close(sock);
return Err(());
}
ifr.ifr_ifru.ifru_flags |= (libc::IFF_UP | libc::IFF_RUNNING) as libc::c_short;
let rc = libc::ioctl(sock, libc::SIOCSIFFLAGS, &ifr);
libc::close(sock);
if rc < 0 {
return Err(());
}
Ok(())
}
}
unsafe fn add_local_default_route() -> Result<(), ()> {
unsafe {
let mut request = [0u8; ROUTE_REQUEST_LEN];
request[0..4].copy_from_slice(&(ROUTE_REQUEST_LEN as u32).to_ne_bytes());
request[4..6].copy_from_slice(&RTM_NEWROUTE.to_ne_bytes());
request[6..8].copy_from_slice(
&(NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL).to_ne_bytes(),
);
request[8..12].copy_from_slice(&1u32.to_ne_bytes()); request[12..16].copy_from_slice(&0u32.to_ne_bytes());
let rtm = NLMSGHDR_LEN;
request[rtm] = libc::AF_INET as u8;
request[rtm + 1] = 0; request[rtm + 2] = 0; request[rtm + 3] = 0; request[rtm + 4] = RT_TABLE_LOCAL;
request[rtm + 5] = RTPROT_BOOT;
request[rtm + 6] = RT_SCOPE_HOST;
request[rtm + 7] = RTN_LOCAL;
request[rtm + 8..rtm + 12].copy_from_slice(&0u32.to_ne_bytes());
let attr = rtm + RTMSG_LEN;
request[attr..attr + 2].copy_from_slice(&(RTATTR_OIF_LEN as u16).to_ne_bytes());
request[attr + 2..attr + 4].copy_from_slice(&RTA_OIF.to_ne_bytes());
request[attr + 4..attr + 8].copy_from_slice(&LOOPBACK_IFINDEX.to_ne_bytes());
let sock = libc::socket(
libc::AF_NETLINK,
libc::SOCK_RAW | libc::SOCK_CLOEXEC,
libc::NETLINK_ROUTE,
);
if sock < 0 {
return Err(());
}
let mut address: libc::sockaddr_nl = std::mem::zeroed();
address.nl_family = libc::AF_NETLINK as u16;
let sent = libc::sendto(
sock,
request.as_ptr().cast(),
ROUTE_REQUEST_LEN,
0,
std::ptr::addr_of!(address).cast(),
std::mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t,
);
if sent < 0 {
libc::close(sock);
return Err(());
}
let mut response = [0u8; 256];
let received = libc::recv(sock, response.as_mut_ptr().cast(), response.len(), 0);
libc::close(sock);
if received < (NLMSGHDR_LEN + 4) as isize {
return Err(());
}
if u16::from_ne_bytes([response[4], response[5]]) == NLMSG_ERROR {
let code = i32::from_ne_bytes([
response[NLMSGHDR_LEN],
response[NLMSGHDR_LEN + 1],
response[NLMSGHDR_LEN + 2],
response[NLMSGHDR_LEN + 3],
]);
if code != 0 {
return Err(());
}
}
Ok(())
}
}
unsafe fn bind_tcp_listener(port: u16) -> RawFd {
unsafe {
let fd = libc::socket(libc::AF_INET, libc::SOCK_STREAM | libc::SOCK_CLOEXEC, 0);
if fd < 0 {
return -1;
}
let mut address: libc::sockaddr_in = std::mem::zeroed();
address.sin_family = libc::AF_INET as libc::sa_family_t;
address.sin_port = port.to_be();
address.sin_addr.s_addr = libc::INADDR_ANY.to_be();
if libc::bind(
fd,
std::ptr::addr_of!(address).cast(),
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
) != 0
|| libc::listen(fd, 128) != 0
{
libc::close(fd);
return -1;
}
fd
}
}
unsafe fn bind_dns_socket() -> RawFd {
unsafe {
let fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0);
if fd < 0 {
return -1;
}
let enable: libc::c_int = 1;
if libc::setsockopt(
fd,
libc::IPPROTO_IP,
libc::IP_PKTINFO,
std::ptr::addr_of!(enable).cast(),
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
) != 0
{
libc::close(fd);
return -1;
}
let mut address: libc::sockaddr_in = std::mem::zeroed();
address.sin_family = libc::AF_INET as libc::sa_family_t;
address.sin_port = super::DNS_PORT_BE;
address.sin_addr.s_addr = libc::INADDR_ANY.to_be();
if libc::bind(
fd,
std::ptr::addr_of!(address).cast(),
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
) != 0
{
libc::close(fd);
return -1;
}
fd
}
}
unsafe fn close_all(fds: &[RawFd]) {
for fd in fds {
if *fd >= 0 {
unsafe { libc::close(*fd) };
}
}
}
const CMSG_BUFFER_LEN: usize = 128;
const _: () = assert!(
unsafe {
libc::CMSG_SPACE((MAX_SESSION_NETNS_FDS * std::mem::size_of::<RawFd>()) as u32) as usize
} <= CMSG_BUFFER_LEN,
"CMSG_BUFFER_LEN is too small to hold MAX_SESSION_NETNS_FDS descriptors plus the \
cmsghdr; raise it to match"
);
#[repr(C, align(8))]
struct CmsgBuffer([u8; CMSG_BUFFER_LEN]);
unsafe fn send_fds(sock_fd: RawFd, fds: &[RawFd]) -> Result<(), ()> {
unsafe {
let mut payload = [0u8; 1];
let mut iov = libc::iovec {
iov_base: payload.as_mut_ptr().cast(),
iov_len: payload.len(),
};
let mut control = CmsgBuffer([0u8; CMSG_BUFFER_LEN]);
let bytes = std::mem::size_of_val(fds) as u32;
let mut msg: libc::msghdr = std::mem::zeroed();
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
msg.msg_control = control.0.as_mut_ptr().cast();
msg.msg_controllen = libc::CMSG_SPACE(bytes) as _;
let cmsg = libc::CMSG_FIRSTHDR(&msg);
if cmsg.is_null() {
return Err(());
}
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
(*cmsg).cmsg_len = libc::CMSG_LEN(bytes) as _;
std::ptr::copy_nonoverlapping(
fds.as_ptr().cast::<u8>(),
libc::CMSG_DATA(cmsg),
bytes as usize,
);
if libc::sendmsg(sock_fd, &msg, 0) < 0 {
return Err(());
}
Ok(())
}
}
fn receive_session_netns(sock_fd: RawFd, expected_sockets: usize) -> Result<Received, String> {
unsafe {
let mut payload = [0u8; FAILURE_RECORD_LEN];
let mut iov = libc::iovec {
iov_base: payload.as_mut_ptr().cast(),
iov_len: payload.len(),
};
let mut control = CmsgBuffer([0u8; CMSG_BUFFER_LEN]);
let mut msg: libc::msghdr = std::mem::zeroed();
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
msg.msg_control = control.0.as_mut_ptr().cast();
msg.msg_controllen = control.0.len() as _;
let received = libc::recvmsg(
sock_fd,
&mut msg,
libc::MSG_DONTWAIT | libc::MSG_CMSG_CLOEXEC,
);
if received < 0 {
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::WouldBlock {
return Ok(Received::Nothing);
}
return Err(format!("receiving the namespace hand-off failed: {error}"));
}
let cmsg = libc::CMSG_FIRSTHDR(&msg);
if cmsg.is_null() {
if received as usize == FAILURE_RECORD_LEN {
if let Some(failure) = CreationFailure::decode(&payload) {
return Ok(Received::Failure(failure));
}
}
return Ok(Received::Nothing);
}
if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS {
return Err(
"the namespace hand-off did not arrive as an SCM_RIGHTS message".to_string(),
);
}
let bytes = (*cmsg).cmsg_len - libc::CMSG_LEN(0) as usize;
let count = bytes / std::mem::size_of::<RawFd>();
let data = libc::CMSG_DATA(cmsg).cast::<RawFd>();
let mut fds = Vec::with_capacity(count);
for index in 0..count {
fds.push(OwnedFd::from_raw_fd(std::ptr::read_unaligned(
data.add(index),
)));
}
if msg.msg_flags & libc::MSG_CTRUNC != 0 {
return Err("the namespace hand-off was truncated".to_string());
}
if count != expected_sockets + 2 {
return Err(format!(
"the namespace creator handed over {count} descriptors; {} were expected \
({expected_sockets} sockets and 2 namespace handles)",
expected_sockets + 2
));
}
Ok(Received::Handles(fds))
}
}
}
#[cfg(target_os = "linux")]
const DNS_PORT_BE: u16 = crate::egress_proxy::EGRESS_DNS_PORT.to_be();
#[cfg(test)]
mod tests {
use super::*;
fn support(apparmor: bool, namespace: EgressNamespaceProbe) -> EgressNamespaceSupport {
EgressNamespaceSupport {
userns_grant: if apparmor {
crate::sealed::UsernsGrant::ProfileConfining
} else {
crate::sealed::UsernsGrant::Withheld
},
namespace,
}
}
#[test]
fn every_permitting_userns_grant_reaches_the_same_verdict() {
for grant in crate::sealed::UsernsGrant::ALL {
let support = EgressNamespaceSupport {
userns_grant: *grant,
namespace: EgressNamespaceProbe::Ok,
};
let expected = if grant.permits_userns() {
None
} else {
Some(EgressNamespaceBlocker::CapabilityGrantMissing)
};
assert_eq!(
egress_namespace_blocker(true, support),
expected,
"grant {grant:?} must decide only through permits_userns()"
);
}
}
#[test]
fn the_capability_grant_reason_offers_the_profile_before_the_sysctl() {
let reason = EgressNamespaceBlocker::CapabilityGrantMissing.reason();
let profile = reason
.find("apparmor_parser -r")
.expect("the profile install command must be named");
let sysctl = reason
.find("kernel.apparmor_restrict_unprivileged_userns=0")
.expect("the fallback must still be named for hosts that cannot load a profile");
assert!(
profile < sysctl,
"the profile install must come first: {reason}"
);
assert!(
reason.contains("LAST RESORT"),
"the sysctl must be labelled a fallback, not offered as a peer: {reason}"
);
assert!(
reason.contains("every program on the machine"),
"the sysctl's host-wide cost must be stated: {reason}"
);
}
#[test]
fn a_working_namespace_blocks_nothing() {
assert_eq!(
egress_namespace_blocker(true, support(true, EgressNamespaceProbe::Ok)),
None
);
}
#[test]
fn apparmor_is_blamed_before_the_namespace_outcome() {
assert_eq!(
egress_namespace_blocker(true, support(false, EgressNamespaceProbe::Unsupported)),
Some(EgressNamespaceBlocker::CapabilityGrantMissing)
);
}
#[test]
fn every_refusal_of_a_supported_mechanism_is_a_capability_grant_problem() {
for probe in [
EgressNamespaceProbe::Denied,
EgressNamespaceProbe::MapDenied,
EgressNamespaceProbe::ConfigDenied,
] {
assert_eq!(
egress_namespace_blocker(true, support(true, probe)),
Some(EgressNamespaceBlocker::CapabilityGrantMissing),
"{probe:?}"
);
}
}
#[test]
fn a_kernel_without_the_mechanism_is_named_separately() {
assert_eq!(
egress_namespace_blocker(true, support(true, EgressNamespaceProbe::Unsupported)),
Some(EgressNamespaceBlocker::KernelSupportMissing)
);
}
#[test]
fn non_linux_is_never_refused_for_a_mechanism_it_never_had() {
for probe in [
EgressNamespaceProbe::Ok,
EgressNamespaceProbe::Denied,
EgressNamespaceProbe::Unsupported,
] {
assert_eq!(egress_namespace_blocker(false, support(false, probe)), None);
}
}
#[test]
fn the_capability_grant_refusal_names_both_remediations() {
let reason = EgressNamespaceBlocker::CapabilityGrantMissing.reason();
assert!(reason.contains("apparmor_parser"), "names the AppArmor fix");
assert!(
reason.contains("--cap-add SYS_ADMIN"),
"names the container fix"
);
}
#[test]
fn the_kernel_refusal_names_the_sysctl_and_the_config_symbol() {
let reason = EgressNamespaceBlocker::KernelSupportMissing.reason();
assert!(reason.contains("user.max_user_namespaces"));
assert!(reason.contains("CONFIG_USER_NS"));
}
#[test]
fn the_socket_count_is_one_per_port_plus_the_resolver() {
let plan = CapsuleNetnsPlan {
tcp_ports: vec![80, 443],
uid: 1000,
gid: 1000,
};
assert_eq!(plan.socket_count(), 3);
}
#[test]
fn a_capsule_with_no_tcp_egress_still_binds_the_resolver() {
let plan = CapsuleNetnsPlan {
tcp_ports: Vec::new(),
uid: 1000,
gid: 1000,
};
assert_eq!(plan.socket_count(), 1);
}
#[test]
fn the_widest_allowlist_still_fits_one_scm_rights_message() {
let plan = CapsuleNetnsPlan {
tcp_ports: (0..crate::egress_proxy::MAX_EGRESS_TCP_PORTS as u16).collect(),
uid: 1000,
gid: 1000,
};
assert!(plan.socket_count() <= MAX_NAMESPACE_SOCKETS);
assert_eq!(MAX_SESSION_NETNS_FDS, MAX_NAMESPACE_SOCKETS + 2);
}
#[cfg(target_os = "linux")]
#[test]
fn an_ended_session_refuses_to_create_a_namespace() {
let netns = SessionNetns::new();
netns.end();
let refusal = netns
.establish(
&[],
crate::egress_proxy::EgressPolicy::new(Vec::new(), Vec::new()),
)
.expect_err("an ended session creates nothing");
assert!(refusal.contains("session has ended"), "{refusal}");
assert_eq!(netns.creations(), 0);
}
#[cfg(target_os = "linux")]
#[test]
fn a_session_creates_its_namespace_once_and_ends_it_for_good() {
if skip_without_egress_namespace(
"a_session_creates_its_namespace_once_and_ends_it_for_good",
) {
return;
}
let policy = || crate::egress_proxy::EgressPolicy::new(Vec::new(), Vec::new());
let netns = SessionNetns::new();
assert_eq!(
netns.creations(),
0,
"nothing is created before it is needed"
);
let first = netns
.establish(&[], policy())
.expect("the namespace is created");
let second = netns
.establish(&[], policy())
.expect("the namespace is reused");
assert_eq!(netns.creations(), 1);
let inode = |fd: std::os::fd::RawFd| {
std::fs::metadata(format!("/proc/self/fd/{fd}"))
.map(|metadata| std::os::unix::fs::MetadataExt::ino(&metadata))
.unwrap()
};
assert_eq!(inode(first.raw_fds().1), inode(second.raw_fds().1));
let host_netns = std::fs::metadata("/proc/self/ns/net")
.map(|metadata| std::os::unix::fs::MetadataExt::ino(&metadata))
.unwrap();
assert_ne!(
inode(first.raw_fds().1),
host_netns,
"the session's namespace is not the runtime's own"
);
netns.end();
netns.end();
assert!(netns.establish(&[], policy()).is_err());
assert_eq!(
netns.creations(),
1,
"an ended namespace is never recreated"
);
}
#[cfg(target_os = "linux")]
#[test]
fn a_creation_failure_names_its_step_and_errno() {
let text = linux::describe_failure_record(4, libc::EPERM).expect("a known step decodes");
assert!(
text.starts_with("installing the local default route failed:"),
"{text}"
);
assert!(text.contains("Operation not permitted"), "{text}");
assert_eq!(linux::describe_failure_record(99, 0), None);
}
}