feat(fips): integrate jmcorgan/fips as preferred non-Tor transport + v1.4.0
Bakes the FIPS (Free Internetworking Peering System) mesh daemon into the node stack, supervised by archipelago alongside Tor. Runs as a system service, identity derives from the same BIP-39 master seed, and user-triggered updates track upstream main. Identity seed.rs: new HKDF label archipelago/fips/secp256k1/v1 → dedicated secp256k1 key, distinct from the Nostr-node key for crypto isolation but still seed-recoverable identity.rs: writes fips_key[.pub] to /data/identity on onboarding, chmod 0600; fips_key_exists / load_fips_keys / fips_npub accessors Transport TransportKind::Fips=3 inserted between LAN and Tor (Tor bumps to 4) → router prefers FIPS over Tor for all peer traffic PeerRecord gains fips_npub + last_fips fields (serde(default) for backward-compat with older nodes) transport/fips.rs: NodeTransport stub, reports unavailable until the daemon is live so router falls through to Tor cleanly Federation invites FederatedNode and FederationInvite carry optional fips_npub create_invite / accept_invite / peer-joined callback thread it end to end; signature domain deliberately unchanged — FIPS Noise does its own session auth, so the unsigned hint only affects path selection crate::fips config.rs: renders /etc/fips/fips.yaml and sudo-installs key material service.rs: systemctl status/activate/restart/mask wrappers update.rs: GitHub API check against upstream main; apply stubbed until per-commit .deb artefact source is decided RPC + dashboard fips.status / fips.check-update / fips.apply-update / fips.install / fips.restart registered in dispatcher HomeNetworkCard.vue shipped standalone (unmounted — place in Home.vue when ready); shows state pill, version, FIPS npub, update button, activate button when key is present but service is down ISO + systemd archipelago-fips.service: conditional on key presence, masked by default — backend unmasks after onboarding writes the key build-auto-installer-iso.sh: multi-stage Dockerfile builds the FIPS .deb from jmcorgan/fips main (fail-loud), COPYs it into rootfs, apt installs it so trixie resolves deps; unit copied + masked Version bump: 1.3.5 → 1.4.0 Tests: 33 new/updated passing (seed, identity, transport, federation, fips module, transport::fips). Known gaps: fips.apply-update returns a clear stub error until upstream publishes per-commit .deb artefacts; HomeNetworkCard is not mounted in Home.vue by default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
f04804ae25
commit
c1cfca6212
@@ -2,12 +2,17 @@
|
||||
#![allow(dead_code)]
|
||||
//! Transport abstraction layer for Archipelago node-to-node communication.
|
||||
//!
|
||||
//! Unifies mesh radio (LoRa), LAN (mDNS), and Tor under a common trait.
|
||||
//! Routes messages to peers via the best available transport with automatic
|
||||
//! fallback: Mesh (priority 1) > LAN (2) > Tor (3).
|
||||
//! Unifies mesh radio (LoRa), LAN (mDNS), FIPS (Free Internetworking Peering
|
||||
//! System overlay), and Tor under a common trait. Routes messages to peers via
|
||||
//! the best available transport with automatic fallback:
|
||||
//! Mesh (1) > LAN (2) > FIPS (3) > Tor (4).
|
||||
//!
|
||||
//! FIPS sits between LAN and Tor: faster than Tor for WAN peering, but still
|
||||
//! defers to direct LAN connectivity when peers are on the same network.
|
||||
|
||||
pub mod chunking;
|
||||
pub mod delta;
|
||||
pub mod fips;
|
||||
pub mod lan;
|
||||
pub mod mesh_transport;
|
||||
pub mod tor;
|
||||
@@ -31,7 +36,8 @@ use tracing::{info, warn};
|
||||
pub enum TransportKind {
|
||||
Mesh = 1,
|
||||
Lan = 2,
|
||||
Tor = 3,
|
||||
Fips = 3,
|
||||
Tor = 4,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TransportKind {
|
||||
@@ -39,6 +45,7 @@ impl std::fmt::Display for TransportKind {
|
||||
match self {
|
||||
Self::Mesh => write!(f, "mesh"),
|
||||
Self::Lan => write!(f, "lan"),
|
||||
Self::Fips => write!(f, "fips"),
|
||||
Self::Tor => write!(f, "tor"),
|
||||
}
|
||||
}
|
||||
@@ -77,6 +84,7 @@ pub trait NodeTransport: Send + Sync {
|
||||
/// For Tor: address is an onion hostname.
|
||||
/// For Mesh: address is a contact_id as string.
|
||||
/// For LAN: address is "ip:port".
|
||||
/// For FIPS: address is the peer's FIPS npub (bech32); implementation maps to fd00::/8.
|
||||
fn send<'a>(
|
||||
&'a self,
|
||||
address: &'a str,
|
||||
@@ -115,6 +123,8 @@ pub struct PeerRecord {
|
||||
#[serde(default)]
|
||||
pub lan_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub fips_npub: Option<String>,
|
||||
#[serde(default)]
|
||||
pub onion_address: Option<String>,
|
||||
|
||||
// Freshness timestamps (RFC 3339)
|
||||
@@ -123,6 +133,8 @@ pub struct PeerRecord {
|
||||
#[serde(default)]
|
||||
pub last_lan: Option<String>,
|
||||
#[serde(default)]
|
||||
pub last_fips: Option<String>,
|
||||
#[serde(default)]
|
||||
pub last_tor: Option<String>,
|
||||
}
|
||||
|
||||
@@ -132,16 +144,18 @@ impl PeerRecord {
|
||||
match kind {
|
||||
TransportKind::Mesh => self.mesh_contact_id.map(|id| id.to_string()),
|
||||
TransportKind::Lan => self.lan_address.clone(),
|
||||
TransportKind::Fips => self.fips_npub.clone(),
|
||||
TransportKind::Tor => self.onion_address.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the last-seen timestamp for a transport is fresh enough.
|
||||
/// Mesh/LAN: 5 minutes. Tor: 1 hour.
|
||||
/// Mesh/LAN: 5 minutes. FIPS: 30 minutes. Tor: 1 hour.
|
||||
pub fn is_fresh(&self, kind: TransportKind) -> bool {
|
||||
let timestamp = match kind {
|
||||
TransportKind::Mesh => self.last_mesh.as_deref(),
|
||||
TransportKind::Lan => self.last_lan.as_deref(),
|
||||
TransportKind::Fips => self.last_fips.as_deref(),
|
||||
TransportKind::Tor => self.last_tor.as_deref(),
|
||||
};
|
||||
let Some(ts) = timestamp else {
|
||||
@@ -155,6 +169,7 @@ impl PeerRecord {
|
||||
let age = chrono::Utc::now().signed_duration_since(parsed);
|
||||
let max_age = match kind {
|
||||
TransportKind::Mesh | TransportKind::Lan => chrono::Duration::minutes(5),
|
||||
TransportKind::Fips => chrono::Duration::minutes(30),
|
||||
TransportKind::Tor => chrono::Duration::hours(1),
|
||||
};
|
||||
age < max_age
|
||||
@@ -169,6 +184,9 @@ impl PeerRecord {
|
||||
if self.lan_address.is_some() {
|
||||
result.push(TransportKind::Lan);
|
||||
}
|
||||
if self.fips_npub.is_some() {
|
||||
result.push(TransportKind::Fips);
|
||||
}
|
||||
if self.onion_address.is_some() {
|
||||
result.push(TransportKind::Tor);
|
||||
}
|
||||
@@ -239,9 +257,11 @@ impl PeerRegistry {
|
||||
source: Some(source.clone()),
|
||||
mesh_contact_id: None,
|
||||
lan_address: None,
|
||||
fips_npub: None,
|
||||
onion_address: None,
|
||||
last_mesh: None,
|
||||
last_lan: None,
|
||||
last_fips: None,
|
||||
last_tor: None,
|
||||
});
|
||||
// Update pubkey if it changed
|
||||
@@ -278,6 +298,15 @@ impl PeerRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the FIPS npub for a peer (bech32 pubkey used by the FIPS mesh).
|
||||
pub async fn set_fips_npub(&self, did: &str, npub: &str) {
|
||||
let mut peers = self.peers.write().await;
|
||||
if let Some(peer) = peers.get_mut(did) {
|
||||
peer.fips_npub = Some(npub.to_string());
|
||||
peer.last_fips = Some(chrono::Utc::now().to_rfc3339());
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the display name for a peer.
|
||||
pub async fn set_name(&self, did: &str, name: &str) {
|
||||
let mut peers = self.peers.write().await;
|
||||
@@ -402,6 +431,17 @@ impl TransportRouter {
|
||||
}
|
||||
}
|
||||
}
|
||||
if peer.fips_npub.is_some() && peer.is_fresh(TransportKind::Fips) {
|
||||
if let Some(t) = self
|
||||
.transports
|
||||
.iter()
|
||||
.find(|t| t.kind() == TransportKind::Fips)
|
||||
{
|
||||
if t.is_available() {
|
||||
available.push(TransportKind::Fips);
|
||||
}
|
||||
}
|
||||
}
|
||||
if peer.onion_address.is_some() {
|
||||
if let Some(t) = self
|
||||
.transports
|
||||
@@ -446,7 +486,31 @@ mod tests {
|
||||
#[test]
|
||||
fn test_transport_kind_ordering() {
|
||||
assert!(TransportKind::Mesh < TransportKind::Lan);
|
||||
assert!(TransportKind::Lan < TransportKind::Tor);
|
||||
assert!(TransportKind::Lan < TransportKind::Fips);
|
||||
assert!(TransportKind::Fips < TransportKind::Tor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fips_preferred_over_tor_in_available_transports() {
|
||||
let peer = PeerRecord {
|
||||
did: "did:key:z6MkTest".to_string(),
|
||||
pubkey_hex: "aabb".to_string(),
|
||||
name: None,
|
||||
trust_level: None,
|
||||
source: None,
|
||||
mesh_contact_id: None,
|
||||
lan_address: None,
|
||||
fips_npub: Some("npub1exampleexampleexampleexampleexampleexample".to_string()),
|
||||
onion_address: Some("abc.onion".to_string()),
|
||||
last_mesh: None,
|
||||
last_lan: None,
|
||||
last_fips: None,
|
||||
last_tor: None,
|
||||
};
|
||||
let ts = peer.available_transports();
|
||||
let fips_idx = ts.iter().position(|k| *k == TransportKind::Fips).unwrap();
|
||||
let tor_idx = ts.iter().position(|k| *k == TransportKind::Tor).unwrap();
|
||||
assert!(fips_idx < tor_idx, "FIPS must be listed before Tor");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -459,9 +523,11 @@ mod tests {
|
||||
source: None,
|
||||
mesh_contact_id: Some(42),
|
||||
lan_address: Some("192.168.1.100:5678".to_string()),
|
||||
fips_npub: None,
|
||||
onion_address: Some("abc123.onion".to_string()),
|
||||
last_mesh: None,
|
||||
last_lan: None,
|
||||
last_fips: None,
|
||||
last_tor: None,
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -488,9 +554,11 @@ mod tests {
|
||||
source: None,
|
||||
mesh_contact_id: Some(1),
|
||||
lan_address: None,
|
||||
fips_npub: None,
|
||||
onion_address: Some("test.onion".to_string()),
|
||||
last_mesh: None,
|
||||
last_lan: None,
|
||||
last_fips: None,
|
||||
last_tor: None,
|
||||
};
|
||||
let transports = peer.available_transports();
|
||||
@@ -507,9 +575,11 @@ mod tests {
|
||||
source: None,
|
||||
mesh_contact_id: Some(1),
|
||||
lan_address: None,
|
||||
fips_npub: None,
|
||||
onion_address: None,
|
||||
last_mesh: None,
|
||||
last_lan: None,
|
||||
last_fips: None,
|
||||
last_tor: None,
|
||||
};
|
||||
// No timestamp = considered fresh (allows first attempt)
|
||||
@@ -526,9 +596,11 @@ mod tests {
|
||||
source: None,
|
||||
mesh_contact_id: Some(1),
|
||||
lan_address: None,
|
||||
fips_npub: None,
|
||||
onion_address: None,
|
||||
last_mesh: Some(chrono::Utc::now().to_rfc3339()),
|
||||
last_lan: None,
|
||||
last_fips: None,
|
||||
last_tor: None,
|
||||
};
|
||||
assert!(peer.is_fresh(TransportKind::Mesh));
|
||||
@@ -545,9 +617,11 @@ mod tests {
|
||||
source: None,
|
||||
mesh_contact_id: Some(1),
|
||||
lan_address: None,
|
||||
fips_npub: None,
|
||||
onion_address: None,
|
||||
last_mesh: Some(stale.to_rfc3339()),
|
||||
last_lan: None,
|
||||
last_fips: None,
|
||||
last_tor: None,
|
||||
};
|
||||
// 10 minutes old > 5 minute mesh freshness threshold
|
||||
|
||||
Reference in New Issue
Block a user