use std::{
fs::File,
os::unix::io::{AsRawFd, RawFd},
path::{Path, PathBuf},
};
use super::{
connection::Connection,
error::{Error, Result},
protocol::{AsyncProtocolInit, ProtocolState},
socket::NetlinkSocket,
};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum NamespaceSpec<'a> {
Default,
Named(&'a str),
Path(&'a Path),
Pid(u32),
}
impl<'a> NamespaceSpec<'a> {
pub fn connection<P: ProtocolState + Default>(&self) -> Result<Connection<P>> {
match self {
NamespaceSpec::Default => Connection::<P>::new(),
NamespaceSpec::Named(name) => connection_for(name),
NamespaceSpec::Path(path) => connection_for_path(path),
NamespaceSpec::Pid(pid) => connection_for_pid(*pid),
}
}
#[inline]
pub fn is_default(&self) -> bool {
matches!(self, NamespaceSpec::Default)
}
pub fn spawn(&self, cmd: std::process::Command) -> Result<std::process::Child> {
match self {
NamespaceSpec::Default => {
let mut cmd = cmd;
cmd.spawn().map_err(Error::Io)
}
NamespaceSpec::Named(name) => spawn(name, cmd),
NamespaceSpec::Path(path) => spawn_path(path, cmd),
NamespaceSpec::Pid(pid) => {
let path = format!("/proc/{}/ns/net", pid);
spawn_path(&path, cmd)
}
}
}
pub fn spawn_output(&self, cmd: std::process::Command) -> Result<std::process::Output> {
match self {
NamespaceSpec::Default => {
let mut cmd = cmd;
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let child = cmd.spawn().map_err(Error::Io)?;
child.wait_with_output().map_err(Error::Io)
}
NamespaceSpec::Named(name) => spawn_output(name, cmd),
NamespaceSpec::Path(path) => spawn_output_path(path, cmd),
NamespaceSpec::Pid(pid) => {
let path = format!("/proc/{}/ns/net", pid);
spawn_output_path(&path, cmd)
}
}
}
pub fn spawn_with_etc(&self, cmd: std::process::Command) -> Result<std::process::Child> {
match self {
NamespaceSpec::Default => {
let mut cmd = cmd;
cmd.spawn().map_err(Error::Io)
}
NamespaceSpec::Named(name) => spawn_with_etc(name, cmd),
NamespaceSpec::Path(path) => spawn_path(path, cmd),
NamespaceSpec::Pid(pid) => {
let path = format!("/proc/{}/ns/net", pid);
spawn_path(&path, cmd)
}
}
}
pub fn spawn_output_with_etc(
&self,
cmd: std::process::Command,
) -> Result<std::process::Output> {
match self {
NamespaceSpec::Default => {
let mut cmd = cmd;
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let child = cmd.spawn().map_err(Error::Io)?;
child.wait_with_output().map_err(Error::Io)
}
NamespaceSpec::Named(name) => spawn_output_with_etc(name, cmd),
NamespaceSpec::Path(path) => spawn_output_path(path, cmd),
NamespaceSpec::Pid(pid) => {
let path = format!("/proc/{}/ns/net", pid);
spawn_output_path(&path, cmd)
}
}
}
}
pub const NETNS_RUN_DIR: &str = "/var/run/netns";
pub fn connection_for<P: ProtocolState + Default>(name: &str) -> Result<Connection<P>> {
let path = PathBuf::from(NETNS_RUN_DIR).join(name);
connection_for_path(&path)
}
pub fn connection_for_path<P: ProtocolState + Default, T: AsRef<Path>>(
path: T,
) -> Result<Connection<P>> {
Connection::<P>::new_in_namespace_path(path)
}
pub fn connection_for_pid<P: ProtocolState + Default>(pid: u32) -> Result<Connection<P>> {
let path = format!("/proc/{}/ns/net", pid);
connection_for_path(&path)
}
pub async fn connection_for_async<P: AsyncProtocolInit>(name: &str) -> Result<Connection<P>> {
let path = PathBuf::from(NETNS_RUN_DIR).join(name);
connection_for_path_async(&path).await
}
pub async fn connection_for_path_async<P: AsyncProtocolInit, T: AsRef<Path>>(
path: T,
) -> Result<Connection<P>> {
let socket = NetlinkSocket::new_in_namespace_path(P::PROTOCOL, path)?;
let state = P::resolve_async(&socket).await?;
Ok(Connection::from_parts(socket, state))
}
pub async fn connection_for_pid_async<P: AsyncProtocolInit>(pid: u32) -> Result<Connection<P>> {
let path = format!("/proc/{}/ns/net", pid);
connection_for_path_async(&path).await
}
pub fn open(name: &str) -> Result<NamespaceFd> {
let path = PathBuf::from(NETNS_RUN_DIR).join(name);
open_path(&path)
}
pub fn open_path<P: AsRef<Path>>(path: P) -> Result<NamespaceFd> {
let file = File::open(path.as_ref()).map_err(|e| {
Error::InvalidMessage(format!(
"cannot open namespace '{}': {}",
path.as_ref().display(),
e
))
})?;
Ok(NamespaceFd { file })
}
pub fn open_pid(pid: u32) -> Result<NamespaceFd> {
let path = format!("/proc/{}/ns/net", pid);
open_path(&path)
}
#[derive(Debug)]
pub struct NamespaceFd {
file: File,
}
impl NamespaceFd {
pub fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
}
impl AsRawFd for NamespaceFd {
fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
}
pub fn enter(name: &str) -> Result<NamespaceGuard> {
let path = PathBuf::from(NETNS_RUN_DIR).join(name);
enter_path(&path)
}
pub fn enter_path<P: AsRef<Path>>(path: P) -> Result<NamespaceGuard> {
let original = File::open("/proc/thread-self/ns/net")
.map_err(|e| Error::InvalidMessage(format!("cannot open current namespace: {}", e)))?;
let target = File::open(path.as_ref()).map_err(|e| {
Error::InvalidMessage(format!(
"cannot open namespace '{}': {}",
path.as_ref().display(),
e
))
})?;
let ret = unsafe { libc::setns(target.as_raw_fd(), libc::CLONE_NEWNET) };
if ret < 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
Ok(NamespaceGuard { original })
}
#[derive(Debug)]
pub struct NamespaceGuard {
original: File,
}
impl NamespaceGuard {
pub fn restore(self) -> Result<()> {
self.do_restore()
}
fn do_restore(&self) -> Result<()> {
let ret = unsafe { libc::setns(self.original.as_raw_fd(), libc::CLONE_NEWNET) };
if ret < 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
Ok(())
}
}
impl Drop for NamespaceGuard {
fn drop(&mut self) {
if let Err(e) = self.do_restore() {
eprintln!("warning: failed to restore namespace: {}", e);
}
}
}
pub fn exists(name: &str) -> bool {
let path = PathBuf::from(NETNS_RUN_DIR).join(name);
path.exists()
}
pub fn create(name: &str) -> Result<()> {
let ns_path = PathBuf::from(NETNS_RUN_DIR).join(name);
if ns_path.exists() {
return Err(Error::InvalidMessage(format!(
"namespace '{}' already exists",
name
)));
}
if !Path::new(NETNS_RUN_DIR).exists() {
std::fs::create_dir_all(NETNS_RUN_DIR).map_err(|e| {
Error::InvalidMessage(format!("cannot create {}: {}", NETNS_RUN_DIR, e))
})?;
}
File::create(&ns_path).map_err(|e| {
Error::InvalidMessage(format!("cannot create namespace file '{}': {}", name, e))
})?;
let original_ns = File::open("/proc/thread-self/ns/net").map_err(|e| {
let _ = std::fs::remove_file(&ns_path);
Error::InvalidMessage(format!("cannot save current namespace: {}", e))
})?;
let ret = unsafe { libc::unshare(libc::CLONE_NEWNET) };
if ret < 0 {
let _ = std::fs::remove_file(&ns_path);
return Err(Error::Io(std::io::Error::last_os_error()));
}
let ns_path_cstr =
std::ffi::CString::new(ns_path.to_string_lossy().as_bytes()).map_err(|_| {
let _ = std::fs::remove_file(&ns_path);
Error::InvalidMessage("invalid namespace path".to_string())
})?;
let self_ns = std::ffi::CString::new("/proc/thread-self/ns/net").unwrap();
let ret = unsafe {
libc::mount(
self_ns.as_ptr(),
ns_path_cstr.as_ptr(),
std::ptr::null(),
libc::MS_BIND,
std::ptr::null(),
)
};
if ret < 0 {
let err = std::io::Error::last_os_error();
unsafe { libc::setns(original_ns.as_raw_fd(), libc::CLONE_NEWNET) };
let _ = std::fs::remove_file(&ns_path);
return Err(Error::Io(err));
}
let ret = unsafe { libc::setns(original_ns.as_raw_fd(), libc::CLONE_NEWNET) };
if ret < 0 {
return Err(Error::InvalidMessage(format!(
"namespace '{}' created but failed to restore original namespace: {}",
name,
std::io::Error::last_os_error()
)));
}
Ok(())
}
pub fn delete(name: &str) -> Result<()> {
let ns_path = PathBuf::from(NETNS_RUN_DIR).join(name);
if !ns_path.exists() {
return Err(Error::NamespaceNotFound {
name: name.to_string(),
});
}
let ns_path_cstr = std::ffi::CString::new(ns_path.to_string_lossy().as_bytes())
.map_err(|_| Error::InvalidMessage("invalid namespace path".to_string()))?;
let ret = unsafe { libc::umount2(ns_path_cstr.as_ptr(), libc::MNT_DETACH) };
if ret < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() != Some(libc::EINVAL) {
return Err(Error::Io(err));
}
}
std::fs::remove_file(&ns_path)
.map_err(|e| Error::InvalidMessage(format!("cannot remove namespace file: {}", e)))?;
Ok(())
}
pub fn execute_in<F, T>(name: &str, f: F) -> Result<T>
where
F: FnOnce() -> T,
{
let guard = enter(name)?;
let result = f();
guard.restore()?;
Ok(result)
}
pub fn execute_in_path<F, T, P: AsRef<Path>>(path: P, f: F) -> Result<T>
where
F: FnOnce() -> T,
{
let guard = enter_path(path)?;
let result = f();
guard.restore()?;
Ok(result)
}
pub fn list() -> Result<Vec<String>> {
let dir = match std::fs::read_dir(NETNS_RUN_DIR) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(Vec::new());
}
Err(e) => {
return Err(Error::Io(e));
}
};
let mut names = Vec::new();
for entry in dir {
let entry = entry.map_err(Error::Io)?;
let name = entry.file_name().to_string_lossy().to_string();
if name != "." && name != ".." {
names.push(name);
}
}
names.sort();
Ok(names)
}
pub fn get_sysctl(ns_name: &str, key: &str) -> Result<String> {
execute_in(ns_name, || super::sysctl::get(key))?
}
pub fn set_sysctl(ns_name: &str, key: &str, value: &str) -> Result<()> {
execute_in(ns_name, || super::sysctl::set(key, value))?
}
pub fn set_sysctls(ns_name: &str, entries: &[(&str, &str)]) -> Result<()> {
execute_in(ns_name, || super::sysctl::set_many(entries))?
}
pub fn get_sysctl_path<P: AsRef<Path>>(path: P, key: &str) -> Result<String> {
execute_in_path(path, || super::sysctl::get(key))?
}
pub fn set_sysctl_path<P: AsRef<Path>>(path: P, key: &str, value: &str) -> Result<()> {
execute_in_path(path, || super::sysctl::set(key, value))?
}
pub fn set_sysctls_path<P: AsRef<Path>>(path: P, entries: &[(&str, &str)]) -> Result<()> {
execute_in_path(path, || super::sysctl::set_many(entries))?
}
pub fn spawn(ns_name: &str, cmd: std::process::Command) -> Result<std::process::Child> {
let path = PathBuf::from(NETNS_RUN_DIR).join(ns_name);
if !path.exists() {
return Err(Error::NamespaceNotFound {
name: ns_name.to_string(),
});
}
spawn_path(&path, cmd)
}
pub fn spawn_output(ns_name: &str, mut cmd: std::process::Command) -> Result<std::process::Output> {
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let child = spawn(ns_name, cmd)?;
child.wait_with_output().map_err(Error::Io)
}
pub fn spawn_path<P: AsRef<Path>>(
path: P,
mut cmd: std::process::Command,
) -> Result<std::process::Child> {
use std::os::unix::process::CommandExt;
let ns_fd = open_path(path)?;
let raw_fd = ns_fd.as_raw_fd();
unsafe {
cmd.pre_exec(move || {
if libc::setns(raw_fd, libc::CLONE_NEWNET) != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let child = cmd.spawn().map_err(Error::Io)?;
drop(ns_fd);
Ok(child)
}
pub fn spawn_output_path<P: AsRef<Path>>(
path: P,
mut cmd: std::process::Command,
) -> Result<std::process::Output> {
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let child = spawn_path(path, cmd)?;
child.wait_with_output().map_err(Error::Io)
}
fn prepare_etc_binds(ns_name: &str) -> Result<Vec<(std::ffi::CString, std::ffi::CString)>> {
let etc_netns = PathBuf::from("/etc/netns").join(ns_name);
let entries = match std::fs::read_dir(&etc_netns) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(Error::Io(e)),
};
let mut binds = Vec::new();
for entry in entries {
let entry = entry.map_err(Error::Io)?;
let file_name = entry.file_name();
let src = entry.path();
let dst = Path::new("/etc").join(&file_name);
if !dst.exists() {
continue;
}
let src_c = std::ffi::CString::new(src.as_os_str().as_encoded_bytes())
.map_err(|_| Error::InvalidMessage("null byte in path".into()))?;
let dst_c = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
.map_err(|_| Error::InvalidMessage("null byte in path".into()))?;
binds.push((src_c, dst_c));
}
Ok(binds)
}
pub fn spawn_with_etc(ns_name: &str, cmd: std::process::Command) -> Result<std::process::Child> {
let path = PathBuf::from(NETNS_RUN_DIR).join(ns_name);
if !path.exists() {
return Err(Error::NamespaceNotFound {
name: ns_name.to_string(),
});
}
spawn_path_with_etc(&path, ns_name, cmd)
}
pub fn spawn_output_with_etc(
ns_name: &str,
mut cmd: std::process::Command,
) -> Result<std::process::Output> {
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let child = spawn_with_etc(ns_name, cmd)?;
child.wait_with_output().map_err(Error::Io)
}
pub fn spawn_path_with_etc<P: AsRef<Path>>(
path: P,
ns_name: &str,
mut cmd: std::process::Command,
) -> Result<std::process::Child> {
use std::os::unix::process::CommandExt;
let ns_fd = open_path(path)?;
let raw_fd = ns_fd.as_raw_fd();
let bind_mounts = prepare_etc_binds(ns_name)?;
let ns_name_c = std::ffi::CString::new(ns_name)
.map_err(|_| Error::InvalidMessage("null byte in namespace name".into()))?;
let c_root = std::ffi::CString::new("/").unwrap();
let c_none = std::ffi::CString::new("none").unwrap();
let c_sys = std::ffi::CString::new("/sys").unwrap();
let c_sysfs = std::ffi::CString::new("sysfs").unwrap();
unsafe {
cmd.pre_exec(move || {
if libc::setns(raw_fd, libc::CLONE_NEWNET) != 0 {
return Err(std::io::Error::last_os_error());
}
if bind_mounts.is_empty() {
return Ok(());
}
if libc::unshare(libc::CLONE_NEWNS) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::mount(
c_none.as_ptr(),
c_root.as_ptr(),
std::ptr::null(),
libc::MS_SLAVE | libc::MS_REC,
std::ptr::null(),
) != 0
{
return Err(std::io::Error::last_os_error());
}
libc::umount2(c_sys.as_ptr(), libc::MNT_DETACH);
if libc::mount(
ns_name_c.as_ptr(),
c_sys.as_ptr(),
c_sysfs.as_ptr(),
0,
std::ptr::null(),
) != 0
{
}
for (src, dst) in &bind_mounts {
if libc::mount(
src.as_ptr(),
dst.as_ptr(),
std::ptr::null(),
libc::MS_BIND,
std::ptr::null(),
) != 0
{
return Err(std::io::Error::last_os_error());
}
}
Ok(())
});
}
let child = cmd.spawn().map_err(Error::Io)?;
drop(ns_fd);
Ok(child)
}
pub fn spawn_output_path_with_etc<P: AsRef<Path>>(
path: P,
ns_name: &str,
mut cmd: std::process::Command,
) -> Result<std::process::Output> {
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let child = spawn_path_with_etc(path, ns_name, cmd)?;
child.wait_with_output().map_err(Error::Io)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_netns_run_dir() {
assert_eq!(NETNS_RUN_DIR, "/var/run/netns");
}
#[test]
fn test_list_namespaces() {
let result = list();
assert!(result.is_ok());
}
#[test]
fn test_exists_nonexistent() {
assert!(!exists("definitely_does_not_exist_12345"));
}
}