Skip to content

mmalmi/nostr-vpn

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

660 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nostr-vpn

Main development is on decentralized git: htree://npub1xdhnr9mrv47kkrn95k6cwecearydeh8e895990n3acntwvmgk2dsdeeycm/nostr-vpn

Downloads

Current release artifacts:

  • Native apps for Apple Silicon macOS, Linux x64, Windows x64, and Android arm64
  • Headless CLI archives for Apple Silicon macOS, Windows x64, Linux x86_64, and Linux arm64
  • The nvpn CLI crate on crates.io: cargo install nvpn
  • iOS TestFlight beta (link exists, public access is not live yet)

Intel macOS remains source-only. iOS builds from source and simulator today; the TestFlight link exists, but public beta access is still pending.

Overview

nostr-vpn is a Rust workspace for a Tailscale-style private mesh VPN built around a FIPS-backed data plane. It includes the nvpn CLI/daemon, a shared native app core, and native platform shells.

Current benchmarks put the FIPS data plane in WireGuard-level territory on Linux and macOS: Linux Docker runs reach roughly 2.8-3.0 Gbit/s TCP and 1 Gbit/s UDP, and macOS LAN runs now match or beat Tailscale in tested directions while the remaining known gap is a Darwin Wi-Fi sender cap. See docs/EXPERIMENTS.md for the raw bench notes.

Nostr VPN desktop app showing a connected network, device identity, status badges, and join controls.

It currently ships:

Component Purpose
nvpn Main CLI for config, daemon lifecycle, networking, diagnostics, and tunnel sessions
nostr-vpn-core Shared library for config, FIPS control state, NAT helpers, diagnostics, and MagicDNS
nostr-vpn-app-core Native app state/action contract and UniFFI bridge used by the Rust-core/native-front rewrite
macos SwiftUI/AppKit native shell over nostr-vpn-app-core
linux GTK/libadwaita native shell over the shared app core
windows WPF native shell and installer over the shared app core
android / ios Native mobile shells with shared Rust state/action and packet-tunnel scaffolding

Getting Started

Install the CLI from crates.io:

cargo install nvpn

Create a local identity/config, then share or import an invite:

nvpn init
nvpn create-invite
nvpn import-invite 'nvpn://invite/...'

Start a foreground tunnel session:

nvpn start --connect

For the background daemon flow used by the desktop apps:

nvpn start --daemon --connect
nvpn status
nvpn stop

For persistent startup through the OS service manager:

sudo nvpn service install
nvpn service status

On Windows, run nvpn service install from an elevated shell instead of using sudo.

Protocol

For the current protocol-level description of invites, admin roster sync, and the FIPS mesh data plane, see docs/protocol.md.

Private mesh traffic defaults to FIPS. nvpn uses the configured VPN participants as the overlay route map, but FIPS connectivity is a separate underlay: FIPS peers can be found through Nostr discovery or supplied as configured fips_peer_endpoints, and those FIPS peers may relay packets even when they are not members of the same VPN. Direct UDP/NAT failure can fall back through established FIPS neighbors, while nvpn still only admits private traffic for the active network roster.

Platform status

Platform Current status
Apple Silicon macOS Native SwiftUI/AppKit desktop app, signed/notarized release artifacts when credentials are configured, CLI tarball
Linux x64 Native GTK/libadwaita desktop app packaged as .deb, CLI tarballs for x86_64 and arm64, Docker e2e coverage
Windows x64 Native WPF desktop app installer, batched WinTun tunnel path, native WireGuard-upstream routing, CLI zip
Android arm64 Native app-core UI, signed APK/AAB release artifacts when signing is configured, VPN runtime still being hardened
iOS Native SwiftUI app builds/runs from source and simulator, NetworkExtension target exists, TestFlight link exists but public beta access is pending
Intel macOS Source-only

