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

nordisk/Skidbladnir

crates/installer/nordisk-narrate/src/boxside.rs

//! **A product's whole DEV-NARRATE start, in one call** (features `dev-narrate`
//! and `bootlog`) — shared by gunnar-server, gunnar-front, holger and
//! holger-estate-front (holger RULE 1: no product copies another's).
//!
//! Called first thing in the product's `main`, right after its boot log
//! captured the ESP's `BOOTLOG.TXT`:
//!
//! 1. korp-start's RAM hand-off when the ESP could not be read
//!    ([`crate::korp_start_fallback`]);
//! 2. the kernel's records, followed into the log ([`crate::kmsg::follow`]);
//! 3. on its own thread: the seed's `installer_pubkey` (leniently, waiting for
//!    the seed up to [`SEED_WAIT`]) and the sender bound on `0.0.0.0:`[`crate::PORT`]
//!    at once — before any NIC has an address — streaming THE SAME log.
//!
//! Nothing here is ever fatal: this is diagnostics.

use crate::{kmsg, sender, seedkey};
use nordisk_pairing::bootlog::BootLog;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};

/// How long the seed may take to appear (korp-start stages the data root
/// before the exec, so it is normally there at once).
pub const SEED_WAIT: Duration = Duration::from_secs(120);

/// **Start narrating `log` as `product`**; returns at once. `data_root` holds
/// `pairing/seed.toml`; `console` writes one line where a person sees it (the
/// product's own console writer — never fd 2 when fd 2 is captured into `log`).
pub fn start(log: &'static BootLog, data_root: PathBuf, product: &'static str, console: fn(&str)) {
    let say = move |l: &str| {
        log.push(l);
        console(l);
    };
    if crate::korp_start_fallback(Path::new(nordisk_pairing::bootlog::ESP_BOOTLOG), Path::new(crate::KORP_START_HANDOFF), product, &|l| log.push(l)) {
        say("dev-narrate: the ESP's BOOTLOG.TXT could not be read — korp-start's narration was taken from its RAM hand-off instead");
    }
    if kmsg::follow(move |l| log.push(l)) {
        say("dev-narrate: the kernel's records (/dev/kmsg, from the first one the ring holds) are followed into the boot log");
    }
    let spawned = std::thread::Builder::new().name("dev-narrate-start".into()).spawn(move || {
        let bind = SocketAddr::from(([0, 0, 0, 0], crate::PORT));
        match start_at(log, &data_root, product, bind, SEED_WAIT) {
            Ok(at) => say(&format!("{} — bound {at}, product {product}", crate::STARTED)),
            Err(e) => say(&format!("dev-narrate: OFF — {e} (the console and BOOTLOG.TXT on the ESP still have every line)")),
        }
    });
    if let Err(e) = spawned {
        log.push(&format!("dev-narrate: OFF — its thread could not start: {e}"));
    }
}

/// [`start`]'s narrating half, pure over the data root and the bind address:
/// wait up to `wait` for the seed, read the installer's public key out of it
/// (a seed that fails its own check still narrates), start the sender.
pub fn start_at(log: &'static BootLog, root: &Path, product: &str, bind: SocketAddr, wait: Duration) -> Result<SocketAddr, String> {
    let path = root.join(seedkey::SEED_REL);
    let t0 = Instant::now();
    let text = loop {
        match std::fs::read_to_string(&path) {
            Ok(t) => break t,
            Err(e) if t0.elapsed() >= wait => return Err(format!("no seed at {}: {e}", path.display())),
            Err(_) => std::thread::sleep(Duration::from_millis(500)),
        }
    };
    let installer_pub = seedkey::installer_pubkey(&text).map_err(|e| format!("{}: {e}", path.display()))?;
    let cfg = sender::Config { installer_pub, bind, product: product.to_string(), mac: sender::mac_of("eth0") };
    sender::start(cfg, log).map_err(|e| format!("udp {bind}: {e}"))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::UdpSocket;

    /// **A DEV box answers a knock with a WHO sealed to its SEED's installer
    /// key**; a data root with no seed narrates nothing and says why.
    #[test]
    fn a_box_narrates_to_the_installer_key_its_seed_names() {
        static LOG: BootLog = BootLog::new(100);
        let installer = crate::wire::generate();
        let root = std::env::temp_dir().join(format!("narrate-boxside-{}", std::process::id()));
        std::fs::create_dir_all(root.join("pairing")).unwrap();
        std::fs::write(root.join(seedkey::SEED_REL), format!("estate = \"e\"\n[installer_link]\ninstaller_pubkey = \"{}\"\nvm_private = \"broken\"\n", seedkey::b64_of_key(installer.public_key().as_bytes()))).unwrap();
        LOG.push("korp-start: STAGE exec");
        let at = start_at(&LOG, &root, "holger", "127.0.0.1:0".parse().unwrap(), Duration::from_secs(1)).unwrap();
        let probe = UdpSocket::bind("127.0.0.1:0").unwrap();
        probe.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
        probe.send_to(&crate::wire::knock([5; 16]), at).unwrap();
        let mut buf = [0u8; 2048];
        let (n, _) = probe.recv_from(&mut buf).unwrap();
        let (_, up) = crate::wire::open_up(&installer, &buf[..n]).expect("sealed to the seed's installer key");
        let crate::wire::Up::Who { id, echo } = up else { panic!("{up:?}") };
        assert_eq!((id.product.as_str(), echo), ("holger", [5; 16]));
        let none = std::env::temp_dir().join(format!("narrate-boxside-none-{}", std::process::id()));
        assert!(start_at(&LOG, &none, "holger", "127.0.0.1:0".parse().unwrap(), Duration::from_millis(10)).unwrap_err().contains("no seed at"));
        let _ = std::fs::remove_dir_all(&root);
    }
}