Files
homelab/hosts/mercury/configuration.nix
T
darmanandClaude Opus 4.8 0995a5fe2f 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>
2026-07-20 23:01:47 +02:00

52 lines
2.5 KiB
Nix

{ config, pkgs, lib, ... }:
# mercury — Raspberry Pi 3B+ (aarch64). Network DNS + DHCP (+ adblock).
# Boots from an SD image (see flake: nixosConfigurations.mercury), so there is
# no disko / hardware-configuration here — the sd-image module provides them.
{
imports = [
../../common.nix # shared base: user / ssh / nix / firewall
./secrets.nix # sops-nix: darman password (age key on boot part.)
../../services/network/unbound.nix # local recursive resolver (127.0.0.1:5335)
../../services/network/pihole.nix # DNS adblock + DHCP (declarative static leases)
../../services/vpn/tailscale.nix # tailnet node (headscale on neptun)
];
networking.hostName = "mercury";
# ---- Static networking ----
# A DNS/DHCP server must have a fixed address. Fill in the Pi's real values
# (from `ip -brief a` / `ip route` on the running Pi). eth0 = the Pi's NIC.
networking.useDHCP = false;
networking.usePredictableInterfaceNames = false; # keep it named eth0
networking.interfaces.eth0.ipv4.addresses = [
{ address = "10.0.0.10"; prefixLength = 24; } # the Pi's current IP
];
# Stable IPv6 (FRITZ!Box ULA prefix) so mercury is a fixed IPv6 DNS target.
# SLAAC still provides the GUA + default route. Announce THIS address as the
# DNSv6 server in the FRITZ!Box so IPv6 clients resolve .sol via pihole.
networking.interfaces.eth0.ipv6.addresses = [
{ address = "fd18:df17:9078:0::10"; prefixLength = 64; }
];
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.
sops.secrets.pihole_webpassword = { };
sops.templates."pihole.env".content =
"FTLCONF_webserver_api_password=${config.sops.placeholder.pihole_webpassword}";
virtualisation.oci-containers.containers.pihole.environmentFiles =
[ config.sops.templates."pihole.env".path ];
# Do not modify after first flash.
system.stateVersion = "26.05";
}