use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::{Command, Stdio};
use nix::unistd::{Gid, Uid, setgid, setuid};
#[allow(clippy::similar_names)]
pub fn run(cmd: &[String]) -> ! {
let host_user = std::env::var("HOST_USER").ok();
let host_uid = std::env::var("HOST_UID")
.ok()
.and_then(|s| s.parse::<u32>().ok());
let host_gid = std::env::var("HOST_GID")
.ok()
.and_then(|s| s.parse::<u32>().ok());
let drop = if let (Some(user), Some(uid), Some(gid)) = (&host_user, host_uid, host_gid) {
let is_root = nix::unistd::getuid().is_root();
if is_root {
setup_user(user, uid, gid);
Some((uid, gid, user.clone()))
} else {
None
}
} else {
None
};
let self_exe = std::env::current_exe().unwrap_or_else(|_| "/usr/local/bin/podbox-guest".into());
if let Err(e) = Command::new(&self_exe)
.arg("--daemon")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
eprintln!(
"podbox-guest: failed to spawn daemon {}: {e}",
self_exe.display()
);
std::process::exit(1);
}
if let Some((uid, gid)) = drop.as_ref().map(|(u, g, _)| (*u, *g)) {
let _ = setgid(Gid::from_raw(gid));
let _ = setuid(Uid::from_raw(uid));
}
let is_tty = nix::unistd::isatty(std::io::stdin()).unwrap_or(false);
let user_env: Option<(String, String, String)> = drop
.as_ref()
.map(|(_, _, user)| (format!("/home/{user}"), user.clone(), user.clone()));
let _ = std::fs::create_dir_all("/run/podbox");
let _ = std::fs::write("/run/podbox/guest-version", crate::VERSION);
let exec_shell_cmd = |program: &str, args: &[String], arg0: Option<String>| -> ! {
let mut c = Command::new(program);
c.args(args);
if let Some(a0) = arg0 {
let _ = c.arg0(a0);
}
c.env("PODBOX_GUEST_VERSION", crate::VERSION);
if let Some((ref home, ref user, ref logname)) = user_env {
c.env("HOME", home)
.env("USER", user)
.env("LOGNAME", logname);
}
let e: std::io::Error = c.exec();
eprintln!("podbox-guest: failed to execute command: {e}");
std::process::exit(1);
};
if is_tty && !cmd.is_empty() {
exec_shell_cmd(&cmd[0], &cmd[1..], None);
} else if is_tty {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/usr/bin/fish".into());
let arg0 = format!("-{shell}");
exec_shell_cmd(&shell, &[], Some(arg0));
} else {
loop {
std::thread::sleep(std::time::Duration::from_hours(1));
}
}
}
#[allow(clippy::too_many_lines)]
fn setup_user(user: &str, uid: u32, gid: u32) {
let home_dir = Path::new("/home").join(user);
let marker = Path::new("/run/podbox/.setup_done");
if marker.exists() {
return;
}
let lock_path = Path::new("/run/podbox/setup.lock");
let _ = std::fs::create_dir_all(lock_path.parent().unwrap());
let Ok(lock_file) = std::fs::File::create(lock_path) else {
return;
};
let mut lock = fd_lock::RwLock::new(lock_file);
let Ok(_guard) = lock.write() else { return };
if marker.exists() {
return;
}
let passwd = || std::fs::read_to_string("/etc/passwd").unwrap_or_default();
let group_file = || std::fs::read_to_string("/etc/group").unwrap_or_default();
if let Some(existing) = passwd()
.lines()
.find(|l| l.split(':').nth(2).is_some_and(|u| u == uid.to_string()))
.and_then(|l| l.split(':').next())
&& existing != user
{
let _ = std::process::Command::new("userdel")
.arg("-r")
.arg(existing)
.status();
}
let group_exists = group_file()
.lines()
.any(|l| l.starts_with(&format!("{user}:")));
if !group_exists {
let status = std::process::Command::new("groupadd")
.args(["-g", &gid.to_string(), user])
.status();
if status.is_err() || !status.unwrap().success() {
let _ = std::process::Command::new("addgroup")
.args(["-g", &gid.to_string(), user])
.status();
}
}
let user_exists = passwd().lines().any(|l| l.starts_with(&format!("{user}:")));
if !user_exists {
let shell = ["/bin/bash", "/bin/zsh", "/bin/fish", "/bin/sh"]
.iter()
.find(|s| std::path::Path::new(s).exists())
.copied()
.unwrap_or("/bin/sh");
let status = std::process::Command::new("useradd")
.args([
"-u",
&uid.to_string(),
"-g",
&gid.to_string(),
"-d",
&home_dir.to_string_lossy(),
"-s",
shell,
"-m",
user,
])
.status();
if status.is_err() || !status.unwrap().success() {
let _ = std::process::Command::new("adduser")
.args([
"-u",
&uid.to_string(),
"-D",
"-h",
&home_dir.to_string_lossy(),
"-s",
shell,
user,
])
.status();
}
}
let _ = std::process::Command::new("chmod")
.args(["755", &home_dir.to_string_lossy()])
.status();
let supp_groups = ["wheel", "sudo", "video", "audio", "render"];
let existing_groups: Vec<&str> = supp_groups
.iter()
.filter(|g| {
group_file()
.lines()
.any(|l| l.starts_with(&format!("{g}:")))
})
.copied()
.collect();
if !existing_groups.is_empty() {
let mut usermod_ok = false;
let groups_arg = existing_groups.join(",");
if let Ok(status) = std::process::Command::new("usermod")
.args(["-aG", &groups_arg, user])
.status()
{
usermod_ok = status.success();
}
if !usermod_ok {
let group_content = std::fs::read_to_string("/etc/group").unwrap_or_default();
let mut modified = false;
let patched: String = group_content
.lines()
.map(|line| {
let parts: Vec<&str> = line.splitn(4, ':').collect();
if parts.len() == 4 && existing_groups.contains(&parts[0]) {
let members = parts[3];
if members.split(',').any(|m| m == user) {
return line.to_string();
}
modified = true;
if members.is_empty() {
format!("{}:{}:{}:{}", parts[0], parts[1], parts[2], user)
} else {
format!(
"{}:{}:{}:{},{}",
parts[0], parts[1], parts[2], members, user
)
}
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n");
if modified {
let _ = std::fs::write("/etc/group", patched + "\n");
}
}
}
let sudoers_dir = Path::new("/etc/sudoers.d");
if sudoers_dir.exists() {
let sudo_file = sudoers_dir.join("podbox");
let _ = std::fs::write(&sudo_file, format!("{user} ALL=(ALL) NOPASSWD: ALL\n"));
let _ = std::fs::set_permissions(&sudo_file, PermissionsExt::from_mode(0o440));
}
let runtime_dir =
std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| format!("/run/user/{uid}"));
let _ = std::process::Command::new("chmod")
.args(["777", &runtime_dir])
.status();
let flatpak_info_symlink = Path::new(&runtime_dir).join("flatpak-info");
let _ = std::fs::remove_file(&flatpak_info_symlink);
let _ = std::os::unix::fs::symlink("//.flatpak-info", &flatpak_info_symlink);
let dconf_dir = Path::new(&runtime_dir).join("dconf");
let _ = std::fs::create_dir_all(&dconf_dir);
let owner = format!("{uid}:{gid}");
let _ = std::process::Command::new("chown")
.args([&owner, dconf_dir.to_str().unwrap_or_default()])
.status();
let _ = std::process::Command::new("chmod")
.args(["700", &dconf_dir.to_string_lossy()])
.status();
let local_dir = home_dir.join(".local");
let share_dir = local_dir.join("share");
for dir in [&local_dir, &share_dir, &share_dir.join("fish")] {
let _ = std::fs::create_dir_all(dir);
}
for dir in [&local_dir, &share_dir, &share_dir.join("fish")] {
let needs_fix = match std::fs::metadata(dir) {
Ok(m) => m.uid() != uid || m.gid() != gid,
Err(_) => true,
};
if needs_fix {
let _ = std::process::Command::new("chown")
.args([&owner, dir.to_str().unwrap_or_default()])
.status();
}
}
if let Some(parent) = marker.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(marker, b"");
}