mmalmi/nostr-vpn

Private mesh VPN for humans and agents. Local keys, signed enrollment, no provider account. Native apps and CLI.

What it is

nostr-vpn is a private mesh VPN — like Tailscale — that lets you connect your own devices, servers, and agents into a private network. It uses Nostr identities (public/private key pairs) and signed device rosters to control who can join. No email, phone number, or VPN provider account is required.

The data plane is powered by an independently evolved implementation of the FIPS protocol (a VPN tunneling protocol), based on the original FIPS project. The project also includes an experimental marketplace where you can buy or sell public exit-node bandwidth, priced per byte and paid in Bitcoin via Cashu.

How it works

  • Enrollment: A device generates a key pair, sends a signed join request, and an admin must explicitly approve it. The admin's app signs and distributes the updated roster. Approval is never automatic.
  • Connections: Peers connect directly when possible (UDP). When direct UDP is unavailable, traffic routes through FIPS neighbors.
  • Built-in features: MagicDNS (name resolution), subnet routes, exit nodes, and WireGuard upstream egress.
  • Performance: The fork is optimized for high-rate VPN traffic. In comparable direct-path benchmarks it delivers roughly 3× the original implementation's throughput by keeping packet ownership, buffers, and session state together, batching I/O and cryptography, reusing packet storage, and avoiding per-packet queue hops, allocations, copies, and repeated lookups. It preserves the FIPS protocol surface.

Paid exits (marketplace)

  • Providers advertise per-byte prices over Nostr; buyers fund Cashu Spilman channels and sign payment updates as they use bandwidth.
  • Uploads count as sent; UDP replies and acknowledged TCP downloads count too, without double-billing TCP retransmissions.
  • Provider selection can be manual or automatic (based on connection quality, trusted ratings, and price).
  • Channels renew before a 12-hour service cutoff; providers begin automatic collection then. Buyers can reclaim uncollected funds after 24 hours.
  • DNS is encrypted by default, but exit nodes still see destination IPs and unencrypted traffic — the README advises using HTTPS for sensitive data.

Components and platforms

  • CLI and daemon: nvpn (installable via cargo install nvpn or prebuilt archives).
  • Native apps: macOS (Apple Silicon), Linux (x64), Windows (x64), Android (arm64), iOS (arm64). Intel macOS is source-only.
  • Server packages: signed StartOS (Start9) .s9pk packages and a multi-architecture Umbrel image.
  • Downloads: git.iris.to releases or the GitHub mirror; iOS via App Store/TestFlight; Android via APK or Zapstore.

Quick start (two devices)

First device (creates the network):

nvpn init
DEVICE_ID='<paste nostr_pubkey from nvpn init>'
nvpn set --device "$DEVICE_ID"
nvpn start --daemon --connect

Second device (joins):

nvpn init
nvpn start --daemon --connect
nvpn join-request

Admin approves in the app: Devices → Add Join Request, scan/paste the request, check details, confirm Add device. The joining daemon must keep running until it receives approval.

Daemon lifecycle: nvpn start --daemon --connect → nvpn status → nvpn stop.

Boot service: sudo nvpn service install --config <path> (Linux/macOS). Use the same --config path with status, set, and service status commands; run with sudo if the service owns the config/state files. On Windows, run from an elevated shell.

Containers and headless nodes

  • Use a Linux CLI archive matching the container architecture; persist the config directory (including key sidecars) and pass the same --config path to every command.
  • Requires: root, /dev/net/tun, NET_ADMIN, iproute2, plus nftables (exit protection) and iptables (exit forwarding and legacy-rule cleanup).
  • Container restrictions can block route cleanup (e.g., a read-only /proc/sys/net/ipv4/route/flush makes ip route flush cache fail). Privileged mode is a workaround for isolated test containers, not a universal production requirement.
  • Without systemd-resolved, split MagicDNS may only be available at the local address/port printed in the log (normally 127.0.0.1:1053) — that does not make names resolve through the system resolver automatically.
  • For health checks, use nvpn start --daemon --connect and require status_source == "daemon" in nvpn status --json. Foreground nvpn connect sessions do not publish daemon state.

Build and verify

just build
just run
just verify-fast

(just run-macos / just run-linux for specific desktop targets; see docs/verification-tiers.md for broader checks.)

Documentation links

  • Protocol details: docs/protocol.md (enrollment, roster sync, routing, DNS privacy)
  • StartOS packaging: CONTRIBUTING.md
  • Changelog: CHANGELOG.md
  • Security reports: SECURITY.md
  • Performance experiments: docs/EXPERIMENTS.md
  • Platform implementation status: docs/native-ui-parity-matrix.md

Canonical repository: git.iris.to (GitHub is a mirror).

Related

  • Project
  • Project
  • Project
  • Project