{ config, dns-nix, hostName, lib, pkgs, self, ... }: let serial = builtins.substring 0 10 self.lastModifiedDate; generateZoneFile = let util = dns-nix.util.${pkgs.system}; in { name, ns, records, ... }: util.writeZone name { TTL = 60*60; SOA = { nameServer = "${lib.dns.ns}."; adminEmail = "astro@spaceboyz.net"; serial = lib.toInt serial; refresh = 1*60*60; retry = 5*60; expire = 2*60*60; minimum = 1*60; }; NS = map (a: a+".") ns; subdomains = lib.foldl (a: b: lib.recursiveUpdate a b) { } (map ({ name, type, data }: { ${name}.${type} = [ data ]; }) records); }; in { options = with lib; let recordOpts = { name = mkOption { description = "DNS label"; type = types.str; }; type = mkOption { type = types.enum [ "A" "AAAA" "MX" "SRV" "CNAME" "TXT" "PTR" ]; }; data = mkOption { type = types.oneOf [ types.str (types.attrsOf (types.oneOf [ types.int types.str ]))]; }; }; zoneOpts = { name = mkOption { description = "DNS FQDN w/o trailing dot"; type = types.str; }; ns = mkOption { type = with types; listOf str; }; records = mkOption { type = with types; listOf (submodule { options = recordOpts; }); }; dynamic = mkOption { type = types.bool; default = false; }; }; in { site.dns.localZones = mkOption { type = with types; listOf (submodule { options = zoneOpts; }); }; }; config = { site.dns.localZones = lib.dns.localZones; services.knot = lib.mkIf config.site.hosts.${hostName}.services.dns.enable ( let generateZone = zone@{ name, dynamic, ... }: { domain = name; template = "zentralwerk"; acl = [ "zone_xfr" ] ++ lib.optional dynamic "dyndns"; file = if dynamic then "/var/lib/knot/zones/${name}.zone" else generateZoneFile zone; notify = [ "all" ]; }; in { enable = true; settings = { acl = [ { id = "dyndns"; action = "update"; key = "dyndns"; } { id = "zone_xfr"; address = with config.site.net.serv; [ # ns.c3d2.de hosts4.knot hosts6.dn42.knot hosts6.up4.knot "2a00:8180:2c00:282:2041:cbff:fe0c:8516" "fd23:42:c3d2:582:2041:cbff:fe0c:8516" # old ns.spaceboyz.net "95.217.229.209" "2a01:4f9:4b:39ec::4" # new ns.spaceboyz.net "172.22.24.4" "37.27.116.148" "2a01:4f9:3070:2728::4" # ns1.supersandro.de "188.34.196.104" "2a01:4f8:1c1c:1d38::1" ]; action = "transfer"; } ]; key = [ { id = "dyndns"; algorithm = "hmac-sha256"; secret = config.site.dyndnsKey; } ]; log = [ { target = "syslog"; any = "info"; } ]; mod-stats = [ { id = "default"; query-type = "on"; } ]; remote = let via = with config.site.net.serv; [ hosts4.dns hosts6.up4.dns ]; in [ { id = "ns.c3d2.de"; address = with config.site.net.serv; [ hosts4.knot hosts6.dn42.knot hosts6.up4.knot ]; inherit via; } { id = "ns.spaceboyz.net"; address = [ "172.22.24.4" # old "95.217.229.209" "2a01:4f9:4b:39ec::4" # new "37.27.116.148" "2a01:4f9:3070:2728::4" ]; inherit via; } { id = "ns1.supersandro.de"; address = [ /*"188.34.196.104"*/ "2a01:4f8:1c1c:1d38::1" ]; inherit via; } ]; remotes = [ { id = "all"; remote = [ "ns.c3d2.de" "ns.spaceboyz.net" "ns1.supersandro.de" ]; } ]; server = { answer-rotation = true; automatic-acl = true; identity = "dns.serv.zentralwerk.org"; listen = with config.site.net; [ "127.0.0.1" "::1" serv.hosts4.dns serv.hosts6.up4.dns serv.hosts6.dn42.dns ]; tcp-fastopen = true; version = null; }; template = [ { # default is a magic name and is always loaded. # Because we want to use catalog-role/catalog-zone settings for all zones *except* the catalog zone itself, we must split the templates id = "default"; global-module = [ "mod-stats" ]; } { id = "zentralwerk"; catalog-role = "member"; catalog-zone = "zentralwerk."; dnssec-signing = true; journal-content = "all"; # required for zonefile-load=difference-no-serial and makes cold starts like zone reloads module = "mod-stats/default"; semantic-checks = true; serial-policy = "increment"; storage = "/var/lib/knot/zones"; zonefile-load = "difference-no-serial"; } ]; zone = [ { acl = "zone_xfr"; catalog-role = "generate"; domain = "zentralwerk."; notify = [ "ns1.supersandro.de" ]; storage = "/var/lib/knot/catalog"; } ] ++ map generateZone config.site.dns.localZones; }; }); systemd.services = { create-dynamic-zones = { description = "Creates dynamic zone files"; requiredBy = [ "knot.service" ]; before = [ "knot.service" ]; serviceConfig.Type = "oneshot"; script = '' mkdir -p /var/lib/knot/zones ${lib.concatMapStringsSep "\n" (zone@{ name, ... }: '' [ -e /var/lib/knot/zones/${name}.zone ] || \ cp ${generateZoneFile zone} /var/lib/knot/zones/${name}.zone chown -R knot /var/lib/knot/zones chmod -R u+rwX /var/lib/knot/zones '') (builtins.filter ({ dynamic, ... }: dynamic) config.site.dns.localZones)} ''; }; update-dynamic-zones = { description = "Creates initial records in dynamic zone files"; requiredBy = [ "knot.service" ]; after = [ "knot.service" ]; serviceConfig.Type = "oneshot"; path = [ pkgs.dnsutils ]; script = lib.concatMapStrings (zone: '' nsupdate -v -y "hmac-sha256:dyndns:${config.site.dyndnsKey}" <