diff options
Diffstat (limited to '')
| -rw-r--r-- | hooks/README | 2 | ||||
| -rwxr-xr-x | hooks/lib/update-zone-serials | 8 | ||||
| -rwxr-xr-x | hooks/pre-commit | 8 | ||||
| -rw-r--r-- | pkgs/default.nix | 1 | ||||
| -rw-r--r-- | pkgs/increment-zone-serials/default.nix | 17 | ||||
| -rwxr-xr-x | pkgs/increment-zone-serials/increment-zone-serials.py | 45 | ||||
| -rw-r--r-- | sys/ns/rr.nix | 545 |
7 files changed, 384 insertions, 242 deletions
diff --git a/hooks/README b/hooks/README new file mode 100644 index 0000000..176dc1c --- /dev/null +++ b/hooks/README @@ -0,0 +1,2 @@ +$ git config core.hooksPath hooks +$ man githooks diff --git a/hooks/lib/update-zone-serials b/hooks/lib/update-zone-serials new file mode 100755 index 0000000..272d5f6 --- /dev/null +++ b/hooks/lib/update-zone-serials @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e +set -o pipefail + +nix eval --json '.#nixosConfigurations.gate.config.lib.local.zoneSerialUpdates' \ + | nix run '.#increment-zone-serials' -- sys/ns/zones \ + | while read file; do nix fmt -- "$file"; git add -v -- "$file"; done diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..93e9b9a --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e +exec >&2 + +if git diff-index --cached HEAD -- | grep -q $'\tsys/ns/'; then + exec hooks/lib/update-zone-serials +fi diff --git a/pkgs/default.nix b/pkgs/default.nix index 82f6380..36dc309 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -8,6 +8,7 @@ in btclone = callPackage ./btclone { }; gem5 = callPackage ./gem5.nix { gem5ISA = "x86"; }; git-aliases = callPackage ./git-aliases.nix { }; + increment-zone-serials = callPackage ./increment-zone-serials { }; kbuild-standalone = callPackage ./kbuild-standalone.nix { }; mssql-tools = callPackage ./mssql-tools.nix { }; oregano = callPackage ./oregano { }; diff --git a/pkgs/increment-zone-serials/default.nix b/pkgs/increment-zone-serials/default.nix new file mode 100644 index 0000000..b5b6cd6 --- /dev/null +++ b/pkgs/increment-zone-serials/default.nix @@ -0,0 +1,17 @@ +{ python3 +, stdenv +}: +stdenv.mkDerivation { + pname = "increment-zone-serials"; + version = "1.0.0"; + + propagatedBuildInputs = [ + (python3.withPackages (py: [ ])) + ]; + + dontUnpack = true; + + installPhase = '' + install -Dm755 ${./increment-zone-serials.py} $out/bin/increment-zone-serials + ''; +} diff --git a/pkgs/increment-zone-serials/increment-zone-serials.py b/pkgs/increment-zone-serials/increment-zone-serials.py new file mode 100755 index 0000000..2d5753a --- /dev/null +++ b/pkgs/increment-zone-serials/increment-zone-serials.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +import datetime, json, os.path, sys + +base_dir = sys.argv[1] + +serial_num = lambda n: f'{today}{n:02}' +today_str = datetime.datetime.utcnow().date().strftime('%Y%m%d') +today = int(today_str) + +paths = [] + +for zone, data in json.load(sys.stdin).items(): + expected_hash = data['expected'] + + old_serial_int = data['serial'] + new_serial = serial_num(0) + + if old_serial_int is not None: + old_serial = str(old_serial_int) + if len(old_serial) > len(new_serial): + new_serial = None + elif len(old_serial) == len(new_serial): + old_day = int(old_serial[:len(today_str)]) + if old_day >= today: + new_serial = None + + if new_serial is None: + new_serial = str(old_serial_int + 1) + + path = os.path.join(base_dir, zone, 'serial.nix') + with open(os.path.join(base_dir, zone, 'serial.nix'), 'w') as serial_file: + print(f'''\ +{{ + config = {{ + soa.serial = {new_serial}; + nullSerialHash = "{expected_hash}"; + }}; +}} +''', file=serial_file) + paths.append(path) + +# Se imprime al final para evitar estados intermedios si algo tira excepción +for path in paths: + print(path) diff --git a/sys/ns/rr.nix b/sys/ns/rr.nix index 11e1aa6..8f9318d 100644 --- a/sys/ns/rr.nix +++ b/sys/ns/rr.nix @@ -1,297 +1,358 @@ -{ lib, pkgs, ... }: +{ config, lib, options, pkgs, ... }: with lib; let + cfg = config.local.ns; + 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}\\."; + + zoneHashCheck = name: zone: + let + zoneHash = algorithm: "${algorithm}-${builtins.hashString algorithm cfg.nullSerialZones.${name}.content}"; + expected = zoneHash "sha256"; + in + { + inherit expected zone; + needsUpdate = zone.soa.serial == null || zone.nullSerialHash != expected; + }; + + rrTypes = [ + "A" + "AAAA" + "CNAME" + "MX" + "NS" + "SOA" + "SRV" + "TXT" + ]; 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; - }; - - nsdConfig = mkOption { - type = attrsOf unspecified; - default = { }; - }; - - content = mkOption { - type = lines; - readOnly = true; + options.local.ns = { + nullSerialZones = mkOption { + type = options.local.ns.zones.type; + readOnly = true; + }; + + 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; }; - rr = mkOption { + rrType = options: 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" - ]; - }; + type = listOf (submodule { + options = options // { + name = nameOption; - data = mkOption { - type = listOf (either int str); - default = [ ]; - }; + ttl = mkOption { + type = int; + default = config.defaultTTL; }; - }); + }; + }); }; - soa = { - ttl = mkOption { - type = int; - default = config.defaultTTL; - }; + rrConfig = { rrs, type, format, applyName ? (rr: rr.name) }: (map + (rr: { + inherit type; + inherit (rr) ttl; - 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 { + data = format rr; + name = applyName rr; + }) + rrs); + in + { + options = { + defaultTTL = mkOption { type = int; + default = 3600; }; - refresh = mkOption { - type = int; - default = 3 * 3600; + nsdConfig = mkOption { + type = attrsOf unspecified; + default = { }; }; - retry = mkOption { - type = int; - default = 3600; + content = mkOption { + type = lines; + readOnly = true; }; - expire = mkOption { - type = int; - default = 7 * 24 * 3600; + nullSerialHash = mkOption { + type = nullOr str; + default = null; }; - negativeTTL = mkOption { - type = int; - default = 3600; + rr = mkOption { + default = [ ]; + type = listOf + (submodule { + options = { + name = nameOption; + + ttl = mkOption { + type = int; + }; + + class = mkOption { + type = enum [ "IN" ]; + default = "IN"; + }; + + type = mkOption { + type = enum rrTypes; + }; + + data = mkOption { + type = listOf (either int str); + default = [ ]; + }; + }; + }); }; - }; - a = rrType { - ipv4 = mkOption { - type = str; - }; - }; + soa = { + ttl = mkOption { + type = int; + default = config.defaultTTL; + }; - aaaa = rrType { - ipv6 = mkOption { - type = str; - }; - }; + primary = nameOption; - cname = rrType { - target = nameOption; - }; + hostmaster = mkOption { + type = emailType; + apply = address: + let + split = splitString "@" address; - mx = rrType { - host = nameOption; + user = head split; + domain = if length split == 2 then head (tail split) else name; + in + if hasSuffix "." address + then address + else "${replaceStrings [ "." ] [ "\\." ] user}.${domain}."; + }; - priority = mkOption { - type = int; - }; - }; + serial = mkOption { + type = nullOr int; + default = null; + }; - ns = rrType { - host = nameOption; - }; + refresh = mkOption { + type = int; + default = 3 * 3600; + }; - srv = rrType { - host = nameOption; + retry = mkOption { + type = int; + default = 3600; + }; - port = mkOption { - type = port; + expire = mkOption { + type = int; + default = 7 * 24 * 3600; + }; + + negativeTTL = mkOption { + type = int; + default = 3600; + }; }; - priority = mkOption { - type = int; + a = rrType { + ipv4 = mkOption { + type = str; + }; }; - proto = mkOption { - type = enum [ "tcp" "udp" ]; + aaaa = rrType { + ipv6 = mkOption { + type = str; + }; }; - service = mkOption { - type = str; + cname = rrType { + target = nameOption; }; - weight = mkOption { - type = int; + mx = rrType { + host = nameOption; + + priority = mkOption { + type = int; + }; }; - }; - txt = rrType { - text = mkOption { - type = strMatching "[^\"\n\\]*\n?"; - apply = removeSuffix "\n"; + ns = rrType { + host = nameOption; }; - }; - }; - config = { - nsdConfig.data = config.content; - - 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 ]; - }) + srv = rrType { + host = nameOption; - (rrConfig { - rrs = config.aaaa; - type = "AAAA"; - format = rr: [ rr.ipv6 ]; - }) + port = mkOption { + type = port; + }; - (rrConfig { - rrs = config.cname; - type = "CNAME"; - format = rr: [ rr.target ]; - }) + priority = mkOption { + type = int; + }; - (rrConfig { - rrs = config.mx; - type = "MX"; - format = rr: [ rr.priority rr.host ]; - }) + proto = mkOption { + type = enum [ "tcp" "udp" ]; + }; - (rrConfig { - rrs = config.srv; - type = "SRV"; + service = mkOption { + type = str; + }; - format = rr: [ rr.priority rr.weight rr.port rr.host ]; - applyName = rr: "_${rr.service}._${rr.proto}.${rr.name}"; - }) + weight = mkOption { + type = int; + }; + }; - (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); - }) - ]; + txt = rrType { + text = mkOption { + type = strMatching "[^\"\n\\]*\n?"; + apply = removeSuffix "\n"; + }; + }; + }; + + config = { + nsdConfig.data = config.content; + + 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 + (throwIf (serial == null) "No serial defined for zone ${name}" 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); + }) + ]; + }; + })); + }; + }; + + config = { + assertions = [ + ( + let + badZones = attrNames (filterAttrs (name: zone: (zoneHashCheck name zone).needsUpdate) cfg.zones); + in + { + assertion = badZones == [ ]; + message = "Update serials for these zones (null-serial hash mismatch): ${concatStringsSep ", " badZones}"; + } + ) + ]; + + lib.local.zoneSerialUpdates = + let + zoneChecks = mapAttrs zoneHashCheck cfg.zones; + updateInfo = check: { + 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; }; } |
