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}"
);
}
}