DNS and DHCP (needed to replace a dnsmasq-based router): - router.dns.domain/hosts/overrides/forwardZones: authoritative local domain, split-horizon host records with reverse entries, wildcard overrides and per-zone forwarders (private zones skip DNSSEC). - lans.<n>.dhcp.reservations: Kea host reservations, published in DNS. - Tunnels with dns = true admit what their peers route, so a site-to-site peer without a transfer address can use the resolver. Installer: - router-install wipes a disk, creates an ESP and an ext4 root and installs a prebuilt system with its secrets; nothing is built on the box. installed-layout is the shared disk layout contract. - lib.mkInstaller builds an ISO for a given nixosSystem; packages.installer-example is the example's ISO. Covered by the dns and installer VM tests (the latter installs onto an empty disk and boots it under UEFI), wireguard DNS and assertion cases. PLAN.md records the remaining gaps for replacing the hand-written router. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
89 lines
3.3 KiB
Nix
89 lines
3.3 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 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 (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;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|