feat: propagate Tor address rotation to Nostr relays and federation peers

After rotation, spawns background task that publishes updated .onion to
Nostr relays and sends federation.peer-address-changed RPC to all peers
over Tor. Peers update their nodes.json with the new address.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 00:08:16 +00:00
parent fe2934a917
commit ccaeb10a92
4 changed files with 130 additions and 1 deletions

View File

@@ -323,4 +323,46 @@ impl RpcHandler {
info!(app = %app_id, peer = %peer_did, "Deployed app to federated peer");
Ok(result)
}
/// federation.peer-address-changed — A peer notifies us that their .onion changed.
pub(super) async fn handle_federation_peer_address_changed(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let did = params
.get("did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing did"))?;
let new_onion = params
.get("new_onion")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing new_onion"))?;
// Load existing nodes, find the peer by DID, update their onion
let mut nodes = federation::load_nodes(&self.config.data_dir).await?;
let found = nodes.iter_mut().find(|n| n.did == did);
match found {
Some(node) => {
let old = node.onion.clone();
node.onion = new_onion.to_string();
federation::save_nodes(&self.config.data_dir, &nodes).await?;
info!(did = %did, old_onion = %old, new_onion = %new_onion, "Updated federated peer address");
Ok(serde_json::json!({
"updated": true,
"did": did,
"old_onion": old,
"new_onion": new_onion,
}))
}
None => {
info!(did = %did, "Received address change from unknown peer — ignoring");
Ok(serde_json::json!({
"updated": false,
"reason": "Unknown peer DID",
}))
}
}
}
}

View File

@@ -445,6 +445,7 @@ impl RpcHandler {
"federation.get-state" => self.handle_federation_get_state().await,
"federation.peer-joined" => self.handle_federation_peer_joined(params).await,
"federation.deploy-app" => self.handle_federation_deploy_app(params).await,
"federation.peer-address-changed" => self.handle_federation_peer_address_changed(params).await,
// VPN & Remote Access
"vpn.status" => self.handle_vpn_status().await,

View File

@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::{debug, info, warn};
use crate::{federation, identity, nostr_discovery};
const TOR_DATA_DIR: &str = "/var/lib/archipelago/tor";
const SERVICES_CONFIG: &str = "services.json";
/// How long old service directories are kept during transition (seconds).
@@ -181,6 +183,24 @@ impl RpcHandler {
// Wait up to 60s for new hostname file to appear
let new_onion = wait_for_hostname(name, 60).await;
// Propagate address change to Nostr relays and federation peers (fire-and-forget)
if let Some(ref new_addr) = new_onion {
let data_dir = self.config.data_dir.clone();
let nostr_relays = self.config.nostr_relays.clone();
let tor_proxy = self.config.nostr_tor_proxy.clone();
let new_addr_clone = new_addr.clone();
let old_onion_clone = old_onion.clone();
tokio::spawn(async move {
propagate_address_change(
&data_dir,
&new_addr_clone,
old_onion_clone.as_deref(),
&nostr_relays,
tor_proxy.as_deref(),
).await;
});
}
Ok(serde_json::json!({
"rotated": true,
"name": name,
@@ -408,6 +428,72 @@ async fn save_services_config(config_dir: &std::path::Path, config: &ServicesCon
Ok(())
}
/// Propagate address change: publish to Nostr relays and notify federation peers.
async fn propagate_address_change(
data_dir: &std::path::Path,
new_onion: &str,
old_onion: Option<&str>,
relays: &[String],
tor_proxy: Option<&str>,
) {
// 1. Publish updated identity to Nostr relays
let identity_dir = data_dir.join("identity");
match identity::NodeIdentity::load_or_create(&identity_dir).await {
Ok(node_id) => {
let did = node_id.did_key();
if !relays.is_empty() {
match nostr_discovery::publish_node_identity(
&identity_dir,
&did,
new_onion,
env!("CARGO_PKG_VERSION"),
relays,
tor_proxy,
).await {
Ok(_) => info!("Published updated .onion to Nostr relays"),
Err(e) => warn!("Failed to publish to Nostr relays: {}", e),
}
}
// 2. Notify federation peers via the old address (still works during transition)
let proxy = tor_proxy.unwrap_or("127.0.0.1:9050");
match federation::load_nodes(data_dir).await {
Ok(peers) => {
for peer in peers {
if peer.onion.is_empty() {
continue;
}
let target_onion = &peer.onion;
let payload = serde_json::json!({
"method": "federation.peer-address-changed",
"params": {
"did": did,
"new_onion": new_onion,
"old_onion": old_onion,
}
});
let url = format!("http://{}/rpc/v1", target_onion);
let client = match reqwest::Client::builder()
.proxy(reqwest::Proxy::all(format!("socks5h://{}", proxy)).unwrap_or_else(|_| reqwest::Proxy::all("socks5h://127.0.0.1:9050").expect("valid proxy")))
.timeout(std::time::Duration::from_secs(30))
.build()
{
Ok(c) => c,
Err(_) => continue,
};
match client.post(&url).json(&payload).send().await {
Ok(_) => info!(peer_did = %peer.did, "Notified peer of address change"),
Err(e) => warn!(peer_did = %peer.did, "Failed to notify peer: {}", e),
}
}
}
Err(e) => warn!("Failed to load federation peers: {}", e),
}
}
Err(e) => warn!("Failed to load node identity for propagation: {}", e),
}
}
/// Wait for a hostname file to appear after Tor restart (up to max_secs).
async fn wait_for_hostname(service_name: &str, max_secs: u64) -> Option<String> {
for _ in 0..max_secs {