Wires the FIPS transport end-to-end so peer-to-peer calls can reach other nodes over the mesh without going through Tor: - fips::dial — raw RFC 1035 DNS client (zero new deps) that queries the FIPS daemon's local resolver at 127.0.0.1:5354 for `<npub>.fips` AAAA records. Exposes peer_base_url(npub) → "http://[fd9d:…]:5679" plus a reqwest client factory for call-site migrations. - fips::iface — parses /proc/net/if_inet6 to find the ULA address on `fips0`. Runs under the archipelago service user without extra caps. - FipsTransport::is_available() — live probe of archipelago-fips and upstream fips.service via `systemctl is-active`, cached 10s so the send hot path doesn't thrash DBus. - FipsTransport::send() — resolve npub, POST TransportMessage JSON to the peer's /transport/inbox. Today /transport/inbox isn't wired on the receive side, so call-site migrations use dial::peer_base_url directly against the already-signed endpoints (/rpc/v1, /archipelago/node-message, /content/*). The inbox handler lands as part of the Settings/transport work. - server::serve_with_shutdown — takes an optional peer_addr and spawns a second listener bound specifically to the fips0 ULA on port 5679. The peer listener applies is_peer_allowed_path() — a whitelist of endpoints that already do per-request signature auth — and returns 404 for everything else. Shutdown cascades to both listeners via a watch channel; 5s drain window preserved. - main.rs — if fips0 has a ULA at startup, pass the peer SocketAddr to serve_with_shutdown; otherwise run the main listener only. Security: the peer listener is bound to the fips0 ULA directly, not wildcard, so it's unreachable from WAN IPv6. The path whitelist limits exposure to endpoints whose handlers verify ed25519 signatures or federation DID headers server-side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
130 lines
4.6 KiB
Rust
130 lines
4.6 KiB
Rust
//! 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<Box<dyn std::future::Future<Output = Result<()>> + 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);
|
|
}
|
|
}
|