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:
parent
f1ca60e732
commit
e74bf9bcfd
@ -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",
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -494,7 +494,7 @@
|
||||
|
||||
- [x] **TOR-01** — Implement Tor address rotation RPC. In `core/archipelago/src/api/rpc/tor.rs`, add `tor.rotate-service` handler. Flow: (1) Read current service from `services.json`, (2) Rename the hidden service directory from `hidden_service_{name}` to `hidden_service_{name}_old`, (3) Create a new hidden service directory (Tor will auto-generate new keys on restart), (4) Regenerate torrc from updated services.json, (5) Restart `archy-tor` container, (6) Wait up to 60s for new hostname file to appear, (7) Return both old and new .onion addresses. Keep the old directory for a configurable transition period (default 24h) then delete via a cleanup task. Add `tor.cleanup-rotated` RPC that deletes expired old service directories. **Acceptance**: Call `tor.rotate-service("archipelago")`, verify new .onion address is different from old one. Both addresses resolve during transition period. After cleanup, old address stops working. Deploy and verify.
|
||||
|
||||
- [ ] **TOR-02** — Propagate Tor address change to federation peers. After a successful rotation in `tor.rotate-service`, automatically: (1) Update the node's Nostr discovery event with the new onion address by calling `publish_node_identity()` from `nostr_discovery.rs`, (2) For each federated peer in `federation.rs`, send a `federation.peer-address-changed` notification over Tor (using the OLD address which still works during transition) containing the new onion address signed with the node's DID key, (3) Peers receiving this notification update their `FederatedNode.onion` field and re-save `federation/nodes.json`. Add `federation.peer-address-changed` as a new inter-node RPC handler. **Acceptance**: Rotate address on node A, verify node B's federation list updates to show the new address within 5 minutes. Verify Nostr relay shows new address.
|
||||
- [x] **TOR-02** — Propagate Tor address change to federation peers. After a successful rotation in `tor.rotate-service`, automatically: (1) Update the node's Nostr discovery event with the new onion address by calling `publish_node_identity()` from `nostr_discovery.rs`, (2) For each federated peer in `federation.rs`, send a `federation.peer-address-changed` notification over Tor (using the OLD address which still works during transition) containing the new onion address signed with the node's DID key, (3) Peers receiving this notification update their `FederatedNode.onion` field and re-save `federation/nodes.json`. Add `federation.peer-address-changed` as a new inter-node RPC handler. **Acceptance**: Rotate address on node A, verify node B's federation list updates to show the new address within 5 minutes. Verify Nostr relay shows new address.
|
||||
|
||||
- [x] **TOR-03** — Add per-app Tor toggle. In `core/archipelago/src/api/rpc/tor.rs`, add `tor.toggle-app` handler that takes `app_id` and `enabled` (bool). When disabling: remove the app's `HiddenServiceDir`/`HiddenServicePort` lines from the generated torrc, restart archy-tor, delete the hidden service directory. When enabling: add the service entry to `services.json`, regenerate torrc, restart archy-tor, wait for hostname. Update `TorServiceEntry` struct to include an `enabled` field (default true). The `tor.list-services` response should include the `enabled` state per service. **Acceptance**: Disable Tor for filebrowser, verify its .onion address no longer resolves. Re-enable, verify a new .onion address is generated and works. Deploy and verify.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user