Using an exit node could destroy direct peer connections: the agent's UID bypass protected outbound routing, but reverse-path checks look up replies without that UID and selected the overlay default. A real-host A/B check confirmed that relaxing only strict reverse-path filtering restored direct connections and exit HTTP traffic; the diagnostic host changes were removed. Keep strict filtering and protect the Linux underlay with a dedicated masked mark bit, 0x40000000. Mark the dedicated service UID in OUTPUT, save only that bit in conntrack and restore it before PREROUTING reverse-path checks. Install IPv4/IPv6 mark and UID main-table bypass rules before Tailscale's priorities; leave application-specific and exit-default routing after them. Preserve foreign mark bits, main-table routes and foreign firewall/policy rules. Protect the first inbound TCP/UDP packet using bounded, locally owned socket addresses/ports from /proc and an actual socket match. Refresh socket protection before sending private transport negotiations and on discovery ticks, without firewall writes for an unchanged socket set. Remote announcements never become OS arguments. A failed refresh leaves active exit routing in place. Install protection before routes, remove routes before protection, roll back partial activation, and remove only exact interface tags, including legacy UID rules. Reject strict kernel rp_filter without src_valid_mark with an actionable error; never weaken filtering or change host sysctls. Document mark-aware Netfilter requirements and provide the trusted ip6tables path in the NixOS module. Other platforms retain their existing exit implementation. Peer wire formats, identity derivation, local IPC and persistent storage are unchanged. Make the tray devices grid fill and shrink with the window, constrain the first cell and give remaining width to traffic plots. Keep displayed end-to-end protocol and its short encryption tooltip consistent, independent of carrier protocol. Move handshake/session age into its own column beside the existing iroh control RTT. Use stable per-device hover identities and concise tooltips; missing protocols/measurements remain explicit rather than invented. Validation on Linux through nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets: 529 passed, 3 ignored; inherited TSUNAGI_CONTROL_SOCKET and TSUNAGI_CONTROL_GROUP were unset - explicit privileged kernel regression: 1 passed, using disposable namespaces, real TCP/UDP IPv4/IPv6, strict kernel and Netfilter RPF, first inbound packets, dynamic UDP binding, application exit routing/IPv6 blocking, repeated apply and exact cleanup with foreign Tailscale-priority rules retained - headless egui grid/scroll-area rendering at multiple sizes and control RTT, mark masks/order, passive socket parsing, partial failure and cleanup tests - git diff --check Earlier iterations caught rtnetlink family API and nonexhaustive initializer errors, Clippy warnings, egui's implicit minimum column width, and kernel-test fixture issues (wildcard echo source, blocked IPv6 neighbour discovery and a closed child stdout pipe). Corrected before final checks; the stdout-only test fix was checked with fmt, Clippy and the explicit kernel regression afterward. Native tray rendering and macOS/Windows checks were not run. The running host agent was not replaced or restarted. Requested release tag: v0.1.0-rc.32.
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 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.
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 · Threat model · Contributing
Licence
WTFPL: do what the fuck you want to.