use std::{
ffi::CString,
io::Write,
os::fd::{BorrowedFd, FromRawFd, IntoRawFd},
os::unix::{io::RawFd, process::CommandExt},
path::{Path, PathBuf},
process::{Child, Command, Stdio},
};
use anyhow::{Context, bail};
use log::debug;
use nix::{
fcntl::{OFlag, open},
mount::{MsFlags, mount},
sched::{CloneFlags, setns, unshare},
sys::stat::Mode,
unistd::close,
};
use super::{netns::NetworkNamespace, port_forwarding::Forwarder};
use crate::util::{
check_process_running, env_vars::set_env_vars, get_running_process_pids, parse_command_str,
process_is_in_network_namespace,
};
const SINGLE_INSTANCE_APPLICATIONS: &[&str] = &[
"google-chrome-stable",
"google-chrome-beta",
"google-chrome",
"google-chrome-unstable",
"chromium",
"chromium-browser",
"brave",
"brave-browser",
"firefox",
"firefox-developer-edition",
"firefox-bin",
"librewolf",
"vivaldi",
"vivaldi-stable",
"opera",
"microsoft-edge",
"microsoft-edge-stable",
];
pub struct ApplicationWrapper {
pub handle: Child,
pub port_forwarding: Option<Box<dyn Forwarder>>,
_etc_overlay: Option<tempfile::TempDir>,
}
impl ApplicationWrapper {
#[allow(clippy::too_many_arguments)]
pub fn new(
netns: &NetworkNamespace,
application: &str,
user: Option<String>,
group: Option<String>,
working_directory: Option<PathBuf>,
port_forwarding: Option<Box<dyn Forwarder>>,
silent: bool,
host_env_vars: &std::collections::HashMap<String, String>,
pipe_io: bool,
stdio_fds: Option<(RawFd, RawFd, RawFd)>,
take_controlling_tty: bool,
) -> anyhow::Result<Self> {
let app_vec = parse_command_str(application)?;
let shared_process_name = app_vec.first().and_then(|program| {
Path::new(program)
.file_name()
.and_then(|name| name.to_str())
.filter(|name| SINGLE_INSTANCE_APPLICATIONS.contains(name))
});
let shared_process_pids = shared_process_name
.map(|name| {
get_running_process_pids(name)
.into_iter()
.filter(|pid| {
!matches!(process_is_in_network_namespace(*pid, &netns.name), Ok(true))
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if let Some(shared_process_name) = shared_process_name
&& !shared_process_pids.is_empty()
{
report_warning(
format!(
"{shared_process_name} is already running outside network namespace '{}' (PID(s): {}). It may reuse that process instead of starting inside vopono; use a separate profile/data directory or stop the existing instance.",
netns.name,
shared_process_pids
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(", ")
),
silent,
stdio_fds,
);
}
let app_vec_ptrs: Vec<&str> = app_vec.iter().map(|s| s.as_str()).collect();
let (mut handle, etc_overlay) = Self::run_with_env_in_netns(
netns,
app_vec_ptrs.as_slice(),
user,
group,
silent,
pipe_io,
pipe_io,
stdio_fds,
take_controlling_tty,
working_directory,
port_forwarding.as_deref(),
host_env_vars,
)?;
let pid = handle.id();
if check_process_running(pid) {
match process_is_in_network_namespace(pid, &netns.name) {
Ok(true) => {
debug!(
"Verified application PID {pid} is in network namespace '{}'",
netns.name
);
}
Ok(false) => {
let _ = handle.kill();
let _ = handle.wait();
bail!(
"Refusing to launch application: PID {pid} is not in network namespace '{}'",
netns.name
);
}
Err(error) => {
log::warn!(
"Could not verify that application PID {pid} is in network namespace '{}': {error}",
netns.name
);
}
}
} else if !shared_process_pids.is_empty() {
report_warning(
format!(
"Application launcher PID {pid} exited before its network namespace could be verified; the existing process may have handled the request instead."
),
silent,
stdio_fds,
);
}
Ok(Self {
handle,
port_forwarding,
_etc_overlay: etc_overlay,
})
}
pub fn wait_with_output(self) -> anyhow::Result<std::process::Output> {
let output = self.handle.wait_with_output()?;
Ok(output)
}
#[allow(clippy::too_many_arguments)]
pub fn run_with_env_in_netns(
netns: &NetworkNamespace,
command: &[&str],
user: Option<String>,
group: Option<String>,
silent: bool,
capture_output: bool,
capture_input: bool,
stdio_fds: Option<(RawFd, RawFd, RawFd)>,
take_controlling_tty: bool,
set_dir: Option<PathBuf>,
forwarder: Option<&dyn Forwarder>,
host_env_vars: &std::collections::HashMap<String, String>,
) -> anyhow::Result<(Child, Option<tempfile::TempDir>)> {
let (prog, args) = command.split_first().context("Command cannot be empty")?;
let mut handle: Command;
let use_direct_setns = nix::unistd::getuid().is_root()
&& (stdio_fds.is_some() || (capture_output && capture_input));
let mut etc_overlay = None;
if use_direct_setns {
handle = Command::new(prog);
handle.args(args);
let user_details = if let Some(user_name) = user {
debug!(
"(daemon) Preparing to run '{}' in netns '{}' as user '{}'",
command.join(" "),
netns.name,
user_name
);
let target_user = nix::unistd::User::from_name(&user_name)?
.with_context(|| format!("User '{}' not found", user_name))?;
let target_group = if let Some(group_name) = group {
nix::unistd::Group::from_name(&group_name)?
.with_context(|| format!("Group '{}' not found", group_name))?
} else {
nix::unistd::Group::from_gid(target_user.gid)?
.with_context(|| "Primary group for user not found")?
};
let dbus_socket_path = format!("/run/user/{}/bus", target_user.uid.as_raw());
if std::path::Path::new(&dbus_socket_path).exists() {
let dbus_address = format!("unix:path={}", dbus_socket_path);
debug!("Setting DBUS_SESSION_BUS_ADDRESS to {}", dbus_address);
handle.env("DBUS_SESSION_BUS_ADDRESS", dbus_address);
} else {
log::warn!(
"Could not find user DBus socket at {}. Graphical applications may fail to integrate with the desktop.",
dbus_socket_path
);
}
handle.env("HOME", &target_user.dir);
handle.env("USER", &target_user.name);
handle.env("LOGNAME", &target_user.name);
if let Some(dir) = set_dir {
handle.current_dir(dir);
} else {
handle.current_dir(&target_user.dir);
}
Some((
target_user.uid,
target_group.gid,
CString::new(target_user.name)?,
))
} else {
if let Some(dir) = set_dir {
handle.current_dir(dir);
}
None
};
let netns_path_cstr = CString::new(format!("/var/run/netns/{}", netns.name))?;
let want_controlling_tty = take_controlling_tty;
let root_c = CString::new("/").unwrap();
let etc_ns_dir = format!("/etc/netns/{}", netns.name);
let overlay = tempfile::Builder::new()
.prefix("vopono-etc-")
.tempdir()
.context("Failed to create private /etc overlay directory")?;
let upper_dir = overlay.path().join("upper");
let work_dir = overlay.path().join("work");
std::fs::create_dir(&upper_dir)?;
std::fs::create_dir(&work_dir)?;
for name in ["resolv.conf", "hosts", "nsswitch.conf"] {
let source = PathBuf::from(&etc_ns_dir).join(name);
if source.is_file() {
std::fs::copy(&source, upper_dir.join(name)).with_context(|| {
format!(
"Failed to stage {} for namespace {}",
source.display(),
netns.name
)
})?;
}
}
let overlay_options = CString::new(format!(
"lowerdir=/etc,upperdir={},workdir={}",
upper_dir.display(),
work_dir.display()
))?;
let overlay_source = CString::new("vopono-etc").unwrap();
let overlay_type = CString::new("overlay").unwrap();
let etc_c = CString::new("/etc").unwrap();
let ping_path = CString::new("/proc/sys/net/ipv4/ping_group_range").unwrap();
etc_overlay = Some(overlay);
unsafe {
handle.pre_exec(move || {
let ns_fd = open(netns_path_cstr.as_c_str(), OFlag::O_RDONLY, Mode::empty())?;
setns(
ns_fd.try_clone().expect("Clone failed"),
CloneFlags::CLONE_NEWNET,
)?;
close(ns_fd)?;
unshare(CloneFlags::CLONE_NEWNS)?;
mount::<std::ffi::CStr, std::ffi::CStr, std::ffi::CStr, std::ffi::CStr>(
None,
root_c.as_c_str(),
None,
MsFlags::MS_REC | MsFlags::MS_PRIVATE,
None,
)?;
mount(
Some(overlay_source.as_c_str()),
etc_c.as_c_str(),
Some(overlay_type.as_c_str()),
MsFlags::empty(),
Some(overlay_options.as_c_str()),
)?;
let fd = libc::open(ping_path.as_ptr(), libc::O_WRONLY);
if fd >= 0 {
let data = b"0 2147483647\n";
let _ = libc::write(fd, data.as_ptr() as *const _, data.len());
libc::close(fd);
}
if want_controlling_tty {
let _ = libc::setsid();
let fd0: i32 = 0;
if libc::isatty(fd0) == 1 {
let acquire_res = libc::ioctl(fd0, libc::TIOCSCTTY as _, 1);
if acquire_res == 0 {
let pgrp = libc::getpgrp();
let _ = libc::tcsetpgrp(fd0, pgrp);
}
}
}
if let Some((uid, gid, user_name_cstr)) = &user_details {
nix::unistd::initgroups(user_name_cstr, *gid)?;
nix::unistd::setgid(*gid)?;
nix::unistd::setuid(*uid)?;
}
Ok(())
});
}
} else {
handle = Command::new("ip");
handle.args(["netns", "exec", netns.name.as_str()]);
let mut sudo_args: Vec<String> = vec![
"sudo".to_string(),
"--preserve-env".to_string(),
"--set-home".to_string(),
];
if let Some(user_str) = &user {
sudo_args.push("--user".to_string());
sudo_args.push(user_str.clone());
}
if let Some(group_str) = &group {
sudo_args.push("--group".to_string());
sudo_args.push(group_str.clone());
}
debug!(
"ip netns exec {} {} {}",
netns.name,
sudo_args.join(" "),
command.join(" ")
);
handle.args(sudo_args);
handle.args(command);
if let Some(cdir) = set_dir {
handle.current_dir(cdir);
}
}
set_env_vars(netns, forwarder, &mut handle, host_env_vars);
if silent {
handle.stdout(Stdio::null());
handle.stderr(Stdio::null());
}
match (stdio_fds, capture_input, capture_output) {
(Some((fd_in, fd_out_orig, fd_err_orig)), _, _) => unsafe {
let in_fd = fd_in;
let mut out_fd = fd_out_orig;
let mut err_fd = fd_err_orig;
if out_fd == in_fd {
out_fd = nix::unistd::dup(BorrowedFd::borrow_raw(out_fd))
.map_err(|e| std::io::Error::other(format!("dup stdout failed: {e}")))?
.into_raw_fd();
}
if err_fd == in_fd || err_fd == out_fd {
err_fd = nix::unistd::dup(BorrowedFd::borrow_raw(err_fd))
.map_err(|e| std::io::Error::other(format!("dup stderr failed: {e}")))?
.into_raw_fd();
}
handle.stdin(Stdio::from_raw_fd(in_fd));
handle.stdout(Stdio::from_raw_fd(out_fd));
handle.stderr(Stdio::from_raw_fd(err_fd));
},
(None, true, true) => {
handle.stdin(Stdio::piped());
handle.stdout(Stdio::piped());
handle.stderr(Stdio::piped());
}
(None, true, false) => {
handle.stdin(Stdio::piped());
}
(None, false, true) => {
handle.stdout(Stdio::piped());
handle.stderr(Stdio::piped());
}
_ => {}
}
let child = handle.spawn()?;
Ok((child, etc_overlay))
}
}
fn report_warning(message: String, silent: bool, stdio_fds: Option<(RawFd, RawFd, RawFd)>) {
if silent {
if let Some((_, _, stderr_fd)) = stdio_fds
&& let Ok(dup_fd) = nix::unistd::dup(unsafe { BorrowedFd::borrow_raw(stderr_fd) })
{
let mut stderr = std::fs::File::from(dup_fd);
let _ = writeln!(stderr, "warning: {message}");
let _ = stderr.flush();
}
} else {
log::warn!("{message}");
}
}