Files
nixfiles/nixos/boxes/home/routing-common/dns.nix
T
jackos1998 314be7620f
Update docs / update (push) Successful in 1m0s
CI / Check, build and cache nixfiles (push) Successful in 47m46s
nixos/home: Gate DDNS updates on WAN readiness
Start updates when the WAN gate opens and skip timer runs while it
is inactive. Keep periodic retries and wait for the local DNS services.
2026-11-15 16:23:06 +00:00

323 lines
11 KiB
Nix

index: { lib, pkgs, config, assignments, allAssignments, ... }:
let
inherit (builtins) attrNames elemAt;
inherit (lib.my) net;
inherit (lib.my.c) pubDomain;
inherit (lib.my.c.home) prefixes vips routers;
name = elemAt routers index;
otherIndex = 1 - index;
otherName = elemAt routers otherIndex;
authZones = attrNames config.my.pdns.auth.bind.zones;
in
{
config = {
# Let pdns-recursor bind the VRRP VIPs even on the backup, where the addresses
# aren't present locally
boot.kernel.sysctl = {
"net.ipv4.ip_nonlocal_bind" = 1;
"net.ipv6.ip_nonlocal_bind" = 1;
};
my = {
secrets.files = {
"home/pdns/auth.conf" = {
owner = "pdns";
group = "pdns";
};
"home/pdns/recursor.yml" = {
owner = "pdns-recursor";
group = "pdns-recursor";
};
"home/ddclient-cloudflare.key" = {};
};
pdns.recursor = {
enable = true;
extraSettingsFile = config.age.secrets."home/pdns/recursor.yml".path;
};
};
services = {
pdns-recursor = {
settings = {
incoming = {
listen = [
"127.0.0.1" "::1"
assignments.hi.ipv4.address assignments.hi.ipv6.address
assignments.lo.ipv4.address assignments.lo.ipv6.address
# VRRP VIPs: DNS follows the master, so clients only ever have one
# (always-live) resolver address and never hang on a dead router
vips.hi.v4 vips.hi.v6
vips.lo.v4 vips.lo.v6
];
allow_from = [
"127.0.0.0/8" "::1/128"
prefixes.hi.v4 prefixes.hi.v6
prefixes.lo.v4 prefixes.lo.v6
] ++ (with lib.my.c.tailscale.prefix; [ v4 v6 ]);
# DNS NOTIFY messages override TTL
allow_notify_for = authZones;
allow_notify_from = [ "127.0.0.0/8" "::1/128" ];
};
outgoing = {
# Query authoritative servers over IPv4 only. Our IPv6 default route runs over the
# as211024 mesh (`ip -6 route show default`), a proto-static route that isn't
# withdrawn when the mesh flaps (e.g. during ipsec churn) — it just blackholes. With
# "::" here the recursor keeps picking IPv6 to reach NS, stalls on timeouts, and
# takes recursion down with it. IPv4 upstream goes out the WAN directly and stays up;
# we still serve AAAA records regardless of transport.
source_address = [ "0.0.0.0" ];
};
recursor = {
forward_zones = map (z: {
zone = z;
forwarders = [ "127.0.0.1:5353" ];
}) authZones;
lua_dns_script = pkgs.writeText "pdns-script.lua" ''
blocklist = newDS()
function preresolve(dq)
local name = dq.qname:toString()
-- Disney+ doesn't like our IP space...
if dq.qtype == pdns.AAAA and (string.find(name, "disneyplus") or string.find(name, "disney-plus") or string.find(name , "disney.api")) then
dq.rcode = 0
return true
end
if blocklist:check(dq.qname) then
if dq.qtype == pdns.A then
dq:addAnswer(dq.qtype, "127.0.0.1")
elseif dq.qtype == pdns.AAAA then
dq:addAnswer(dq.qtype, "::1")
end
return true
end
return false
end
for line in io.lines("${./dns-blocklist.txt}") do
entry = line:gsub("%s+", "")
if entry ~= "" and string.sub(entry, 1, 1) ~= "#" then
blocklist:add(entry)
end
end
'';
};
webservice = {
webserver = true;
address = "::";
allow_from = [ "127.0.0.1" "::1" ];
};
};
};
};
systemd = {
services = {
# Add AF_NETLINK to allow pulling IP from network interfaces
pdns.serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
ddns-update = {
description = "DNS update script";
after = [ "wan-online.target" "pdns.service" "pdns-recursor.service" ];
wantedBy = [ "wan-online.target" ];
partOf = [ "wan-online.target" ];
path = [
(pkgs.python3.withPackages (ps: [ ps.cloudflare ]))
pkgs.ldns
];
serviceConfig = {
Type = "oneshot";
# Timer retries must not pull in the WAN gate or run before it opens.
ExecCondition = "${config.systemd.package}/bin/systemctl --quiet is-active wan-online.target";
ExecStart =
''${./dns_update.py} -k ${config.age.secrets."home/ddclient-cloudflare.key".path} '' +
''${pubDomain} ns${toString (index + 1)}.${config.networking.domain}'';
};
};
};
timers = {
ddns-update = {
description = "Periodically update DNS";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5min";
OnUnitInactiveSec = "5min";
};
};
};
};
environment.systemPackages = with pkgs; [
# For rec_control
pdns-recursor
sqlite
];
my.pdns.auth = {
enable = true;
extraSettingsFile = config.age.secrets."home/pdns/auth.conf".path;
settings = {
primary = true;
resolver = "127.0.0.1";
expand-alias = true;
local-address = [
"0.0.0.0:5353" "[::]:5353"
];
also-notify = [ "127.0.0.1" ];
allow-axfr-ips = [
"127.0.0.0/8" "::1/128"
allAssignments.estuary.internal.ipv4.address
]
++ lib.my.c.as211024.trusted.v4
++ lib.my.c.as211024.trusted.v6;
enable-lua-records = true;
# loglevel = 7;
# log-dns-queries = true;
# log-dns-details = true;
api = true;
webserver = true;
webserver-address = "::";
webserver-allow-from = [ "127.0.0.1" "::1" ];
dnsupdate = true;
launch = [ "gsqlite3" ];
gsqlite3-database = "/var/lib/pdns/dynamic.sqlite3";
};
bind.zones =
let
names = [ "core" "hi" "lo" "untrusted" ];
i = toString (index + 1);
in
{
"${config.networking.domain}" = {
type = "master";
text = ''
$TTL 60
@ IN SOA ns${i}.${config.networking.domain}. dev.nul.ie. (
@@SERIAL@@ ; serial
3h ; refresh
1h ; retry
1w ; expire
1h ; minimum
)
${name} IN LUA ${lib.my.dns.ifaceA {
inherit pkgs;
iface = "wan";
skipBroadcasts = config.my.homeRouter.dns.wanSkipBroadcasts;
}}
${otherName} IN LUA ${lib.my.dns.lookupIP {
inherit pkgs;
hostname = "${otherName}.${config.networking.domain}";
server = net.cidr.host (otherIndex + 1) prefixes.hi.v4;
}}
${elemAt routers 0} IN AAAA ${allAssignments."${elemAt routers 0}".as211024.ipv6.address}
${elemAt routers 1} IN AAAA ${allAssignments."${elemAt routers 1}".as211024.ipv6.address}
boot IN CNAME river-hi.${config.networking.domain}.
@ IN NS ns1
@ IN NS ns2
ns1 IN ALIAS ${elemAt routers 0}.${config.networking.domain}.
ns2 IN ALIAS ${elemAt routers 1}.${config.networking.domain}.
dyn IN NS ns1.dyn.h.nul.ie.
dyn IN NS ns2.dyn.h.nul.ie.
ns1.dyn.h.nul.ie. IN ALIAS ${elemAt routers 0}.${config.networking.domain}.
ns2.dyn.h.nul.ie. IN ALIAS ${elemAt routers 1}.${config.networking.domain}.
jim-core IN A ${net.cidr.host 10 prefixes.core.v4}
jim IN A ${net.cidr.host 10 prefixes.hi.v4}
jim IN AAAA ${net.cidr.host (65536+1) prefixes.hi.v6}
jim-lo IN A ${net.cidr.host 10 prefixes.lo.v4}
jim-lo IN AAAA ${net.cidr.host (65536+1) prefixes.lo.v6}
dave-core IN A ${net.cidr.host 11 prefixes.core.v4}
dave IN A ${net.cidr.host 11 prefixes.hi.v4}
dave IN AAAA ${net.cidr.host (65536+2) prefixes.hi.v6}
dave-lo IN A ${net.cidr.host 11 prefixes.lo.v4}
dave-lo IN AAAA ${net.cidr.host (65536+2) prefixes.lo.v6}
shytzel IN A ${net.cidr.host 12 prefixes.core.v4}
brian IN A ${net.cidr.host 13 prefixes.core.v4}
vibe-core IN A ${net.cidr.host 15 prefixes.core.v4}
vibe IN A ${net.cidr.host 15 prefixes.hi.v4}
vibe IN AAAA ${net.cidr.host (65536+6) prefixes.hi.v6}
vibe-lo IN A ${net.cidr.host 15 prefixes.lo.v4}
vibe-lo IN AAAA ${net.cidr.host (65536+6) prefixes.lo.v6}
wave-core IN A ${net.cidr.host 14 prefixes.core.v4}
wave IN A ${net.cidr.host 14 prefixes.lo.v4}
wave IN AAAA ${net.cidr.host (65536+5) prefixes.lo.v6}
ups IN A ${net.cidr.host 20 prefixes.lo.v4}
palace-kvm IN A ${net.cidr.host 21 prefixes.lo.v4}
reolink-living-room IN A ${net.cidr.host 45 prefixes.lo.v4}
nixlight IN A ${net.cidr.host 46 prefixes.lo.v4}
${lib.my.dns.fwdRecords {
inherit allAssignments names;
domain = config.networking.domain;
}}
'';
};
"168.192.in-addr.arpa" = {
type = "master";
text = ''
$TTL 60
@ IN SOA ns${i}.${config.networking.domain}. dev.nul.ie. (
@@SERIAL@@ ; serial
3h ; refresh
1h ; retry
1w ; expire
1h ; minimum
)
@ IN NS ns1.${config.networking.domain}.
@ IN NS ns2.${config.networking.domain}.
${lib.my.dns.ptrRecords {
inherit allAssignments names;
domain = config.networking.domain;
ndots = 2;
}}
'';
};
"0.d.4.0.0.c.7.9.e.0.a.2.ip6.arpa" = {
type = "master";
text = ''
$TTL 60
@ IN SOA ns${i}.${config.networking.domain}. dev.nul.ie. (
@@SERIAL@@ ; serial
3h ; refresh
1h ; retry
1w ; expire
1h ; minimum
)
@ IN NS ns1.${config.networking.domain}.
@ IN NS ns2.${config.networking.domain}.
${lib.my.dns.ptr6Records {
inherit allAssignments names;
domain = config.networking.domain;
ndots = 20;
}}
'';
};
};
};
};
}