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:
@@ -31,6 +31,12 @@
|
||||
networking.defaultGateway = { address = "10.0.0.1"; interface = "eth0"; };
|
||||
networking.nameservers = [ "1.1.1.1" "9.9.9.9" ];
|
||||
|
||||
# Never take the tailnet's DNS on THIS host: headscale points every node at
|
||||
# pihole, which runs here — mercury would be resolving through itself. Keep
|
||||
# the public resolvers above for the Pi's own lookups, exactly as the
|
||||
# unbound resolveLocalQueries note in CLAUDE.md requires.
|
||||
services.tailscale.extraUpFlags = [ "--accept-dns=false" ];
|
||||
|
||||
# ---- pihole web admin password (from sops) ----
|
||||
# The pihole container reads FTLCONF_* env vars. Render an env file from the
|
||||
# sops secret and feed it to the container — password stays out of repo/store.
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user