headscale: move the tailnet to orbit.sol, route all DNS through pihole

Three connected changes, all triggered by the same outage.

base_domain leaves mgaction.town. That zone has a wildcard A+AAAA pointing
at neptun, and DNS wildcards match multi-label names, so
jupiter.hosts.mgaction.town resolved publicly to NEPTUN and Caddy proxied
to itself -- a silent loop rather than a lookup failure. Nesting the
tailnet inside the LAN domain as orbit.sol keeps the theme and resolves
unambiguously, since tailscale matches routes by longest suffix.

override_local_dns = true with pihole as the only global nameserver, so
roaming devices get ad blocking and .sol names off-LAN. With it false,
globalResolvers land in the netmap's FallbackResolvers, which a phone
with carrier DNS never consults. No public fallback is listed on purpose:
tailscale treats the list as a set, so a second entry would let queries
slip past the filter whenever mercury is slow. The cost is that mercury
is now a single point of failure for tailnet DNS.

neptun and mercury opt out individually. mercury would otherwise resolve
through itself. neptun must not depend on a Pi behind a domestic line to
renew the certificates for the control server every other node needs --
and it is circular besides, since tailscaled has to resolve
vpn.mgaction.town to connect at all. Instead neptun runs a dnsmasq stub
forwarding just orbit.sol to MagicDNS on 100.100.100.100, which tailscaled
answers whenever it is running regardless of --accept-dns. That resolves
jupiter live, so the hardcoded /etc/hosts pin is gone.

Also sets dns.nameservers.split explicitly: nixpkgs renders its own
dns.split option one level too high, but headscale reads
dns.nameservers.split (hscontrol/types/config.go:722) and so does
headplane, whose DNS page dies on the missing key with "Cannot convert
undefined or null to object". The module's option is dead as written.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 23:01:47 +02:00
co-authored by Claude Opus 4.8
parent 6bf0eeab04
commit 0995a5fe2f
5 changed files with 104 additions and 25 deletions
+28 -5
View File
@@ -43,6 +43,29 @@
networking.defaultGateway6 = { address = "fe80::1"; interface = "eth0"; };
networking.nameservers = [ "9.9.9.9" "1.1.1.1" "2620:fe::fe" ];
# ---- Local split-DNS stub ----
# neptun must NOT take the tailnet's DNS: headscale points every node at
# pihole on mercury, and making a public reverse proxy's name resolution
# depend on a Pi behind a domestic line would take ACME renewals — and so
# the certs for the control server every node needs — down with it. It is
# also circular, since tailscaled has to resolve vpn.mgaction.town to
# connect in the first place.
#
# So neptun opts out with --accept-dns=false and does its own split DNS.
# tailscaled still answers MagicDNS on 100.100.100.100 whenever it is
# running (--accept-dns only governs whether it rewrites resolv.conf), so
# dnsmasq forwards just the tailnet suffix there and everything else to the
# public resolvers above. jupiter's address is therefore resolved live and
# never pinned — nothing to update when the tailnet is rebuilt.
#
# resolveLocalQueries (default) points resolv.conf at 127.0.0.1 and feeds
# networking.nameservers to dnsmasq as upstreams via resolvconf.
services.tailscale.extraUpFlags = [ "--accept-dns=false" ];
services.dnsmasq = {
enable = true;
settings.server = [ "/orbit.sol/100.100.100.100" ];
};
# firewall (enable + 22), caddy (80/443), tailscale (trust tailscale0 + join
# headscale) come from ../../common.nix and ../../services/{caddy,tailscale}.nix.
@@ -63,24 +86,24 @@
# ---- Audiobookshelf ----
services.caddy.virtualHosts."abs.mgaction.town".extraConfig = ''
reverse_proxy http://jupiter.hosts.mgaction.town:8000
reverse_proxy http://jupiter.orbit.sol:8000
'';
# ---- Seerr ----
services.caddy.virtualHosts."seerr.mgaction.town".extraConfig = ''
reverse_proxy http://jupiter.hosts.mgaction.town:5055
reverse_proxy http://jupiter.orbit.sol:5055
'';
# ---- Jellyfin ----
services.caddy.virtualHosts."jellyfin.mgaction.town".extraConfig = ''
reverse_proxy http://jupiter.hosts.mgaction.town:8096
reverse_proxy http://jupiter.orbit.sol:8096
'';
# ---- Gitea WebUI ----
# Gitea's web UI and HTTPS clones (services/dev/gitea.nix, HTTP_PORT 3000).
# Its SSH side is the separate :2222 forward further down.
services.caddy.virtualHosts."git.mgaction.town".extraConfig = ''
reverse_proxy http://jupiter.hosts.mgaction.town:3000
reverse_proxy http://jupiter.orbit.sol:3000
'';
# ---- Gitea SSH forward ----
@@ -95,7 +118,7 @@
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
ExecStart = "${pkgs.socat}/bin/socat TCP-LISTEN:2222,fork,reuseaddr TCP:jupiter.hosts.mgaction.town:2222";
ExecStart = "${pkgs.socat}/bin/socat TCP-LISTEN:2222,fork,reuseaddr TCP:jupiter.orbit.sol:2222";
Restart = "always";
};
};