From 1039d1d47a53be0c814a03608e94a9d0e8f4405b Mon Sep 17 00:00:00 2001 From: Alejandro Soto Date: Sat, 19 Apr 2025 10:48:15 -0600 Subject: sys/ns: implement automatic PTR zones --- sys/ns/rr.nix | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 156 insertions(+), 24 deletions(-) (limited to 'sys/ns/rr.nix') diff --git a/sys/ns/rr.nix b/sys/ns/rr.nix index a80eaf4..e4fbe12 100644 --- a/sys/ns/rr.nix +++ b/sys/ns/rr.nix @@ -1,5 +1,7 @@ { config, lib, options, pkgs, ... }: with lib; let + inherit (config.local) nets; + cfg = config.local.ns; globalConfig = config; @@ -25,6 +27,7 @@ with lib; let "CNAME" "MX" "NS" + "PTR" "SOA" "SRV" "TXT" @@ -37,21 +40,63 @@ in readOnly = true; }; + ptr = mkOption { + default = { }; + + type = with lib.types; attrsOf (submodule { + options = { + v4 = { + zone = mkOption { + type = nullOr str; + default = null; + }; + + targets = mkOption { + type = attrsOf str; + default = { }; + }; + }; + + v6 = { + zone = mkOption { + type = nullOr str; + default = null; + }; + + targets = mkOption { + type = attrsOf str; + default = { }; + }; + }; + }; + }); + }; + zones = mkOption { default = { }; type = with lib.types; attrsOf (submodule ({ config, name, ... }: let - nameOption = extra: mkOption (extra // { - type = domainRefType; - - apply = value: - if value == "@" - then "${name}." - else if ! hasSuffix "." value - then "${value}.${name}." - else value; - }); + nameOption = args@{ defaultZone ? "${name}.", permitRelative ? true, ... }: + mkOption (removeAttrs args [ "defaultZone" "permitRelative" ] // { + type = domainRefType; + + apply = value: + let + zone = + throwIfNot + (hasSuffix "." defaultZone) + "zone expression '${defaultZone}' must be absolute, not relative" + defaultZone; + in + if value == "@" + then zone + else if hasSuffix "." value + then value + else if permitRelative + then "${value}.${zone}" + else throw "zone expression '${value}' in zone '${zone}' must be absolute, not relative"; + }); rrType = options: mkOption { default = [ ]; @@ -90,6 +135,23 @@ in default = 3600; }; + ptrName = mkOption { + type = nullOr str; + default = null; + }; + + defaultPtr = { + v4 = mkOption { + type = nullOr str; + default = null; + }; + + v6 = mkOption { + type = nullOr str; + default = null; + }; + }; + nsdConfig = mkOption { type = attrsOf unspecified; default = { }; @@ -134,12 +196,17 @@ in }; soa = { + authorityZone = nameOption { default = "@"; permitRelative = false; }; + ttl = mkOption { type = int; default = config.defaultTTL; }; - primary = nameOption { default = "ns1"; }; + primary = nameOption { + default = "ns1"; + defaultZone = config.soa.authorityZone; + }; hostmaster = mkOption { type = emailType; @@ -150,7 +217,7 @@ in split = splitString "@" address; user = head split; - domain = if length split == 2 then head (tail split) else name; + domain = if length split == 2 then head (tail split) else removeSuffix "." config.soa.authorityZone; in if hasSuffix "." address then address @@ -187,12 +254,22 @@ in ipv4 = mkOption { type = str; }; + + ptr = mkOption { + type = nullOr str; + default = config.defaultPtr.v4; + }; }; aaaa = rrType { ipv6 = mkOption { type = str; }; + + ptr = mkOption { + type = nullOr str; + default = config.defaultPtr.v6; + }; }; cname = rrType { @@ -211,6 +288,10 @@ in host = nameOption { }; }; + ptr = rrType { + target = nameOption { }; + }; + srv = rrType { host = nameOption { }; @@ -303,6 +384,12 @@ in format = rr: [ rr.priority rr.host ]; }) + (rrConfig { + rrs = config.ptr; + type = "PTR"; + format = rr: [ rr.target ]; + }) + (rrConfig { rrs = config.srv; type = "SRV"; @@ -344,24 +431,69 @@ in message = "Update serials for these zones (null-serial hash mismatch): ${concatStringsSep ", " badZones}"; } ) - ]; + ] ++ flatten (mapAttrsToList + (name: ptr: [ + { + assertion = ptr.v4.targets != { } -> ptr.v4.zone != null; + message = "undefined v4 PTR net '${name}': ${concatStringsSep ", " (attrValues ptr.v4.targets)}"; + } + { + assertion = ptr.v6.targets != { } -> ptr.v6.zone != null; + message = "undefined v6 PTR net '${name}': ${concatStringsSep ", " (attrValues ptr.v6.targets)}"; + } + ]) + cfg.ptr); lib.local.zoneSerialUpdates = let - zoneChecks = mapAttrs zoneHashCheck cfg.zones; - updateInfo = check: { + ptrChecks = filterAttrs (_: check: check.zone.ptrName != null) allZoneChecks; + zoneChecks = filterAttrs (_: check: check.zone.ptrName == null) allZoneChecks; + allZoneChecks = filterAttrs (_: check: check.needsUpdate) (mapAttrs zoneHashCheck cfg.zones); + + updateInfo = name: check: { + inherit name; inherit (check) expected; inherit (check.zone.soa) serial; }; in - mapAttrs (_: updateInfo) (filterAttrs (_: check: check.needsUpdate) zoneChecks); - - local.ns.nullSerialZones = - mapAttrs - (_: zone: mkMerge [ - (filterAttrs (name: _: elem name ([ "defaultTTL" ] ++ map toLower rrTypes)) zone) - { soa.serial = mkOverride 0 0; } - ]) - cfg.zones; + { + ptr = mapAttrs (_: check: updateInfo check.zone.ptrName check) ptrChecks; + zones = mapAttrs updateInfo zoneChecks; + }; + + local.ns = { + nullSerialZones = + let + defaultAttrs = [ "defaultTTL" "defaultPtr" "ptrName" ]; + filteredAttrs = defaultAttrs ++ map toLower rrTypes; + in + mapAttrs + (_: zone: mkMerge [ + (filterAttrs (name: _: elem name filteredAttrs) zone) + { soa.serial = mkOverride 0 0; } + ]) + cfg.zones; + + ptr = + let + zonePtrs = zone: + let + v4Ptrs = map + (a: { + ${a.ptr}.v4.targets.${nets.${a.ptr}.v4.ptrRecordName a.ipv4 32} = a.name; + }) + (filter (a: a.ptr != null) zone.a); + + v6Ptrs = map + (aaaa: { + ${aaaa.ptr}.v6.targets.${nets.${aaaa.ptr}.v6.ptrRecordName aaaa.ipv6 128} = aaaa.name; + }) + (filter (aaaa: aaaa.ptr != null) zone.aaaa); + in + v4Ptrs ++ v6Ptrs; + + in + mkMerge (flatten (mapAttrsToList (_: zonePtrs) cfg.zones)); + }; }; } -- cgit v1.2.3