fix(vpn): refresh a stored peer's WireGuard endpoint to the node's current IP

vpn.peer-config returned the config exactly as stored at creation time, so
after a node moves networks the reused companion peer's QR encodes the old
location's address (seen on .116: Endpoint = 10.125.9.0 from the previous
LAN) and the tunnel can never connect. Rewrite the Endpoint line with the
node's current address before rendering the QR, and persist it back so the
downloadable .conf matches. Endpoint detection is shared with
vpn.create-peer via current_wg_endpoint_host().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-20 01:57:08 -04:00
parent f13fdc6451
commit f25febf3bb

View File

@ -437,6 +437,23 @@ impl RpcHandler {
Ok(serde_json::json!({ "added": true, "npub": npub })) 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. /// vpn.create-peer — Generate a WireGuard peer config + QR code for mobile devices.
pub(super) async fn handle_vpn_create_peer( pub(super) async fn handle_vpn_create_peer(
&self, &self,
@ -501,22 +518,7 @@ impl RpcHandler {
.ok_or_else(|| anyhow::anyhow!("Cannot read server public key"))? .ok_or_else(|| anyhow::anyhow!("Cannot read server public key"))?
}; };
// Detect host IP — prefer config, then nvpn, then system detection let endpoint = format!("{}:51820", self.current_wg_endpoint_host().await);
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);
// Allocate a peer IP (simple: hash the peer name) // Allocate a peer IP (simple: hash the peer name)
let peer_num = (name.bytes().map(|b| b as u32).sum::<u32>() % 253) + 2; let peer_num = (name.bytes().map(|b| b as u32).sum::<u32>() % 253) + 2;
@ -667,15 +669,41 @@ impl RpcHandler {
let content = tokio::fs::read_to_string(&peer_file) let content = tokio::fs::read_to_string(&peer_file)
.await .await
.map_err(|_| anyhow::anyhow!("Peer '{}' not found", name))?; .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!( anyhow::anyhow!(
"No config stored for peer '{}' — recreate the device to get a new QR code", "No config stored for peer '{}' — recreate the device to get a new QR code",
name 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::<Vec<_>>()
.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()) let qr = qrcode::QrCode::new(config.as_bytes())
.map_err(|e| anyhow::anyhow!("QR generation failed: {}", e))?; .map_err(|e| anyhow::anyhow!("QR generation failed: {}", e))?;
let svg = qr let svg = qr