lans.<n>.ipv6.address (a ULA /64) is assigned to the LAN, announced with an explicit prefix (networkd only announces the delegated prefix when prefix delegation is on) and advertised as RDNSS; Unbound listens and answers on it. Checked at build time: valid IPv6 CIDR, /64, fc00::/7. Covered by the onestick VM test (client SLAAC address, ping and DNS over the ULA, RA captured on the wire), assertion cases and a unit test for the /64 prefix helper. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
95 lines
3.5 KiB
Nix
95 lines
3.5 KiB
Nix
{ config, lib, ... }:
|
|
let
|
|
cfg = config.router;
|
|
lans = lib.attrValues cfg.lans;
|
|
ipv4 = (import ./lib.nix { inherit lib; }).ipv4;
|
|
|
|
# Tunnels that opted into DNS: listen on each of their addresses.
|
|
dnsAddrs = lib.concatMap (w: w.addresses) (lib.filter (w: w.dns) (lib.attrValues cfg.wireguard));
|
|
addrOf = a: builtins.head (lib.splitString "/" a);
|
|
# IPv4 gets the proper network address; IPv6 netblocks are masked by Unbound.
|
|
netOf =
|
|
a:
|
|
let
|
|
parts = lib.splitString "/" a;
|
|
in
|
|
if lib.hasInfix ":" a then
|
|
a
|
|
else
|
|
ipv4.cidr (builtins.head parts) (lib.toInt (builtins.elemAt parts 1));
|
|
|
|
dns = cfg.dns;
|
|
inherit (dns) domain;
|
|
fqdn = name: if lib.hasInfix "." name || domain == null then name else "${name}.${domain}";
|
|
isV6 = lib.hasInfix ":";
|
|
rrOf = a: if isV6 a then "AAAA" else "A";
|
|
|
|
# name -> addresses: explicit hosts plus DHCP reservations (A records only).
|
|
reservations = lib.concatMap (
|
|
l: lib.mapAttrsToList (n: r: lib.nameValuePair n [ r.address ]) l.dhcp.reservations
|
|
) lans;
|
|
records =
|
|
lib.mapAttrsToList (n: a: lib.nameValuePair (fqdn n) a) dns.hosts
|
|
++ map (r: lib.nameValuePair (fqdn r.name) r.value) reservations
|
|
++ lib.mapAttrsToList (n: a: lib.nameValuePair n a) dns.overrides;
|
|
|
|
# Unbound reads these as one quoted argument.
|
|
localData = lib.concatMap (r: map (a: ''"${r.name}. ${rrOf a} ${a}"'') r.value) records;
|
|
# Reverse records for hosts and reservations (not for wildcard overrides).
|
|
ptrs = lib.concatMap (r: map (a: ''"${a} ${r.name}."'') r.value) (
|
|
lib.filter (r: !(lib.hasAttr r.name dns.overrides)) records
|
|
);
|
|
privateZones = lib.attrNames dns.forwardZones ++ lib.optional (domain != null) domain;
|
|
|
|
# Peers of tunnels that use the resolver may be remote networks with no
|
|
# address of their own on the tunnel (site-to-site); allow what they route.
|
|
dnsPeerNets = lib.filter (n: !lib.hasSuffix "/0" n) (
|
|
lib.concatMap (w: lib.concatMap (p: p.allowedIPs) (lib.attrValues w.peers)) (
|
|
lib.filter (w: w.dns) (lib.attrValues cfg.wireguard)
|
|
)
|
|
);
|
|
in
|
|
{
|
|
config = lib.mkIf cfg.enable {
|
|
services.unbound = {
|
|
enable = true;
|
|
settings = {
|
|
server = {
|
|
interface = [
|
|
"127.0.0.1"
|
|
]
|
|
++ map (l: l.ipv4.address) lans
|
|
++ map (l: addrOf l.ipv6.address) (lib.filter (l: l.ipv6.address != null) lans)
|
|
++ map addrOf dnsAddrs;
|
|
# LAN addresses are assigned by networkd, possibly after Unbound starts.
|
|
ip-freebind = true;
|
|
access-control = [
|
|
"127.0.0.0/8 allow"
|
|
]
|
|
++ map (l: "${l.ipv4.subnet} allow") lans
|
|
++ map (l: "${l.ipv6.address} allow") (lib.filter (l: l.ipv6.address != null) lans)
|
|
++ map (a: "${netOf a} allow") dnsAddrs
|
|
++ map (n: "${n} allow") dnsPeerNets;
|
|
local-zone =
|
|
lib.optional (domain != null) "${domain}. static"
|
|
++ map (n: "${n}. redirect") (lib.attrNames dns.overrides);
|
|
local-data = localData;
|
|
local-data-ptr = ptrs;
|
|
domain-insecure = privateZones ++ lib.attrNames dns.overrides;
|
|
};
|
|
}
|
|
// lib.optionalAttrs (cfg.dns.forwarders != [ ] || dns.forwardZones != { }) {
|
|
forward-zone =
|
|
lib.mapAttrsToList (name: addrs: {
|
|
inherit name;
|
|
forward-addr = addrs;
|
|
}) dns.forwardZones
|
|
++ lib.optional (cfg.dns.forwarders != [ ]) {
|
|
name = ".";
|
|
forward-addr = cfg.dns.forwarders;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|