summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--hooks/README2
-rwxr-xr-xhooks/lib/update-zone-serials8
-rwxr-xr-xhooks/pre-commit8
-rw-r--r--pkgs/default.nix1
-rw-r--r--pkgs/increment-zone-serials/default.nix17
-rwxr-xr-xpkgs/increment-zone-serials/increment-zone-serials.py45
-rw-r--r--sys/ns/rr.nix545
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;
};
}