//! FIPS mesh transport (Free Internetworking Peering System). //! //! Delegates the actual wire protocol to the `fips` system daemon //! (github.com/jmcorgan/fips), which archipelago supervises via the //! `archipelago-fips.service` unit (or respects the upstream //! `fips.service` on legacy nodes). This module is the in-process //! `NodeTransport` adapter: it checks daemon liveness, maps a peer's //! FIPS npub to a `fd00::/8` IPv6 address via the daemon's local DNS //! resolver, and POSTs the `TransportMessage` payload over plain HTTP //! to the peer's `/transport/inbox` endpoint. //! //! Sits at priority 3 between LAN and Tor — preferred over Tor for //! federation and peer traffic but yielding to direct LAN. use super::{NodeTransport, TransportKind, TransportMessage}; use anyhow::{Context, Result}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// How long a successful `is_available()` probe is cached — the hot path /// may poll this per-send, and `systemctl is-active` takes ~50ms. A short /// TTL keeps the result responsive to daemon flaps without pounding DBus. const AVAILABILITY_CACHE_TTL: Duration = Duration::from_secs(10); pub struct FipsTransport { identity_dir: PathBuf, available_cached: AtomicBool, available_cached_at_ms: AtomicU64, } impl FipsTransport { pub fn new(identity_dir: &Path) -> Self { Self { identity_dir: identity_dir.to_path_buf(), available_cached: AtomicBool::new(false), available_cached_at_ms: AtomicU64::new(0), } } fn probe_daemon_active() -> bool { // Cheap blocking probe: spawn `systemctl is-active` synchronously. // Short-circuit if either the archipelago-managed unit or the // upstream fips.service is active — legacy/dev nodes run only the // upstream unit. for unit in [ crate::fips::SERVICE_UNIT, crate::fips::UPSTREAM_SERVICE_UNIT, ] { let out = std::process::Command::new("systemctl") .args(["is-active", unit]) .output(); if let Ok(o) = out { if String::from_utf8_lossy(&o.stdout).trim() == "active" { return true; } } } false } } impl NodeTransport for FipsTransport { fn kind(&self) -> TransportKind { TransportKind::Fips } fn is_available(&self) -> bool { let now_ms = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_millis() as u64) .unwrap_or(0); let cached_at = self.available_cached_at_ms.load(Ordering::Relaxed); if now_ms.saturating_sub(cached_at) < AVAILABILITY_CACHE_TTL.as_millis() as u64 { return self.available_cached.load(Ordering::Relaxed); } let val = Self::probe_daemon_active(); self.available_cached.store(val, Ordering::Relaxed); self.available_cached_at_ms.store(now_ms, Ordering::Relaxed); val } fn send<'a>( &'a self, address: &'a str, message: &'a TransportMessage, ) -> std::pin::Pin> + Send + 'a>> { Box::pin(async move { let base = crate::fips::dial::peer_base_url(address) .await .with_context(|| format!("resolve {}.fips", address))?; let url = format!("{}/transport/inbox", base); let client = crate::fips::dial::client(); let body = serde_json::to_vec(message).context("serialize TransportMessage")?; let resp = client .post(&url) .header("Content-Type", "application/json") .body(body) .send() .await .with_context(|| format!("POST {}", url))?; if !resp.status().is_success() { anyhow::bail!("peer FIPS inbox returned {}", resp.status()); } Ok(()) }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_kind_is_fips() { let t = FipsTransport::new(std::path::Path::new("/tmp")); assert_eq!(t.kind(), TransportKind::Fips); } #[test] fn is_available_caches_negative_result() { // No fips.service in the test env → probe returns false. // Two rapid calls must both be false without relying on a live daemon. let t = FipsTransport::new(std::path::Path::new("/tmp")); let a = t.is_available(); let b = t.is_available(); assert_eq!(a, b); } }