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

nordisk/gunnar

crates/gunnar-front/src/role.rs

//! **The front's role, for the life of the process** — from what the
//! installer fed ([`crate::vm::FrontVm::latest`], persisted, so a reboot runs
//! it again at once):
//!
//! * the WireGuard relay on udp/[`nordisk_pairing::RELAY_PORT`], keyed with
//!   this box's own tunnel key (`wg0.key` — the key its presentation carried,
//!   which the installer feeds every box as the `front` peer), listing every
//!   fed box by its overlay /32;
//! * the twin's certification against the primary on the fed peers' utility
//!   addresses, signed with the certify key ([`crate::keys`]) the primary
//!   enrolled from the front's offer.
//!
//! Both are gunnar-pairing-door's [`FrontRole`]. Moved here from gunnar-server's
//! `front_role` (lane A 54fa7bd), which ran them on the gunnar medium.

use crate::vm::FrontVm;
use gunnar_pairing_door::front::FrontRole;
use nordisk_pairing::join::JoinReply;
use std::sync::Arc;
use std::time::Duration;

/// How often the fed reply is looked at again.
const POLL: Duration = Duration::from_secs(2);

fn start_role(vm: &FrontVm) -> Result<FrontRole, String> {
    let root = vm.root();
    let relay = gunnar_pairing_door::front::relay_key_from_b64(
        &gunnar_server::pairing::ensure_tunnel_key(root).map_err(|e| format!("{e:#}"))?,
    )?;
    let key = crate::keys::certify_key(root).map_err(|e| format!("{e:#}"))?;
    FrontRole::start(
        nordisk_pairing::RELAY_PORT,
        relay,
        key,
        &root.join(crate::keys::CERTIFY_RECORD_REL),
    )
}

/// **Be the front once fed, and keep taking what is fed.** As a jera job;
/// returns at once.
pub fn start(vm: Arc<FrontVm>) {
    // A jera job (A28 M18, A32-14): listed, a look that panics is said and a FAILED job and the next one runs, stopped at the
    // clean stop. The relay is built by the first fed look and kept in the job's state.
    let say: gunnar_common::jobs::Say = Arc::new(|l: &str| tracing::error!("front: {l}"));
    let spawned = gunnar_common::jobs::start("front-role", POLL, say, {
        let mut role: Option<FrontRole> = None;
        let mut last: Option<JoinReply> = None;
        let mut said_refused = false;
        move || {
            if let Some(reply) = vm.latest() {
                if role.is_none() {
                    match start_role(&vm) {
                        Ok(r) => {
                            tracing::info!(relay = %r.public(), port = nordisk_pairing::RELAY_PORT, "front: the relay is up on this box's own tunnel key");
                            role = Some(r);
                            vm.relay_started();
                        }
                        Err(e) if !said_refused => {
                            tracing::error!(error = %e, "front: the front's role cannot start — asking again");
                            said_refused = true;
                        }
                        Err(_) => {}
                    }
                }
                if let Some(r) = role.as_mut() {
                    if last.as_ref() != Some(&reply) {
                        for line in r.take(&reply) {
                            tracing::info!("front: {line}");
                        }
                        last = Some(reply);
                    }
                }
            }
            gunnar_common::jobs::Round::Again
        }
    });
    if let Err(e) = spawned {
        tracing::error!(error = %e, "ALERT front-role the front's role could not start");
    }
}

#[cfg(test)]
mod tests {
    /// The relay runs on the SAME key the presentation carried — the key the
    /// installer feeds every box as the `front` peer.
    #[test]
    fn the_relay_key_is_the_key_the_presentation_carried() {
        let dir = tempfile::tempdir().unwrap();
        let presented = gunnar_server::pairing::tunnel_pubkey(dir.path());
        let private = gunnar_pairing_door::front::relay_key_from_b64(
            &gunnar_server::pairing::ensure_tunnel_key(dir.path()).unwrap(),
        )
        .unwrap();
        let key = crate::keys::certify_key(dir.path()).unwrap();
        let r = gunnar_pairing_door::front::FrontRole::start(
            0,
            private,
            key,
            &dir.path().join(crate::keys::CERTIFY_RECORD_REL),
        )
        .unwrap();
        assert_eq!(r.public(), presented);
    }
}

/// ★ **The front's four watches are named jera jobs, stopped at the clean stop** (A28 M18, then
/// A32-14): each was a raw thread whose panic ended it without a word, then a named-thread duty;
/// now each is a jera ChainQueue job (a panicked look a FAILED row and an ALERT line, the next look
/// as due), and the clean stop ends them before the disk is quiesced. Read off the source.
#[cfg(test)]
mod m18_duties {
    #[test]
    fn the_fronts_watches_are_jera_jobs_not_threads() {
        for (file, name) in [
            (include_str!("role.rs"), "front-role"),
            (include_str!("update.rs"), "front-update"),
            (include_str!("recharge.rs"), "front-recharge"),
            (include_str!("autofill.rs"), "front-autofill"),
            (include_str!("web/hole.rs"), "front-hole"),
            // P4b (lane mon-shared): the mesh probe, a jera job as Holger's front's is.
            (include_str!("mesh_probe.rs"), "front-mesh-probe"),
        ] {
            let code = &file[..file.find("#[cfg(test)]").unwrap_or(file.len())];
            assert!(
                code.contains(&format!("gunnar_common::jobs::start(\"{name}\"")),
                "{name} starts as a jera job"
            );
            assert!(
                !code.contains(&format!("nordisk_pairing::duty::start_with(\"{name}\"")),
                "{name} is still a named thread"
            );
            assert!(
                !code.contains(&format!(".name(\"{name}\".into())")),
                "{name} is still a raw thread"
            );
        }
        let stop = include_str!("stop.rs");
        let body = &stop[stop.find("pub async fn stop_cleanly").unwrap()..];
        assert!(
            body[..body.find("quiesce()").unwrap()].contains("gunnar_common::jobs::stop_all("),
            "the jobs stop before the disk is quiesced"
        );
    }

    /// The front links the jobs half of gunnar-common (jera), and nothing in it starts a duty thread.
    #[test]
    fn the_front_names_no_duty_thread() {
        let manifest = include_str!("../Cargo.toml");
        let common = manifest
            .lines()
            .find(|l| l.starts_with("gunnar-common"))
            .expect("the front names gunnar-common");
        assert!(
            common.contains("\"jobs\""),
            "the front turns gunnar-common's jobs on: {common}"
        );
    }
}