843 lines
35 KiB
Rust
Raw Normal View History

2026-01-24 22:59:20 +00:00
use crate::api::ApiHandler;
use crate::config::{Config, ContainerRuntime};
use crate::container::{docker_packages, DockerPackageScanner};
use crate::identity::{self, NodeIdentity};
use crate::monitoring::MetricsStore;
use crate::node_message;
use crate::nostr_discovery;
2026-03-12 12:56:59 +00:00
use crate::nostr_handshake;
use crate::peers;
use crate::state::StateManager;
2026-01-24 22:59:20 +00:00
use anyhow::Result;
use hyper::server::conn::Http;
use hyper::service::service_fn;
use std::collections::HashMap;
2026-01-24 22:59:20 +00:00
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
2026-01-24 22:59:20 +00:00
use tokio::net::TcpListener;
use tracing::{debug, error, info, warn};
2026-01-24 22:59:20 +00:00
pub struct Server {
_config: Config,
_identity: Arc<NodeIdentity>,
2026-01-24 22:59:20 +00:00
api_handler: Arc<ApiHandler>,
_state_manager: Arc<StateManager>,
2026-01-24 22:59:20 +00:00
}
impl Server {
pub async fn new(config: Config) -> Result<Self> {
let state_manager = Arc::new(StateManager::new());
// Load node identity and set stable server_info.
// Detect seed-backed vs legacy vs fresh install.
let identity_dir = config.data_dir.join("identity");
let has_seed = crate::seed::seed_exists(&config.data_dir);
let has_node_key = NodeIdentity::key_exists(&identity_dir);
let identity = if has_node_key {
// Existing keys on disk (seed-derived or legacy random) — load them.
NodeIdentity::load_or_create(&identity_dir).await?
} else {
// Fresh install — create a temporary identity.
// Onboarding will overwrite this with seed-derived keys.
NodeIdentity::load_or_create(&identity_dir).await?
};
let (mut data, _) = state_manager.get_snapshot().await;
data.server_info.id = identity.node_id();
data.server_info.pubkey = identity.pubkey_hex();
data.server_info.seed_backed = has_seed;
// Load persisted server name
let name_file = config.data_dir.join("server-name");
if let Ok(name) = tokio::fs::read_to_string(&name_file).await {
let name = name.trim().to_string();
if !name.is_empty() {
data.server_info.name = Some(name);
}
}
data.server_info.tor_address = docker_packages::read_tor_address("archipelago").await;
if let Some(ref tor) = data.server_info.tor_address {
data.server_info.node_address = Some(identity.node_address(tor));
}
state_manager.update_data(data.clone()).await;
// Retry Tor address in background — Tor may not be ready at startup
if data.server_info.tor_address.is_none() {
let sm = state_manager.clone();
let pubkey = identity.pubkey_hex();
tokio::spawn(async move {
for delay in [5, 10, 20, 30, 60] {
tokio::time::sleep(std::time::Duration::from_secs(delay)).await;
if let Some(tor) = docker_packages::read_tor_address("archipelago").await {
let (mut d, _) = sm.get_snapshot().await;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let addr =
format!("archipelago://{}#{}", tor.trim_end_matches('/'), pubkey);
d.server_info.tor_address = Some(tor.clone());
d.server_info.node_address = Some(addr);
sm.update_data(d).await;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
tracing::info!(
"Tor address discovered after startup: {}",
&tor[..20.min(tor.len())]
);
break;
}
}
});
}
// Load persisted messages (Archipelago channel)
node_message::init(&config.data_dir).await;
// Auto-create default identity if none exist (fresh boot or factory reset)
{
let im = crate::identity_manager::IdentityManager::new(&config.data_dir).await;
if let Ok(mgr) = im {
if let Ok((list, _)) = mgr.list().await {
if list.is_empty() {
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
match mgr
.create(
"Default".to_string(),
crate::identity_manager::IdentityPurpose::Personal,
)
.await
{
Ok(record) => {
let _ = mgr.create_nostr_key(&record.id).await;
tracing::info!(did = %record.did, "Auto-created default identity with Nostr key");
}
Err(e) => tracing::debug!("Auto-identity creation (non-fatal): {}", e),
}
}
}
}
}
// Revoke any previously published Nostr data (runs before publish so revocation is not overwritten)
let identity_dir = config.data_dir.join("identity");
let tor_proxy_revoke = config.nostr_tor_proxy.clone();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
if let Err(e) =
nostr_discovery::revoke_if_needed(&identity_dir, tor_proxy_revoke.as_deref()).await
{
tracing::debug!("Nostr revoke (non-fatal): {}", e);
}
// Publish presence-only to Nostr (DID + Nostr pubkey, NO onion address).
// Onion addresses are exchanged privately via NIP-44 encrypted DMs.
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
if config.nostr_discovery_enabled && !config.nostr_relays.is_empty() {
let identity_dir = config.data_dir.join("identity");
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let did =
identity::did_key_from_pubkey_hex(&data.server_info.pubkey).unwrap_or_default();
let version = data.server_info.version.clone();
let relays = config.nostr_relays.clone();
let tor_proxy = config.nostr_tor_proxy.clone();
tokio::spawn(async move {
if let Err(e) = nostr_handshake::publish_presence(
&identity_dir,
&did,
&version,
&relays,
tor_proxy.as_deref(),
)
.await
{
tracing::debug!("Nostr presence publish (non-fatal): {}", e);
}
});
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
info!(
"🔑 Node identity: {} (pubkey: {}...)",
identity.node_id(),
&identity.pubkey_hex()[..16.min(identity.pubkey_hex().len())]
);
let identity = Arc::new(identity);
// Create metrics store and spawn background collector
let metrics_store = Arc::new(MetricsStore::with_data_dir(config.data_dir.clone()).await);
let metrics_for_telemetry = metrics_store.clone();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
crate::monitoring::spawn_metrics_collector(
metrics_store.clone(),
Some(state_manager.clone()),
Some(config.data_dir.clone()),
);
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let api_handler =
Arc::new(ApiHandler::new(config.clone(), state_manager.clone(), metrics_store).await?);
2026-03-17 00:03:08 +00:00
// Initialize mesh networking service (if config has enabled: true)
{
let data_dir = config.data_dir.clone();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let did =
identity::did_key_from_pubkey_hex(&data.server_info.pubkey).unwrap_or_default();
2026-03-17 00:03:08 +00:00
let pubkey_hex = identity.pubkey_hex();
let signing_key = identity.signing_key();
match crate::mesh::MeshService::new(&data_dir, signing_key, &did, &pubkey_hex).await {
Ok(mut mesh_service) => {
// Pass the human-readable server name for mesh adverts
mesh_service.set_server_name(data.server_info.name.clone());
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let mut mesh_config = crate::mesh::load_config(&data_dir)
.await
.unwrap_or_default();
// Auto-enable mesh if a radio is detected and no config exists yet
if !mesh_config.enabled {
let devices = crate::mesh::detect_devices().await;
if !devices.is_empty() {
info!("📡 Auto-detected mesh radio: {:?} — enabling mesh", devices);
mesh_config.enabled = true;
mesh_config.device_path = Some(devices[0].clone());
let _ = crate::mesh::save_config(&data_dir, &mesh_config).await;
}
}
2026-03-17 00:03:08 +00:00
if mesh_config.enabled {
if let Err(e) = mesh_service.start() {
warn!("Mesh service start failed (non-fatal): {}", e);
} else {
info!("📡 Mesh networking started");
}
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
api_handler
.rpc_handler()
.set_mesh_service(mesh_service)
.await;
2026-03-17 00:03:08 +00:00
info!("📡 Mesh service initialized");
}
Err(e) => {
warn!("Mesh service init failed (non-fatal): {}", e);
}
}
}
// Initialize transport router (unified routing: mesh > lan > tor)
{
let data_dir = config.data_dir.clone();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let did =
identity::did_key_from_pubkey_hex(&data.server_info.pubkey).unwrap_or_default();
2026-03-17 00:03:08 +00:00
let pubkey_hex = identity.pubkey_hex();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let mesh_config = crate::mesh::load_config(&data_dir)
.await
.unwrap_or_default();
2026-03-17 00:03:08 +00:00
let mesh_only = mesh_config.mesh_only_mode.unwrap_or(false);
match crate::transport::PeerRegistry::load(&data_dir).await {
Ok(registry) => {
let registry = std::sync::Arc::new(registry);
let mut transports: Vec<Box<dyn crate::transport::NodeTransport>> = Vec::new();
// Tor transport (always register — availability checked dynamically)
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
transports.push(Box::new(crate::transport::tor::TorTransport::new(
&pubkey_hex,
)));
2026-03-17 00:03:08 +00:00
// Mesh transport (wraps the mesh service)
transports.push(Box::new(
crate::transport::mesh_transport::MeshTransport::new(
api_handler.rpc_handler().mesh_service_arc(),
),
));
// LAN transport (mDNS discovery)
let mut lan = crate::transport::lan::LanTransport::new(&did, &pubkey_hex, 5678);
match lan.start(registry.clone()) {
Ok(()) => info!("📡 LAN transport (mDNS) started"),
Err(e) => debug!("LAN transport init (non-fatal): {}", e),
}
transports.push(Box::new(lan));
let router = std::sync::Arc::new(crate::transport::TransportRouter::new(
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
transports, registry, mesh_only,
2026-03-17 00:03:08 +00:00
));
api_handler.rpc_handler().set_transport_router(router).await;
info!("📡 Transport router initialized (mesh_only={})", mesh_only);
}
Err(e) => {
warn!("Transport router init failed (non-fatal): {}", e);
}
}
}
// Register Archipelago DWN protocols (background, non-blocking)
{
let data_dir = config.data_dir.clone();
tokio::spawn(async move {
if let Err(e) = register_dwn_protocols(&data_dir).await {
debug!("DWN protocol registration (non-fatal): {}", e);
}
});
}
// Periodic Tor address refresh (runs regardless of dev_mode)
// Picks up hostname when Tor creates it after startup/rotation (30-60s delay)
{
let state = state_manager.clone();
let identity_clone = identity.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
if let Err(e) = refresh_tor_address(&state, identity_clone.as_ref()).await {
debug!("Tor address refresh (non-fatal): {}", e);
}
}
});
}
// Initialize container scanner — discovers installed apps from Podman/Docker
{
let scanner = create_docker_scanner(&config).await?;
let state = state_manager.clone();
let identity_clone = identity.clone();
// Initial scan (delayed to let crash recovery finish first)
tokio::spawn(async move {
// Brief delay for containers to stabilize after boot
tokio::time::sleep(Duration::from_secs(3)).await;
info!("🐳 Scanning containers...");
// Tracks how many consecutive scans each container has been absent from.
// Prevents UI flapping when podman intermittently returns incomplete results.
let mut absence_tracker: HashMap<String, u32> = HashMap::new();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
if let Err(e) = scan_and_update_packages(
&scanner,
&state,
identity_clone.as_ref(),
&mut absence_tracker,
)
.await
{
error!("Failed to scan containers: {}", e);
}
// Periodic scan every 60 seconds (only broadcasts if state changed)
// Uses an in-flight guard to skip scans when a previous one is still running
let mut interval = tokio::time::interval(Duration::from_secs(60));
// Skip missed ticks instead of catching up — prevents burst of scans
// after a slow podman response (which causes DB lock storms)
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let scanning = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
loop {
interval.tick().await;
if scanning.load(std::sync::atomic::Ordering::Relaxed) {
debug!("Skipping container scan — previous scan still in progress");
continue;
}
scanning.store(true, std::sync::atomic::Ordering::Relaxed);
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
if let Err(e) = scan_and_update_packages(
&scanner,
&state,
identity_clone.as_ref(),
&mut absence_tracker,
)
.await
{
error!("Failed to update containers: {}", e);
}
scanning.store(false, std::sync::atomic::Ordering::Relaxed);
}
});
}
2026-01-24 22:59:20 +00:00
// Peer health monitoring — check every 5 minutes
{
let state = state_manager.clone();
let data_dir = config.data_dir.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(300));
loop {
interval.tick().await;
if let Err(e) = check_peer_health(&state, &data_dir).await {
debug!("Peer health check (non-fatal): {}", e);
}
}
});
}
// did:dht auto-refresh — re-publish DHT records every 2 hours
if config.nostr_discovery_enabled {
let data_dir = config.data_dir.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(7200));
loop {
interval.tick().await;
let identity_dir = data_dir.join("identity");
let node_key_path = identity_dir.join("node_key");
if !node_key_path.exists() {
continue;
}
match tokio::fs::read(&node_key_path).await {
Ok(key_bytes) if key_bytes.len() == 32 => {
let mut seed = [0u8; 32];
seed.copy_from_slice(&key_bytes);
let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
match crate::network::did_dht::create_and_publish(&signing_key, &[])
.await
{
Ok(did) => tracing::info!(did = %did, "did:dht record refreshed"),
Err(e) => tracing::debug!("did:dht refresh (non-fatal): {}", e),
}
}
_ => {
tracing::debug!("did:dht refresh skipped: no valid node key");
}
}
}
});
}
// Periodic federation state sync — every 30 min we call
// federation::sync_with_peer on each Trusted peer. Without this
// users had to manually click Sync for `fips_npub`/transport
// badge/state updates to propagate; now it happens in the
// background. Staggers peers with a 5s delay so we don't thunder
// the Tor SOCKS proxy. Sync itself already prefers FIPS.
{
let data_dir = config.data_dir.clone();
let state = state_manager.clone();
tokio::spawn(async move {
// First run 60s after boot to let onboarding settle.
tokio::time::sleep(Duration::from_secs(60)).await;
let mut interval = tokio::time::interval(Duration::from_secs(1800));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
interval.tick().await;
let Ok(nodes) = crate::federation::load_nodes(&data_dir).await else {
continue;
};
if nodes.is_empty() {
continue;
}
let (data, _) = state.get_snapshot().await;
let Ok(local_did) =
crate::identity::did_key_from_pubkey_hex(&data.server_info.pubkey)
else {
continue;
};
let identity_dir = data_dir.join("identity");
let Ok(node_identity) =
crate::identity::NodeIdentity::load_or_create(&identity_dir).await
else {
continue;
};
for node in &nodes {
if node.trust_level == crate::federation::TrustLevel::Untrusted {
continue;
}
match crate::federation::sync_with_peer(
&data_dir,
node,
&local_did,
|bytes| node_identity.sign(bytes),
)
.await
{
Ok(_) => debug!(
"Periodic federation sync ok: {}",
node.did.chars().take(20).collect::<String>()
),
Err(e) => debug!(
"Periodic federation sync with {}: {}",
node.did.chars().take(20).collect::<String>(),
e
),
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
}
});
}
// Container health monitoring — auto-restart unhealthy containers
// Respects webhook config: skips when disabled or ContainerCrash not subscribed
crate::health_monitor::spawn_health_monitor(state_manager.clone(), config.data_dir.clone());
// Periodic telemetry reporter (every 15 min when opted in)
crate::monitoring::spawn_telemetry_reporter(
metrics_for_telemetry,
Some(state_manager.clone()),
config.data_dir.clone(),
);
2026-01-24 22:59:20 +00:00
Ok(Self {
_config: config,
_identity: identity,
2026-01-24 22:59:20 +00:00
api_handler,
_state_manager: state_manager,
2026-01-24 22:59:20 +00:00
})
}
/// Serve with a graceful shutdown signal.
feat(fips): peer dialing + dedicated fips0 listener with path whitelist 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>
2026-04-19 01:12:39 -04:00
///
/// `main_addr` is the primary listener (historically `127.0.0.1:5678`).
/// The main listener always comes up on `main_addr`. The FIPS peer
/// listener (path-filtered, bound to `fips0`'s ULA) is managed by a
/// late-binding task that polls every 30s: if fips0 isn't up at
/// startup (pre-onboarding install, legacy node pre-fips.install),
/// it keeps trying until the interface appears — no archipelago
/// restart required after the user activates FIPS.
feat(fips): peer dialing + dedicated fips0 listener with path whitelist 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>
2026-04-19 01:12:39 -04:00
///
/// When `shutdown` completes, both listeners stop accepting and drain
/// in-flight requests (bounded by `DRAIN_TIMEOUT`).
pub async fn serve_with_shutdown(
&self,
feat(fips): peer dialing + dedicated fips0 listener with path whitelist 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>
2026-04-19 01:12:39 -04:00
main_addr: SocketAddr,
shutdown: impl std::future::Future<Output = ()>,
) -> Result<()> {
let active_connections = Arc::new(tokio::sync::Semaphore::new(1024));
feat(fips): peer dialing + dedicated fips0 listener with path whitelist 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>
2026-04-19 01:12:39 -04:00
let (tx, rx_main) = tokio::sync::watch::channel(false);
let main_task = tokio::spawn(accept_loop(
self.api_handler.clone(),
TcpListener::bind(main_addr).await?,
active_connections.clone(),
false, // main listener: no path filter
rx_main,
main_addr,
));
// Peer listener: late-binding so we don't need an archipelago
// restart when fips0 comes up after onboarding.
let peer_task = tokio::spawn(peer_late_bind_loop(
self.api_handler.clone(),
active_connections.clone(),
tx.subscribe(),
));
feat(fips): peer dialing + dedicated fips0 listener with path whitelist 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>
2026-04-19 01:12:39 -04:00
shutdown.await;
info!("Shutdown signal received, draining connections...");
let _ = tx.send(true);
// Wait up to 5s for in-flight requests.
let drain_start = std::time::Instant::now();
let drain_timeout = std::time::Duration::from_secs(5);
while active_connections.available_permits() < 1024 {
if drain_start.elapsed() > drain_timeout {
warn!("Drain timeout reached, forcing shutdown");
break;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
2026-01-24 22:59:20 +00:00
feat(fips): peer dialing + dedicated fips0 listener with path whitelist 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>
2026-04-19 01:12:39 -04:00
let _ = main_task.await;
let _ = peer_task.await;
feat(fips): peer dialing + dedicated fips0 listener with path whitelist 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>
2026-04-19 01:12:39 -04:00
info!("Shutdown complete");
Ok(())
}
}
/// Poll every 30s for `fips0`'s ULA; when it appears, bind the peer
/// listener and run the normal accept loop. If the bind fails (port
/// already taken, permissions), log and keep retrying. Returns on
/// shutdown. First tick fires immediately so the hot path for
/// already-up fips0 is still zero-cost.
async fn peer_late_bind_loop(
handler: Arc<ApiHandler>,
active_connections: Arc<tokio::sync::Semaphore>,
mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
) {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
tokio::select! {
_ = interval.tick() => {
let Some(ip) = crate::fips::iface::fips0_ula() else { continue };
let addr = SocketAddr::new(
std::net::IpAddr::V6(ip),
crate::fips::dial::PEER_PORT,
);
let listener = match TcpListener::bind(addr).await {
Ok(l) => l,
Err(e) => {
warn!("FIPS peer listener bind {} failed: {} — retrying in 30s", addr, e);
continue;
}
};
info!("FIPS peer listener bound {}", addr);
// Once bound, serve until shutdown fires. accept_loop
// returns on shutdown, which also ends this outer loop.
accept_loop(
handler,
listener,
active_connections,
true, // peer listener: apply path filter
shutdown_rx,
addr,
)
.await;
return;
}
_ = shutdown_rx.changed() => {
if *shutdown_rx.borrow() { return; }
}
}
}
}
feat(fips): peer dialing + dedicated fips0 listener with path whitelist 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>
2026-04-19 01:12:39 -04:00
/// Whitelist of HTTP paths reachable via the peer-facing (FIPS) listener.
/// Every entry is an endpoint already protected by cryptographic auth
/// (ed25519 signature verification inside the handler, federation DID
/// headers checked by the content server, or JSON-RPC methods whose
/// handlers verify per-message signatures).
///
/// Anything not on this list returns 404 on the peer listener.
pub fn is_peer_allowed_path(path: &str) -> bool {
// Exact matches
matches!(
path,
"/health"
| "/rpc/v1"
| "/archipelago/node-message"
| "/archipelago/mesh-typed"
| "/dwn"
| "/transport/inbox"
)
// Prefix-matched content endpoints (peer file browse + fetch)
|| path.starts_with("/content/")
}
async fn accept_loop(
handler: Arc<ApiHandler>,
listener: TcpListener,
active_connections: Arc<tokio::sync::Semaphore>,
peer_only: bool,
mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
local_addr: SocketAddr,
) {
loop {
tokio::select! {
result = listener.accept() => {
let (stream, peer_addr) = match result {
Ok(c) => c,
Err(e) => {
error!("{} accept error: {}", local_addr, e);
continue;
}
};
let handler = handler.clone();
let permit = active_connections.clone().acquire_owned().await;
tokio::spawn(async move {
let _permit = permit;
let service = service_fn(move |req: hyper::Request<hyper::Body>| {
let handler = handler.clone();
async move {
if peer_only && !is_peer_allowed_path(req.uri().path()) {
let resp = hyper::Response::builder()
.status(hyper::StatusCode::NOT_FOUND)
.body(hyper::Body::empty())
.expect("static response builds");
return Ok::<_, std::io::Error>(resp);
}
handler
.handle_request(req)
.await
.map_err(|e| std::io::Error::other(format!("{}", e)))
}
});
feat(fips): peer dialing + dedicated fips0 listener with path whitelist 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>
2026-04-19 01:12:39 -04:00
if let Err(e) = Http::new()
.http1_keep_alive(false)
.serve_connection(stream, service)
.with_upgrades()
.await
{
error!("Error serving connection from {}: {}", peer_addr, e);
2026-01-24 22:59:20 +00:00
}
feat(fips): peer dialing + dedicated fips0 listener with path whitelist 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>
2026-04-19 01:12:39 -04:00
});
}
_ = shutdown_rx.changed() => {
if *shutdown_rx.borrow() {
return;
2026-01-24 22:59:20 +00:00
}
}
2026-01-24 22:59:20 +00:00
}
}
}
async fn create_docker_scanner(config: &Config) -> Result<DockerPackageScanner> {
let user = std::env::var("USER").unwrap_or_else(|_| "archipelago".to_string());
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let runtime: Arc<dyn archipelago_container::ContainerRuntime> = match &config.container_runtime
{
ContainerRuntime::Podman => {
Arc::new(archipelago_container::PodmanRuntime::new(user.clone()))
}
ContainerRuntime::Docker => {
Arc::new(archipelago_container::DockerRuntime::new(user.clone()))
}
ContainerRuntime::Auto => {
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
Arc::new(archipelago_container::AutoRuntime::new(user.clone()).await?)
}
};
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
Ok(DockerPackageScanner::new(runtime))
}
async fn refresh_tor_address(state: &StateManager, identity: &NodeIdentity) -> Result<()> {
let tor_addr = docker_packages::read_tor_address("archipelago").await;
let (current_data, _) = state.get_snapshot().await;
if tor_addr != current_data.server_info.tor_address {
let mut data = current_data;
data.server_info.tor_address = tor_addr.clone();
data.server_info.node_address = tor_addr.as_ref().map(|t| identity.node_address(t));
state.update_data(data).await;
if let Some(ref addr) = tor_addr {
info!("🔒 Tor address updated: {}", addr);
}
}
Ok(())
}
/// Number of consecutive absent scans before removing a container from state.
/// 3 scans × 30s = 90 seconds of absence before removal.
const CONTAINER_ABSENCE_THRESHOLD: u32 = 3;
async fn scan_and_update_packages(
scanner: &DockerPackageScanner,
state: &StateManager,
identity: &NodeIdentity,
absence_tracker: &mut HashMap<String, u32>,
) -> Result<()> {
let packages = scanner.scan_containers().await?;
let (current_data, _) = state.get_snapshot().await;
let tor_addr = docker_packages::read_tor_address("archipelago").await;
let tor_changed = tor_addr != current_data.server_info.tor_address;
let first_scan = !current_data.server_info.status_info.containers_scanned;
// Check if update scheduler has found an available update
let update_available = crate::update::load_state(std::path::Path::new("/var/lib/archipelago"))
.await
.map(|s| s.available_update.is_some())
.unwrap_or(false);
let update_changed = update_available != current_data.server_info.status_info.updated;
// Empty scan result = podman failure or timeout, preserve existing state
if packages.is_empty() && !first_scan {
if tor_changed || update_changed {
let mut data = current_data;
data.server_info.tor_address = tor_addr.clone();
data.server_info.node_address = tor_addr.as_ref().map(|t| identity.node_address(t));
data.server_info.status_info.updated = update_available;
state.update_data(data).await;
}
return Ok(());
}
// Merge scan results with current state instead of full replacement.
// This prevents containers from vanishing when podman intermittently
// returns incomplete results under heavy load.
let mut merged = current_data.package_data.clone();
let mut changed = false;
// Update/add containers found in this scan
for (id, pkg) in &packages {
absence_tracker.remove(id);
if merged.get(id) != Some(pkg) {
merged.insert(id.clone(), pkg.clone());
changed = true;
}
}
// Track containers in state but missing from this scan.
// Only remove after CONTAINER_ABSENCE_THRESHOLD consecutive absent scans.
let current_ids: Vec<String> = merged.keys().cloned().collect();
for id in current_ids {
if !packages.contains_key(&id) {
let count = absence_tracker.entry(id.clone()).or_insert(0);
*count += 1;
if *count >= CONTAINER_ABSENCE_THRESHOLD {
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
debug!(
"Removing {} from state after {} consecutive absent scans",
id, count
);
merged.remove(&id);
absence_tracker.remove(&id);
changed = true;
}
}
}
if changed || tor_changed || first_scan || update_changed {
let mut data = current_data;
data.package_data = merged;
data.server_info.tor_address = tor_addr.clone();
data.server_info.node_address = tor_addr.as_ref().map(|t| identity.node_address(t));
data.server_info.status_info.containers_scanned = true;
data.server_info.status_info.updated = update_available;
state.update_data(data).await;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
debug!(
"📦 State changed (packages={}, tor={}, first_scan={}, update={}), broadcasting update",
changed, tor_changed, first_scan, update_changed
);
}
Ok(())
}
/// Register Archipelago DWN protocols on startup.
async fn register_dwn_protocols(data_dir: &std::path::Path) -> Result<()> {
use crate::network::dwn_store::{DwnStore, ProtocolDefinition};
let protocols = [
("https://archipelago.dev/protocols/node-identity/v1", true),
("https://archipelago.dev/protocols/file-catalog/v1", true),
("https://archipelago.dev/protocols/federation/v1", false),
("https://archipelago.dev/protocols/app-deploy/v1", false),
];
let store = DwnStore::new(data_dir).await?;
let existing = store.list_protocols().await?;
let existing_uris: std::collections::HashSet<String> =
existing.iter().map(|p| p.protocol.clone()).collect();
let mut registered = 0;
for (uri, published) in &protocols {
if existing_uris.contains(*uri) {
continue;
}
let def = ProtocolDefinition {
protocol: uri.to_string(),
published: *published,
types: std::collections::HashMap::new(),
structure: std::collections::HashMap::new(),
date_registered: chrono::Utc::now().to_rfc3339(),
};
store.register_protocol(&def).await?;
registered += 1;
}
if registered > 0 {
info!("📋 Registered {registered} DWN protocols");
}
Ok(())
}
/// Periodically check peer reachability and broadcast status changes.
async fn check_peer_health(state: &StateManager, data_dir: &std::path::Path) -> Result<()> {
let known_peers = peers::load_peers(data_dir).await.unwrap_or_default();
if known_peers.is_empty() {
return Ok(());
}
let mut new_health = std::collections::HashMap::new();
for peer in &known_peers {
feat(messaging,dwn,mesh): route peer messaging + DWN sync + blob fetch via FIPS first Migrates the remaining Tor-direct peer call sites to PeerRequest so FIPS is the default when the peer is federated and running the daemon: - node_message::send_to_peer / check_peer_reachable: gain a fips_npub parameter. Error messages updated to reference both transports. - Callers (api/rpc/network.rs, api/rpc/peers.rs, server health loop): look up fips_npub from federation storage by onion and pass it. - mesh::send_typed_wire_via_federation: the spawned background POST for the /archipelago/mesh-typed endpoint now uses PeerRequest with federation-resolved fips_npub. Signature domain unchanged. - api/rpc/mesh/typed_messages.rs fetch_blob_from_peer: blob URL rebuilt as (base_url, path_with_query) so PeerRequest can append the query string after swapping the host. Cap/exp/peer parameters are still signed over the content ref itself, so transport choice is invisible to the signature. - network/dwn_sync.rs sync_with_peers: per-peer fips_npub lookup before sync_single_peer; health/pull/push each dial through PeerRequest, so any DWN peer known to federation gets FIPS. Left Tor-only on purpose: - api/rpc/identity/handlers.rs handle_identity_resolve_peer_onion — resolving TO a DID, no anchor yet. - content.browse / preview calls to non-federated peers fall through to Tor naturally inside PeerRequest (no fips_npub → skip FIPS branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:36:04 -04:00
let fips_npub = crate::federation::fips_npub_for_onion(data_dir, &peer.onion).await;
let reachable =
node_message::check_peer_reachable(&peer.onion, fips_npub.as_deref())
.await
.unwrap_or(false);
new_health.insert(peer.onion.clone(), reachable);
}
let (current_data, _) = state.get_snapshot().await;
if current_data.peer_health != new_health {
let mut data = current_data;
data.peer_health = new_health;
state.update_data(data).await;
debug!("🔗 Peer health updated, broadcasting changes");
}
Ok(())
}