Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/cli/src/pid1.rs

//! **The one-way appliance's init** (lane HOL-CDS-5, owner 2026-10-30): what `holger-downstream`
//! owes the system when it IS PID 1 on its own korp medium, WITHOUT pairing.
//!
//! Pairing (`paired`) brings PID 1's network up and then dials the estate's front; a
//! `boundary: OneWay` box refuses that at start. So the one-way appliance carries only these
//! duties (feature `pid1`, edda `appliance-pid1`):
//!
//! * **the reaper** (PID 1 inherits every orphan);
//! * **the network from the MEDIUM**: `<release>/holger-net` (the admin's `holger_net` when the
//!   medium was composed), else the medium's `HOLGER_NET`, else `eth0:dhcp`. Static
//!   (`eth0:10.0.2.15/24:10.0.2.2`) or DHCP on the declared NIC, leases kept alive. No resolver
//!   probe: the box dials nothing;
//! * **the USB stick's inbox**: a filesystem labelled [`STICK_LABEL`] (ext2/3/4 or FAT) on any
//!   block device is mounted READ-ONLY (`nosuid,nodev,noexec`) and the signed bundles at its top
//!   level (`*.holger.znippy`, at most [`MAX_STICK_FILE`] each) are copied into `bundle.inbox`
//!   (a hidden `.part`, then a rename), where the import door's sweep verifies and lands them.
//!   NOTHING else comes off a stick: no declaration, no trust policy, no key, no `FIRST-IMPORT`
//!   marker. Those are the medium's (provisioned by an admin) and the admin's first-import
//!   authorization (`POST /-/bundle/first-import`);
//! * **the panic and the clean stop**: a panic ends as fault `HL-99`, an `Err` as `HL-02`; the
//!   power button or SIGTERM quiesces `/data`, keeps the stop on the ESP and powers off.

use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

/// The medium's network file on the release (the `holger_net` parameter's staged file).
pub const NET_FILE: &str = "holger-net";
/// The medium's network variable (the row's env, and the paired box's).
pub const NET_ENV: &str = "HOLGER_NET";
/// One NIC by DHCP: an on-prem box has no utility net.
pub const DEFAULT_NET: &str = "eth0:dhcp";
/// Where korp-start hands the app its release (the medium's staged parameters).
pub const RELEASE_ROOT: &str = "/run/korp/release";
/// The label a USB stick carries to be read as the inbox (11 characters: a FAT label's room).
pub const STICK_LABEL: &str = "HOLGERINBOX";
/// The largest bundle file taken off a stick.
pub const MAX_STICK_FILE: u64 = 8 << 30;
/// How often PID 1 looks for a stick.
pub const STICK_LOOK: std::time::Duration = std::time::Duration::from_secs(5);
/// Where a stick is mounted while it is read.
pub const STICK_MOUNT: &str = "/run/holger-stick";
/// The fault codes (Holger's, as the paired box's: `HL-02` start refused, `HL-99` panic).
pub const START_CODE: (&str, &str) = (
    "HL-02",
    "holger-downstream could not start: its configuration or its start was refused",
);
pub const PANIC_CODE: (&str, &str) = ("HL-99", "the app panicked");
/// The product name of the stop record on the ESP.
pub const STOP_RECORD_PRODUCT: &str = "holger-downstream";

/// Pure: **the network spec**: the medium's file, else its `HOLGER_NET`, else [`DEFAULT_NET`].
pub fn net_spec(file: Option<&str>, env: Option<&str>) -> String {
    [file, env]
        .into_iter()
        .flatten()
        .map(str::trim)
        .find(|s| !s.is_empty())
        .unwrap_or(DEFAULT_NET)
        .to_string()
}

/// Pure: the plans of a spec, refused by name (`[holger-net]`).
pub fn net_plans(spec: &str) -> Result<Vec<appliance_pid1::net::NetConfig>, String> {
    appliance_pid1::net::NetConfig::parse_list(spec)
        .map_err(|e| format!("REFUSED [holger-net] the medium's network `{spec}`: {e:#}"))
}

/// Pure: **the label of the filesystem** whose first bytes are `head` (at least 4 KiB): ext2/3/4
/// (the superblock's volume name) or FAT12/16/32 (the boot sector's volume label). `None`: no
/// such filesystem, or no label.
pub fn fs_label(head: &[u8]) -> Option<String> {
    let clean = |b: &[u8]| -> Option<String> {
        let s: String = b
            .iter()
            .take_while(|c| **c != 0)
            .map(|c| *c as char)
            .collect();
        let s = s.trim().to_string();
        (!s.is_empty() && s.chars().all(|c| c.is_ascii_graphic() || c == ' ')).then_some(s)
    };
    if head.len() < 4096 {
        return None;
    }
    // ext2/3/4: the superblock at 1024, magic 0xEF53 at +56, volume name at +120 (16 bytes).
    if head[1024 + 56] == 0x53 && head[1024 + 57] == 0xEF {
        return clean(&head[1024 + 120..1024 + 136]);
    }
    // FAT: the boot signature 0x55AA; the extended boot signature 0x29 at 66 (FAT32, label at
    // 71..82) or at 38 (FAT12/16, label at 43..54).
    if head[510] == 0x55 && head[511] == 0xAA {
        if head[66] == 0x29 && &head[82..87] == b"FAT32" {
            return clean(&head[71..82]);
        }
        if head[38] == 0x29 {
            return clean(&head[43..54]);
        }
    }
    None
}

