Files
tsunagi/README.md
T
Aleksandr Bogomiakov 23af1a2d9d Add shared TCP and UDP virtual services
Provide named VIPs with independent protocol/port listeners and node/port
backends so applications can use one DNS endpoint without a separate load
balancer. Every authenticated member may create, edit or delete any service.
Client agents select the eligible backend with the lowest measured encrypted
data-plane RTT, including relayed paths, and pin existing flows. UDP recovers
on another backend after failure; established TCP is never replayed or moved.
An unanswered empty TCP SYN may retry another backend before establishment.

Keep signed multi-writer field registers in state/vip.rs. Independent backend
edits merge, same-field conflicts use Lamport/author/canonical ordering, and
listener generations prevent old backend entries returning on port recreation.
Permanent service tombstones protect replay while allowing obsolete fields to
be compacted. Active-service and deletion-history limits are separate. Commit
complete snapshots and the author's durable counter atomically before local
routing changes or publication. Restore counters from newer genuine author
updates learned from replicas after an old backup is restored.

Implement bounded client-side address/port NAT at overlay ingress and reverse
translation only after authenticated source/destination admission. Reserve OS
source ports to avoid direct-flow collisions and quarantine retired ports for
late replies. Handle a backend on the client itself, scoped fragment assembly,
checksums, re-fragmentation and quoted ICMP/PMTU headers. Plugins and encrypted
transit retain their existing contracts. Data-path probes measure RTT; bounded
fair TCP connect checks are enabled by default and configurable per listener.
Arbitrary UDP application readiness remains unknown; path checks and explicit
port errors drive eligibility without protocol-specific probe payloads.

Expose the shared draft API through local IPC, simple vip/listener/backend CLI
commands, JSON, editor-based atomic updates and watch output. Add a VIP button
on each network tile and a separate tray editor. Edits and removals stay local
until Save; stale revisions retain the draft, and closing a dirty window offers
explicit discard. Report local routing choices, health reasons, RTT age, active
and draining bindings, IP counters, rates, drops and UDP failovers. Extend DNS
with service names; keep numeric addresses secondary and preserve existing
bindings while VIP addresses or listeners change.

This changes control wire ALPN/version to tsunagi/ctrl/8 and local IPC to 22;
all members and local clients must upgrade together. Data ALPN remains 5.
SQLite schema 10 migrates existing mandatory state without changing identity,
secrets, network ids, derivation labels, handshake encoding or node records.
Crate versions and release tags are unchanged. No installed agent or tray was
replaced. Commands, limits and operational semantics are in docs/vip.md.

Checks on Linux in nix develop:
- cargo fmt --all -- --check
- cargo clippy --locked --workspace --all-targets -- -D warnings
- cargo test --locked --workspace --all-targets: 559 passed, 3 ignored
- git diff --cached --check

The full suite includes real encrypted TCP/UDP VIP traffic, backend-stop UDP
failover, real loopback TCP check recovery, source admission, signed merges,
stale/deleted state, SQLite migration and rollback, restart/author recovery,
network isolation, actual CLI/DNS processes, and headless text-entry/Save-button
interaction. Discovery and memory TUNs are the existing test substitutions;
authentication, transport, encryption and SQLite remain real.

Initial checks outside Nix failed for missing GTK pkg-config metadata, and an
initial CLI test inherited TSUNAGI_CONTROL_SOCKET. Final checks use the Nix
environment with that variable unset so tests address their temporary sockets.
Root/internet-dependent tests remain ignored. No live host TUN, native desktop
session, macOS or Windows validation was performed.
2026-10-21 15:40:45 +01:00

12 KiB
Raw Permalink Blame History

tsunagi

A mesh VPN with no server.
Pick a network name, share a secret, and your machines are on one private network.

release CI license

laptop$ tsng join -n home
joined `home`
  secret  tsn1u7c…

server$ tsng join -n home -s tsn1u7c…
server$ tsng id hostname server

laptop$ ssh server.home

