195 lines
8.7 KiB
Rust
Raw Normal View History

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>
2026-04-18 22:57:51 -04:00
//! RPC handlers for the FIPS mesh transport subsystem.
//!
//! Surface is deliberately thin: a read-only `fips.status`, a user-gated
//! `fips.check-update`, a stubbed `fips.apply-update`, and a
//! `fips.install` that (re-)materialises the daemon config + key and
//! activates the service. All writes go through `sudo` helpers in
//! `crate::fips`.
use super::RpcHandler;
use crate::fips;
use anyhow::Result;
impl RpcHandler {
pub(super) async fn handle_fips_status(&self) -> Result<serde_json::Value> {
let identity_dir = fips::identity_dir_from(&self.config.data_dir);
let status = fips::FipsStatus::query(&identity_dir).await;
Ok(serde_json::to_value(status)?)
}
pub(super) async fn handle_fips_check_update(&self) -> Result<serde_json::Value> {
let check = fips::update::check().await?;
Ok(serde_json::to_value(check)?)
}
pub(super) async fn handle_fips_apply_update(&self) -> Result<serde_json::Value> {
fips::update::apply().await?;
Ok(serde_json::json!({ "applied": true }))
}
/// Install config + key into /etc/fips and activate the service.
/// Intended to be called:
/// - once by the seed-onboarding flow, right after the FIPS key
/// is written to /data/identity/fips_key, and
/// - on user demand from the dashboard if something drifted.
pub(super) async fn handle_fips_install(&self) -> Result<serde_json::Value> {
let identity_dir = fips::identity_dir_from(&self.config.data_dir);
fips::config::install(&identity_dir).await?;
fips::service::activate(fips::SERVICE_UNIT).await?;
let status = fips::FipsStatus::query(&identity_dir).await;
Ok(serde_json::to_value(status)?)
}
pub(super) async fn handle_fips_restart(&self) -> Result<serde_json::Value> {
fips::service::restart(fips::SERVICE_UNIT).await?;
Ok(serde_json::json!({ "restarted": true }))
}
release(v1.7.14-alpha): install overlay + FIPS real fix + AIUI restore Install UX SystemUpdate.vue now shows a full-screen overlay after apply: the BitcoinFaceAscii logo, a target-version label, an indeterminate progress stripe (solid orange; solid green on ready), and an elapsed-time readout. Polls /health every 1.5s and auto-reloads once the backend reports the new version. 3-min stall → "Reload now" button. Download UI also shows a spinner + "Finishing download — verifying checksum…" while the fake bar sits at 95%. FIPS reconnect — for real this time New fips.reconnect RPC does stop → start → wait 20s → re-poll → classify. Classification buckets: connected / daemon_down / no_seed_key / no_outbound_udp_or_anchor_down / peers_but_no_anchor, each with a plain-language hint surfaced verbatim by the Reconnect button. The real reason nodes like .198/.253 couldn't reach the anchor: identity::write_fips_key_from_seed was writing fips_key.pub as a bech32 npub TEXT file, but upstream fips expects 32 raw bytes. The daemon silently authenticated with garbage. Fix: PublicKey::to_bytes() → raw 32 bytes, and new fips::config::normalize_pub_file migrates legacy files by decoding the npub and rewriting in place. fips.reconnect also re-installs the config + healed keys to /etc/fips before restarting. AIUI preservation + restore apply_update was wiping /opt/archipelago/web-ui/aiui because the Vue build doesn't include it — every OTA lost the Claude sidebar. The preserve block now copies aiui/ + archipelago-companion.apk from the old web-ui into the staging dir before the swap, and prefers new-tar versions if present. To restore it on the three nodes that already lost it (.116/.198/.253), this release bundles the 85 MB aiui build into the frontend tarball. Frontend component size is now ~155 MB. Download / install timeouts Backend download client timeout 1800s → 3600s (1 h). Larger tarball + slow gitea raw throughput put us above the old cap. Frontend update.download rpc timeout 30 min → 65 min to match. package.install rpc timeout 15 min → 45 min — IndeedHub pulls 6 images and was timing out mid-install. UI nit "Rollback to Previous" → "Rollback Available". App-catalog proxy already landed in v1.7.13. Artefacts: archipelago 725e18e6…3c525e6 40462288 archipelago-frontend-1.7.14-alpha.tar.gz c35284be…ff2c16 162077052 (+aiui) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:40:25 -04:00
/// Full reconnect: stop the daemon, bring it back, wait for the DHT
/// bootstrap window, poll the identity-cache + peer list, and
/// classify what recovered (or didn't) so the UI can explain it to
/// the user instead of showing a generic failure.
///
/// Runtime: ~20s. Needs an RPC timeout ≥ 45s on the client.
pub(super) async fn handle_fips_reconnect(&self) -> Result<serde_json::Value> {
let identity_dir = fips::identity_dir_from(&self.config.data_dir);
let before = fips::FipsStatus::query(&identity_dir).await;
// Heal the pre-fix bech32-text fips_key.pub → 32-raw-bytes
// mismatch. The daemon silently authenticates with a garbage
// pubkey when the .pub file is 63-char text, which looks like
// "anchor unreachable" to the user even though the real fault
// was an identity malformed on the node itself. Re-install the
// config + keys so /etc/fips gets the healed .pub.
let key_src = identity_dir.join("fips_key");
let pub_src = identity_dir.join("fips_key.pub");
if key_src.exists() {
let _ = fips::config::normalize_pub_file(&key_src, &pub_src).await;
// Re-install refreshes /etc/fips/fips.pub from the healed
// source. No-op if nothing changed.
let _ = fips::config::install(&identity_dir).await;
}
// Clean stop+start rather than `restart`, so a daemon that
// fails to come back up surfaces as service_active=false
// instead of quietly sticking with the old process.
let _ = fips::service::stop(fips::SERVICE_UNIT).await;
tokio::time::sleep(std::time::Duration::from_millis(800)).await;
fips::service::activate(fips::SERVICE_UNIT).await?;
// Anchor bootstrap window: poll the status every ~3s for up to
// 20s. Bail as soon as the anchor is connected.
let mut last_status: Option<fips::FipsStatus> = None;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
loop {
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
let s = fips::FipsStatus::query(&identity_dir).await;
if s.anchor_connected {
last_status = Some(s);
break;
}
last_status = Some(s);
if std::time::Instant::now() >= deadline {
break;
}
}
let after = last_status.unwrap_or_else(|| before.clone());
let recovered = after.anchor_connected && !before.anchor_connected;
let likely_cause = if after.anchor_connected {
"connected"
} else if !after.service_active {
"daemon_down"
} else if !after.key_present {
"no_seed_key"
} else if after.authenticated_peer_count == 0 {
// Daemon is up with a key but hasn't authenticated any
// peers — almost always outbound UDP/8668 dropped by the
// local firewall/router, or the anchor itself being down.
"no_outbound_udp_or_anchor_down"
} else {
"peers_but_no_anchor"
};
let hint = match likely_cause {
"connected" => "Anchor is reachable.",
"daemon_down" => "The FIPS daemon didn't come back up — check archipelago-fips.service.",
"no_seed_key" => "No seed-derived FIPS key on disk. Re-run the onboarding unlock step.",
"no_outbound_udp_or_anchor_down" =>
"Daemon is running but no peers handshook. Your router / ISP might be blocking outbound UDP 8668, or the anchor (fips.v0l.io) could be down.",
"peers_but_no_anchor" =>
"Mesh has peers but the anchor hasn't been seen yet. Give it a minute and re-check.",
_ => "",
};
Ok(serde_json::json!({
"recovered": recovered,
"likely_cause": likely_cause,
"hint": hint,
"before": before,
"after": after,
}))
}
/// List the seed-anchor entries configured on this node.
pub(super) async fn handle_fips_list_seed_anchors(&self) -> Result<serde_json::Value> {
let list = fips::anchors::load(&self.config.data_dir).await?;
Ok(serde_json::json!({ "seed_anchors": list }))
}
/// Add (or update) a seed anchor and immediately push it into the
/// running daemon. Params: `{ npub, address, transport?, label? }`.
pub(super) async fn handle_fips_add_seed_anchor(
&self,
params: &serde_json::Value,
) -> Result<serde_json::Value> {
let anchor: fips::anchors::SeedAnchor = serde_json::from_value(params.clone())
.map_err(|e| anyhow::anyhow!("bad seed anchor payload: {}", e))?;
if !anchor.npub.starts_with("npub1") {
anyhow::bail!("npub must be bech32 (npub1...)");
}
if !anchor.address.contains(':') {
anyhow::bail!("address must be host:port (e.g. 192.168.1.116:8668)");
}
let list =
fips::anchors::add(&self.config.data_dir, anchor.clone()).await?;
// Push just the newly-added anchor into the running daemon so
// the user sees effect without waiting for the periodic apply.
let results = fips::anchors::apply(&[anchor]).await;
Ok(serde_json::json!({
"seed_anchors": list,
"apply": results.iter().map(|r| {
serde_json::json!({ "npub": r.npub, "ok": r.ok, "message": r.message })
}).collect::<Vec<_>>(),
}))
}
/// Remove a seed anchor by npub. Params: `{ npub }`. Does NOT tear
/// down an already-authenticated peer connection — it only stops
/// us from re-dialing the anchor on the next apply cycle.
pub(super) async fn handle_fips_remove_seed_anchor(
&self,
params: &serde_json::Value,
) -> Result<serde_json::Value> {
let npub = params
.get("npub")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("missing npub"))?;
let list = fips::anchors::remove(&self.config.data_dir, npub).await?;
Ok(serde_json::json!({ "seed_anchors": list }))
}
/// Re-apply all seed anchors to the running daemon. Useful after a
/// FIPS restart or when the user wants to force a reconnection
/// attempt without waiting for the periodic apply loop.
pub(super) async fn handle_fips_apply_seed_anchors(&self) -> Result<serde_json::Value> {
let list = fips::anchors::load(&self.config.data_dir).await?;
let results = fips::anchors::apply(&list).await;
Ok(serde_json::json!({
"applied": results.len(),
"results": results.iter().map(|r| {
serde_json::json!({ "npub": r.npub, "ok": r.ok, "message": r.message })
}).collect::<Vec<_>>(),
}))
}
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>
2026-04-18 22:57:51 -04:00
}