/// Pure: **the block devices a stick may be**, by name under `/sys/class/block`: disks and their
/// partitions; never a loop, a RAM disk, a CD drive or a zram swap.
pub fn stick_candidates(sys_class_block: &Path) -> Vec<String> {
    let mut out: Vec<String> = std::fs::read_dir(sys_class_block)
        .into_iter()
        .flatten()
        .flatten()
        .map(|e| e.file_name().to_string_lossy().into_owned())
        .filter(|n| {
            !["loop", "ram", "sr", "zram", "fd", "nbd"]
                .iter()
                .any(|p| n.starts_with(p))
        })
        .collect();
    out.sort();
    out
}

/// Pure: **may this stick entry go into the inbox?** A regular file named `*.holger.znippy` (not
/// hidden, not a `.part`, no path), at most [`MAX_STICK_FILE`].
pub fn stick_wants(name: &str, is_file: bool, len: u64) -> bool {
    is_file
        && len <= MAX_STICK_FILE
        && name.ends_with(".holger.znippy")
        && !name.starts_with('.')
        && !name.contains('/')
        && name.len() > ".holger.znippy".len()
}

/// **Copy a mounted stick's signed bundles into `inbox`** (a hidden `.part`, then a rename),
/// each `(name, size)` once per boot (`seen`). Returns the names copied.
pub fn copy_from_stick(
    mount: &Path,
    inbox: &Path,
    seen: &mut BTreeSet<(String, u64)>,
) -> std::io::Result<Vec<String>> {
    std::fs::create_dir_all(inbox)?;
    let mut copied = Vec::new();
    let mut entries: Vec<_> = std::fs::read_dir(mount)?.flatten().collect();
    entries.sort_by_key(|e| e.file_name());
    for e in entries {
        let name = e.file_name().to_string_lossy().into_owned();
        // `symlink_metadata`: a link on a stick is not followed.
        let Ok(meta) = e.path().symlink_metadata() else {
            continue;
        };
        if !stick_wants(&name, meta.file_type().is_file(), meta.len())
            || !seen.insert((name.clone(), meta.len()))
        {
            continue;
        }
        let dest = inbox.join(&name);
        if dest.exists() {
            continue;
        }
        let part = inbox.join(format!(".{name}.part"));
        std::fs::copy(e.path(), &part)?;
        if let Ok(f) = std::fs::File::open(&part) {
            let _ = f.sync_all();
        }
        std::fs::rename(&part, &dest)?;
        copied.push(name);
    }
    Ok(copied)
}

/// The appliance's data root (`HOLGER_APPLIANCE_DATA`, else `/data`).
pub fn appliance_root() -> PathBuf {
    std::env::var_os("HOLGER_APPLIANCE_DATA")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("/data"))
}

fn say(l: &str) {
    log::info!("{l}");
    eprintln!("holger-downstream: {l}");
}

/// **PID 1's duties, before anything binds** (inside the tokio runtime): the reaper and the
/// medium's network. Not PID 1: nothing, `false`.
pub fn duties() -> Result<bool, String> {
    if !appliance_pid1::net::is_init() {
        return Ok(false);
    }
    // A32-14, stays a task: PID 1's zombie reaper, for the box's life.
    tokio::spawn(appliance_pid1::runtime::reaper());
    appliance_pid1::install::announce_install_mode(None, &|l| say(l));
    let file = std::fs::read_to_string(Path::new(RELEASE_ROOT).join(NET_FILE)).ok();
    let spec = net_spec(file.as_deref(), std::env::var(NET_ENV).ok().as_deref());
    say(&format!("pid1: one-way, no pairing; the medium's network `{spec}`; no resolver probe (it dials nothing)"));
    let how = appliance_pid1::net::Bringup {
        hostname_prefix: "holger-downstream",
        resolver_probe: None,
        narrate: &|l| say(l),
    };
    let late = appliance_pid1::net::LateLease {
        narrate: say,
        t1_override_var: None,
    };
    let sessions = appliance_pid1::net::bring_up_network_with(net_plans(&spec)?, &how, late)
        .map_err(|e| format!("REFUSED [holger-net] {e:#}"))?;
    #[allow(clippy::never_loop, unused_variables, unreachable_code)]
    for s in sessions {
        // A32-14, stays a task: PID 1's lease renewal, one per lease, for the box's life.
        tokio::spawn(appliance_pid1::runtime::dhcp_renewal(s, None));
    }
    appliance_pid1::fault::reached("network");
    appliance_pid1::stop_record::say_previous(STOP_RECORD_PRODUCT, &|l| say(l));
    Ok(true)
}

