From 3671841eca6fe86793bdf6765a8e5195a6ffc727 Mon Sep 17 00:00:00 2001 From: Erik Simon Date: Mon, 20 Jul 2026 09:45:29 +0200 Subject: [PATCH] headscale: run our own DERP relay instead of Tailscale's By default headscale fetches https://controlplane.tailscale.com/derpmap/default at startup and treats failure as fatal, so it cannot boot when that URL is unreachable. A self-hosted control plane that will not start without Tailscale's infrastructure rather misses the point of self-hosting -- and it crash-looped for exactly that reason while neptun had no DNS. Enable the embedded DERP server on region 999 and drop the upstream map. The relay rides Caddy on :443, which is why that vhost already sets flush_interval -1; only STUN needs a port of its own. Verified against headscale 0.28.0 before committing: it starts clean with urls = [], registers "DERP region: {RegionID:999 ...}" pointing at vpn.mgaction.town with DERPPort 443, and brings up STUN. Co-Authored-By: Claude Opus 4.8 --- services/vpn/headscale.nix | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/services/vpn/headscale.nix b/services/vpn/headscale.nix index 7bafe0a..b307a0d 100644 --- a/services/vpn/headscale.nix +++ b/services/vpn/headscale.nix @@ -20,6 +20,34 @@ base_domain = "hosts.mgaction.town"; nameservers.global = [ "1.1.1.1" "9.9.9.9" ]; }; + + # Run our own DERP relay instead of pulling Tailscale's map. + # + # With the default (urls = [controlplane.tailscale.com/derpmap/default], + # auto_update_enabled = true) headscale fetches that map at startup and + # treats failure as FATAL — so a DNS blip or a Tailscale outage stops the + # control server from booting at all. A self-hosted control plane that + # can't start without Tailscale's infrastructure rather misses the point. + # + # The relay itself rides Caddy on :443 (hence the flush_interval -1 on + # that vhost); only STUN needs its own UDP port. + derp = { + urls = [ ]; + auto_update_enabled = false; + server = { + enabled = true; + region_id = 999; # 900-999 is the custom range + region_code = "neptun"; + region_name = "neptun"; + stun_listen_addr = "0.0.0.0:3478"; + automatically_add_embedded_derp_region = true; + }; + }; }; }; + + # STUN for the embedded DERP server above. Also needs a matching inbound-UDP + # rule in netcup's edge firewall — it is stateless and defaults to denying + # inbound UDP outright, which silently kills every DNS/NTP reply too. + networking.firewall.allowedUDPPorts = [ 3478 ]; }