use eyre::{format_err, Result}; use log::{error, info, warn}; use std::collections::BTreeSet as Set; use std::os::unix::fs::symlink; use tokio::sync::Mutex; use tokio::{fs, process::Command}; use crate::{bootstrap::config::Config, cmd::version::version_string, dklog, input, utils}; mod bootstrap; mod dmcrypt; mod lvm; mod networks; mod sshd; const INIT_LOG: &str = "/var/log/init.log"; // devices used by any block target (DM, LVM...) static USED_DEVS: Mutex> = Mutex::const_new(Set::new()); pub async fn run() { if std::process::id() != 1 { error!("init must run as PID 1, not {}", std::process::id()); std::process::exit(1); } unsafe { use std::env; env::set_var("PATH", "/bin:/sbin:/usr/bin:/usr/sbin"); env::set_var("HOME", "/root"); } // open input channels tokio::spawn(crate::input::answer_requests_from_stdin()); tokio::spawn(crate::input::answer_requests_from_socket()); // also log to file dklog::LOG.spawn(async { let Ok(log_file) = (tokio::fs::File::create(INIT_LOG).await) .inspect_err(|e| warn!("failed to open init.log: {e}")) else { return; }; dklog::LOG.copy_to(log_file).await; }); // welcome info!("Welcome to {}", version_string()); let uname = nix::sys::utsname::uname().expect("uname should work"); let kernel_version = uname.release().to_string_lossy(); info!("Linux version {kernel_version}"); // mount basic filesystems mount(None, "/proc", Some("proc"), None).await; mount(None, "/sys", Some("sysfs"), None).await; mount(None, "/dev", Some("devtmpfs"), None).await; mount(None, "/dev/pts", Some("devpts"), Some("gid=5,mode=620")).await; if utils::bool_param("debug") { log::set_max_level(log::LevelFilter::Debug); } // extract system archive retry_or_ignore(async || { if fs::try_exists("system.azstd").await? { info!("unpacking system.azstd"); let zarch = fs::read("system.azstd").await?; let arch = zstd::Decoder::new(zarch.as_slice())?; extract_cpio(arch).await } else if fs::try_exists("system.alz4").await? { info!("unpacking system.alz4"); let zarch = fs::read("system.alz4").await?; let arch = lz4::Decoder::new(zarch.as_slice())?; extract_cpio(arch).await } else { return Ok(()); } }) .await; // load config let cfg: Config = retry(async || { let cfg = (fs::read("config.yaml").await) .map_err(|e| format_err!("failed to read config: {e}"))?; serde_yaml::from_slice(cfg.as_slice()) .map_err(|e| format_err!("failed to parse config: {e}")) }) .await; info!("config loaded"); info!("anti-phishing-code: {}", cfg.anti_phishing_code); // seem to occasionaly race with Child::wait // tokio::spawn(child_reaper()); // mount modules if let Some(ref modules) = cfg.modules { retry_or_ignore(async || { info!("mounting modules"); mount(Some(modules), "/modules", Some("squashfs"), None).await; fs::create_dir_all("/lib/modules").await?; let modules_path = &format!("/modules/lib/modules/{kernel_version}"); if !std::fs::exists(modules_path)? { return Err(format_err!( "invalid modules package: {modules_path} should exist" )); } symlink(modules_path, format!("/lib/modules/{kernel_version}"))?; Ok(()) }) .await; } else { warn!("modules NOT mounted (not configured)"); } // init devices info!("initializing devices"); start_daemon("udevd", &[]).await; exec("udevadm", &["trigger", "-c", "add", "-t", "devices"]).await; exec("udevadm", &["trigger", "-c", "add", "-t", "subsystems"]).await; exec("udevadm", &["settle"]).await; // resolv.conf if let Some(ref resolv_conf) = cfg.resolv_conf { retry_or_ignore(async || { let path = "/etc/resolv.conf"; info!("writing /etc/resolv.conf"); fs::write(path, resolv_conf.as_bytes()).await?; Ok(()) }) .await; } // Wireguard VPNs for (name, conf) in &cfg.vpns { retry_or_ignore(async || { let dir = "/etc/wireguard"; fs::create_dir_all(dir).await?; let path = &format!("{dir}/{name}.conf"); fs::write(path, conf.as_bytes()).await?; chmod(path, 0o600).await?; try_exec("wg-quick", &["up", name]).await?; Ok(()) }) .await } // SSH service sshd::start(&cfg).await; // networks networks::setup(&cfg).await; // pre-lvm dmcrypt devs dmcrypt::setup(&cfg.pre_lvm_crypt).await; // LVM lvm::setup(&cfg).await; // post-lvm dmcrypt devs dmcrypt::setup(&cfg.crypt).await; // bootstrap the system bootstrap::bootstrap(cfg).await; // finalize if let Err(e) = tokio::fs::copy(INIT_LOG, format!("/system{INIT_LOG}")).await { warn!("failed to copy {INIT_LOG} to system: {e}"); } retry(async || switch_root("/system").await).await; } use std::path::Path; async fn chmod(path: impl AsRef, mode: u32) -> std::io::Result<()> { use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; let perms = Permissions::from_mode(mode); fs::set_permissions(path, perms).await } async fn extract_cpio(mut arch: impl std::io::Read) -> Result<()> { loop { let rd = cpio::NewcReader::new(&mut arch)?; let entry = rd.entry(); if entry.is_trailer() { return Ok(()); } let path = entry.name().to_string(); if let Err(e) = extract_cpio_entry(rd, &path).await { return Err(format_err!("failed to extract {path}: {e}")); } } } async fn extract_cpio_entry( rd: cpio::NewcReader, path: impl AsRef, ) -> Result<()> { use std::os::unix::fs::chown; use unix_mode::Type; let entry = rd.entry(); let path = path.as_ref(); if let Some(parent) = path.parent() { fs::create_dir_all(parent).await?; } let mode = entry.mode(); let uid = entry.uid(); let gid = entry.gid(); match Type::from(mode) { Type::Dir => { fs::create_dir_all(path).await?; } Type::File => { let mut data = vec![]; rd.to_writer(&mut data)?; fs::write(path, data).await?; } Type::Symlink => { let mut data = vec![]; rd.to_writer(&mut data)?; let target = &Path::new(std::str::from_utf8(&data)?); tokio::fs::symlink(target, path).await?; return Ok(()); } _ => { warn!("{path:?}: unknown file type: {:?}", Type::from(mode)); return Ok(()); } } chmod(path, mode).await?; chown(path, Some(uid), Some(gid))?; Ok(()) } async fn mount(src: Option<&str>, dst: &str, fstype: Option<&str>, opts: Option<&str>) { if let Err(e) = fs::create_dir_all(dst).await { error!("failed to create dir {dst}: {e}"); } match (fstype, src) { (Some("ext4"), Some(src)) => { exec("fsck", &["-p", src]).await; } _ => {} } let mut args = vec![src.unwrap_or("none"), dst]; if let Some(fstype) = fstype { args.extend(&["-t", fstype]); } if let Some(opts) = opts { args.extend(["-o", opts]); } retry_or_ignore(async || { // check source, use a loopdev if needed if let Some(src) = src { if fs::metadata(src).await?.is_file() { // loopdev crate has annoying dependencies, just use the normal mount program return try_exec("mount", &args).await; } } let (cmd_str, _) = cmd_str("mount", &args); let flags = nix::mount::MsFlags::empty(); info!("# {cmd_str}",); nix::mount::mount(src, dst, fstype, flags, opts) .map_err(|e| format_err!("mount {dst} failed: {e}")) }) .await } async fn start_daemon(prog: &str, args: &[&str]) { let (cmd_str, mut cmd) = cmd_str(prog, args); retry_or_ignore(async || { info!("starting as daemon: {cmd_str}"); cmd.spawn()?; Ok(()) }) .await; } async fn try_exec(prog: &str, args: &[&str]) -> Result<()> { let (cmd_str, mut cmd) = cmd_str(prog, args); info!("# {cmd_str}"); let s = cmd.status().await?; if s.success() { Ok(()) } else { Err(format_err!("command failed: {s}")) } } async fn exec(prog: &str, args: &[&str]) { retry_or_ignore(async || try_exec(prog, args).await).await; } async fn retry_or_ignore(mut action: impl AsyncFnMut() -> Result<()>) { loop { match action().await { Ok(_) => return, Err(e) => { error!("{e}"); match input::read_choice(["[r]etry", "[i]gnore", "[s]hell"]).await { 'r' => {} 'i' => return, 's' => exec_shell().await, _ => unreachable!(), } } } } } async fn retry(mut action: impl AsyncFnMut() -> Result) -> T { loop { match action().await { Ok(v) => return v, Err(e) => { error!("{e}"); match input::read_choice(["[r]etry", "[s]hell"]).await { 'r' => {} 's' => exec_shell().await, _ => unreachable!(), } } } } } async fn exec_shell() { let mut child = match Command::new("ash").spawn() { Ok(c) => c, Err(e) => { error!("failed to exec shell: {e}"); return; } }; let _ = child.wait().await; } fn cmd_str(prog: &str, args: &[&str]) -> (String, Command) { use std::borrow::Cow; let mut buf = String::new(); buf.push_str(&shell_escape::escape(Cow::Borrowed(prog))); for &arg in args { buf.push(' '); buf.push_str(&shell_escape::escape(Cow::Borrowed(arg))); } let mut cmd = Command::new(prog); cmd.args(args); (buf, cmd) } #[allow(unused)] async fn child_reaper() { use nix::sys::wait::{waitpid, WaitPidFlag}; use nix::unistd::Pid; use tokio::signal::unix::{signal, SignalKind}; let Ok(mut sigs) = signal(SignalKind::child()).inspect_err(|e| warn!("failed to setup SIGCHLD handler: {e}")) else { return; }; loop { sigs.recv().await; let _ = waitpid(Some(Pid::from_raw(-1)), Some(WaitPidFlag::WNOHANG)); } } macro_rules! cstr { ($s:expr) => { std::ffi::CString::new($s)?.as_c_str() }; } async fn switch_root(root: &str) -> Result<()> { info!("killing all processes and switching root"); dklog::LOG.close().await; use nix::sys::signal::{kill, SIGKILL}; use nix::unistd::Pid; if let Err(e) = kill(Pid::from_raw(-1), SIGKILL) { eprintln!("failed to kill processes: {e}"); } nix::unistd::execv( cstr!("/sbin/switch_root"), &[ cstr!("switch_root"), cstr!("-c"), cstr!("/dev/console"), cstr!(root), cstr!("/sbin/init"), ], ) .unwrap(); unreachable!(); }