What the project does today

  • Generates Nostr identity keys automatically
  • Stores a single app config with one or more named networks, each with participant allowlists and its own stable mesh ID
  • Brings up FIPS private mesh tunnels for private network traffic
  • Routes private FIPS traffic directly when possible and through FIPS neighbors when direct discovery fails
  • Tracks FIPS peer/link state and NAT-discovered public endpoints
  • Supports route advertisement and exit-node selection
  • Supports WireGuard upstream configs for local egress and exit-node providers on Linux, macOS, and Windows
  • Exposes JSON status, network diagnostics, and doctor bundles
  • Includes native macOS and Linux GUIs with service-first session control, invite QR/import flows, tray/menu-bar integration, MagicDNS controls, health reporting, and port-mapping status
  • Includes a native Windows WPF shell with tray integration, installer packaging, and the same shared app-core action/state contract
  • Includes LAN invite broadcast/discovery, CLI self-update, desktop updater e2e coverage, and Linux-focused Docker e2e coverage for FIPS mesh formation, NAT traversal, routed UDP, safe MTU, and WireGuard upstream egress

Config model

By default, nvpn uses the OS config directory:

  • Linux: ~/.config/nvpn/config.toml
  • macOS: ~/Library/Application Support/nvpn/config.toml
  • Fallback when no config dir is available: ./nvpn.toml

nvpn init creates that file if it does not exist and generates keys automatically.

The config contains:

  • global app settings such as autoconnect, tray behavior, and MagicDNS suffix
  • Nostr settings used by FIPS discovery, including relay URLs and identity keys
  • NAT settings including STUN servers and discovery timeout
  • node settings including endpoint, tunnel IP, listen port, and advertised routes
  • a [[networks]] list of named participant sets with one active network at a time

Each [[networks]] entry carries its own network_id, which is the mesh identity used for roster scope and auto-derived tunnel addressing. If an older config still only has the legacy top-level default, nostr-vpn promotes it into per-network stable IDs and then stops recomputing them on participant changes.

Nodes that should talk to each other must share the same network_id and list each other as participants. Only the active network participates in the live runtime; inactive networks stay saved for later activation.

Build and validate

Prerequisites:

  • Rust stable
  • OS permissions to create tunnel interfaces when running real sessions
  • On Linux Docker e2e: Docker with Compose and /dev/net/tun

The normal Rust gate is:

cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace

Additional automation:

  • .github/workflows/windows-smoke.yml can manually build the Windows CLI on windows-latest
  • .github/workflows/release.yml publishes CLI archives plus native macOS, Linux, Windows, and Android app artifacts when signing/package credentials are configured
  • scripts/local-release.mjs builds local release artifacts and stages a hashtree-style release directory that can be published to releases/nostr-vpn
  • just release-gate also runs version sync, CLI update e2e, routed-FIPS Docker e2e, and safe-MTU Docker e2e

Local release

Typical flow:

cp .env.release.example .env.release.local
$EDITOR .env.release.local
just release-publish

Notes:

  • .env.release.local is local-only and gitignored
  • the script auto-loads .env.release.local when present
  • shell environment variables override values from those files
  • on Apple Silicon macOS it can build the native macOS app/CLI locally and Windows CLI/installer artifacts through a reachable Windows VM
  • Linux release artifacts include the native .deb package and musl CLI tarball
  • Windows VM selection can be forced with NVPN_WINDOWS_VM_NAME; otherwise the script auto-detects a single running Windows guest
  • by default it runs the same sync/version, cargo fmt --check, cargo clippy, cargo test, update, and Docker release-gate steps as the release workflow
  • staged local release notes include the matching CHANGELOG.md section for the release tag, so update CHANGELOG.md before publishing
  • omit --publish if you only want staged release metadata under the local temp directory

Native macOS App

Build the native app for this machine:

just build

For macOS specifically, just build runs just macos-build. After a successful build, it prints the app output path.

Launch the native macOS shell:

just run

If you only want the CLI and test binaries:

cargo build -p nvpn

Install nvpn

Latest releases publish CLI archives for Apple Silicon macOS, Windows x64, Linux x86_64, and Linux arm64. The quick installer below auto-detects only Apple Silicon macOS and Linux:

case "$(uname -s)/$(uname -m)" in
  Darwin/arm64) ASSET=nvpn-aarch64-apple-darwin.tar.gz ;;
  Linux/x86_64) ASSET=nvpn-x86_64-unknown-linux-musl.tar.gz ;;
  Linux/aarch64|Linux/arm64) ASSET=nvpn-aarch64-unknown-linux-musl.tar.gz ;;
  Darwin/x86_64)
    echo "No prebuilt Intel macOS release is currently published. Build from source or use an older release." >&2
    exit 1
    ;;
  *)
    echo "Unsupported platform: $(uname -s)/$(uname -m)" >&2
    exit 1
    ;;
