Files
archy/core/archipelago/src/api/rpc/fips.rs
T
DorianandClaude Opus 4.7 be8e5ee46b 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

133 lines
5.9 KiB
Rust

//! 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 }))
}
/// 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,
}))
}
}