summaryrefslogtreecommitdiff
path: root/sys
diff options
context:
space:
mode:
Diffstat (limited to 'sys')
-rw-r--r--sys/net/options.nix89
-rw-r--r--sys/ns/default.nix1
-rw-r--r--sys/ns/ns.nix152
-rw-r--r--sys/ns/ptr/default.nix11
-rw-r--r--sys/ns/ptr/gate-public-v4/default.nix16
-rw-r--r--sys/ns/ptr/gate-public-v4/serial.nix7
-rw-r--r--sys/ns/ptr/gate-public-v6/default.nix16
-rw-r--r--sys/ns/ptr/gate-public-v6/serial.nix7
-rw-r--r--sys/ns/ptr/static-prefix-v6/default.nix16
-rw-r--r--sys/ns/ptr/static-prefix-v6/serial.nix7
-rw-r--r--sys/ns/rr.nix180
11 files changed, 433 insertions, 69 deletions
diff --git a/sys/net/options.nix b/sys/net/options.nix
index 2930567..11b913c 100644
--- a/sys/net/options.nix
+++ b/sys/net/options.nix
@@ -1,5 +1,46 @@
{ config, lib, ... }:
-with lib; {
+with lib; let
+ v4PtrHierarchy = address: bits: reverseList (sublist 0 (bits / 8) (splitString "." address));
+
+ v6PtrHierarchy = address: bits:
+ let
+ separator = lists.findFirstIndex (hextet: hextet == "") null colonSplit;
+ colonSplit = splitString ":" address;
+
+ zeroFill = replicate (8 - length colonSplit + 1) "0000";
+ leftSplit = sublist 0 separator colonSplit;
+ rightSplit = sublist (separator + 1) (length colonSplit - separator - 1) colonSplit;
+
+ fullSplit =
+ if separator != null
+ then leftSplit ++ zeroFill ++ rightSplit
+ else colonSplit;
+
+ padded = map (hextet: strings.replicate (4 - stringLength hextet) "0" + hextet) fullSplit;
+ in
+ reverseList (sublist 0 (bits / 4) (flatten (map stringToCharacters padded)));
+
+ matchPtrRecordName = { splitter, netAddress, netBits, targetAddress, targetBits }:
+ let
+ netSplit = splitter netAddress netBits;
+ targetSplit = splitter targetAddress targetBits;
+
+ netLength = length netSplit;
+ lengthDelta = length targetSplit - netLength;
+
+ withinNet = lengthDelta >= 0 && sublist lengthDelta netLength targetSplit == netSplit;
+ throwMessage = "${targetAddress}/${toString targetBits} is not a subset of ${netAddress}/${toString netBits}";
+
+ recordHierarchy = sublist 0 lengthDelta targetSplit;
+
+ recordName =
+ if recordHierarchy != [ ]
+ then concatStringsSep "." recordHierarchy
+ else "@";
+ in
+ throwIfNot withinNet throwMessage recordName;
+in
+{
options.local.nets = with lib.types; mkOption {
readOnly = true;
@@ -126,14 +167,37 @@ with lib; {
type = str;
readOnly = true;
};
+
+ ptrDomain = mkOption {
+ type = str;
+ readOnly = true;
+ };
+
+ ptrRecordName = mkOption {
+ type = functionTo (functionTo str);
+ readOnly = true;
+ };
};
config = {
cidr = "${config.subnet}/${toString config.bits}";
+
subnet =
if config.bits != 0
then config.prefix + strings.replicate (4 - config.bits / 8) ".0"
else "0.0.0.0";
+
+ ptrDomain = concatStrings (map (x: x + ".") (v4PtrHierarchy config.subnet config.bits)) + "in-addr.arpa";
+
+ ptrRecordName = address: bits: matchPtrRecordName {
+ splitter = v4PtrHierarchy;
+
+ netBits = config.bits;
+ netAddress = config.subnet;
+
+ targetBits = bits;
+ targetAddress = address;
+ };
};
}));
};
@@ -162,14 +226,37 @@ with lib; {
type = str;
readOnly = true;
};
+
+ ptrDomain = mkOption {
+ type = str;
+ readOnly = true;
+ };
+
+ ptrRecordName = mkOption {
+ type = functionTo (functionTo str);
+ readOnly = true;
+ };
};
config = {
cidr = "${config.subnet}/${toString config.bits}";
+
subnet =
if config.bits == 128 || length (splitString "::" config.prefix) > 1
then config.prefix
else "${config.prefix}::";
+
+ ptrDomain = concatStrings (map (x: x + ".") (v6PtrHierarchy config.subnet config.bits)) + "ip6.arpa";
+
+ ptrRecordName = address: bits: matchPtrRecordName {
+ splitter = v6PtrHierarchy;
+
+ netBits = config.bits;
+ netAddress = config.subnet;
+
+ targetBits = bits;
+ targetAddress = address;
+ };
};
}));
};
diff --git a/sys/ns/default.nix b/sys/ns/default.nix
index d9e0063..b1a4da3 100644
--- a/sys/ns/default.nix
+++ b/sys/ns/default.nix
@@ -3,6 +3,7 @@
./mx.nix
./ns.nix
./nsd.nix
+ ./ptr
./rr.nix
./zones
];
diff --git a/sys/ns/ns.nix b/sys/ns/ns.nix
index 4c242b6..a1b1605 100644
--- a/sys/ns/ns.nix
+++ b/sys/ns/ns.nix
@@ -1,54 +1,118 @@
{ config, lib, ... }:
with lib; let
- inherit (config.local.ns.server) tsigName;
+ inherit (config.networking) domain;
inherit (config.local.nets) gate-public;
+ inherit (config.local.ns.server) tsigName;
+
+ ptrNets = config.local.ns.ptr;
in
{
options.local.ns.zones = mkOption {
- type = with lib.types; attrsOf (submodule ({ config, name, ... }:
- let
- cfg = config.localNS;
- in
- {
- options.localNS = {
- enable = mkEnableOption "local NS settings";
-
- primary = mkOption {
- type = str;
- default = "ns1";
- };
- };
-
- config = mkIf cfg.enable {
- # https://docs.gandi.net/en/domain_names/advanced_users/secondary_nameserver.html
- nsdConfig =
- let
- providerSecondary = [
- "37.205.15.45 ${tsigName}" # ns3.vpsfree.cz
- "37.205.11.85 ${tsigName}" # ns4.vpsfree.cz
- "2a03:3b40:fe:2be::1 ${tsigName}" # ns3.vpsfree.cz
- "2a03:3b40:101:4::1 ${tsigName}" # ns4.vpsfree.cz
- ];
- in
- {
- notify = providerSecondary;
- provideXFR = providerSecondary;
+ type = with lib.types; attrsOf
+ (submodule
+ ({ config, name, ... }:
+ let
+ inherit (config.soa) primary;
+
+ cfg = config.localNS;
+ ptrDomain = cfg.ptrNet.v4 != null || cfg.ptrNet.v6 != null;
+ in
+ {
+ options.localNS = {
+ enable = mkEnableOption "local NS settings";
+
+ ptrNet = {
+ v4 = mkOption {
+ type = nullOr str;
+ default = null;
+ };
+
+ v6 = mkOption {
+ type = nullOr str;
+ default = null;
+ };
+ };
};
- ns = [
- { name = "@"; host = cfg.primary; }
- { name = "@"; host = "ns3.vpsfree.cz."; }
- { name = "@"; host = "ns4.vpsfree.cz."; }
- ];
-
- a = [
- { name = cfg.primary; ipv4 = gate-public.hosts.gate.v4.address; }
- ];
-
- aaaa = [
- { name = cfg.primary; ipv6 = gate-public.hosts.gate.v6.address; }
- ];
- };
- }));
+ config = mkIf cfg.enable
+ {
+ ptrName =
+ let
+ name =
+ if cfg.ptrNet.v6 != null
+ then "${cfg.ptrNet.v6}-v6"
+ else "${cfg.ptrNet.v4}-v4";
+ in
+ mkIf ptrDomain name;
+
+ # https://docs.gandi.net/en/domain_names/advanced_users/secondary_nameserver.html
+ nsdConfig =
+ let
+ providerSecondary = [
+ "37.205.15.45 ${tsigName}" # ns3.vpsfree.cz
+ "37.205.11.85 ${tsigName}" # ns4.vpsfree.cz
+ "2a03:3b40:fe:2be::1 ${tsigName}" # ns3.vpsfree.cz
+ "2a03:3b40:101:4::1 ${tsigName}" # ns4.vpsfree.cz
+ ];
+ in
+ {
+ notify = providerSecondary;
+ provideXFR = providerSecondary;
+ };
+
+ ns = [
+ { name = "@"; host = primary; }
+ { name = "@"; host = "ns3.vpsfree.cz."; }
+ { name = "@"; host = "ns4.vpsfree.cz."; }
+ ];
+
+ a = optional (!ptrDomain)
+ { name = primary; ipv4 = gate-public.hosts.gate.v4.address; ptr = null; };
+
+ aaaa = optional (!ptrDomain)
+ { name = primary; ipv6 = gate-public.hosts.gate.v6.address; ptr = null; };
+
+ ptr =
+ let
+ ptrsToRecords = mapAttrsToList (suffix: target: {
+ name = suffix;
+ inherit target;
+ });
+
+ v4Net = cfg.ptrNet.v4;
+ v6Net = cfg.ptrNet.v6;
+
+ v4Records = optionals (v4Net != null) (ptrsToRecords ptrNets.${v4Net}.v4.targets);
+ v6Records = optionals (v6Net != null) (ptrsToRecords ptrNets.${v6Net}.v6.targets);
+ in
+ v4Records ++ v6Records;
+
+ soa = mkIf ptrDomain {
+ authorityZone = mkDefault "${domain}.";
+ };
+ };
+ }));
};
+
+ config =
+ {
+ assertions = mapAttrsToList
+ (name: zone: {
+ assertion = zone.localNS.ptrNet.v4 != null -> zone.localNS.ptrNet.v6 == null;
+ message = "zone '${name}' defined as both a v4 and v6 PTR zone";
+ })
+ config.local.ns.zones;
+
+ local.ns.ptr =
+ let
+ zonePtrNets = name: zone:
+ optionalAttrs (zone.localNS.ptrNet.v4 != null)
+ {
+ ${zone.localNS.ptrNet.v4}.v4.zone = name;
+ } // optionalAttrs (zone.localNS.ptrNet.v6 != null) {
+ ${zone.localNS.ptrNet.v6}.v6.zone = name;
+ };
+ in
+ mkMerge (flatten (mapAttrsToList zonePtrNets (filterAttrs (_: zone: zone.localNS.enable) config.local.ns.zones)));
+ };
}
diff --git a/sys/ns/ptr/default.nix b/sys/ns/ptr/default.nix
new file mode 100644
index 0000000..d583dd7
--- /dev/null
+++ b/sys/ns/ptr/default.nix
@@ -0,0 +1,11 @@
+{ config, ... }:
+let
+ inherit (config.local) nets;
+in
+{
+ config.local.ns.zones = {
+ ${nets.gate-public.v4.ptrDomain} = import ./gate-public-v4;
+ ${nets.gate-public.v6.ptrDomain} = import ./gate-public-v6;
+ ${nets.static-prefix.v6.ptrDomain} = import ./static-prefix-v6;
+ };
+}
diff --git a/sys/ns/ptr/gate-public-v4/default.nix b/sys/ns/ptr/gate-public-v4/default.nix
new file mode 100644
index 0000000..a2595d9
--- /dev/null
+++ b/sys/ns/ptr/gate-public-v4/default.nix
@@ -0,0 +1,16 @@
+{ config, ... }:
+let
+ inherit (config.local) nets;
+in
+{
+ imports = [
+ ./serial.nix
+ ];
+
+ config = {
+ localNS = {
+ enable = true;
+ ptrNet.v4 = "gate-public";
+ };
+ };
+}
diff --git a/sys/ns/ptr/gate-public-v4/serial.nix b/sys/ns/ptr/gate-public-v4/serial.nix
new file mode 100644
index 0000000..c3a41e9
--- /dev/null
+++ b/sys/ns/ptr/gate-public-v4/serial.nix
@@ -0,0 +1,7 @@
+{
+ config = {
+ soa.serial = 2025042402;
+ nullSerialHash = "sha256-afaedee02017aabd45b944a657ce91515866982c7cb900927edcee6d2b39c731";
+ };
+}
+
diff --git a/sys/ns/ptr/gate-public-v6/default.nix b/sys/ns/ptr/gate-public-v6/default.nix
new file mode 100644
index 0000000..15a4095
--- /dev/null
+++ b/sys/ns/ptr/gate-public-v6/default.nix
@@ -0,0 +1,16 @@
+{ config, ... }:
+let
+ inherit (config.local) nets;
+in
+{
+ imports = [
+ ./serial.nix
+ ];
+
+ config = {
+ localNS = {
+ enable = true;
+ ptrNet.v6 = "gate-public";
+ };
+ };
+}
diff --git a/sys/ns/ptr/gate-public-v6/serial.nix b/sys/ns/ptr/gate-public-v6/serial.nix
new file mode 100644
index 0000000..2f1b4a9
--- /dev/null
+++ b/sys/ns/ptr/gate-public-v6/serial.nix
@@ -0,0 +1,7 @@
+{
+ config = {
+ soa.serial = 2025042402;
+ nullSerialHash = "sha256-9a8ac8849ea6c8993e44feefe439b96c643e2ccf3a03d0d700558e9a188f57d7";
+ };
+}
+
diff --git a/sys/ns/ptr/static-prefix-v6/default.nix b/sys/ns/ptr/static-prefix-v6/default.nix
new file mode 100644
index 0000000..f02222c
--- /dev/null
+++ b/sys/ns/ptr/static-prefix-v6/default.nix
@@ -0,0 +1,16 @@
+{ config, ... }:
+let
+ inherit (config.local) nets;
+in
+{
+ imports = [
+ ./serial.nix
+ ];
+
+ config = {
+ localNS = {
+ enable = true;
+ ptrNet.v6 = "static-prefix";
+ };
+ };
+}
diff --git a/sys/ns/ptr/static-prefix-v6/serial.nix b/sys/ns/ptr/static-prefix-v6/serial.nix
new file mode 100644
index 0000000..c09a24a
--- /dev/null
+++ b/sys/ns/ptr/static-prefix-v6/serial.nix
@@ -0,0 +1,7 @@
+{
+ config = {
+ soa.serial = 2025042402;
+ nullSerialHash = "sha256-92c2046d390891a99618c5cf92efee1cda3549799ef26f1f0ca234e0a105aec8";
+ };
+}
+
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 { };
@@ -304,6 +385,12 @@ in
})
(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));
+ };
};
}