Hawser
A private network between your own machines. Each machine gets a fixed address, and every machine knows how to find the others by address and by name, wherever they are.
One machine with a public address is the coordinator. It hands out the addresses and tells each machine where the rest are. Every other machine runs an agent.
Hawser does not carry your traffic itself. WireGuard, in the Linux kernel, does that: it is fast, and it is the encryption. Hawser is the part WireGuard leaves to you: who is in the network, which address each machine has, and how they reach each other. There is no account and no third party's server.
What you need
- Linux 5.6 or later on every machine (WireGuard is part of the kernel).
- One machine that the others can dial: a public address and one open UDP port.
- Root on each machine.
Install from a download
The releases page has, for each release, a package and a tarball for x86-64, arm64 and armv7 (Raspberry Pi). Nothing else is needed: the binary is static.
| File | For | Install with |
|---|---|---|
hawser_<version>_<arch>.deb | Debian, Ubuntu, Raspberry Pi OS | sudo apt install ./hawser_*.deb |
hawser-<version>-1.<arch>.rpm | Fedora, RHEL, openSUSE | sudo dnf install ./hawser-*.rpm |
hawser_<version>_<arch>.apk | Alpine | sudo apk add --allow-untrusted ./hawser_*.apk |
hawser-v<version>-linux-<arch>.tar.gz | Any other distribution | unpack, then sudo ./install.sh ./hawser |
A package puts hawser in /usr/bin with its service; the tarball's install.sh puts it in /usr/local/bin. Either way nothing is set up and nothing is started: carry on at "Create a network".
Build
docker build --target binary --output dist .
writes dist/hawser, one static file with no dependencies. The same file runs on any Linux distribution. The build runs the unit tests and fails if one does.
Without Docker, on a machine with a C++20 compiler and CMake:
cmake -S . -B build && cmake --build build -j && ./build/hawser_tests
Install from a checkout
On each machine, as root, from a checkout of this repository:
scripts/install.sh dist/hawser
It puts the binary in /usr/local/bin and adds a hawser service. It sets up nothing and starts nothing.
Create a network
On the coordinator:
hawser init --network-name home --network 10.77.0.0/24 \
--name vps --endpoint vpn.example.com:51820
systemctl enable --now hawser
| Option | Meaning |
|---|---|
--network-name | A short name for the network. Machines are also reachable as <machine>.<network-name> |
--network | The range addresses are handed out from. Pick one nothing else on your machines uses |
--name | This machine's name in the network |
--endpoint | The host:port other machines dial to reach this one. The UDP port must be open from outside |
--address | This machine's address. Default: the first in the range |
--listen-port | The UDP port to listen on. Default: the endpoint's port |
--api-port | The TCP port agents call, inside the network only. Default: 51821 |
--mtu | The largest packet the network carries. Default: 1420, which fits a standard 1500-byte path |
--interface | The interface's name. Default: hawser0 |
Every required option must be given. Nothing is guessed.
Add a machine
On the coordinator:
hawser invite workstation
prints one line. On the new machine:
hawser join <that line>
systemctl enable --now hawser
The machine now has its address and can reach every other machine.
The invite carries a one-time key, so send it the way you would send a password. It works once, and for 24 hours unless --valid-hours says otherwise. While joining, the machine makes a key of its own and hands the public half to the coordinator; from then on the invite's key is refused. --address on invite picks the machine's address instead of taking the next free one.
Day to day
| Command | Where | What it does |
|---|---|---|
hawser status | any machine | Lists the machines, their addresses, and how each is reached. --json prints the same as one line of JSON, for a program |
hawser invite <name> | coordinator | Prints the line that lets a new machine join |
hawser remove <name> | coordinator | Cuts a machine off at once; the others forget it within seconds |
hawser up | any machine | Runs the network. This is what the service runs |
hawser down | any machine | Removes the interface and hawser's lines in the hosts file |
hawser status on an agent:
Network home, 10.77.0.0/24. This machine: workstation, 10.77.0.2. NAME ADDRESS PATH LAST HEARD vps 10.77.0.1 direct (coordinator) 12s ago workstation 10.77.0.2 this machine - laptop 10.77.0.3 direct 40s ago build-box 10.77.0.4 through vps -
How machines find each other
By address. The coordinator gives each machine one address for good.
By name. Each machine keeps a block in /etc/hosts listing every machine, so ssh laptop and ping laptop.home work everywhere. Only that block is ever touched. Set manages_hosts=no in /etc/hawser/hawser.conf to leave the file alone.
By the best path available. Every machine calls the coordinator, so the coordinator sees the public address and port each one is calling from, and passes those on. Then, for each pair of machines:
- Both sides start trying to reach each other directly, in the background. Meanwhile their traffic goes through the coordinator, so the pair can talk from the first second.
- Once the direct path has actually carried something, traffic moves to it.
- If the direct path goes quiet for 75 seconds, traffic moves back to the coordinator, and the background attempts carry on.
Traffic is never sent down a path that has not been shown to work. Two machines that cannot reach each other at all, for example each behind its own strict NAT, simply stay on the coordinator.
If the coordinator stops, machines connected directly keep talking. Machines that relied on it wait until it is back. Restarting the hawser service does not cut anything: the interface stays up.