{ 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; }; }; }; }; }