No account, no coordination server, no admin console, no keys to approve. Anyone who knows the name and the secret is in. Nobody else can find the network, let alone enter it.

Why

Tailscale, Headscale and ZeroTier are great, but each needs a control server: theirs, or one you have to run and keep alive. tsunagi doesn't. Machines find each other through the public BitTorrent DHT, connect directly (hole punching, with relay fallback), and keep the network state among themselves.

Tailscale Headscale ZeroTier tsunagi
Control server hosted by vendor you run it hosted or self-run none
Account / sign-in yes yes yes no
To join a network sign in, get approved pre-auth key network ID, approval name + secret
Direct connections yes yes yes yes
Exit nodes yes yes via routes yes

Features

  • Two things to know: a network name and a secret. A secret is generated for you when you make a network.
  • Direct, encrypted links between members (WireGuard cryptography), with relay fallback behind strict NATs.
  • Names for machines: server.home just works, even while server is switched off. A name belongs to whoever claimed it first; a later device with the same name keeps working by address and is told so.
  • Exit nodes: send all your internet traffic through any member that offers one.
  • Multi-hop: members that can't reach each other directly are routed through a member that can, over whatever kind of link each hop has. A member reached that way is online, and shown as relayed; traffic moves to a direct link by itself the moment one comes up.
  • LAN games: UDP broadcasts are relayed, so "LAN" lobbies show up across the mesh.
  • Several networks at once on one device.
  • Tray app for Windows, macOS and Linux; a command line for everything.

Install

Grab a build from the latest release.

Platform How
Windows Run tsunagi-setup-<version>-x86_64.exe. Installs the service, the tray and tsng.
macOS brew tap house-of-vanity/tap && brew install --cask tsunagi-gui
CLI only: brew install tsunagi
Debian / Ubuntu sudo apt install ./tsunagi-gui_<version>_amd64.deb (tsunagi_… for no tray)
Arch sudo pacman -U tsunagi-gui-<version>-1-x86_64.pkg.tar.zst
NixOS Flake and module below
Any other Linux Unpack the tarball, then sudo ./install.sh && sudo systemctl enable --now tsunagi

Linux packages run the agent as an unprivileged service user. Add yourself to the tsunagi group (sudo usermod -aG tsunagi $USER, then log in again) to use tsng and the tray without sudo.

Quick start

tsng join -n home                 # first machine: makes the network and prints a secret
tsng join -n home -s tsn1…        # every other machine
tsng status                       # who is online, over which path

Every new network gets a stable pseudorandom /24 in 10.0.0.0/8, derived from its network id. Its members agree on that range even when starting offline, and each gets a name like <host>.home. Existing signed ranges are preserved.

If two configured networks have overlapping ranges, the agent runs only one. tsng status, tsng network and the tray show which network blocks the other. tsng network start <name-or-id> or the tray’s “Switch to this network” button suspends the conflicting networks. They resume automatically when the selected network stops; the selection survives restarting the agent. Explicitly stopping a suspended network cancels its automatic resume.

Machines are called tsunagi-<id> until you rename them: tsng id hostname laptop.

Virtual services

tsng vip create home api
tsng vip port add home api tcp/443 --backend server1:6443 --backend server2:7443
tsng vip status home api --watch

Use api.home:443. Each client selects the available backend with the lowest measured data-plane RTT. A VIP can have multiple independent TCP and UDP ports. Every member may edit it; the tray's VIP button opens a draft editor with explicit Save. Commands, health, metrics and limits.

Exit nodes

tsng network exit-node home on    # on the machine that should be the exit
tsng exit-node                    # on another one: who offers an exit
tsng exit-node laptop             # send everything through it
tsng exit-node off

The tray does the same with a checkbox. An exit node sees everything you send through it, so use only members you trust. Details and per-OS notes.

The default data transport order is wg,awg,wg-quic,tcp-tls. AWG negotiates a fresh masking profile and outer keys for each pair connection attempt, including reentry after fallback. Sustained data loss triggers switching independently of the iroh control connection. Configuration and limits.

