holger-estate-front/src/front.rs
//! **holger's half of vm1's `Vm`** — what only holger's front knows, under
//! Skidbladnir `nordisk-pairing::frontside::Front` (kind `front`, only role
//! `front`, PAIRED once the relay is up and every box is listed).
//!
//! | offer | value | who takes it |
//! |---|---|---|
//! | `version` | this build (`HOLGER_FRONT_GIT_HASH`) | the operator's install check |
//! | `fleet`, `fleet_instance`, `fleet_at`, `fleet_ca`, `monetize_pubkey` | the ONE monetize's door, this estate's name there, the key its orders verify under — relayed from the shop's feed (paid front) | every metered holger box's pull channel (holger-fleet) |
//! | `clock_key` | the front's time key, the whole OpenSSH line (edda `estate-clock`) | every holger box's clock |
//! | `clock_peer` | `peer://<utility>:50123`, where signed time is served | every holger box's clock |
//!
//! Its SECRET offer (asked once, before the installer closes): every valid web
//! certificate kept on the data disk with its key, `tls.<host>.cert|key` — the
//! successor front is fed exactly that and asks Let's Encrypt nothing (edda
//! `estate-front::web::certs::handover`).
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use nordisk_pairing::bootstrap::SecretOffer;
use nordisk_pairing::frontside::FrontProduct;
use nordisk_pairing::join::JoinReply;
use nordisk_pairing::seed::TunnelKind;
/// The offer key of the build (the boxes' word).
pub const OFFER_VERSION: &str = "version";
/// **The ONE monetize's words, relayed to every box** (paid front): what the shop's
/// pairing fed this front, offered as `offer.<front>.<key>` (holger-fleet `config`).
pub const RELAYED: [&str; 5] = [
"fleet",
"fleet_instance",
"fleet_at",
"fleet_ca",
"monetize_pubkey",
];
/// Where the front keeps its tunnel key (the relay's), under the data root —
/// the same place a holger box keeps its own.
pub const WG_REL: &str = "wg";
/// **holger's front.**
pub struct HolgerFront {
root: PathBuf,
tunnel: TunnelKind,
clock: Mutex<Option<estate_clock::offer::ClockOffer>>,
/// **The front's own live slot** (L20, holger-live): `None` off a korp appliance — the live
/// door then refuses by name and the shop keep-boots the front.
live: Option<std::sync::Arc<holger_live::LiveSlot>>,
}
impl HolgerFront {
pub fn new(root: &Path, tunnel: TunnelKind) -> HolgerFront {
HolgerFront {
root: root.to_path_buf(),
tunnel,
clock: Mutex::new(None),
live: None,
}
}
/// The front with its own live slot.
pub fn with_live(mut self, slot: std::sync::Arc<holger_live::LiveSlot>) -> HolgerFront {
self.live = Some(slot);
self
}
/// The front's own live slot, if it has one.
pub fn live_slot(&self) -> Option<&std::sync::Arc<holger_live::LiveSlot>> {
self.live.as_ref()
}
/// The clock started (edda `estate-clock`): its key and peer are offered
/// from now on, so every box follows this front's signed time.
pub fn clock_started(&self, offer: estate_clock::offer::ClockOffer) {
if let Ok(mut c) = self.clock.lock() {
*c = Some(offer);
}
}
/// The relay's private key — the key the presentation carried.
pub fn relay_key(&self) -> Result<[u8; 32], String> {
wg_appliance_core::ceremony::load_or_mint_identity(&self.root.join(WG_REL))
.map(|id| id.private)
.map_err(|e| format!("the front's tunnel key: {e}"))
}
}
impl FrontProduct for HolgerFront {
fn tunnel_pubkey(&self) -> String {
if self.tunnel != TunnelKind::Wireguard {
return String::new();
}
match wg_appliance_core::ceremony::load_or_mint_identity(&self.root.join(WG_REL)) {
Ok(id) => id.public_b64,
Err(e) => {
tracing::error!(error = %e, "front: its tunnel key cannot be minted — the join carries none, the relay cannot start");
String::new()
}
}
}
fn offer(&self, fed: Option<&JoinReply>) -> BTreeMap<String, String> {
let mut o = BTreeMap::from([(OFFER_VERSION.to_string(), crate::GIT_HASH.to_string())]);
// The console key's public half (A7 row 14): every box derives its console login from it.
match crate::console_key::adopt(&self.root) {
Ok(public) => {
o.insert(crate::console_key::OFFER_CONSOLE_PUBKEY.to_string(), public);
}
Err(e) => {
tracing::error!(error = %e, "front: the console's key cannot be minted — no box can derive the console's login")
}
}
if let Some(c) = self.clock.lock().ok().and_then(|c| c.clone()) {
o.extend(c.rows());
}
// Lane HOL-WGDS: the downstreams' interface (its key and public endpoint), on an
// estate: main keeps it for the invites of its WireGuard downstreams.
if self.tunnel == TunnelKind::Wireguard {
match crate::downstreams::identity(&self.root) {
Ok(id) => o.extend(crate::downstreams::offer(
&id.public_b64,
appliance_pid1::net::interface_ipv4(crate::PUBLIC_INTERFACE)
.ok()
.flatten(),
)),
Err(e) => {
tracing::error!(error = %e, "front: the downstreams' key cannot be minted — no WireGuard downstream can be invited")
}
}
}
// The ONE monetize's door and key, as the shop's pairing fed them: every box
// reports there and verifies its orders under the key (no monetize runs here).
#[cfg(feature = "metered")]
if let Some(r) = fed {
o.extend(relayed(&r.feed));
}
#[cfg(not(feature = "metered"))]
let _ = fed;
o
}
fn live_running(&self) -> Result<nordisk_pairing::live::Running, String> {
self.live
.as_ref()
.ok_or_else(|| {
"REFUSED [live-unsupported] this front runs off a korp appliance: no live slot"
.to_string()
})?
.running()
}
fn live_stage(&self, stage: &nordisk_pairing::live::Stage) -> Result<String, String> {
self.live
.as_ref()
.ok_or_else(|| {
"REFUSED [live-unsupported] this front runs off a korp appliance: no live slot"
.to_string()
})?
.stage(stage)
}
/// Lane B5: with what the release declares (korp `stage_declared` on the front's own slot).
fn live_stage_declared(
&self,
stage: &nordisk_pairing::live::Stage,
declared: &nordisk_pairing::live::Declared,
) -> Result<String, String> {
self.live
.as_ref()
.ok_or_else(|| {
"REFUSED [live-unsupported] this front runs off a korp appliance: no live slot"
.to_string()
})?
.stage_declared(stage, declared)
}
fn secret_offer(&self) -> SecretOffer {
let (values, hosts) =
estate_front::web::certs::handover(&self.root, std::time::SystemTime::now());
if hosts.is_empty() {
tracing::info!("installer: no valid web certificate kept on the data disk — nothing to hand over to the next front");
} else {
tracing::info!(hosts = ?hosts, "installer: the web certificate(s) and key(s) handed over for the next front (values not logged)");
}
SecretOffer::new(values)
}
}
/// Pure: the ONE monetize's words of a feed, as this front relays them.
pub fn relayed(feed: &BTreeMap<String, String>) -> BTreeMap<String, String> {
RELAYED
.iter()
.filter_map(|k| {
feed.get(*k)
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.map(|v| (k.to_string(), v.to_string()))
})
.collect()
}
#[cfg(test)]
mod relay_tests {
use super::*;
/// What the front relays is what a holger box's pull channel reads (`offer.<vm>.<key>`).
#[test]
fn the_one_monetizes_door_and_key_are_relayed_and_nothing_else() {
let feed: BTreeMap<String, String> = [
("fleet", "https://monetize.ignalina.dk"),
("fleet_instance", "acme"),
("monetize_pubkey", "ab"),
("secret.shop_client_key", "never"),
]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let r = relayed(&feed);
assert_eq!(
r.keys().map(String::as_str).collect::<Vec<_>>(),
["fleet", "fleet_instance", "monetize_pubkey"],
"never a secret"
);
}
}
#[cfg(test)]
mod clock_offer_tests {
use super::*;
/// **What holger's front offers is what a holger box's clock reads**: once
/// the clock is started, `clock_key` + `clock_peer` ride the offer; relayed to
/// a box as `offer.<vm>.<key>` they make a box configuration pinned to this
/// front's time key at `<utility>:50123` (edda `estate-clock::offer`).
/// ★ A7 row 14: the front offers its console key's public half, the same on every offer,
/// minted once under its data root; no secret is in the offer.
#[test]
fn the_front_offers_its_console_keys_public_half() {
let dir = tempfile::tempdir().unwrap();
let front = HolgerFront::new(dir.path(), TunnelKind::None);
let first = front.offer(None)[crate::console_key::OFFER_CONSOLE_PUBKEY].clone();
assert_eq!(
wg_appliance_core::keys::decode_key(&first)
.map(|k| k.len())
.ok(),
Some(32),
"{first}"
);
assert_eq!(
front.offer(None)[crate::console_key::OFFER_CONSOLE_PUBKEY],
first,
"minted once, kept"
);
assert_eq!(
HolgerFront::new(dir.path(), TunnelKind::None).offer(None)
[crate::console_key::OFFER_CONSOLE_PUBKEY],
first,
"kept across boots"
);
assert!(front
.offer(None)
.keys()
.all(|k| !k.contains("token") && !k.contains("private")));
}
#[test]
fn the_started_clock_is_offered_and_a_box_parses_the_offer() {
let dir = tempfile::tempdir().unwrap();
let front = HolgerFront::new(dir.path(), TunnelKind::None);
assert!(
!front
.offer(None)
.contains_key(estate_clock::offer::OFFER_CLOCK_KEY),
"no clock yet, nothing offered"
);
let (_, key_line) =
estate_clock::front::ensure_key(&dir.path().join(estate_clock::offer::CLOCK_KEY_REL))
.unwrap();
let peer = estate_clock::offer::peer_spelling(std::net::Ipv4Addr::new(10, 13, 8, 1));
front.clock_started(estate_clock::offer::ClockOffer {
key_line: key_line.clone(),
peer: Some(peer.clone()),
});
let offer = front.offer(None);
assert_eq!(
(offer["clock_key"].as_str(), offer["clock_peer"].as_str()),
(key_line.as_str(), "peer://10.13.8.1:50123")
);
let relayed: BTreeMap<String, String> = offer
.iter()
.map(|(k, v)| (format!("offer.vm1.{k}"), v.clone()))
.collect();
match estate_clock::offer::box_config(&relayed) {
Ok(estate_clock::config::Config::Sources {
sources,
peer_key: Some(k),
}) => {
assert_eq!(
sources,
vec![estate_clock::config::Source::Peer(
"10.13.8.1:50123".parse().unwrap()
)]
);
assert_eq!(
k.split_whitespace().nth(1),
key_line.split_whitespace().nth(1)
);
}
other => panic!("a holger box must follow this front: {other:?}"),
}
}
}