//! FIPS (Free Internetworking Peering System) daemon integration. //! //! github.com/jmcorgan/fips — a spanning-tree mesh routing protocol that //! uses Nostr secp256k1 keys as native node identity. Archipelago ships //! the daemon as an apt package, feeds it the seed-derived key from //! `/data/identity/fips_key`, and supervises it via //! `archipelago-fips.service`. //! //! This module is the in-process bridge: //! - [`service`]: systemctl status / start / stop / restart / unmask. //! - [`config`]: materialise `/etc/fips/fips.yaml` + install the key. //! - [`update`]: query GitHub (tracking `main`) for a newer build, //! verify SHA256, install via dpkg, restart. //! //! Privileged operations shell out via `sudo systemctl …` and `sudo dpkg …` //! (mirroring the vpn/update patterns already in the codebase); the //! sudoers rule shipped in the ISO whitelists exactly those commands for //! the `archipelago` service user. //! //! FIPS is dark on the wire until onboarding writes the key. Before that, //! `FipsStatus::installed` reports the package state and `service_active` //! returns false; the transport router keeps routing via Tor. // Consumers land in the next phase (RPC endpoints + onboarding hookup); // the module is deliberately API-ready ahead of those call-sites. #![allow(dead_code)] pub mod anchors; pub mod config; pub mod dial; pub mod iface; pub mod service; pub mod update; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; /// Systemd unit name supervised by archipelago. pub const SERVICE_UNIT: &str = "archipelago-fips.service"; /// Path the FIPS daemon reads its config from (Debian package default). pub const DAEMON_CONFIG_PATH: &str = "/etc/fips/fips.yaml"; /// Path the FIPS daemon reads its private key from. pub const DAEMON_KEY_PATH: &str = "/etc/fips/fips.key"; /// Path the FIPS daemon reads its public key from. pub const DAEMON_PUB_PATH: &str = "/etc/fips/fips.pub"; /// Upstream repository the updater tracks (branch `main`). pub const UPSTREAM_REPO: &str = "jmcorgan/fips"; /// Default UDP port the daemon listens on. pub const DEFAULT_UDP_PORT: u16 = 8668; /// Upstream systemd unit shipped by the `fips` debian package. Archipelago /// prefers its own supervision (`archipelago-fips.service`) but respects an /// already-running upstream unit so legacy/dev nodes — where no seed-derived /// key exists — still report FIPS as active in the UI. pub const UPSTREAM_SERVICE_UNIT: &str = "fips.service"; /// Aggregated runtime status of the FIPS subsystem, surfaced to the dashboard. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FipsStatus { /// Whether the `fips` debian package is installed on the host. pub installed: bool, /// Installed daemon version string reported by `fipsctl --version`, /// or None if not installed / not queryable. pub version: Option, /// `systemctl is-active archipelago-fips.service` result: "active", /// "inactive", "failed", "masked", "unknown". pub service_state: String, /// State of the upstream `fips.service` (shipped by the debian package). pub upstream_service_state: String, /// True if either the archipelago-managed or upstream unit is active. pub service_active: bool, /// Whether the seed-derived FIPS key has been materialised on disk. /// The archipelago-managed service cannot start meaningfully until /// this is true; legacy nodes may still report FIPS active via the /// upstream unit without this file. pub key_present: bool, /// Local FIPS npub (bech32). Prefers the seed-derived key when /// present; falls back to the upstream daemon's own key on legacy /// nodes where `/etc/fips/fips.pub` is readable. pub npub: Option, /// Number of currently authenticated FIPS peers, per /// `fipsctl show peers`. 0 → isolated / anchor unreachable; /// >0 → DHT routing is viable. #[serde(default)] pub authenticated_peer_count: u32, /// True when at least one peer in the identity cache is a known /// public anchor (currently `fips.v0l.io`). Anchors bootstrap DHT /// routing for general-case deployments, so a red anchor status is /// the top UX indicator of "FIPS traffic will probably degrade to /// Tor until the anchor is reachable." #[serde(default)] pub anchor_connected: bool, } impl FipsStatus { /// Snapshot the current state across package, key, and service. /// /// `data_dir` is the archipelago data-dir (used to load the /// operator-configured seed-anchor list so "anchor_connected" means /// "at least one authenticated peer matches a public or configured /// seed anchor", not just "fips.v0l.io specifically"). pub async fn query(data_dir: &Path) -> Self { let identity_dir = identity_dir_from(data_dir); let installed = service::package_installed().await; let version = if installed { service::daemon_version().await.ok() } else { None }; let service_state = service::unit_state(SERVICE_UNIT).await; let upstream_service_state = service::unit_state(UPSTREAM_SERVICE_UNIT).await; let service_active = service_state == "active" || upstream_service_state == "active"; let key_present = crate::identity::fips_key_exists(&identity_dir); // Prefer the seed-derived npub; otherwise read the daemon's own // key file at /etc/fips/fips.pub (world-readable per debian pkg). let npub = match crate::identity::fips_npub(&identity_dir).await { Ok(Some(n)) => Some(n), _ => service::read_upstream_npub().await.ok().flatten(), }; let (authenticated_peer_count, anchor_connected) = if service_active { // Build the anchor-candidate list: hardcoded public anchor // plus every entry in the operator's seed-anchors.json. // The card lights up if any of them is authenticated. let mut anchor_npubs = vec![service::PUBLIC_ANCHOR_NPUB.to_string()]; if let Ok(seed) = anchors::load(data_dir).await { anchor_npubs.extend(seed.into_iter().map(|a| a.npub)); } service::peer_connectivity_summary(&anchor_npubs).await } else { (0, false) }; Self { installed, version, service_state, upstream_service_state, service_active, key_present, npub, authenticated_peer_count, anchor_connected, } } } /// Compose a data-dir–relative identity directory path. /// Mirrors the convention used elsewhere in the codebase so callers don't /// have to repeat the `.join("identity")` each time. pub fn identity_dir_from(data_dir: &Path) -> PathBuf { data_dir.join("identity") } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_status_reports_no_key_pre_onboarding() { let dir = tempfile::tempdir().unwrap(); // query() now takes a data_dir (parent) rather than identity_dir, // since it also reads seed-anchors.json for the anchor check. // No identity/ subdir → no key; no seed-anchors.json → public // anchor is the only candidate. let status = FipsStatus::query(dir.path()).await; assert!(!status.key_present, "no key before onboarding"); assert!(status.npub.is_none()); // `installed`, `service_state`, `version` depend on the host and are // not asserted here — query() must return cleanly regardless. } #[test] fn test_identity_dir_from() { let data = Path::new("/var/lib/archipelago"); assert_eq!( identity_dir_from(data), Path::new("/var/lib/archipelago/identity") ); } #[test] fn test_constants_have_expected_shape() { assert!(SERVICE_UNIT.ends_with(".service")); assert!(DAEMON_CONFIG_PATH.starts_with('/')); assert!(DAEMON_KEY_PATH.starts_with('/')); assert_eq!(UPSTREAM_REPO, "jmcorgan/fips"); } }