A tunnel no longer depends on the kind of link under it. The relay hub keeps one forwarding table per network instead of one per protocol, the envelope names the end-to-end protocol, and a neighbour reached over several kinds of transport is sent to over the best open one. A member in the middle needs no protocol in common with either end. Which protocol a pair runs is settled by the lower endpoint id's announced order, so both ends agree without asking. A direct link that comes up later is shorter than the path through others, so traffic moves to it with the same logical link. The agent now answers "is it online" once: a live control session or any data path. Members and peers report how they are reached (direct over a named transport, relayed through a named member, or no data path yet), and `tsng status` and the tray both read that instead of a session list and a WireGuard handshake respectively. A hostname belongs to whoever claimed it first. A claim carries the time its author first claimed the name, signed with the record, and replicas order by (time, id) so every member names the same holder. A later claimant keeps working by address, is not found by the name, and is told so in its status, `tsng status`, the tray and a HostnameTaken event. Wire change: control and data ALPN 5, control PROTOCOL_VERSION 5, envelope version 2, signing domain v3, state schema 7, local control protocol 18. Upgrade every member together. Co-Authored-By: Claude <noreply@anthropic.com>
8.5 KiB
tsunagi
A mesh VPN with no server.
Pick a network name, share a secret, and your machines are on one private network.
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.homejust works, even whileserveris 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-guiCLI 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 member gets an address in 10.13.37.0/24 and a name like <host>.home.
Machines are called tsunagi-<id> until you rename them: tsng id hostname laptop.
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.
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 · Routing · Exit nodes · Threat model · Contributing
Licence
WTFPL: do what the fuck you want to.