#![cfg(target_os = "linux")]
use clap::Command as ClapCommand;
use color_eyre::Result;
use color_eyre::eyre::{eyre, WrapErr};
use std::collections::HashMap;
use std::net::Ipv4Addr;
use std::path::Path;
use std::sync::OnceLock;
use nix::unistd::{setgid, sethostname, setuid, Gid, Uid};
static CMDLINE: OnceLock<HashMap<String, String>> = OnceLock::new();
pub fn kmsg_write(msg: &str) -> std::io::Result<()> {
use std::io::Write;
let mut kmsg = std::fs::OpenOptions::new().write(true).open("/dev/kmsg")?;
write!(kmsg, "{}", msg)?;
kmsg.flush()
}
fn parse_cmdline() -> HashMap<String, String> {
let mut map = HashMap::new();
match std::fs::read_to_string("/proc/cmdline") {
Ok(cmdline) => {
let trimmed = cmdline.trim();
log::debug!("init: /proc/cmdline={}", trimmed);
map.insert("_raw_proc_cmdline".to_string(), trimmed.to_string());
for token in trimmed.split_whitespace() {
if let Some((k, v)) = token.split_once('=') {
map.insert(k.to_string(), v.to_string());
}
}
}
Err(e) => {
log::debug!("init: FAILED to read /proc/cmdline: {}", e);
}
}
map
}
fn get_cmdline_param(key: &str) -> Option<String> {
CMDLINE.get_or_init(parse_cmdline).get(key).cloned()
}
pub fn command() -> ClapCommand {
ClapCommand::new("init")
.about("Minimal init for VMM guest: mount proc/sys/dev, read epkg.init_cmd/epkg.init_pwd from cmdline, exec")
.arg(clap::arg!([command] ... "Command to exec"))
}
pub fn run(_options: ()) -> Result<()> {
let _ = kmsg_write("<6>init: run() started\n");
run_init()
}
pub fn parse_options(_matches: &clap::ArgMatches) -> Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
pub fn init_logging_early() {
use std::io::Write;
use std::fs::OpenOptions;
let mut kmsg = OpenOptions::new()
.write(true)
.open("/dev/kmsg")
.ok();
let write_kmsg = |kmsg: &mut Option<std::fs::File>, msg: &str| {
if let Some(ref mut k) = kmsg {
let _ = write!(k, "<6>{}", msg);
let _ = k.flush();
}
};
write_kmsg(&mut kmsg, "init: init_logging_early() started\n");
let mut console = OpenOptions::new()
.write(true)
.open("/dev/console")
.ok();
let write_msg = |console: &mut Option<std::fs::File>, kmsg: &mut Option<std::fs::File>, msg: &str, dbg: bool| {
if !dbg {
return;
}
if let Some(ref mut c) = console {
match c.write_all(msg.as_bytes()) {
Ok(_) => {}
Err(e) => {
write_kmsg(kmsg, &format!("init: console write error: {}\n", e));
}
}
match c.flush() {
Ok(_) => {}
Err(e) => {
write_kmsg(kmsg, &format!("init: console flush error: {}\n", e));
}
}
} else {
write_kmsg(kmsg, &format!("init: no console for: {}", msg));
}
};
write_kmsg(&mut kmsg, "init: after first console write\n");
if let Some(ref c) = console.as_ref() {
use std::os::fd::{AsRawFd, BorrowedFd};
let console_fd = c.as_raw_fd();
match nix::unistd::dup2_stderr(unsafe { BorrowedFd::borrow_raw(console_fd) }) {
Ok(_) => write_kmsg(&mut kmsg, "init: dup2_stderr ok\n"),
Err(e) => write_kmsg(&mut kmsg, &format!("init: dup2_stderr failed: {}\n", e)),
}
match nix::unistd::dup2_stdout(unsafe { BorrowedFd::borrow_raw(console_fd) }) {
Ok(_) => write_kmsg(&mut kmsg, "init: dup2_stdout ok\n"),
Err(e) => write_kmsg(&mut kmsg, &format!("init: dup2_stdout failed: {}\n", e)),
}
}
write_kmsg(&mut kmsg, "init: after dup2 (kmsg)\n");
let proc_path = Path::new("/proc");
write_kmsg(&mut kmsg, "init: creating /proc dir (no existence check)\n");
match std::fs::create_dir(proc_path) {
Ok(_) => write_kmsg(&mut kmsg, "init: created /proc\n"),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
write_kmsg(&mut kmsg, "init: /proc already exists\n")
}
Err(e) => write_kmsg(&mut kmsg, &format!("init: create /proc error: {}\n", e)),
}
write_kmsg(&mut kmsg, "init: mounting procfs\n");
match nix::mount::mount(
Some("proc"),
proc_path,
Some("proc"),
nix::mount::MsFlags::empty(),
None::<&str>,
) {
Ok(_) => {
write_kmsg(&mut kmsg, "init: proc mount ok\n");
}
Err(e) => {
write_kmsg(&mut kmsg, &format!("init: proc mount failed: {}\n", e));
}
}
let debug = get_cmdline_param("epkg.debug").as_deref() == Some("1");
write_msg(&mut console, &mut kmsg, "init: checking epkg.rust_log\n", debug);
if let Some(v) = get_cmdline_param("epkg.rust_log") {
write_msg(&mut console, &mut kmsg, "init: rust_log found\n", debug);
let decoded = percent_decode(&v);
if !decoded.is_empty() {
std::env::set_var("RUST_LOG", &decoded);
std::env::set_var("RUST_BACKTRACE", "1");
}
}
for (k, v) in CMDLINE.get_or_init(parse_cmdline).iter() {
if let Some(var_name) = k.strip_prefix("epkg.var.") {
let decoded = percent_decode(v);
std::env::set_var(var_name, &decoded);
write_msg(&mut console, &mut kmsg, &format!("init: {}={}\n", var_name, decoded), debug);
}
}
write_msg(&mut console, &mut kmsg, "init: init_logging_early() complete\n", debug);
}
fn run_init() -> Result<()> {
let _ = kmsg_write("<6>run_init: started\n");
let (pwd, cmd_str, run_user) = (
get_cmdline_param("epkg.init_pwd"),
get_cmdline_param("epkg.init_cmd"),
get_cmdline_param("epkg.init_user"),
);
let _ = kmsg_write(&format!("<6>run_init: cmd={:?}\n", cmd_str));
log::debug!("init: config pwd={:?} cmd={:?} user={:?}", pwd, cmd_str.as_deref(), run_user.as_deref());
let _ = kmsg_write("<6>run_init: about to setup_mounts\n");
if let Err(e) = setup_mounts() {
log::debug!("init: setup_mounts failed: {}", e);
return Err(e).wrap_err("init: setup_mounts failed");
}
let _ = kmsg_write("<6>run_init: setup_mounts done\n");
if let Err(e) = sethostname("localhost") {
log::debug!("init: sethostname failed: {}. Continuing.", e);
} else {
log::debug!("init: sethostname to 'localhost' ok");
}
let _ = kmsg_write("<6>run_init: about to mount_virtiofs_volumes\n");
if let Err(e) = mount_virtiofs_volumes() {
log::debug!("init: mount_virtiofs_volumes failed: {}", e);
}
let _ = kmsg_write("<6>run_init: mount_virtiofs_volumes done\n");
let _ = kmsg_write("<6>run_init: about to chdir\n");
if let Some(ref r) = pwd {
if let Err(e) = std::env::set_current_dir(r) {
log::debug!("init: chdir {} failed: {}", r, e);
} else {
log::debug!("init: chdir {} ok", r);
}
}
let _ = kmsg_write("<6>run_init: about to raise_system_file_limit\n");
raise_system_file_limit();
let _ = kmsg_write("<6>run_init: about to fork\n");
match unsafe { nix::unistd::fork() } {
Ok(nix::unistd::ForkResult::Parent { child }) => {
let _ = kmsg_write(&format!("<6>run_init: parent, child pid={}\n", child));
log::debug!("init: forked child pid={}, parent entering idle loop", child);
let status = pid1_idle_loop();
log::debug!("init: parent idle_loop returned with status: {:?}", status);
Ok(())
}
Ok(nix::unistd::ForkResult::Child) => {
let _ = kmsg_write("<6>run_init: child started\n");
let _ = kmsg_write("<6>run_init: child about to exec\n");
match exec_init_command(cmd_str, run_user) {
Ok(_) => unreachable!(),
Err(e) => {
log::debug!("init: exec_init_command failed: {}", e);
if let Err(e2) = exec_command("/bin/sh -i") {
log::debug!("init: /bin/sh fallback failed: {}", e2);
poweroff_guest();
}
unreachable!()
}
}
}
Err(e) => {
log::debug!("init: fork failed: {}", e);
Err(eyre!("init: fork failed: {}", e))
}
}
}
fn pid1_idle_loop() -> Result<()> {
let mut child_exited = false;
let mut first_echild = true;
loop {
match nix::sys::wait::waitpid(None, Some(nix::sys::wait::WaitPidFlag::WNOHANG)) {
Ok(nix::sys::wait::WaitStatus::Exited(pid, status)) => {
let _ = kmsg_write(&format!("<6>init: child exited pid={} status={}\n", pid, status));
log::debug!("init: reaped child pid={} status={}", pid, status);
child_exited = true;
}
Ok(nix::sys::wait::WaitStatus::Signaled(pid, sig, _)) => {
let _ = kmsg_write(&format!("<6>init: child signaled pid={} sig={:?}\n", pid, sig));
log::debug!("init: reaped child pid={} signal={:?}", pid, sig);
child_exited = true;
}
Ok(nix::sys::wait::WaitStatus::StillAlive) => {}
Ok(other) => {
let _ = kmsg_write(&format!("<6>init: waitpid other: {:?}\n", other));
}
Err(nix::errno::Errno::ECHILD) => {
if first_echild {
let _ = kmsg_write(&format!("<6>init: first ECHILD, child_exited={}\n", child_exited));
first_echild = false;
}
if child_exited {
let _ = kmsg_write("<6>init: ECHILD with child_exited=true, powering off\n");
log::debug!("init: ECHILD received (child_exited=true), powering off guest");
poweroff_guest();
}
}
Err(e) => {
let _ = kmsg_write(&format!("<3>init: waitpid error: {}\n", e));
log::debug!("init: waitpid error: {}", e);
}
}
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
fn apply_requested_user(run_user: Option<&str>) -> Result<()> {
let Some(user) = run_user else {
return Ok(());
};
if user.is_empty() {
return Ok(());
}
let passwd_entries = crate::userdb::read_passwd(None)?;
let (uid, gid) = if user == "root" {
(0, 0)
} else if let Ok(uid_raw) = user.parse::<u32>() {
let gid_raw = passwd_entries
.iter()
.find(|u| u.uid == uid_raw)
.map(|u| u.gid)
.unwrap_or(uid_raw);
(uid_raw, gid_raw)
} else {
match passwd_entries.iter().find(|u| u.name == user) {
Some(u) => (u.uid, u.gid),
None => return Err(eyre!("init: requested user not found: {}", user)),
}
};
log::debug!("init: applying requested user uid={} gid={}", uid, gid);
setgid(Gid::from_raw(gid)).map_err(|e| eyre!("init: setgid({}) failed: {}", gid, e))?;
setuid(Uid::from_raw(uid)).map_err(|e| eyre!("init: setuid({}) failed: {}", uid, e))?;
Ok(())
}
fn exec_init_command(cmd_str: Option<String>, run_user: Option<String>) -> Result<()> {
let _ = kmsg_write("<6>exec_init_command: started\n");
let _ = kmsg_write("<6>exec_init_command: applying user\n");
apply_requested_user(run_user.as_deref())?;
let _ = kmsg_write("<6>exec_init_command: checking for user cmd\n");
if let Some(cmd) = cmd_str {
log::debug!("init: exec user command: {:?}", cmd);
exec_command(&cmd)
} else {
let _ = kmsg_write("<6>exec_init_command: no user cmd, going to vm-daemon\n");
log::debug!("init: no command, starting vm-daemon");
let _ = kmsg_write("<6>exec_init_command: checking /dev/vsock exists\n");
let vsock_ready = std::path::Path::new("/dev/vsock").exists();
let _ = kmsg_write(&format!("<6>exec_init_command: vsock_ready={}\n", vsock_ready));
if !vsock_ready {
log::debug!("init: loading vsock modules");
try_load_module("vsock");
try_load_module("vmw_vsock_virtio_transport");
} else {
let _ = kmsg_write("<6>exec_init_command: vsock available\n");
log::debug!("init: vsock already available (/dev/vsock exists)");
}
let _ = kmsg_write("<6>exec_init_command: checking TSI\n");
let tsi_enabled = get_cmdline_param("epkg.tsi").map_or(true, |v| v == "1" || v.is_empty());
let _ = kmsg_write(&format!("<6>exec_init_command: tsi_enabled={}\n", tsi_enabled));
if tsi_enabled {
log::debug!("init: TSI enabled, skipping virtio_net/network setup (using host network via TSI)");
} else {
log::debug!("init: TSI disabled, setting up traditional virtio networking");
match setup_network_for_vm_daemon() {
Ok(()) => log::debug!("init: guest network ready"),
Err(e) => log::debug!("init: guest network setup failed (continuing; vsock only): {}", e),
}
}
let _ = kmsg_write("<6>exec_init_command: about to exec_vm_daemon\n");
log::debug!("init: exec vm-daemon");
log::debug!("init: about to call exec_vm_daemon()");
exec_vm_daemon()
}
}
#[cfg(target_os = "linux")]
fn setup_mounts() -> Result<()> {
let _ = kmsg_write("<6>setup_mounts: starting\n");
let _ = kmsg_write("<6>setup_mounts: remounting root rw\n");
if let Err(e) = crate::mount::remount_root_rw() {
log::debug!("init: remount / rw failed: {} (continuing; /dev creation may fail)", e);
}
let _ = kmsg_write("<6>setup_mounts: remount done\n");
let _ = kmsg_write("<6>setup_mounts: checking self_epkg exists\n");
let self_epkg = Path::new("/home/wfg/.epkg/envs/self/usr/bin/epkg");
if self_epkg.exists() {
log::debug!("init: self epkg exists at {:?}", self_epkg);
} else {
log::debug!("init: self epkg NOT found at {:?}", self_epkg);
}
let _ = kmsg_write("<6>setup_mounts: self_epkg check done\n");
let _ = kmsg_write("<6>setup_mounts: checking vm-daemon exists\n");
let vm_daemon = Path::new("/usr/bin/vm-daemon");
let _ = vm_daemon.exists();
let _ = kmsg_write("<6>setup_mounts: vm-daemon check done\n");
let init_specs = crate::mount::vmm_init_mount_spec_strings();
let _ = kmsg_write("<6>setup_mounts: about to mount_spec_strings\n");
log::debug!("init: applying {} mount specs (proc, tmp, ...)", init_specs.len());
crate::mount::mount_spec_strings(
&init_specs,
Path::new("/"),
crate::models::IsolateMode::Vm,
).wrap_err_with(|| format!("init: mount_spec_strings failed (specs: {:?})", init_specs))?;
let _ = kmsg_write("<6>setup_mounts: mount_spec_strings done\n");
let _ = kmsg_write("<6>setup_mounts: checking /dev exists\n");
if Path::new("/dev").exists() && Path::new("/dev/null").exists() {
log::debug!("init: /dev already populated, skip devtmpfs/tmpfs");
} else {
let _ = kmsg_write("<6>setup_mounts: creating /dev\n");
fs_create_dir_if_missing("/dev").wrap_err("init: create /dev")?;
let _ = kmsg_write("<6>setup_mounts: mounting devtmpfs\n");
if let Err(e) = nix::mount::mount(
Some("devtmpfs"),
Path::new("/dev"),
Some("devtmpfs"),
nix::mount::MsFlags::empty(),
None::<&str>,
) {
log::debug!("init: mount devtmpfs on /dev failed: {}, trying tmpfs", e);
nix::mount::mount(
Some("tmpfs"),
Path::new("/dev"),
Some("tmpfs"),
nix::mount::MsFlags::empty(),
None::<&str>,
).map_err(|e2| eyre!("init: mount devtmpfs and tmpfs on /dev failed: devtmpfs={}, tmpfs={}", e, e2))?;
} else {
log::debug!("init: mounted devtmpfs on /dev");
}
}
let _ = kmsg_write("<6>setup_mounts: /dev setup done\n");
let _ = kmsg_write("<6>setup_mounts: ensure_minimal_dev\n");
log::debug!("init: ensure_minimal_dev (symlinks, nodes, devpts)");
ensure_minimal_dev().wrap_err("init: ensure_minimal_dev")?;
let _ = kmsg_write("<6>setup_mounts: complete\n");
Ok(())
}
#[cfg(target_os = "linux")]
fn mount_virtiofs_volumes() -> Result<()> {
let cmdline = CMDLINE.get_or_init(parse_cmdline);
let mut vol_specs: Vec<(&String, &String)> = cmdline
.iter()
.filter(|(k, _)| k.starts_with("epkg.vol_"))
.collect();
vol_specs.sort_by_key(|(k, _)| *k);
if vol_specs.is_empty() {
log::debug!("init: no virtiofs volumes to mount");
return Ok(());
}
log::debug!("init: mounting {} virtiofs volume(s)", vol_specs.len());
for (key, spec) in vol_specs {
let spec = percent_decode(spec);
log::debug!("init: {} = {}", key, spec);
let parts: Vec<&str> = spec.split(':').collect();
if parts.len() < 2 {
log::warn!("init: invalid virtiofs spec '{}': expected tag:guest_path[:ro]", spec);
continue;
}
let tag = parts[0];
let guest_path = parts[1];
let read_only = parts.get(2).map(|&m| m == "ro").unwrap_or(false);
if let Err(e) = fs_create_dir_if_missing(guest_path) {
log::warn!("init: cannot create mount point {}: {}", guest_path, e);
continue;
}
let flags = if read_only {
nix::mount::MsFlags::MS_RDONLY
} else {
nix::mount::MsFlags::empty()
};
match nix::mount::mount(
Some(tag),
Path::new(guest_path),
Some("virtiofs"),
flags,
None::<&str>,
) {
Ok(()) => {
log::debug!("init: mounted virtiofs {} on {} ({})", tag, guest_path,
if read_only { "ro" } else { "rw" });
}
Err(e) => {
log::warn!("init: failed to mount virtiofs {} on {}: {}", tag, guest_path, e);
}
}
}
Ok(())
}
#[cfg(target_os = "linux")]
fn exec_vm_daemon() -> Result<()> {
log::debug!("init: exec_vm_daemon() started");
let _ = kmsg_write("<6>exec_vm_daemon: starting\n");
if std::env::var("PATH").is_err() {
std::env::set_var("PATH", "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin");
}
let reverse_mode = get_cmdline_param("epkg.vsock_reverse").map_or(false, |v| v == "1");
log::debug!("init: exec_vm_daemon() mode={}", if reverse_mode { "reverse" } else { "forward" });
let _ = kmsg_write(&format!("<6>exec_vm_daemon: reverse_mode={}\n", reverse_mode));
log::debug!("init: starting vm-daemon directly (no exec), reverse_mode={}", reverse_mode);
let _ = kmsg_write("<6>exec_vm_daemon: creating options\n");
let options = crate::vm::guest_daemon::VmDaemonOptions {
reverse_mode,
..Default::default()
};
let _ = kmsg_write("<6>exec_vm_daemon: about to call vm_daemon::run\n");
let result = crate::vm::guest_daemon::run(options);
log::debug!("init: vm::guest_daemon::run() returned: {:?}", result);
if result.is_err() {
log::debug!("init: vm_daemon failed, still powering off");
}
poweroff_guest();
}
#[cfg(target_os = "linux")]
fn exec_command(cmd_str: &str) -> Result<()> {
use nix::unistd::execvp;
use std::ffi::CString;
let decoded_cmd = percent_decode(cmd_str);
if decoded_cmd != cmd_str {
log::debug!("init: decoded percent-encoded cmd: {:?}", decoded_cmd);
}
let parts: Vec<String> = shlex::split(&decoded_cmd)
.ok_or_else(|| eyre!("init: failed to parse command: {:?}", decoded_cmd))?;
let (cmd, args) = if parts.is_empty() {
log::debug!("init: empty command, fallback to /bin/sh -i");
("/bin/sh".to_string(), vec!["-i".to_string()])
} else {
(parts[0].clone(), parts[1..].to_vec())
};
let cmd_name = std::path::Path::new(&cmd)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
let set_ps1 = if cmd_name == "bash" {
true
} else if cmd_name == "sh" {
std::fs::canonicalize(&cmd)
.ok()
.and_then(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|name| name != "dash")
})
.unwrap_or(false)
} else {
false
};
if set_ps1 {
std::env::set_var("PS1", "\\[\\033[01;32m\\]\\w\\[\\033[0m\\] $ ");
}
if std::env::var("PATH").is_err() {
std::env::set_var("PATH", "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin");
}
log::debug!("init: exec cmd={:?} args={:?}", cmd, args);
let cmd_c = CString::new(cmd.as_str()).map_err(|e| eyre!("init: command CString: {}", e))?;
let mut args_c: Vec<CString> = vec![cmd_c.clone()];
for a in &args {
args_c.push(CString::new(a.as_str()).map_err(|e| eyre!("init: arg {:?} CString: {}", a, e))?);
}
execvp(&cmd_c, &args_c).map_err(|e| eyre!("init: exec {} failed: {}", cmd, e))?;
unreachable!()
}
fn fs_create_dir_if_missing(p: &str) -> Result<()> {
let path = Path::new(p);
if !path.exists() {
std::fs::create_dir_all(path).map_err(|e| eyre!("mkdir {}: {}", p, e))?;
}
Ok(())
}
#[cfg(target_os = "linux")]
fn raise_system_file_limit() {
const FILE_MAX_PATH: &str = "/proc/sys/fs/file-max";
const NR_OPEN_PATH: &str = "/proc/sys/fs/nr_open";
const TARGET: u64 = 2_097_152;
if let Err(e) = std::fs::write(FILE_MAX_PATH, TARGET.to_string()) {
log::debug!("init: could not set {} to {}: {} (kernel cmdline sysctl.fs.file-max may still apply)", FILE_MAX_PATH, TARGET, e);
} else {
log::debug!("init: set {} to {}", FILE_MAX_PATH, TARGET);
}
if let Err(e) = std::fs::write(NR_OPEN_PATH, TARGET.to_string()) {
log::debug!("init: could not set {} to {}: {}", NR_OPEN_PATH, TARGET, e);
} else {
log::debug!("init: set {} to {}", NR_OPEN_PATH, TARGET);
}
const MIN_FD_LIMIT: u64 = 65_536;
const TARGET_FD_LIMIT: u64 = 2_097_152;
let mut rlim: libc::rlimit = unsafe { std::mem::zeroed() };
if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut rlim) } == 0 {
let soft = rlim.rlim_cur;
let hard = rlim.rlim_max;
log::debug!("init: current RLIMIT_NOFILE: soft={}, hard={}", soft, hard);
if soft < MIN_FD_LIMIT {
rlim.rlim_cur = TARGET_FD_LIMIT;
rlim.rlim_max = TARGET_FD_LIMIT;
if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &rlim) } != 0 {
let err = std::io::Error::last_os_error();
log::debug!("init: failed to set RLIMIT_NOFILE to {}: {}", TARGET_FD_LIMIT, err);
rlim.rlim_cur = hard;
rlim.rlim_max = hard;
if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &rlim) } != 0 {
let err2 = std::io::Error::last_os_error();
log::debug!("init: fallback also failed: {}", err2);
} else {
log::debug!("init: increased RLIMIT_NOFILE soft from {} to {} (hard limit)", soft, hard);
}
} else {
log::debug!("init: increased RLIMIT_NOFILE from soft={}, hard={} to {}", soft, hard, TARGET_FD_LIMIT);
}
}
} else {
let err = std::io::Error::last_os_error();
log::debug!("init: failed to get RLIMIT_NOFILE: {}", err);
}
}
#[cfg(target_os = "linux")]
fn ensure_minimal_dev() -> Result<()> {
let dev_root = Path::new("/dev");
crate::mount::ensure_dev_symlinks(dev_root)
.wrap_err("init: ensure_dev_symlinks(/dev)")?;
crate::mount::ensure_minimal_dev_nodes(dev_root)
.wrap_err("init: ensure_minimal_dev_nodes(/dev)")?;
if let Err(e) = crate::mount::ensure_devpts_mount(dev_root) {
log::debug!("init: ensure_devpts_mount(/dev) failed: {} (PTY may not work)", e);
}
Ok(())
}
#[cfg(target_os = "linux")]
fn try_load_module(name: &str) -> bool {
let r = crate::busybox::modprobe::run(crate::busybox::modprobe::ModprobeOptions {
remove: false,
quiet: false,
module: name.to_string(),
params: vec![],
});
match &r {
Ok(()) => {
log::debug!("init: modprobe {} -> ok", name);
true
}
Err(e) => {
log::debug!("init: modprobe {} -> failed: {}", name, e);
false
}
}
}
#[cfg(target_os = "linux")]
fn setup_network_for_vm_daemon() -> Result<(), String> {
log::debug!("init: checking virtio_net module / interfaces for vm-daemon");
let net_dir = std::path::Path::new("/sys/class/net");
if net_dir.exists() {
if let Ok(entries) = std::fs::read_dir(net_dir) {
let has_non_lo = entries
.flatten()
.filter_map(|e| e.file_name().to_str().map(|s| s.to_string()))
.any(|name| name != "lo");
if has_non_lo {
log::debug!("init: non-loopback interface already present, skipping virtio_net modprobe");
} else if std::path::Path::new("/lib/modules").exists() {
log::debug!("init: no non-loopback interface yet, trying virtio_net modprobe");
let net_loaded = try_load_module("virtio_net");
if net_loaded {
log::debug!("init: virtio_net module loaded");
} else {
log::debug!(
"init: virtio_net modprobe failed (kernel may have it built-in or no /lib/modules tree)"
);
}
} else {
log::debug!(
"init: /lib/modules missing and no non-loopback interface yet; \
assuming built-in virtio_net or delayed network bring-up"
);
}
}
}
log::debug!("init: configuring network for vm-daemon");
configure_network()
}
#[cfg(target_os = "linux")]
fn parse_net_flags(s: &str) -> Option<u32> {
let s = s.trim();
if s.starts_with("0x") || s.starts_with("0X") {
u32::from_str_radix(&s[2..], 16).ok()
} else {
s.parse::<u32>().ok()
}
}
#[cfg(target_os = "linux")]
fn is_interface_suitable(name: &str, net_dir: &Path) -> bool {
const IFF_LOOPBACK: u32 = 0x8;
if name == "lo" {
return false;
}
let flags_path = net_dir.join(name).join("flags");
match std::fs::read_to_string(&flags_path) {
Ok(flags) => {
if let Some(v) = parse_net_flags(&flags) {
if v & IFF_LOOPBACK != 0 {
return false;
}
} else {
log::debug!("init: {} flags parse failed (content: {:?}), treating as non-loopback", name, flags.trim());
}
}
Err(e) => {
log::debug!("init: read {} failed: {}, treating as non-loopback", flags_path.display(), e);
}
}
true
}
#[cfg(target_os = "linux")]
fn try_discover_interface_once(net_dir: &Path, attempt: u32, log_first: bool) -> Result<Option<String>, std::io::Error> {
let mut entries: Vec<_> = std::fs::read_dir(net_dir)?
.filter_map(|e| e.ok())
.collect();
entries.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
let names: Vec<String> = entries.iter()
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
if log_first {
log::debug!("init: net discovery attempt 1: interfaces {:?}", names);
}
for entry in &entries {
let name = entry.file_name().to_string_lossy().into_owned();
if is_interface_suitable(&name, net_dir) {
log::debug!("init: found interface {} after {} attempts (candidates: {:?})", name, attempt + 1, names);
return Ok(Some(name));
}
}
Ok(None)
}
#[cfg(target_os = "linux")]
fn discover_primary_interface() -> Result<String, Vec<String>> {
const MAX_ATTEMPTS: u32 = 10;
const RETRY_MS: u64 = 5;
let net_dir = Path::new("/sys/class/net");
let mut last_seen: Vec<String> = vec![];
for attempt in 0..MAX_ATTEMPTS {
match try_discover_interface_once(net_dir, attempt, attempt == 0) {
Ok(Some(name)) => return Ok(name),
Ok(None) => {
if let Ok(rd) = std::fs::read_dir(net_dir) {
last_seen = rd.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
}
}
Err(e) => {
if attempt == 0 || attempt + 1 == MAX_ATTEMPTS {
log::debug!("init: /sys/class/net read_dir failed (attempt {}): {}", attempt + 1, e);
}
}
}
if attempt + 1 == MAX_ATTEMPTS {
log::debug!("init: net discovery gave up after {} attempts: saw {:?}", MAX_ATTEMPTS, last_seen);
}
if attempt + 1 < MAX_ATTEMPTS {
std::thread::sleep(std::time::Duration::from_millis(RETRY_MS));
}
}
Err(last_seen)
}
#[cfg(target_os = "linux")]
fn configure_network() -> Result<(), String> {
const GUEST_IP: (u8, u8, u8, u8) = (10, 0, 2, 15);
const GUEST_NETMASK: (u8, u8, u8, u8) = (255, 255, 255, 0);
const GATEWAY_IP: (u8, u8, u8, u8) = (10, 0, 2, 2);
let iface = discover_primary_interface().map_err(|last_seen| {
format!("no non-loopback network interface found (saw: {:?})", last_seen)
})?;
log::debug!("init: configuring interface {} (up, then {}.{}.{}.{}/{}.{}.{}.{}, then default route)",
iface, GUEST_IP.0, GUEST_IP.1, GUEST_IP.2, GUEST_IP.3,
GUEST_NETMASK.0, GUEST_NETMASK.1, GUEST_NETMASK.2, GUEST_NETMASK.3);
log::debug!("init: ifconfig {} up", iface);
crate::busybox::ifconfig::run(crate::busybox::ifconfig::IfconfigOptions {
interface: iface.clone(),
address: None,
netmask: None,
up: true,
down: false,
})
.map_err(|e| format!("ifconfig {} up: {}", iface, e))?;
log::debug!("init: ifconfig {} {}.{}.{}.{}/{}.{}.{}.{}", iface,
GUEST_IP.0, GUEST_IP.1, GUEST_IP.2, GUEST_IP.3,
GUEST_NETMASK.0, GUEST_NETMASK.1, GUEST_NETMASK.2, GUEST_NETMASK.3);
crate::busybox::ifconfig::run(crate::busybox::ifconfig::IfconfigOptions {
interface: iface.clone(),
address: Some(Ipv4Addr::new(GUEST_IP.0, GUEST_IP.1, GUEST_IP.2, GUEST_IP.3)),
netmask: Some(Ipv4Addr::new(GUEST_NETMASK.0, GUEST_NETMASK.1, GUEST_NETMASK.2, GUEST_NETMASK.3)),
up: false,
down: false,
})
.map_err(|e| format!("ifconfig {} {}.{}.{}.{}: {}", iface,
GUEST_IP.0, GUEST_IP.1, GUEST_IP.2, GUEST_IP.3, e))?;
log::debug!("init: route add default via {}.{}.{}.{} dev {}",
GATEWAY_IP.0, GATEWAY_IP.1, GATEWAY_IP.2, GATEWAY_IP.3, iface);
crate::busybox::route::run(crate::busybox::route::RouteOptions {
operation: crate::busybox::route::Operation::Add,
target: crate::busybox::route::Target::Default,
gateway: Some(Ipv4Addr::new(GATEWAY_IP.0, GATEWAY_IP.1, GATEWAY_IP.2, GATEWAY_IP.3)),
interface: Some(iface),
})
.map_err(|e| format!("route add default: {}", e))?;
log::debug!("init: network configured");
Ok(())
}
#[cfg(target_os = "linux")]
fn poweroff_guest() -> ! {
use std::fs::OpenOptions;
use std::io::Write;
log::debug!("poweroff_guest: initiating VM shutdown");
if let Ok(mut file) = OpenOptions::new().write(true).open("/proc/sysrq-trigger") {
log::debug!("poweroff_guest: trying SysRq 'o' (power off)");
let _ = file.write_all(b"o");
}
match nix::sys::reboot::reboot(nix::sys::reboot::RebootMode::RB_POWER_OFF) {
Ok(infallible) => match infallible {},
Err(e) => {
log::debug!("poweroff_guest: RB_POWER_OFF failed ({}), trying halt", e);
match nix::sys::reboot::reboot(nix::sys::reboot::RebootMode::RB_HALT_SYSTEM) {
Ok(infallible) => match infallible {},
Err(e2) => {
log::debug!("poweroff_guest: RB_HALT_SYSTEM also failed ({}), falling back to exit", e2);
std::process::exit(0);
}
}
}
}
}
fn percent_decode(s: &str) -> String {
let mut bytes = Vec::with_capacity(s.len());
let chars = s.chars().collect::<Vec<char>>();
let mut i = 0;
while i < chars.len() {
if chars[i] == '%' && i + 2 < chars.len() {
let hex = format!("{}{}", chars[i + 1], chars[i + 2]);
match u8::from_str_radix(&hex, 16) {
Ok(byte) => {
bytes.push(byte);
i += 3;
continue;
}
Err(_) => {
bytes.push(b'%');
}
}
} else {
let mut buf = [0u8; 4];
let char_bytes = chars[i].encode_utf8(&mut buf);
bytes.extend_from_slice(char_bytes.as_bytes());
}
i += 1;
}
String::from_utf8(bytes).unwrap_or_else(|e| {
log::debug!("init: percent_decode produced invalid UTF-8, using lossy conversion");
String::from_utf8_lossy(&e.into_bytes()).into_owned()
})
}