esac
curl -fsSL "https://github.com/mmalmi/nostr-vpn/releases/latest/download/${ASSET}" | tar -xz && cd nvpn && ./install.sh

That command supports Apple Silicon macOS and Linux. On Intel macOS it exits with a clear message. The installer creates the target directory when needed and defaults to /opt/homebrew/bin on Apple Silicon macOS when that location exists or is already in PATH; otherwise it uses /usr/local/bin.

The quick-install line still points at GitHub until a verified releases/nostr-vpn/latest tree is published on hashtree. htree release publish already maintains the latest alias automatically; the missing step is publishing the release tree and verifying the public upload.iris.to/<npub>/releases/nostr-vpn/latest/... paths.

On Windows, download the nvpn-<version>-x86_64-pc-windows-msvc.zip release asset and run nvpn.exe, or build from source.

From crates.io:

cargo install nvpn

From source:

cargo install --path crates/nostr-vpn-cli --bin nvpn

cargo install nvpn installs the published CLI/daemon binary. The path-based command is useful when developing from a checkout and remains the supported route on Intel macOS until prebuilt release archives cover that target.

If you already have a release tarball, extract it and run:

./install.sh

You can also pass a custom destination directory, for example ./install.sh ~/.local/bin.

CLI quickstart

Create or refresh config and generate keys:

nvpn init \
  --participant npub1...alice \
  --participant npub1...bob

Adjust persisted settings if needed:

nvpn set \
  --endpoint 192.0.2.10:51820 \
  --listen-port 51820 \
  --fips-advertise-endpoint true \
  --fips-peer-endpoint npub1...bob=192.0.2.11:51820 \
  --tunnel-ip 10.44.0.10/32

Run a full foreground session:

nvpn start --connect

Daemonized flow used by native desktop apps:

nvpn start --daemon --connect
nvpn pause
nvpn resume
nvpn stop

For persistent privileged startup:

sudo nvpn service install
nvpn service status

On Windows, run nvpn service install from an elevated shell instead of using sudo.

The service implementation targets:

  • macOS via launchd
  • Linux via systemd
  • Windows via the Service Control Manager (sc.exe)

nvpn service enable / nvpn service disable are currently implemented only on macOS. On Linux and Windows, install / uninstall handle the persistent service lifecycle directly.

Inspect runtime state:

nvpn status --json
nvpn doctor --json

Write a support bundle:

nvpn doctor --write-bundle /tmp/nvpn-doctor

Advertise routes or use an exit node:

nvpn set --advertise-routes 10.0.0.0/24,192.168.0.0/24
nvpn set --advertise-exit-node true
nvpn set --exit-node npub1...peer

Use a WireGuard upstream for the local device:

nvpn set --wireguard-exit-enabled true --wireguard-exit-config-file ./wg.conf

If this device should also serve as a mesh exit node, enable both:

nvpn set --advertise-exit-node true --wireguard-exit-enabled true

Members still see this as the same FIPS exit node; the provider's own default internet route and forwarded member exit traffic both use the WireGuard upstream.

Clear exit-node selection:

nvpn set --exit-node off

Lower-level commands:

  • init
  • version
  • update
  • install-cli
  • uninstall-cli
  • service
  • start
  • stop
  • repair-network
  • reload
  • pause
  • resume
  • connect
  • status
  • set
  • create-invite
  • import-invite
  • invite-broadcast
  • discover
  • add-participant
  • remove-participant
  • add-admin
  • remove-admin
  • ping
  • doctor
  • ip
  • whois
  • wg-upstream-test

Native Apps

Native app work is split into a Rust-owned state/action core and platform shells.

The native UI rewrite parity target is tracked in docs/native-ui-parity-matrix.md. The shared native app contract lives in crates/nostr-vpn-app-core, with native shell targets under macos, windows, linux, android, and ios. The local native shell can be built with just build and launched with just run. Use just run-macos or just run-linux when you want a specific desktop target.

