{ lib, pkgs, ... }: with lib; let segmentRegex = "[a-z0-9_-]+(\\.[a-z0-9_-]+)*"; emailType = lib.types.strMatching "[a-z0-9._-]+(@${segmentRegex})?"; domainRefType = lib.types.strMatching "@|${segmentRegex}\\.?"; domainNameType = lib.types.strMatching "${segmentRegex}\\."; in { options.local.ns.zones = mkOption { default = { }; type = with lib.types; attrsOf (submodule ({ config, name, ... }: let nameOption = mkOption { type = domainRefType; apply = value: if value == "@" then "${name}." else if ! hasSuffix "." value then "${value}.${name}." else value; }; rrType = options: mkOption { default = [ ]; type = listOf (submodule { options = options // { name = nameOption; ttl = mkOption { type = int; default = config.defaultTTL; }; }; }); }; rrConfig = { rrs, type, format, applyName ? (rr: rr.name) }: (map (rr: { inherit type; inherit (rr) ttl; data = format rr; name = applyName rr; }) rrs); in { options = { defaultTTL = mkOption { type = int; default = 3600; }; content = mkOption { type = lines; readOnly = true; }; rr = mkOption { default = [ ]; type = listOf (submodule { options = { name = nameOption; ttl = mkOption { type = int; }; class = mkOption { type = enum [ "IN" ]; default = "IN"; }; type = mkOption { type = enum [ "A" "AAAA" "CNAME" "MX" "NS" "SOA" "SRV" "TXT" ]; }; data = mkOption { type = listOf (either int str); default = [ ]; }; }; }); }; soa = { ttl = mkOption { type = int; default = config.defaultTTL; }; primary = nameOption; hostmaster = mkOption { type = emailType; apply = address: let split = splitString "@" address; user = head split; domain = if length split == 2 then head (tail split) else name; in "${replaceStrings [ "." ] [ "\\." ] user}.${domain}."; }; serial = mkOption { type = int; }; refresh = mkOption { type = int; default = 3 * 3600; }; retry = mkOption { type = int; default = 3600; }; expire = mkOption { type = int; default = 7 * 24 * 3600; }; negativeTTL = mkOption { type = int; default = 3600; }; }; a = rrType { ipv4 = mkOption { type = str; }; }; aaaa = rrType { ipv6 = mkOption { type = str; }; }; cname = rrType { target = nameOption; }; mx = rrType { host = nameOption; priority = mkOption { type = int; }; }; ns = rrType { host = nameOption; }; srv = rrType { host = nameOption; port = mkOption { type = port; }; priority = mkOption { type = int; }; proto = mkOption { type = enum [ "tcp" "udp" ]; }; service = mkOption { type = str; }; weight = mkOption { type = int; }; }; txt = rrType { text = mkOption { type = strMatching "[^\"\n\\]*\n?"; apply = removeSuffix "\n"; }; }; }; config = { content = let rrLine = rr: concatMapStringsSep " " toString ([ rr.name rr.ttl rr.class rr.type ] ++ rr.data); in '' $ORIGIN ${name}. $TTL ${toString config.defaultTTL} '' + concatLines (map rrLine config.rr); rr = mkMerge [ (mkOrder 0 (singleton { inherit (config.soa) ttl; name = "${name}."; type = "SOA"; data = with config.soa; [ primary hostmaster serial refresh retry expire negativeTTL ]; })) (mkOrder 1 (rrConfig { rrs = config.ns; type = "NS"; format = rr: [ rr.host ]; })) (rrConfig { rrs = config.a; type = "A"; format = rr: [ rr.ipv4 ]; }) (rrConfig { rrs = config.aaaa; type = "AAAA"; format = rr: [ rr.ipv6 ]; }) (rrConfig { rrs = config.cname; type = "CNAME"; format = rr: [ rr.target ]; }) (rrConfig { rrs = config.mx; type = "MX"; format = rr: [ rr.priority rr.host ]; }) (rrConfig { rrs = config.srv; type = "SRV"; format = rr: [ rr.priority rr.weight rr.port rr.host ]; applyName = rr: "_${rr.service}._${rr.proto}.${rr.name}"; }) (rrConfig { rrs = config.txt; type = "TXT"; format = rr: let # nsd-zonecheck: text string is longer than 255 characters, try splitting it into multiple parts txtFragments = text: let max = 255; length = stringLength text; in singleton (substring 0 max text) ++ optionals (length > max) (txtFragments (substring max length text)); in map (fragment: "\"${fragment}\"") (txtFragments rr.text); }) ]; }; })); }; }