crates/installer/nordisk-narrate/tests/loopback.rs
//! **The whole channel on loopback**: a box's sender and the installer's
//! receiver, real UDP sockets, a real buffered log. Proves the owner's four
//! rules end to end: nothing leaves before an authenticated hello; the buffer
//! is flushed from line 0 and then streamed live; only the installer's key
//! reads it; a stranger's hello points nothing anywhere.
use nordisk_narrate::receiver::{self, Target};
use nordisk_narrate::sender::{self, Config, Source};
use nordisk_narrate::wire;
use std::net::UdpSocket;
use std::sync::Mutex;
use std::time::{Duration, Instant};
/// A box's log for the test: every line kept, sequence number = index.
struct Log(Mutex<Vec<String>>);
impl Source for Log {
fn since(&self, from: u64, max: usize) -> (u64, Vec<String>, u64) {
let l = self.0.lock().unwrap();
let from = (from as usize).min(l.len());
let lines: Vec<String> = l[from..].iter().take(max).cloned().collect();
let next = (from + lines.len()) as u64;
(0, lines, next)
}
}
fn leak(lines: &[&str]) -> &'static Log {
Box::leak(Box::new(Log(Mutex::new(lines.iter().map(|s| s.to_string()).collect()))))
}
fn wait_for(what: &str, budget: Duration, mut ok: impl FnMut() -> bool) {
let t0 = Instant::now();
while !ok() {
assert!(t0.elapsed() < budget, "timed out waiting for {what}");
std::thread::sleep(Duration::from_millis(50));
}
}
#[test]
fn the_buffer_is_flushed_from_line_zero_after_the_hello_and_then_streamed_live() {
let installer = wire::generate();
let log = leak(&["korp-start: STAGE entered", "korp-start: STAGE console", "kernel [ 1.000000] virtio_net: eth0 up"]);
let at = sender::start(Config { installer_pub: *installer.public_key().as_bytes(), bind: "127.0.0.1:0".parse().unwrap(), product: "gunnar-server".into(), mac: "52:54:00:00:00:01".into() }, log).unwrap();
// Before any hello: a knock is answered with ONE short WHO, and no line leaves.
let probe = UdpSocket::bind("127.0.0.1:0").unwrap();
probe.set_read_timeout(Some(Duration::from_millis(700))).unwrap();
probe.send_to(&wire::knock([1; 16]), at).unwrap();
let mut buf = [0u8; 2048];
let (n, _) = probe.recv_from(&mut buf).unwrap();
assert_eq!(&buf[..4], wire::TAG_WHO);
assert!(n < wire::KNOCK_LEN);
let (eph, up) = wire::open_up(&installer, &buf[..n]).unwrap();
let wire::Up::Who { id, echo } = up else { panic!("{up:?}") };
assert_eq!((echo, id.product.as_str()), ([1; 16], "gunnar-server"));
assert!(probe.recv_from(&mut buf).is_err(), "a knock made the box stream: nothing may leave before an authenticated hello");
// A stranger's hello (right boot, wrong key) points nothing at the stranger.
let mallory = wire::generate();
let forged = wire::seal_hello(&mallory, &crypto_box::PublicKey::from(eph), &wire::Hello { boot: id.boot, counter: 1, from: 0 });
probe.send_to(&forged, at).unwrap();
assert!(probe.recv_from(&mut buf).is_err(), "a forged hello was obeyed");
// The installer: knock, hello, every line from 0 into the file, in order.
let dir = std::env::temp_dir().join(format!("narrate-loop-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let out = dir.join("main.narrate.log");
let heard = std::sync::Arc::new(Mutex::new(Vec::<String>::new()));
let h = heard.clone();
let rx = receiver::start(installer.to_bytes(), vec![Target { name: "main".into(), addr: at, out: out.clone() }], Box::new(move |vm, line| h.lock().unwrap().push(format!("{vm}|{line}")))).unwrap();
wait_for("the buffered lines", Duration::from_secs(10), || heard.lock().unwrap().iter().filter(|l| !l.contains("── narrate")).count() >= 3);
// Live: a line said now arrives too.
log.0.lock().unwrap().push("gunnar: STAGE lease +6278ms 2 DHCP lease(s) held".into());
wait_for("the live line", Duration::from_secs(10), || heard.lock().unwrap().iter().any(|l| l.contains("STAGE lease")));
let seen = rx.stop();
let text = std::fs::read_to_string(&out).unwrap();
let lines: Vec<&str> = text.lines().filter(|l| !l.starts_with("── narrate")).collect();
assert_eq!(lines, vec!["korp-start: STAGE entered", "korp-start: STAGE console", "kernel [ 1.000000] virtio_net: eth0 up", "gunnar: STAGE lease +6278ms 2 DHCP lease(s) held"], "{text}");
assert!(text.lines().next().unwrap().contains("gunnar-server") && text.lines().next().unwrap().contains("52:54:00:00:00:01"), "the stream says whose it is:\n{text}");
let s = &seen["main"];
assert_eq!((s.lines, s.forged), (5, 0), "4 lines and the boot's opening line");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_box_answers_no_one_when_its_installer_is_someone_else() {
// The box seals to installer A; installer B's receiver hears a WHO it
// cannot open, and never gets a line.
let a = wire::generate();
let b = wire::generate();
let log = leak(&["secret-ish progress line"]);
let at = sender::start(Config { installer_pub: *a.public_key().as_bytes(), bind: "127.0.0.1:0".parse().unwrap(), product: "gunnar-front".into(), mac: "-".into() }, log).unwrap();
let dir = std::env::temp_dir().join(format!("narrate-other-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let rx = receiver::start(b.to_bytes(), vec![Target { name: "front".into(), addr: at, out: dir.join("front.narrate.log") }], Box::new(|_, _| {})).unwrap();
std::thread::sleep(Duration::from_secs(3));
let seen = rx.stop();
assert_eq!(seen["front"].lines, 0);
assert!(seen["front"].forged >= 1, "the WHO it could not open is counted: {:?}", seen["front"]);
let _ = std::fs::remove_dir_all(&dir);
}