/// **The USB stick's inbox** (PID 1 only): every [`STICK_LOOK`], a block device whose filesystem
/// is labelled [`STICK_LABEL`] is mounted read-only, its signed bundles copied into `inbox`,
/// then unmounted. The sweep of `bundle.inbox` imports them.
pub fn spawn_stick_inbox(inbox: PathBuf) {
    // A32-14, stays a thread: PID 1's stick look, for the box's life; it only copies files.
    let _ = std::thread::Builder::new().name("holger-stick".into()).spawn(move || {
        let mut seen = BTreeSet::new();
        loop {
            for dev in stick_candidates(Path::new("/sys/class/block")) {
                let node = Path::new("/dev").join(&dev);
                let mut head = vec![0u8; 4096];
                let read = std::fs::File::open(&node).and_then(|mut f| {
                    use std::io::Read as _;
                    f.read_exact(&mut head)
                });
                if read.is_err() || fs_label(&head).as_deref() != Some(STICK_LABEL) {
                    continue;
                }
                match mount_read_only(&node) {
                    Ok(at) => {
                        match copy_from_stick(&at, &inbox, &mut seen) {
                            Ok(c) if !c.is_empty() => say(&format!("stick {dev} ({STICK_LABEL}): {} signed bundle(s) into the inbox: {}", c.len(), c.join(", "))),
                            Ok(_) => {}
                            Err(e) => say(&format!("stick {dev}: the copy stopped: {e}")),
                        }
                        unmount(&at);
                    }
                    Err(e) => say(&format!("stick {dev} ({STICK_LABEL}) could not be mounted read-only: {e}")),
                }
            }
            std::thread::sleep(STICK_LOOK);
        }
    });
}

#[cfg(target_os = "linux")]
fn mount_read_only(node: &Path) -> std::io::Result<PathBuf> {
    use std::ffi::CString;
    let at = PathBuf::from(STICK_MOUNT);
    std::fs::create_dir_all(&at)?;
    let src = CString::new(node.as_os_str().as_encoded_bytes()).map_err(std::io::Error::other)?;
    let dst = CString::new(at.as_os_str().as_encoded_bytes()).map_err(std::io::Error::other)?;
    let flags = libc::MS_RDONLY | libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC;
    let mut last = std::io::Error::other("no filesystem type tried");
    for fstype in ["ext4", "vfat"] {
        let t = CString::new(fstype).map_err(std::io::Error::other)?;
        // SAFETY: every pointer is a NUL-terminated CString that outlives the call; `data` is null.
        let rc = unsafe {
            libc::mount(
                src.as_ptr(),
                dst.as_ptr(),
                t.as_ptr(),
                flags,
                std::ptr::null(),
            )
        };
        if rc == 0 {
            return Ok(at);
        }
        last = std::io::Error::last_os_error();
    }
    Err(last)
}

#[cfg(not(target_os = "linux"))]
fn mount_read_only(_node: &Path) -> std::io::Result<PathBuf> {
    Err(std::io::Error::other(
        "a stick is mounted on the Linux appliance only",
    ))
}

#[cfg(target_os = "linux")]
fn unmount(at: &Path) {
    if let Ok(dst) = std::ffi::CString::new(at.as_os_str().as_encoded_bytes()) {
        // SAFETY: a NUL-terminated CString that outlives the call.
        unsafe { libc::umount2(dst.as_ptr(), libc::MNT_DETACH) };
    }
}

#[cfg(not(target_os = "linux"))]
fn unmount(_at: &Path) {}

/// **A panic on the appliance ends as a fault** (`HL-99`): the crash record on the data disk,
/// then a serving box restarts (unless it loops) and a booting one powers off. Armed on the
/// appliance only (PID 1 booted by korp).
pub fn install_panic_fault() {
    appliance_pid1::fault::install_panic_fault(
        appliance_pid1::fault::PanicFault {
            code: PANIC_CODE.0,
            meaning: PANIC_CODE.1,
            data: Some(appliance_root()),
        },
        appliance_pid1::stop::on_appliance,
        |l: &str| {
            eprintln!("holger-downstream: {l}");
            log::error!("{l}");
        },
    );
}

/// **An `Err` that would end PID 1** (`HL-02`): the crash record, then restart or power off.
pub fn fault_end(why: &str) -> ! {
    let (stage, serving) = appliance_pid1::fault::progress();
    let data = appliance_root();
    let f = appliance_pid1::fault::Fault {
        code: START_CODE.0,
        meaning: Some(START_CODE.1),
        stage,
        serving,
        reason: why,
        data: data.is_dir().then_some(data.as_path()),
        narration: &[],
    };
    log::logger().flush();
    appliance_pid1::fault::end(&f, &|l| eprintln!("holger-downstream: {l}"), None)
}

/// **The clean stop's end**: `/data` quiesced (each step bounded), the stop kept on the ESP,
/// then the power.
pub fn power_off_cleanly(said: &str) -> ! {
    say(&format!(
        "{said}: the data disk is synced and remounted read-only, then the power goes"
    ));
    log::logger().flush();
    appliance_pid1::power::power_off_cleanly(STOP_RECORD_PRODUCT, &appliance_root(), said, &|l| {
        eprintln!("holger-downstream: {l}")
    })
}