Notes:

  • desktop shells use the installed/bundled nvpn binary for privileged service/session work
  • mobile shells share the Rust app contract and platform packet-tunnel scaffolding while the full mobile data-plane path is still being hardened
  • the legacy Tauri/Svelte app was removed after the native rewrite became the canonical architecture

Docker end-to-end coverage

Run the focused security regression kit:

just security-regressions

Docker e2e scripts under scripts/:

  • ./scripts/e2e-docker.sh Verifies static FIPS peer configuration, mesh formation, and tunnel ping.
  • ./scripts/e2e-connect-docker.sh Verifies config-driven nvpn connect, FIPS mesh formation, and tunnel ping.
  • ./scripts/e2e-active-network-docker.sh Verifies that inactive saved networks do not change the active mesh identity, expected peer count, or auto-derived tunnel IP.
  • ./scripts/e2e-divergent-roster-docker.sh Verifies that peers with a shared mesh ID can still connect when one node has extra configured participants.
  • ./scripts/e2e-fips-routed-udp-docker.sh Verifies that peers can move tunnel payloads through a FIPS neighbor when their direct UDP path is blocked.
  • ./scripts/e2e-fips-nat-safe-mtu-docker.sh Verifies safe-MTU traffic across a NAT-shaped FIPS mesh path.
  • ./scripts/e2e-exit-node-docker.sh Verifies exit-node advertisement, selection, tunnel traffic to the chosen exit node, and default-route traffic crossing the exit path to an external target. Set NVPN_EXIT_NODE_E2E_PUBLIC_IP=9.9.9.9 (or another reachable public IP) to also prove a real internet hop routes through the tunnel.
  • ./scripts/e2e-wireguard-exit-docker.sh Verifies WireGuard-upstream egress and guards against upstream-initiated ingress into nvpn peers, including a hostile upstream route for the mesh tunnel range.
  • ./scripts/e2e-wireguard-exit-userspace-docker.sh Verifies the standalone userspace WireGuard-upstream probe, scoped-host route, and guarded default-route replacement.

These flows are Linux-oriented because they require real tunnel devices and container networking privileges.

Desktop update end-to-end coverage

Desktop updater scripts under scripts/:

  • ./scripts/e2e-update-desktop.sh Runs the macOS app updater path, Linux GTK updater path in Docker, and Windows WPF updater path in a Parallels VM.
  • ./scripts/e2e-update-macos.sh Builds the macOS app, checks a local release manifest, downloads a fake .app.tar.gz, and verifies the app bundle can be unpacked.
  • ./scripts/e2e-update-linux.sh Runs the Linux app in the Docker dev image, checks a local release manifest, downloads the selected AppImage, and verifies it is executable.
  • ./scripts/e2e-update-windows-vm.sh Runs scripts/e2e-update-windows.ps1 inside the Windows VM, builds the WPF app, checks a local release manifest, and verifies the selected setup executable downloads.
  • ./tools/run-windows On macOS, builds and runs the Windows app inside the running Parallels Windows VM. The macOS host is not expected to have PowerShell or .NET installed.

The update E2E scripts set NVPN_UPDATE_MANIFEST_URL to a local fixture and suppress opening installers/packages, so they test update selection and download/install preparation without touching production release storage.

Workspace layout

Release workflow notes

Release workflow (.github/workflows/release.yml):

  • runs on pushed v* tags or manual dispatch
  • verifies sync-versions, formatting, clippy, Rust tests, CLI update e2e, routed-FIPS Docker e2e, and safe-MTU Docker e2e before publishing artifacts
  • publishes CLI archives for Apple Silicon macOS, Windows x64, Linux x86_64, and Linux arm64
  • publishes Apple Silicon macOS as a signed/notarized DMG plus .app.tar.gz updater archive when signing is configured
  • publishes Linux x64 .deb, Windows x64 setup .exe, and signed Android arm64 APK/AAB artifacts
  • requires platform signing/package secrets before release app artifacts can publish
  • generates its GitHub release notes in the workflow; local scripts/local-release.mjs release notes include the matching CHANGELOG.md section for the tag

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors