Mesh VPN with Tailscale (Headscale) [network/tailscale]
Mesh VPN with Tailscale (Headscale) [network/tailscale]
Tailscale solves the problems coming with Nebula, at the expense of a central server, and additionally requiring a valid SSL certificate. Normally their clients connect to the Tailscale coordination server to give a smooth onboarding experience, but I don’t want to depend on freemium services, and they do mess up sometimes. Luckily there’s a self-hosted solution called Headscale for personal use scenarios.
In Tailscale, the coordination server is not a regular node anymore. It is a dedicated public service that handles not only service discovery, but also client registration, key distribution and access control. And relays (DERP servers) are a standalone service that can be hosted separately from the coordination server. Headscale is a coordination server implementation that has simplified access control and account system, with an embedded DERP server.
To run Headscale with embedded DERP server1 2 (again in NixOS, refer to official docs otherwise):
services.headscale = {
enable = true;
address = "0.0.0.0"; # this is the listen address
settings = {
server_url = "<public-url-of-the-server>";
derp = {
server = {
enabled = true;
region_id = 999;
region_code = "hsc";
region_name = "Headscale embedded";
stun_listen_addr = "<listen-addr>:<listen-port>";
ipv4 = "<public-ipv4-addr>";
};
urls = []; # this disables DERP servers provided by Tailscale
};
# set these if you don't acquire certs with headscale
# note you may also want to set `services.headscale.group` to access certs
tls_cert_path = "<tls-cert>";
tls_key_path = "<tls-key>";
dns = {
# note that this shouldn't be the same as your server's public domain
# I use an nonexistent TLD like .tail
base_domain = "<magic-dns-domain>";
};
};
};
This is a very basic config, so you may want to checkout the reference config file for more options available.
Once the Headscale service is up and running you need to first set it up.
headscale users create <user-name>
This user name is required in following auth process of clients.
In contrast it’s much simpler to register and run a Tailscale client. The vanilla client is FOSS so I can just go with it:
services.tailscale = {
enable = true;
openFirewall = true;
extraUpFlags = [
"--login-server"
"<your-headscale-address>"
];
extraDaemonFlags = [
"--no-logs-no-support" # this disables telemetry
];
# this configures your system to be able to serve as an exit node
# (with `sudo tailscale set --advertise-exit-node`,
# or put `--advertise-exit-node` in `extraSetFlags`)
useRoutingFeatures = "server";
# see below
authKeyFile = "<auth-key>";
};
# prevent tailscale from using nebula interface
systemd.services.tailscaled.serviceConfig = {
RestrictNetworkInterfaces = "lo <interfaces-other-than-nebula>";
};
This needs some explanation. Basically with this config, Nix generates a
systemd service, that starts Tailscale daemon with tailscaled --no-log-no-support <other-flags-omitted>
then calls tailscale up
once with
the given auth key. Some intervention is needed because Tailscale doesn’t avoid
Nebula interfaces and this may lead to
various issues
(Nebula is smart enough to avoid routing through Tailscale so the reverse
doesn’t hold).
There are two flows to register a new client with Headscale (official docs here).
-
Non-interactively with a pre-generated key:
# on the server headscale preauthkeys create --user <user-name>
This generates a one-time key that you can put in aforementioned
authKeyFile
(effective for one hour by default). Deploy the config with the key on your device to log it into the network. -
Interactively without setting the key:
# on the client device tailscale login --login-server <user-name>
This leads to a web page on the server that shows a key, then you need to register the client with the key.
# on the server headscale nodes register --user <user-name> --key <key-on-the-page>
In both cases you only need to do this once per client/device.
On mobile devices it’s even simpler, you need to go out of the way to set your
login server but that’s basically it. Upon first login the interactive login
page shows up with the key, and again you run headscale nodes register
to
register the device.
Only IPv4 DERP is supported in my config, set derp.server.ipv6
if
needed.
Headscale config has undergone some breaking changes, so this might be outdated when you see it in the future.