#[cfg(doc)]
use std::path::Path;
use std::{
fmt::Display,
path::PathBuf,
process::{Command, Output},
};
use log::debug;
use crate::{
Error,
RootlessBackend,
RootlessOptions,
utils::{detect_virt, get_command},
};
const ARG_DEBUG: &str = "--debug";
const ARG_COPY_UP: &str = "--copy-up";
const ARG_COPY_UP_MODE: &str = "--copy-up-mode";
const ARG_PROPAGATION: &str = "--propagation";
const ARG_NET: &str = "--net";
const ARG_MTU: &str = "--mtu";
const ARG_CIDR: &str = "--cidr";
const ARG_IFNAME: &str = "--ifname";
const ARG_DISABLE_HOST_LOOPBACK: &str = "--disable-host-loopback";
const ARG_IPV6: &str = "--ipv6";
const ARG_DETACH_NETNS: &str = "--detach-netns";
const ARG_LXC_USER_NIC_BINARY: &str = "--lxc-user-nic-binary";
const ARG_LXC_USER_NIC_BRIDGE: &str = "--lxc-user-nic-bridge";
const ARG_PASTA_BINARY: &str = "--pasta-binary";
const ARG_SLIRP4NETNS_BINARY: &str = "--slirp4netns-binary";
const ARG_SLIRP4NETNS_SANDBOX: &str = "--slirp4netns-sandbox";
const ARG_SLIRP4NETNS_SECCOMP: &str = "--slirp4netns-seccomp";
const ARG_VPNKIT_BINARY: &str = "--vpnkit-binary";
const ARG_PORT_DRIVER: &str = "--port-driver";
const ARG_PUBLISH: &str = "--publish";
const ARG_PIDNS: &str = "--pidns";
const ARG_CGROUPNS: &str = "--cgroupns";
const ARG_UTSNS: &str = "--utsns";
const ARG_IPCNS: &str = "--ipcns";
const ARG_REAPER: &str = "--reaper";
const ARG_EVACUATE_CGROUP2: &str = "--evacuate-cgroup2";
const ARG_STATE_DIR: &str = "--state-dir";
const ARG_SUBID_SOURCE: &str = "--subid-source";
#[derive(Clone, Copy, Debug, Default, strum::Display, Eq, PartialEq)]
#[strum(serialize_all = "lowercase")]
pub enum CopyUpMode {
#[default]
#[strum(serialize = "tmpfs+symlink")]
TmpfsAndSymlink,
}
#[derive(Clone, Copy, Debug, Default, strum::Display, Eq, PartialEq)]
#[strum(serialize_all = "lowercase")]
pub enum Propagation {
#[default]
Rprivate,
Rslave,
}
#[derive(Clone, Copy, Debug, Default, strum::Display, Eq, PartialEq)]
#[strum(serialize_all = "lowercase")]
pub enum Net {
#[default]
Host,
None,
Pasta,
Slirp4netns,
Vpnkit,
#[strum(serialize = "lxc-user-nic")]
LxcUserNic,
}
#[derive(Clone, Copy, Debug, strum::Display, Eq, PartialEq)]
#[strum(serialize_all = "lowercase")]
pub enum AutoOption {
True,
False,
Auto,
}
#[derive(Clone, Copy, Debug, Default, strum::Display, Eq, PartialEq)]
#[strum(serialize_all = "lowercase")]
pub enum PortDriver {
#[default]
None,
Implicit,
Builtin,
Slirp4netns,
}
#[derive(Clone, Copy, Debug, Default, strum::Display, Eq, PartialEq)]
#[strum(serialize_all = "lowercase")]
pub enum SubIdSource {
#[default]
Auto,
Dynamic,
Static,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RootlesskitOptions {
pub debug: bool,
pub copy_up: Vec<String>,
pub copy_up_mode: Option<CopyUpMode>,
pub propagation: Option<Propagation>,
pub net: Option<Net>,
pub mtu: Option<usize>,
pub cidr: Option<String>,
pub ifname: Option<String>,
pub disable_host_loopback: bool,
pub ipv6: bool,
pub detach_netns: bool,
pub lxc_user_nic_binary: Option<PathBuf>,
pub lxc_user_nic_bridge: Option<String>,
pub pasta_binary: Option<PathBuf>,
pub slirp4netns_binary: Option<PathBuf>,
pub slirp4netns_sandbox: Option<AutoOption>,
pub slirp4netns_seccomp: Option<AutoOption>,
pub vpnkit_binary: Option<PathBuf>,
pub port_driver: Option<PortDriver>,
pub publish: Vec<String>,
pub pidns: bool,
pub cgroupns: bool,
pub utsns: bool,
pub ipcns: bool,
pub reaper: Option<AutoOption>,
pub evacuate_cgroup2: Option<String>,
pub state_dir: Option<PathBuf>,
pub subid_source: Option<SubIdSource>,
}
impl Display for RootlesskitOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_vec().join(" "))
}
}
impl RootlessOptions for RootlesskitOptions {
fn to_vec(&self) -> Vec<String> {
let mut options = Vec::new();
if self.debug {
options.push(ARG_DEBUG.to_string());
}
for option in self.copy_up.iter() {
options.push(ARG_COPY_UP.to_string());
options.push(option.to_string());
}
if let Some(option) = self.copy_up_mode {
options.push(ARG_COPY_UP_MODE.to_string());
options.push(option.to_string());
}
if let Some(option) = self.propagation {
options.push(ARG_PROPAGATION.to_string());
options.push(option.to_string());
}
if let Some(option) = self.net {
options.push(ARG_NET.to_string());
options.push(option.to_string());
}
if let Some(option) = self.mtu {
options.push(ARG_MTU.to_string());
options.push(option.to_string());
}
if let Some(option) = self.cidr.as_ref() {
options.push(ARG_CIDR.to_string());
options.push(option.to_string());
}
if let Some(option) = self.ifname.as_ref() {
options.push(ARG_IFNAME.to_string());
options.push(option.to_string());
}
if self.disable_host_loopback {
options.push(ARG_DISABLE_HOST_LOOPBACK.to_string());
}
if self.ipv6 {
options.push(ARG_IPV6.to_string());
}
if self.detach_netns {
options.push(ARG_DETACH_NETNS.to_string());
}
if let Some(option) = self.lxc_user_nic_binary.as_ref() {
options.push(ARG_LXC_USER_NIC_BINARY.to_string());
options.push(option.to_string_lossy().to_string());
}
if let Some(option) = self.lxc_user_nic_bridge.as_ref() {
options.push(ARG_LXC_USER_NIC_BRIDGE.to_string());
options.push(option.to_string());
}
if let Some(option) = self.pasta_binary.as_ref() {
options.push(ARG_PASTA_BINARY.to_string());
options.push(option.to_string_lossy().to_string());
}
if let Some(option) = self.slirp4netns_binary.as_ref() {
options.push(ARG_SLIRP4NETNS_BINARY.to_string());
options.push(option.to_string_lossy().to_string());
}
if let Some(option) = self.slirp4netns_sandbox {
options.push(ARG_SLIRP4NETNS_SANDBOX.to_string());
options.push(option.to_string());
}
if let Some(option) = self.slirp4netns_seccomp {
options.push(ARG_SLIRP4NETNS_SECCOMP.to_string());
options.push(option.to_string());
}
if let Some(option) = self.vpnkit_binary.as_ref() {
options.push(ARG_VPNKIT_BINARY.to_string());
options.push(option.to_string_lossy().to_string());
}
if let Some(option) = self.port_driver {
options.push(ARG_PORT_DRIVER.to_string());
options.push(option.to_string());
}
for option in self.publish.iter() {
options.push(ARG_PUBLISH.to_string());
options.push(option.to_string());
}
if self.pidns {
options.push(ARG_PIDNS.to_string());
}
if self.cgroupns {
options.push(ARG_CGROUPNS.to_string());
}
if self.utsns {
options.push(ARG_UTSNS.to_string());
}
if self.ipcns {
options.push(ARG_IPCNS.to_string());
}
if let Some(option) = self.reaper {
options.push(ARG_REAPER.to_string());
options.push(option.to_string());
}
if let Some(option) = self.evacuate_cgroup2.as_ref() {
options.push(ARG_EVACUATE_CGROUP2.to_string());
options.push(option.to_string());
}
if let Some(option) = self.state_dir.as_ref() {
options.push(ARG_STATE_DIR.to_string());
options.push(option.to_string_lossy().to_string());
}
if let Some(option) = self.subid_source {
options.push(ARG_SUBID_SOURCE.to_string());
options.push(option.to_string());
}
options
}
}
#[derive(Clone, Debug)]
pub struct RootlesskitBackend(RootlesskitOptions);
impl RootlessBackend<RootlesskitOptions> for RootlesskitBackend {
type Err = Error;
fn new(options: RootlesskitOptions) -> Self {
debug!("Create a new rootlesskit backend with options \"{options}\"");
Self(options)
}
fn options(&self) -> &RootlesskitOptions {
&self.0
}
fn run(&self, cmd: &[&str]) -> Result<Output, Self::Err> {
{
let virt = detect_virt()?;
if virt.uses_namespaces() {
return Err(Error::NamespacesInContainer { runtime: virt });
}
}
let command_name = get_command("rootlesskit")?;
let mut command = Command::new(command_name);
if self.0.debug {
command.arg(ARG_DEBUG);
}
for option in self.0.copy_up.iter() {
command.arg(ARG_COPY_UP);
command.arg(option);
}
if let Some(option) = self.0.copy_up_mode {
command.arg(ARG_COPY_UP_MODE);
command.arg(option.to_string());
}
if let Some(option) = self.0.propagation {
command.arg(ARG_PROPAGATION);
command.arg(option.to_string());
}
if let Some(option) = self.0.net {
command.arg(ARG_NET);
command.arg(option.to_string());
}
if let Some(option) = self.0.mtu {
command.arg(ARG_MTU);
command.arg(option.to_string());
}
if let Some(option) = self.0.cidr.as_ref() {
command.arg(ARG_CIDR);
command.arg(option);
}
if let Some(option) = self.0.ifname.as_ref() {
command.arg(ARG_IFNAME);
command.arg(option);
}
if self.0.disable_host_loopback {
command.arg(ARG_DISABLE_HOST_LOOPBACK);
}
if self.0.ipv6 {
command.arg(ARG_IPV6);
}
if self.0.detach_netns {
command.arg(ARG_DETACH_NETNS);
}
if let Some(option) = self.0.lxc_user_nic_binary.as_ref() {
command.arg(ARG_LXC_USER_NIC_BINARY);
command.arg(option);
}
if let Some(option) = self.0.lxc_user_nic_bridge.as_ref() {
command.arg(ARG_LXC_USER_NIC_BRIDGE);
command.arg(option);
}
if let Some(option) = self.0.pasta_binary.as_ref() {
command.arg(ARG_PASTA_BINARY);
command.arg(option);
}
if let Some(option) = self.0.slirp4netns_binary.as_ref() {
command.arg(ARG_SLIRP4NETNS_BINARY);
command.arg(option);
}
if let Some(option) = self.0.slirp4netns_sandbox {
command.arg(ARG_SLIRP4NETNS_SANDBOX);
command.arg(option.to_string());
}
if let Some(option) = self.0.slirp4netns_seccomp {
command.arg(ARG_SLIRP4NETNS_SECCOMP);
command.arg(option.to_string());
}
if let Some(option) = self.0.vpnkit_binary.as_ref() {
command.arg(ARG_VPNKIT_BINARY);
command.arg(option);
}
if let Some(option) = self.0.port_driver {
command.arg(ARG_PORT_DRIVER);
command.arg(option.to_string());
}
for option in self.0.publish.iter() {
command.arg(ARG_PUBLISH);
command.arg(option);
}
if self.0.pidns {
command.arg(ARG_PIDNS);
}
if self.0.cgroupns {
command.arg(ARG_CGROUPNS);
}
if self.0.utsns {
command.arg(ARG_UTSNS);
}
if self.0.ipcns {
command.arg(ARG_IPCNS);
}
if let Some(option) = self.0.reaper {
command.arg(ARG_REAPER);
command.arg(option.to_string());
}
if let Some(option) = self.0.evacuate_cgroup2.as_ref() {
command.arg(ARG_EVACUATE_CGROUP2);
command.arg(option);
}
if let Some(option) = self.0.state_dir.as_ref() {
command.arg(ARG_STATE_DIR);
command.arg(option);
}
if let Some(option) = self.0.subid_source {
command.arg(ARG_SUBID_SOURCE);
command.arg(option.to_string());
}
for command_component in cmd.iter() {
command.arg(command_component);
}
debug!("Run rootless command: {command:?}");
command
.output()
.map_err(|source| crate::Error::CommandExec {
command: format!("{command:?}"),
source,
})
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case::all_options(
RootlesskitOptions{
debug: true,
copy_up: vec!["/etc".to_string(), "/usr".to_string()],
copy_up_mode: Some(CopyUpMode::default()),
propagation: Some(Propagation::default()),
net: Some(Net::Pasta),
mtu: Some(1500),
cidr: Some("10.0.1.0/24".to_string()),
ifname: Some("tap1".to_string()),
disable_host_loopback: true,
ipv6: true,
detach_netns: true,
lxc_user_nic_binary: Some(PathBuf::from("/usr/local/bin/lxc-user-nic")),
lxc_user_nic_bridge: Some("lxcbr1".to_string()),
pasta_binary: Some(PathBuf::from("/usr/local/bin/pasta")),
slirp4netns_binary: Some(PathBuf::from("/usr/local/bin/slirp4netns")),
slirp4netns_sandbox: Some(AutoOption::Auto),
slirp4netns_seccomp: Some(AutoOption::Auto),
vpnkit_binary: Some(PathBuf::from("/usr/local/bin/vpnkit")),
port_driver: Some(PortDriver::Implicit),
publish: vec![
"127.0.0.1:8080:80/tcp".to_string(),
"127.0.0.1:8443:443/tcp".to_string(),
],
pidns: true,
cgroupns: true,
utsns: true,
ipcns: true,
reaper: Some(AutoOption::Auto),
evacuate_cgroup2: Some("testgroup".to_string()),
state_dir: Some(PathBuf::from("/var/foo")),
subid_source: Some(SubIdSource::Dynamic),
},
vec![
ARG_DEBUG.to_string(),
ARG_COPY_UP.to_string(),
"/etc".to_string(),
ARG_COPY_UP.to_string(),
"/usr".to_string(),
ARG_COPY_UP_MODE.to_string(),
"tmpfs+symlink".to_string(),
ARG_PROPAGATION.to_string(),
"rprivate".to_string(),
ARG_NET.to_string(),
"pasta".to_string(),
ARG_MTU.to_string(),
"1500".to_string(),
ARG_CIDR.to_string(),
"10.0.1.0/24".to_string(),
ARG_IFNAME.to_string(),
"tap1".to_string(),
ARG_DISABLE_HOST_LOOPBACK.to_string(),
ARG_IPV6.to_string(),
ARG_DETACH_NETNS.to_string(),
ARG_LXC_USER_NIC_BINARY.to_string(),
"/usr/local/bin/lxc-user-nic".to_string(),
ARG_LXC_USER_NIC_BRIDGE.to_string(),
"lxcbr1".to_string(),
ARG_PASTA_BINARY.to_string(),
"/usr/local/bin/pasta".to_string(),
ARG_SLIRP4NETNS_BINARY.to_string(),
"/usr/local/bin/slirp4netns".to_string(),
ARG_SLIRP4NETNS_SANDBOX.to_string(),
"auto".to_string(),
ARG_SLIRP4NETNS_SECCOMP.to_string(),
"auto".to_string(),
ARG_VPNKIT_BINARY.to_string(),
"/usr/local/bin/vpnkit".to_string(),
ARG_PORT_DRIVER.to_string(),
"implicit".to_string(),
ARG_PUBLISH.to_string(),
"127.0.0.1:8080:80/tcp".to_string(),
ARG_PUBLISH.to_string(),
"127.0.0.1:8443:443/tcp".to_string(),
ARG_PIDNS.to_string(),
ARG_CGROUPNS.to_string(),
ARG_UTSNS.to_string(),
ARG_IPCNS.to_string(),
ARG_REAPER.to_string(),
"auto".to_string(),
ARG_EVACUATE_CGROUP2.to_string(),
"testgroup".to_string(),
ARG_STATE_DIR.to_string(),
"/var/foo".to_string(),
ARG_SUBID_SOURCE.to_string(),
"dynamic".to_string(),
]
)]
#[case::default_options(RootlesskitOptions::default(), Vec::new())]
fn rootlesskit_options_to_vec(
#[case] options: RootlesskitOptions,
#[case] to_vec: Vec<String>,
) {
assert_eq!(options.to_vec(), to_vec);
}
#[test]
fn rootlesskit_backend_options() {
let backend = RootlesskitBackend::new(RootlesskitOptions::default());
assert_eq!(backend.options(), &RootlesskitOptions::default());
}
}