diff --git a/core/archipelago/src/api/rpc/vpn.rs b/core/archipelago/src/api/rpc/vpn.rs index 8c2f1016..62dd14fc 100644 --- a/core/archipelago/src/api/rpc/vpn.rs +++ b/core/archipelago/src/api/rpc/vpn.rs @@ -437,6 +437,23 @@ impl RpcHandler { Ok(serde_json::json!({ "added": true, "npub": npub })) } + /// The host address a WireGuard peer should dial — prefer the configured + /// host IP, then public-IP lookup, then first local address. + async fn current_wg_endpoint_host(&self) -> String { + if self.config.host_ip != "127.0.0.1" { + return self.config.host_ip.clone(); + } + tokio::process::Command::new("sh") + .arg("-c") + .arg("curl -s --connect-timeout 5 https://api.ipify.org 2>/dev/null || hostname -I | awk '{print $1}'") + .output() + .await + .ok() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| self.config.host_ip.clone()) + } + /// vpn.create-peer — Generate a WireGuard peer config + QR code for mobile devices. pub(super) async fn handle_vpn_create_peer( &self, @@ -501,22 +518,7 @@ impl RpcHandler { .ok_or_else(|| anyhow::anyhow!("Cannot read server public key"))? }; - // Detect host IP — prefer config, then nvpn, then system detection - let host_ip = if self.config.host_ip != "127.0.0.1" { - self.config.host_ip.clone() - } else { - // Fallback: get public IP via external service - tokio::process::Command::new("sh") - .arg("-c") - .arg("curl -s --connect-timeout 5 https://api.ipify.org 2>/dev/null || hostname -I | awk '{print $1}'") - .output() - .await - .ok() - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| self.config.host_ip.clone()) - }; - let endpoint = format!("{}:51820", host_ip); + let endpoint = format!("{}:51820", self.current_wg_endpoint_host().await); // Allocate a peer IP (simple: hash the peer name) let peer_num = (name.bytes().map(|b| b as u32).sum::() % 253) + 2; @@ -667,15 +669,41 @@ impl RpcHandler { let content = tokio::fs::read_to_string(&peer_file) .await .map_err(|_| anyhow::anyhow!("Peer '{}' not found", name))?; - let peer: serde_json::Value = serde_json::from_str(&content)?; + let mut peer: serde_json::Value = serde_json::from_str(&content)?; - let config = peer.get("config").and_then(|v| v.as_str()).ok_or_else(|| { + let stored = peer.get("config").and_then(|v| v.as_str()).ok_or_else(|| { anyhow::anyhow!( "No config stored for peer '{}' — recreate the device to get a new QR code", name ) })?; + // The stored Endpoint is the node's address at creation time; after + // the node moves networks it points at a dead IP and the QR produces + // a tunnel that can never connect. Refresh it to the current address. + let endpoint = format!("{}:51820", self.current_wg_endpoint_host().await); + let config: String = stored + .lines() + .map(|l| { + if l.trim_start().starts_with("Endpoint") { + format!("Endpoint = {}", endpoint) + } else { + l.to_string() + } + }) + .collect::>() + .join("\n"); + if config != stored { + if let Some(obj) = peer.as_object_mut() { + obj.insert("config".to_string(), config.clone().into()); + } + if let Ok(json) = serde_json::to_string_pretty(&peer) { + if tokio::fs::write(&peer_file, json).await.is_ok() { + info!("VPN peer '{}' endpoint refreshed to {}", name, endpoint); + } + } + } + let qr = qrcode::QrCode::new(config.as_bytes()) .map_err(|e| anyhow::anyhow!("QR generation failed: {}", e))?; let svg = qr