feat: NostrVPN as native system service, Claude API key input, fix duplicate password
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Failing after 1m0s

- 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) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-07 14:40:33 +01:00
parent dc6496e693
commit e97fee2d7e
7 changed files with 348 additions and 85 deletions

View File

@@ -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<VpnStatus> {
// 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<VpnStatus> {
// Check if tailscale0 interface exists
let output = tokio::process::Command::new("ip")