feat: standalone WireGuard from first install, fix networking stack
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 14m13s

Standalone WireGuard (wg0:51820):
- New archipelago-wg.service creates wg0 independent of NostrVPN
- Keypair generated on first-boot, persisted on LUKS partition
- vpn.create-peer uses wg genkey/pubkey (no nvpn dependency)
- wg-address service depends on archipelago-wg, not nostr-vpn

Networking fixes:
- Remove nos.lol from default relays (requires PoW, events rejected)
- Add Tor hidden service for private relay (port 7777) — NAT'd peers
  can reach relay over Tor for NostrVPN signaling
- Fix Tor hostname sync race: wait loop before copying hostname files
- Add tor-hostnames + wireguard dirs to LUKS partition setup
- Include relay in hostname sync loops (setup-tor.sh + first-boot)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-08 20:27:38 +02:00
parent 5427d4ec5d
commit 2d1536f016
8 changed files with 112 additions and 129 deletions

View File

@@ -41,8 +41,12 @@ impl RpcHandler {
// Prefer onion (always works), fall back to direct IP
let relay_url = relay_onion.clone().or(relay_direct.clone());
// Standalone WireGuard public key
let wg_pubkey = tokio::fs::read_to_string("/var/lib/archipelago/wireguard/public.key")
.await.ok().map(|s| s.trim().to_string());
Ok(serde_json::json!({
"connected": status.connected,
"connected": status.connected || wg_ip.is_some(),
"provider": status.provider,
"interface": status.interface,
"ip_address": status.ip_address,
@@ -53,6 +57,7 @@ impl RpcHandler {
"configured": config.enabled,
"configured_provider": format!("{:?}", config.provider).to_lowercase(),
"wg_ip": wg_ip,
"wg_pubkey": wg_pubkey,
"node_npub": node_npub,
"relay_url": relay_url,
"relay_onion": relay_onion,
@@ -373,44 +378,48 @@ impl RpcHandler {
let params = params.unwrap_or(serde_json::json!({}));
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("Mobile");
// Get server status for endpoint info
let status = vpn::get_status().await;
if !status.connected {
anyhow::bail!("NostrVPN is not running. Start VPN first.");
// Check that wg0 is up (standalone WireGuard)
let wg0_up = tokio::process::Command::new("ip")
.args(["link", "show", "wg0"])
.output().await
.map(|o| o.status.success())
.unwrap_or(false);
if !wg0_up {
anyhow::bail!("WireGuard (wg0) is not running. Wait for first-boot to complete.");
}
// Generate a keypair for the new peer via nvpn keygen
let keygen = tokio::process::Command::new("nvpn")
.arg("keygen")
.output()
.await
.map_err(|e| anyhow::anyhow!("nvpn keygen failed: {}", e))?;
if !keygen.status.success() {
anyhow::bail!("nvpn keygen failed: {}", String::from_utf8_lossy(&keygen.stderr));
// Generate a keypair for the new peer using wg genkey/pubkey
let genkey = tokio::process::Command::new("wg")
.arg("genkey")
.output().await
.map_err(|e| anyhow::anyhow!("wg genkey failed: {}", e))?;
if !genkey.status.success() {
anyhow::bail!("wg genkey failed: {}", String::from_utf8_lossy(&genkey.stderr));
}
let peer_private = String::from_utf8_lossy(&genkey.stdout).trim().to_string();
let keygen_output = String::from_utf8_lossy(&keygen.stdout);
let lines: Vec<&str> = keygen_output.lines().collect();
let mut pubkey_cmd = tokio::process::Command::new("wg");
pubkey_cmd.arg("pubkey");
pubkey_cmd.stdin(std::process::Stdio::piped());
pubkey_cmd.stdout(std::process::Stdio::piped());
let mut pubkey_child = pubkey_cmd.spawn()
.map_err(|e| anyhow::anyhow!("wg pubkey spawn failed: {}", e))?;
if let Some(ref mut stdin) = pubkey_child.stdin {
use tokio::io::AsyncWriteExt;
stdin.write_all(peer_private.as_bytes()).await?;
stdin.shutdown().await?;
}
let pubkey_out = pubkey_child.wait_with_output().await?;
let peer_public = String::from_utf8_lossy(&pubkey_out.stdout).trim().to_string();
// Parse private and public keys from keygen output (format: "private_key=<key>\npublic_key=<key>")
let parse_key = |line: &str| -> String {
if let Some(pos) = line.find('=') {
line[pos + 1..].trim().to_string()
} else {
line.trim().to_string()
}
};
let (peer_private, peer_public) = if lines.len() >= 2 {
(parse_key(lines[0]), parse_key(lines[1]))
// Read server's WireGuard public key (standalone WG key, then fall back to nvpn)
let server_pubkey = if let Ok(key) = tokio::fs::read_to_string("/var/lib/archipelago/wireguard/public.key").await {
key.trim().to_string()
} else {
anyhow::bail!("Unexpected keygen output: {}", keygen_output);
vpn::read_nvpn_config_value("node", "public_key").await
.ok_or_else(|| anyhow::anyhow!("Cannot read server public key"))?
};
// Get server's WireGuard public key from nvpn config
let server_pubkey = vpn::read_nvpn_config_value("node", "public_key").await
.ok_or_else(|| anyhow::anyhow!("Cannot read server public key from nvpn config"))?;
// 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()

View File

@@ -38,7 +38,7 @@ pub struct RelayStats {
/// Default relays seeded on first use.
const DEFAULT_RELAYS: &[&str] = &[
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.primal.net",
"wss://relay.nostr.band",
"wss://relay.snort.social",
"wss://nostr.wine",
@@ -451,7 +451,7 @@ mod tests {
let _ = load_relays(tmp.path()).await.unwrap();
toggle_relay(tmp.path(), "wss://relay.damus.io", false).await.unwrap();
toggle_relay(tmp.path(), "wss://nos.lol", false).await.unwrap();
toggle_relay(tmp.path(), "wss://relay.primal.net", false).await.unwrap();
let stats = get_stats(tmp.path()).await.unwrap();
assert_eq!(stats.total_relays, DEFAULT_RELAYS.len());