Change a network's protocol selection without editing its service or restarting:

tsng protocols                         # available protocols and settings
tsng protocols home                    # this device's choice for home
tsng protocols home awg,wg-quic         # prefer AWG, retain QUIC as fallback
tsng protocols home none                # control plane only

Exact network names, full ids and unique id prefixes work here. The choice is local to this device, saved per network and applied immediately; an offline change applies on the next start. In the tray, expand protocols on a network tile to toggle protocols or change their preference order. Both endpoints need a common protocol at the same wire version. tsng up --protocol supplies the initial defaults for networks without a saved choice; it does not restrict which built-in protocols can be enabled later.

Remove an obsolete offline device from a network's shared history:

tsng network forget-node home old-laptop
tsng network forget-node home 10.13.37.143

Networks accept exact names, full ids or unique id prefixes; nodes accept those id selectors, exact hostnames or overlay IPv4 addresses. Ambiguous selectors change nothing. Start the network first if it is stopped. In Show devices, click Forget beside an offline node and confirm its identity.

Every member has the same cleanup right. The signed cleanup frees the observed claim's name and IPv4 reservation, survives restarts and reaches offline replicas when they reconnect. It does not ban the device: a newer signed claim can return. Online nodes and the currently selected exit node cannot be forgotten. State and limits.

Platforms

Platform Build CLI tsng Tray Exit node: offer / use Packages
Linux x86_64 release ✔ ✔ ✔ / ✔ .deb, .pkg.tar.zst, tarball, Nix
Linux aarch64 release ✔ –¹ ✔ / ✔ .deb, .pkg.tar.zst, tarball, Nix
Linux armv7 release ✔ –¹ ✔ / ✔ .deb, .pkg.tar.zst, tarball
macOS, Apple silicon release ✔ ✔ ✔ / ✔² Homebrew, tarball
macOS, Intel from source ✔³ ✔³ ✔ / ✔²³ –
Windows x86_64 release ✔ ✔ ✔ / ✔² installer, zip

¹ Not in the release; builds from source (and through Nix). ² Newer and less battle-tested than on Linux. ³ Builds from source, not covered by CI.

The Linux CLI is a single static binary: it runs on any distribution.

NixOS

The repository is a flake with a package and a module.

# flake.nix
{
  inputs.tsunagi.url = "github:house-of-vanity/tsunagi";

  outputs = { nixpkgs, tsunagi, ... }: {
    nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
      modules = [
        tsunagi.nixosModules.default
        {
          services.tsunagi = {
            enable = true;
            tray.enable = true;      # optional, for desktops
            users = [ "alice" ];     # may use tsng and the tray without sudo
          };
        }
      ];
    };
  };
}

Just trying it: nix run github:house-of-vanity/tsunagi#tsunagi-cli -- --help. packages.<system>.tsunagi (with the tray) and tsunagi-cli are also exported, along with overlays.default.

Build from source

Rust 1.91 or newer.

cargo build --release --bin tsng               # the agent and CLI
cargo build --release --bin tsunagi-tray       # the tray (Linux needs GTK 3 and libayatana-appindicator)

Windows needs Wintun next to tsng.exe. Per-platform notes: Linux, Arch, macOS, Windows.

How it works

  • Your name and secret derive a network identity. Members publish signed contact records to the public Mainline DHT under it, and find each other there.
  • Every connection proves knowledge of the secret before it's accepted. Traffic between members is end-to-end encrypted, relays included.
  • Addresses and names are claimed with each device's own key and replicated between members. There is no one in charge, so nothing to be taken down.

The secret is the only key, so make it a good one (the generated ones are). The threat model says what this does and doesn't protect against.

Documentation

Architecture · Protocol · WireGuard data plane · Pairwise Amnezia transport · Routing · Exit nodes · Virtual services · Threat model · Contributing

Licence

WTFPL: do what the fuck you want to.