From e97fee2d7e10362e47dd71e624ea3a14647464dc Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 7 Apr 2026 14:40:33 +0100 Subject: [PATCH] feat: NostrVPN as native system service, Claude API key input, fix duplicate password - Add NostrVPN as a native systemd service (extracted from container) - Add VPN status detection for nostr-vpn in backend vpn.rs - ISO build extracts nvpn binary from container image - First-boot auto-configures NostrVPN with node's Nostr identity - Change Claude Auth from login iframe to API key input field - Remove duplicate ChangePasswordSection from Settings.vue - FIPS and Routstr remain as installable container apps Co-Authored-By: Claude Opus 4.6 (1M context) --- core/archipelago/src/vpn.rs | 101 +++++++++++ image-recipe/build-auto-installer-iso.sh | 30 ++++ image-recipe/configs/nostr-vpn.service | 20 +++ neode-ui/src/views/Settings.vue | 2 - .../src/views/settings/ClaudeAuthSection.vue | 162 +++++++++--------- .../src/views/settings/VpnStatusSection.vue | 98 +++++++++++ scripts/first-boot-containers.sh | 20 +++ 7 files changed, 348 insertions(+), 85 deletions(-) create mode 100644 image-recipe/configs/nostr-vpn.service create mode 100644 neode-ui/src/views/settings/VpnStatusSection.vue diff --git a/core/archipelago/src/vpn.rs b/core/archipelago/src/vpn.rs index ea705eba..c7d24072 100644 --- a/core/archipelago/src/vpn.rs +++ b/core/archipelago/src/vpn.rs @@ -16,6 +16,7 @@ const VPN_CONFIG_FILE: &str = "vpn-config.json"; pub enum VpnProvider { Tailscale, Wireguard, + NostrVpn, } /// Persisted VPN configuration. @@ -172,6 +173,11 @@ pub fn generate_wireguard_conf(config: &WireGuardConfig) -> String { /// Get the current VPN status by checking network interfaces. pub async fn get_status() -> VpnStatus { + // Check for NostrVPN (native system service) + if let Ok(nvpn) = get_nostr_vpn_status().await { + return nvpn; + } + // Check for Tailscale interface if let Ok(tailscale) = get_tailscale_status().await { return tailscale; @@ -194,6 +200,101 @@ pub async fn get_status() -> VpnStatus { } } +/// Check if NostrVPN system service is running and get its status. +async fn get_nostr_vpn_status() -> Result { + // Check if nostr-vpn service is active + let active = tokio::process::Command::new("systemctl") + .args(["is-active", "nostr-vpn"]) + .output() + .await + .map(|o| o.status.success()) + .unwrap_or(false); + + if !active { + anyhow::bail!("nostr-vpn service not active"); + } + + // Try to get status from nvpn CLI + let output = tokio::process::Command::new("nvpn") + .arg("status") + .output() + .await; + + let (peers, ip) = match output { + Ok(o) if o.status.success() => { + let stdout = String::from_utf8_lossy(&o.stdout); + let peers = stdout.lines() + .filter(|l| l.contains("peer") || l.contains("connected")) + .count() as u32; + let ip = stdout.lines() + .find(|l| l.contains("address") || l.contains("ip")) + .and_then(|l| l.split_whitespace().last()) + .map(|s| s.to_string()); + (peers, ip) + } + _ => (0, None), + }; + + Ok(VpnStatus { + connected: true, + provider: Some("nostr-vpn".to_string()), + interface: Some("nvpn0".to_string()), + ip_address: ip, + hostname: None, + peers_connected: peers, + bytes_in: 0, + bytes_out: 0, + }) +} + +/// Configure NostrVPN with the node's Nostr identity. +pub async fn configure_nostr_vpn(data_dir: &Path) -> Result<()> { + let nostr_secret = tokio::fs::read_to_string( + data_dir.join("identity/nostr_secret") + ).await.context("No Nostr secret key — complete onboarding first")?; + + let nostr_pubkey = tokio::fs::read_to_string( + data_dir.join("identity/nostr_pubkey") + ).await.unwrap_or_default(); + + let vpn_dir = data_dir.join("nostr-vpn"); + tokio::fs::create_dir_all(&vpn_dir).await.context("Failed to create nostr-vpn dir")?; + + // Write env file for the systemd service + let env_content = format!( + "NOSTR_SECRET={}\nNOSTR_PUBKEY={}\n", + nostr_secret.trim(), + nostr_pubkey.trim() + ); + tokio::fs::write(vpn_dir.join("env"), &env_content) + .await + .context("Failed to write nostr-vpn env")?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions( + vpn_dir.join("env"), + std::fs::Permissions::from_mode(0o600), + ).ok(); + } + + // Enable and start the service + tokio::process::Command::new("systemctl") + .args(["enable", "--now", "nostr-vpn"]) + .output() + .await + .context("Failed to enable nostr-vpn service")?; + + let mut config = load_config(data_dir).await?; + config.provider = VpnProvider::NostrVpn; + config.enabled = true; + config.configured_at = Some(chrono::Utc::now().to_rfc3339()); + save_config(data_dir, &config).await?; + + Ok(()) +} + async fn get_tailscale_status() -> Result { // Check if tailscale0 interface exists let output = tokio::process::Command::new("ip") diff --git a/image-recipe/build-auto-installer-iso.sh b/image-recipe/build-auto-installer-iso.sh index 18d36f20..b9becc72 100755 --- a/image-recipe/build-auto-installer-iso.sh +++ b/image-recipe/build-auto-installer-iso.sh @@ -365,6 +365,7 @@ COPY archipelago-reconcile.service /etc/systemd/system/archipelago-reconcile.ser COPY archipelago-reconcile.timer /etc/systemd/system/archipelago-reconcile.timer COPY archipelago-tor-helper.service /etc/systemd/system/archipelago-tor-helper.service COPY archipelago-tor-helper.path /etc/systemd/system/archipelago-tor-helper.path +COPY nostr-vpn.service /etc/systemd/system/nostr-vpn.service # Copy container doctor + reconcile scripts (referenced by the services above) RUN mkdir -p /home/archipelago/archy/scripts/lib @@ -474,6 +475,12 @@ NGINXCONF echo " Using tor-helper path unit from configs/" fi + # Copy NostrVPN system service (native mesh VPN, not a container) + if [ -f "$SCRIPT_DIR/configs/nostr-vpn.service" ]; then + cp "$SCRIPT_DIR/configs/nostr-vpn.service" "$WORK_DIR/nostr-vpn.service" + echo " Using nostr-vpn.service from configs/" + fi + # Use archipelago.service from configs/ (User=root for Podman container access) if [ -f "$SCRIPT_DIR/configs/archipelago.service" ]; then cp "$SCRIPT_DIR/configs/archipelago.service" "$WORK_DIR/archipelago.service" @@ -923,6 +930,29 @@ BACKENDFILE fi fi +# Extract NostrVPN binary from container image (native system service, not a container app) +echo " Extracting NostrVPN binary..." +NVPN_IMAGE="$($CONTAINER_CMD images -q 80.71.235.15:3000/archipelago/nostr-vpn:v0.3.4 2>/dev/null)" +if [ -z "$NVPN_IMAGE" ]; then + $CONTAINER_CMD pull 80.71.235.15:3000/archipelago/nostr-vpn:v0.3.4 2>/dev/null || true +fi +NVPN_CONTAINER=$($CONTAINER_CMD create 80.71.235.15:3000/archipelago/nostr-vpn:v0.3.4 2>/dev/null) || true +if [ -n "$NVPN_CONTAINER" ]; then + $CONTAINER_CMD cp "$NVPN_CONTAINER:/usr/local/bin/nvpn" "$ARCH_DIR/bin/nvpn" 2>/dev/null && \ + chmod +x "$ARCH_DIR/bin/nvpn" && \ + echo " ✅ NostrVPN binary extracted ($(du -h "$ARCH_DIR/bin/nvpn" | cut -f1))" + $CONTAINER_CMD rm "$NVPN_CONTAINER" 2>/dev/null || true +else + echo " ⚠ NostrVPN image not available — nvpn binary will be missing" +fi + +# Copy NostrVPN UI dashboard for nginx serving +if [ -d "$SCRIPT_DIR/../docker/nostr-vpn-ui" ]; then + mkdir -p "$ARCH_DIR/web-ui/nostr-vpn" + cp "$SCRIPT_DIR/../docker/nostr-vpn-ui/index.html" "$ARCH_DIR/web-ui/nostr-vpn/" + echo " ✅ NostrVPN UI dashboard included" +fi + # Capture web UI from live server if [ "$BUILD_FROM_SOURCE" = "1" ]; then echo " Building web UI from source..." diff --git a/image-recipe/configs/nostr-vpn.service b/image-recipe/configs/nostr-vpn.service new file mode 100644 index 00000000..65ac0dca --- /dev/null +++ b/image-recipe/configs/nostr-vpn.service @@ -0,0 +1,20 @@ +[Unit] +Description=Nostr VPN - Mesh VPN with Nostr signaling +After=network-online.target tor.service +Wants=network-online.target + +[Service] +Type=simple +User=root +EnvironmentFile=-/var/lib/archipelago/nostr-vpn/env +ExecStart=/usr/local/bin/nvpn daemon +Restart=on-failure +RestartSec=10 +TimeoutStopSec=10 + +# Logging +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/neode-ui/src/views/Settings.vue b/neode-ui/src/views/Settings.vue index 73601951..f5506d86 100644 --- a/neode-ui/src/views/Settings.vue +++ b/neode-ui/src/views/Settings.vue @@ -1,13 +1,11 @@ diff --git a/neode-ui/src/views/settings/ClaudeAuthSection.vue b/neode-ui/src/views/settings/ClaudeAuthSection.vue index 1a02cbaa..3c00f943 100644 --- a/neode-ui/src/views/settings/ClaudeAuthSection.vue +++ b/neode-ui/src/views/settings/ClaudeAuthSection.vue @@ -1,104 +1,100 @@