feat(settings): per-service FIPS/Tor transport preference

Adds a user-configurable toggle for how each peer-to-peer service
reaches federated peers. Three options per service:

- Auto (default) — FIPS preferred, Tor fallback (current behavior).
- FIPS only — fail rather than fall through to Tor.
- Tor only — explicit opt-in to onion anonymity for that service.

Services covered (matching the UI rows):
- Federation — state sync, invites, peer notifications
- Peers — address/DID rotation broadcasts
- Peer Files — content catalog download/browse/preview
- Messaging — archipelago channel + mesh bridge
- Mesh File Sharing — content_ref blob fetches

Implementation:
- settings::transport — persisted struct + process-wide OnceLock handle
  (so deep call sites don't need data_dir threaded through signatures).
  On-disk file: <data_dir>/settings/transport_preferences.json; missing
  or corrupt → defaults (Auto everywhere).
- settings::transport::init() called from main.rs after config load.
- fips::dial::PeerRequest gains a .service(kind) builder; send_* checks
  the preference before choosing a transport. FIPS-only fails loudly
  when FIPS is unavailable (so users who pick it know when something
  falls back).
- Every FIPS-first migration site tags its PeerRequest with the
  matching PeerService so the toggle actually applies.
- transport.preferences + transport.set-preference RPCs added; wired
  into the dispatcher.
- neode-ui/src/views/settings/TransportPrefsCard.vue — standalone card
  with a 5-row Auto/FIPS/Tor tri-state. Not wired into Settings.vue —
  the user places components themselves (see feedback_ui_entry_points).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-19 01:44:41 -04:00
co-authored by Claude Opus 4.7
parent dbd19006f2
commit 8b88c45262
16 changed files with 478 additions and 4 deletions
+4
View File
@@ -237,6 +237,7 @@ impl RpcHandler {
let path = format!("/content/{}", content_id);
let (response, _transport) =
crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, &path)
.service(crate::settings::transport::PeerService::PeerFiles)
.header("X-Federation-DID", local_did)
.timeout(std::time::Duration::from_secs(120))
.send_get()
@@ -293,6 +294,7 @@ impl RpcHandler {
let (response, _transport) =
crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, "/content")
.service(crate::settings::transport::PeerService::PeerFiles)
.timeout(std::time::Duration::from_secs(30))
.send_get()
.await
@@ -352,6 +354,7 @@ impl RpcHandler {
let path = format!("/content/{}", content_id);
let (response, _transport) =
crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, &path)
.service(crate::settings::transport::PeerService::PeerFiles)
.header("X-Federation-DID", local_did)
.header("X-Payment-Token", token_str)
.timeout(std::time::Duration::from_secs(120))
@@ -412,6 +415,7 @@ impl RpcHandler {
let (response, _transport) =
crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, &path)
.service(crate::settings::transport::PeerService::PeerFiles)
.timeout(std::time::Duration::from_secs(30))
.send_get()
.await
@@ -363,6 +363,8 @@ impl RpcHandler {
"transport.peers" => self.handle_transport_peers().await,
"transport.send" => self.handle_transport_send(params).await,
"transport.set-mode" => self.handle_transport_set_mode(params).await,
"transport.preferences" => self.handle_transport_preferences().await,
"transport.set-preference" => self.handle_transport_set_preference(params).await,
// Server settings
"server.set-name" => self.handle_server_set_name(params).await,
@@ -656,6 +656,7 @@ impl RpcHandler {
&node.onion,
"/rpc/v1",
)
.service(crate::settings::transport::PeerService::Peers)
.timeout(std::time::Duration::from_secs(30));
match req.send_json(&body).await {
@@ -742,6 +742,7 @@ impl RpcHandler {
let (resp, transport) =
crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), &onion_bare, &path)
.service(crate::settings::transport::PeerService::MeshFileSharing)
.timeout(std::time::Duration::from_secs(120))
.send_get()
.await
+1
View File
@@ -442,6 +442,7 @@ pub(super) async fn notify_federation_peers_address_change(
&peer.onion,
"/rpc/v1",
)
.service(crate::settings::transport::PeerService::Peers)
.timeout(std::time::Duration::from_secs(30));
match req.send_json(&payload).await {
Ok((_, transport)) => {
+40
View File
@@ -108,6 +108,46 @@ impl RpcHandler {
}))
}
/// transport.preferences — Return the user's per-service transport
/// preferences. The UI renders these as five FIPS/Auto/Tor rows.
pub(super) async fn handle_transport_preferences(&self) -> Result<serde_json::Value> {
let prefs = crate::settings::transport::snapshot().await;
Ok(serde_json::to_value(prefs)?)
}
/// transport.set-preference — Change a single service preference.
/// Persists to disk and hot-swaps the in-memory handle so future
/// calls see the new value without restart.
pub(super) async fn handle_transport_set_preference(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
use crate::settings::transport::{set, PeerService, TransportPref};
let params = params
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let service: PeerService = serde_json::from_value(
params
.get("service")
.cloned()
.ok_or_else(|| anyhow::anyhow!("Missing 'service' param"))?,
)
.map_err(|e| anyhow::anyhow!("Invalid service: {}", e))?;
let pref: TransportPref = serde_json::from_value(
params
.get("pref")
.cloned()
.ok_or_else(|| anyhow::anyhow!("Missing 'pref' param"))?,
)
.map_err(|e| anyhow::anyhow!("Invalid pref: {}", e))?;
set(&self.config.data_dir, service, pref).await?;
info!(service = ?service, pref = ?pref, "Transport preference updated");
let current = crate::settings::transport::snapshot().await;
Ok(serde_json::to_value(current)?)
}
/// transport.set-mode — Toggle mesh-only (off-grid) mode.
pub(super) async fn handle_transport_set_mode(
&self,