{ hostName, config, lib, pkgs, self, ... }: lib.mkIf config.site.hosts.${hostName}.services.dns.enable { services.bind = let fqdn = "${hostName}.serv.zentralwerk.org"; # public servers (slaves) publicNS = [ "ns.c3d2.de" "ns.spaceboyz.net" ]; # allowed for zone-transfer slaves = [ # ns.c3d2.de "217.197.84.53" "2001:67c:1400:2240::a" # ns.spaceboyz.net "172.22.24.4" "2a01:4f9:4b:39ec::4" ]; # ip6.arpa aggregation size in CIDR bits reverseZone6Size = 60; serial = let timestamp = toString self.lastModified; datePkg = pkgs.runCommandLocal "date-${timestamp}" {} '' date -d @${timestamp} +%Y%m%d%H > $out ''; in toString (import datePkg); staticZone = { name, ns, records }: { inherit name; master = true; file = builtins.toFile "${name}.zone" '' $ORIGIN ${name}. $TTL 1h @ IN SOA ${fqdn}. astro.spaceboyz.net. ( ${serial} ; serial 1h ; refresh 1m ; retry 2h ; expire 1m ; minimum ) ${lib.concatMapStrings (ns: " IN NS ${ns}.\n") ns} ${lib.concatMapStrings ({ name, type, data }: "${name} IN ${type} ${data}\n" ) records} ''; }; hosts4Records = hosts4: builtins.attrValues ( builtins.mapAttrs (name: addr: { inherit name; type = "A"; data = addr; }) hosts4 ); hosts6Records = hosts6: builtins.attrValues ( builtins.mapAttrs (name: addr: { inherit name; type = "AAAA"; data = addr; }) hosts6 ); # generate zones only for nets with hosts namedNets = lib.filterAttrs (_: { hosts4, hosts6, dynamicDomain, ... }: (hosts4 != [] && hosts6 != []) || dynamicDomain ) config.site.net; # converts an IPv4 address to its reverse DNS form ipv4ToReverse = ipv4: builtins.concatStringsSep "." ( lib.reverseList ( builtins.filter builtins.isString ( builtins.split "\\." ipv4 ) ) ) + ".in-addr.arpa"; # `{ "1,0.0.127.in-addr.arpa" = "lo.core.zentralwerk.dn42"; }` reverseHosts4 = builtins.foldl' (result: { hosts4, domainName, ... }: builtins.foldl' (result: host: result // { "${ipv4ToReverse hosts4.${host}}" = "${host}.${domainName}"; }) result (builtins.attrNames hosts4) ) {} (builtins.attrValues namedNets); # `[ "0.0.127.in-addr.arpa" ]` reverseZones4 = builtins.attrNames ( builtins.foldl' (result: rname: let zone = builtins.head ( builtins.match "[[:digit:]]+\\.(.+)" rname ); in result // { "${zone}" = true; } ) {} (builtins.attrNames reverseHosts4) ); # turns `::` into `0000:0000:0000:0000:0000:0000:0000:0000` expandIpv6 = ipv6: if lib.hasPrefix "::" ipv6 then expandIpv6 "0${ipv6}" else if lib.hasSuffix "::" ipv6 then expandIpv6 "${ipv6}0" else let words = builtins.filter builtins.isString ( builtins.split ":" ipv6 ); fillWordCount = 8 - builtins.length words; fillWords = n: if n >= 0 then [ "0000" ] ++ fillWords (n - 1) else []; words' = builtins.concatMap (word: if word == "" then fillWords fillWordCount else [ word ] ) words; leftPad = padding: target: s: if builtins.stringLength s < target then leftPad padding target "${padding}${s}" else s; words'' = map (leftPad "0" 4) words'; in builtins.concatStringsSep ":" words''; # turns `::1` into `1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa` ipv6ToReverse = ipv6: builtins.concatStringsSep "." ( lib.reverseList ( lib.stringToCharacters ( builtins.replaceStrings [":"] [""] (expandIpv6 ipv6) ) ) ) + ".ip6.arpa"; # `{ dn42 = { "...ip6.arpa" = "lo.core.zentralwerk.dn42"; }; }` reverseHosts6 = builtins.foldl' (result: net: lib.recursiveUpdate result ( builtins.mapAttrs (ctx: hosts: builtins.foldl' (result: host: let domain = if ctx == "dn42" then namedNets.${net}.domainName else if builtins.match "up.*" ctx != null then "${net}.zentralwerk.org" else throw "Invalid IPv6 context: ${ctx}"; in lib.recursiveUpdate result { "${ipv6ToReverse hosts.${host}}" = "${host}.${domain}"; } ) {} (builtins.attrNames hosts) ) namedNets.${net}.hosts6 )) {} (builtins.attrNames namedNets); # `{ dn42 = [ "....ip6.arpa" ]; }` reverseZones6 = builtins.mapAttrs (ctx: reverseHosts6ctx: builtins.attrNames ( builtins.foldl' (result: rname: result // { "${builtins.substring ((128 - reverseZone6Size) / 2) (72 - ((128 - reverseZone6Size) / 2)) rname}" = true; }) {} (builtins.attrNames reverseHosts6ctx) ) ) reverseHosts6; in { enable = true; zones = [ (staticZone { name = "zentralwerk.org"; ns = publicNS; records = []; }) (staticZone { name = "zentralwerk.dn42"; ns = [ fqdn ]; records = [ { name = "ipa"; type = "A"; data = config.site.net.serv.hosts4.ipa; } ]; }) (staticZone { name = "dyn.zentralwerk.org"; ns = publicNS; # TODO: implement dyndns records = [ { name = "upstream1"; type = "A"; data = "24.134.104.53"; } { name = "upstream2"; type = "A"; data = "24.134.252.105"; } ]; }) ] ++ builtins.concatLists ( builtins.attrValues ( builtins.mapAttrs (net: { dynamicDomain, hosts4, hosts6, ... }: [ (if dynamicDomain then throw "TODO" else staticZone { name = "${net}.zentralwerk.dn42"; ns = [ fqdn ]; records = hosts4Records hosts4 ++ lib.optionals (hosts6 ? dn42) (hosts6Records hosts6.dn42); }) (staticZone { name = "${net}.zentralwerk.org"; ns = publicNS; records = lib.optionals (hosts6 ? up1) (hosts6Records hosts6.up1) ++ lib.optionals (hosts6 ? up2) (hosts6Records hosts6.up2); }) ]) namedNets ) ) ++ map (zone: staticZone { name = zone; ns = [ fqdn ]; records = map (reverse: { name = builtins.head ( builtins.match "([[:digit:]]+)\\..*" reverse ); type = "PTR"; data = "${reverseHosts4.${reverse}}."; }) ( builtins.filter (lib.hasSuffix ".${zone}") (builtins.attrNames reverseHosts4) ); }) reverseZones4 ++ builtins.concatMap (ctx: map (zone: staticZone { name = zone; ns = if ctx == "dn42" then [ fqdn ] else if builtins.match "up.*" ctx != null then publicNS else throw "Invalid IPv6 context: ${ctx}"; records = map (reverse: { name = builtins.substring 0 ((128 - reverseZone6Size) / 2 - 1) reverse; type = "PTR"; data = "${reverseHosts6.${ctx}.${reverse}}."; }) ( builtins.filter (lib.hasSuffix ".${zone}") (builtins.attrNames reverseHosts6.${ctx}) ); }) reverseZones6.${ctx} ) (builtins.attrNames reverseZones6); }; # TODO: dyn }