diff --git a/core/Cargo.lock b/core/Cargo.lock index 86dcc38d..3949d227 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "archipelago" -version = "1.7.24-alpha" +version = "1.7.25-alpha" dependencies = [ "anyhow", "archipelago-container", diff --git a/core/archipelago/Cargo.toml b/core/archipelago/Cargo.toml index 2c48ef8c..cd3464a8 100644 --- a/core/archipelago/Cargo.toml +++ b/core/archipelago/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "archipelago" -version = "1.7.24-alpha" +version = "1.7.25-alpha" edition = "2021" description = "Archipelago Bitcoin Node OS - Native backend" authors = ["Archipelago Team"] diff --git a/core/archipelago/src/fips/config.rs b/core/archipelago/src/fips/config.rs index b6083dec..af17d2e7 100644 --- a/core/archipelago/src/fips/config.rs +++ b/core/archipelago/src/fips/config.rs @@ -13,22 +13,27 @@ use anyhow::{Context, Result}; use std::path::Path; use tokio::process::Command; -use super::{DAEMON_CONFIG_PATH, DAEMON_KEY_PATH, DAEMON_PUB_PATH, DEFAULT_UDP_PORT}; +use super::{DAEMON_CONFIG_PATH, DAEMON_KEY_PATH, DAEMON_PUB_PATH, DEFAULT_TCP_PORT, DEFAULT_UDP_PORT}; /// Write the FIPS daemon config based on the local npub and default /// transports. Overwrites any existing file — callers are expected to /// re-run this whenever the key or daemon version changes. /// /// Schema is intentionally minimal: node identity comes from the key -/// file on disk (the daemon handles it), transports enable UDP + Tor, -/// IPv6 TUN + DNS on defaults. Static peer list is empty — archipelago -/// feeds peers dynamically via federation updates. +/// file on disk (the daemon handles it), transports enable UDP + TCP +/// (matching upstream factory default), IPv6 TUN + DNS on defaults. +/// Static peer list is empty — archipelago feeds peers dynamically via +/// the seed-anchors apply loop and federation-invite hooks. pub fn render_config_yaml() -> String { // Schema matches upstream jmcorgan/fips as of 2026-04. With // `node.identity.persistent: true` the daemon reuses the key file at // config-dir/fips.key (= DAEMON_KEY_PATH). Transports take `bind_addr` - // rather than `enabled: true / port: N`, and the upstream no longer - // has a `tor:` transport — archipelago's own Tor fallback handles that. + // rather than `enabled: true / port: N`. Both UDP and TCP are + // enabled by default because the public anchor (fips.v0l.io) + // currently answers on TCP/8443 only, and networks that block UDP + // outbound can still bootstrap via TCP. Upstream fips no longer + // has a `tor:` transport variant — archipelago's own Tor fallback + // handles that layer. format!( "# Generated by archipelago — do not edit by hand.\n\ # Regenerated on every key change and daemon upgrade.\n\ @@ -44,9 +49,12 @@ pub fn render_config_yaml() -> String { bind_addr: \"127.0.0.1\"\n\ transports:\n \ udp:\n \ - bind_addr: \"0.0.0.0:{port}\"\n\ + bind_addr: \"0.0.0.0:{udp}\"\n \ + tcp:\n \ + bind_addr: \"0.0.0.0:{tcp}\"\n\ peers: []\n", - port = DEFAULT_UDP_PORT, + udp = DEFAULT_UDP_PORT, + tcp = DEFAULT_TCP_PORT, ) } @@ -185,7 +193,9 @@ mod tests { let yaml = render_config_yaml(); assert!(yaml.contains("persistent: true")); assert!(yaml.contains(&format!("0.0.0.0:{}", DEFAULT_UDP_PORT))); + assert!(yaml.contains(&format!("0.0.0.0:{}", DEFAULT_TCP_PORT))); assert!(yaml.contains("udp:")); + assert!(yaml.contains("tcp:")); assert!(yaml.contains("tun:")); assert!(yaml.contains("name: fips0")); // Upstream fips dropped the `tor:` transport variant; archipelago diff --git a/core/archipelago/src/fips/mod.rs b/core/archipelago/src/fips/mod.rs index a32c0d18..105aad4a 100644 --- a/core/archipelago/src/fips/mod.rs +++ b/core/archipelago/src/fips/mod.rs @@ -53,6 +53,14 @@ pub const UPSTREAM_REPO: &str = "jmcorgan/fips"; /// Default UDP port the daemon listens on. pub const DEFAULT_UDP_PORT: u16 = 8668; +/// Default TCP port the daemon listens on. Used as a fallback when a +/// peer can't be reached over UDP — common on networks that block UDP +/// (corporate/guest wifi) and the path the public fips.v0l.io anchor +/// currently accepts. Upstream factory default enables both transports +/// and archipelago intentionally matches that baseline so fresh nodes +/// can reach the broader FIPS mesh without operator config. +pub const DEFAULT_TCP_PORT: u16 = 8443; + /// Upstream systemd unit shipped by the `fips` debian package. Archipelago /// prefers its own supervision (`archipelago-fips.service`) but respects an /// already-running upstream unit so legacy/dev nodes — where no seed-derived diff --git a/core/archipelago/src/server.rs b/core/archipelago/src/server.rs index 8f9f73d4..cebf2c75 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -510,10 +510,37 @@ impl Server { tracing::warn!("FIPS key load/migrate failed: {}", e); return; } + // Check if the installed fips.yaml matches what we'd + // render now. If not, we need to restart the daemon after + // reinstalling so it picks up schema changes (e.g. the + // v1.7.25 re-addition of the TCP transport). Without this, + // OTA'd nodes would be stuck on the old UDP-only config + // until someone manually clicked Reconnect. + let expected = crate::fips::config::render_config_yaml(); + let installed = tokio::fs::read_to_string("/etc/fips/fips.yaml") + .await + .ok(); + let config_changed = installed.as_deref() != Some(expected.as_str()); + if let Err(e) = crate::fips::config::install(&identity_dir).await { tracing::warn!("FIPS config install failed on startup: {}", e); return; } + if config_changed { + tracing::info!( + "FIPS config schema changed on disk — restarting daemon to pick up new transports" + ); + // Restart whichever unit is actually supervising + // the daemon (archipelago-fips vs upstream fips). + let unit = crate::fips::service::active_unit().await; + if let Err(e) = crate::fips::service::restart(unit).await { + tracing::warn!( + "FIPS restart after config migration failed on {}: {} — user can retry via fips.reconnect", + unit, + e + ); + } + } if let Err(e) = crate::fips::service::activate(crate::fips::SERVICE_UNIT).await { tracing::warn!( "archipelago-fips activate failed on startup: {} — user can retry via fips.install RPC", diff --git a/neode-ui/src/views/server/FipsNetworkCard.vue b/neode-ui/src/views/server/FipsNetworkCard.vue index c590d382..171554a5 100644 --- a/neode-ui/src/views/server/FipsNetworkCard.vue +++ b/neode-ui/src/views/server/FipsNetworkCard.vue @@ -34,27 +34,23 @@ + doesn't crowd the card but is still one click away. Close + button and layout mirror the What's New modal (and the rest + of the app) so it feels like a first-class modal. -->
-
-
- -
- +
+
diff --git a/neode-ui/src/views/server/FipsSeedAnchorsCard.vue b/neode-ui/src/views/server/FipsSeedAnchorsCard.vue index 0941f5fb..9c218ae5 100644 --- a/neode-ui/src/views/server/FipsSeedAnchorsCard.vue +++ b/neode-ui/src/views/server/FipsSeedAnchorsCard.vue @@ -1,9 +1,23 @@