434 lines
11 KiB
Rust
434 lines
11 KiB
Rust
![]() |
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!();
|
||
|
}
|