Plain WG and QUIC can be blocked or severely degraded while the iroh control connection still works. Add an awg carrier with one independent UDP socket, WireGuard engine and randomized masking profile per network/peer attempt. The smaller endpoint id negotiates fresh outer keys, PSK, header protection, header ranges, padding and signature templates through private authenticated control messages. Restart or fallback reentry generates a new profile; ordinary rekeying preserves the current profile. No pair parameters enter signed state, capabilities, persistent storage or public message events. Private payloads and serialized control buffers redact and zeroize themselves. Adapt the bounded packet masking engine from RunaRoo/AmneziaWG-RS at commit 51579a7d6e8882d1d8d6d00822f09e18a9423b73, retaining its Apache license and provenance. Declare WTFPL AND Apache-2.0 in package metadata to cover both new glue and the adapted source. Reuse boringtun instead of upstream's Node/global executors and interface machinery. This is Tsunagi AWG wire version 1, with standard WG headers restored before authentication; external Amnezia server compatibility is not implemented. DNS/SIP signature generators vary fields instead of replaying captures. Each attempt closes independently, bounds allocation, queues, candidates and fragment assemblies, and supports the full 64 KiB carrier contract using UDP packets at most 1200 bytes. Yield between fragment batches so large messages do not starve local socket readers. Keep carrier encryption separate from end-to-end tunnels so multihop relays still forward opaque ciphertext. The system owns the sole interface, signed addresses and admission. Plugins receive neither network secrets nor paths. Expose a bounded opaque control sender at the transport boundary; core control messages remain outside protocol crates. Optional STUN on the pair's actual socket adds IPv4 public candidates in CLI direct/relay mode, with a finite query deadline and an off/alternate-server option. Local mode remains offline. Symmetric NAT, IPv6-only paths and arbitrary UDP blocking can still require fallback; no AWG-over-QUIC or hidden AWG relay is introduced. Default carrier order is wg, awg, wg-quic, tcp-tls. Advance the fallback cycle after setup failures and sustained runtime loss, even after earlier cooldowns expire. Authenticated delivery probes traverse the actual encrypted tunnel, fit the interface MTU and never enter the TUN. Retire after a 20-second warmup and at least 60% loss over at least 15 completed recent observations. Preserve logical tunnels and control sessions while explicitly closing physical links, including TCP reader/writer tasks. Retain the newest private probe settings across independently queued control negotiation and inbound installation. CLI/tray reports keep end-to-end protocol and actual carrier separate. Control ALPN/version becomes tsunagi/ctrl/6 for the new private message; upgrade all members together. Data ALPN/envelopes remain version 5. Network identity derivation, transcript encoding, device identities, signed records, saved network configuration and release versions are unchanged. Validation on Linux through nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets, with the inherited TSUNAGI_CONTROL_SOCKET override removed: 504 passed, 2 pre-existing ignored Tests use real iroh membership, SQLite, UDP sockets and WireGuard encryption with memory TUNs. Cover three independent pairs, full-size IP and carrier frames, restart freshness/socket release, 80% carrier datagram loss, fallback to real iroh, fresh retries with working delivery checks, unequal small MTUs, malformed negotiations/profiles, role and resource bounds, probe forgery and STUN parsing. Earlier compile/lint iterations and focused tests caught RNG API/initializer mistakes, a retry assertion racing path installation, and a large-fragment burst overflowing loopback buffers; corrected before these successful checks. Normalized upstream license line endings for diff checks. Public NAT/STUN connectivity, actual DPI effectiveness, WAN performance, per-pair memory measurement, macOS/Windows, privileged TUN operations and manual tray interaction were not exercised.
201 lines
9.5 KiB
Markdown
201 lines
9.5 KiB
Markdown
<p align="center">
|
||
<img src="dist/linux/tsunagi.svg" width="96" alt="">
|
||
</p>
|
||
|
||
<h1 align="center">tsunagi</h1>
|
||
|
||
<p align="center">
|
||
<b>A mesh VPN with no server.</b><br>
|
||
Pick a network name, share a secret, and your machines are on one private network.
|
||
</p>
|
||
|
||
<p align="center">
|
||
<a href="https://github.com/house-of-vanity/tsunagi/releases/latest"><img alt="release" src="https://img.shields.io/github/v/release/house-of-vanity/tsunagi?include_prereleases"></a>
|
||
<a href="https://github.com/house-of-vanity/tsunagi/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/house-of-vanity/tsunagi/actions/workflows/ci.yml/badge.svg"></a>
|
||
<a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-WTFPL-blue"></a>
|
||
</p>
|
||
|
||
```console
|
||
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](https://github.com/house-of-vanity/tsunagi/releases/latest).
|
||
|
||
| 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`<br>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](#nixos) 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
|
||
|
||
```sh
|
||
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
|
||
|
||
```sh
|
||
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](docs/exit-node.md).
|
||
|
||
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](docs/amnezia.md).
|
||
|
||
## 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 |
|
||
|
||
<sup>¹ 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.</sup>
|
||
|
||
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.
|
||
|
||
```nix
|
||
# 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.
|
||
|
||
```sh
|
||
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](https://www.wintun.net) next to `tsng.exe`.
|
||
Per-platform notes: [Linux](dist/linux/README.md), [Arch](dist/arch/README.md),
|
||
[macOS](dist/macos/README.md), [Windows](dist/windows/README.md).
|
||
|
||
## 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](docs/threat-model.md) says what this does and doesn't protect against.
|
||
|
||
## Documentation
|
||
|
||
[Architecture](docs/architecture.md) ·
|
||
[Protocol](docs/protocol.md) ·
|
||
[WireGuard data plane](docs/wireguard.md) ·
|
||
[Pairwise Amnezia transport](docs/amnezia.md) ·
|
||
[Routing](docs/routing.md) ·
|
||
[Exit nodes](docs/exit-node.md) ·
|
||
[Threat model](docs/threat-model.md) ·
|
||
[Contributing](AGENTS.md)
|
||
|
||
## Licence
|
||
|
||
[WTFPL](LICENSE): do what the fuck you want to.
|