Files
initrd/src/cmd/init.rs

434 lines
11 KiB
Rust
Raw Normal View History

2024-04-29 12:54:25 +02:00
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<Set<String>> = 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<Path>, 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<R: std::io::Read>(
rd: cpio::NewcReader<R>,
path: impl AsRef<Path>,
) -> 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<T>(mut action: impl AsyncFnMut() -> Result<T>) -> 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!();
}