summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlejandro Soto <alejandro@34project.org>2024-08-04 16:36:17 -0600
committerAlejandro Soto <alejandro@34project.org>2024-08-04 16:57:19 -0600
commitc918238932f0d776666e552b8ccf353375703249 (patch)
tree5c5f2bc90d9fa3a5ad19989df823b413363ca285
parentc1fed32b662c4697fa1e1e9ce85a42d88d4e3db5 (diff)
sys/ns: initial commit
-rw-r--r--sys/default.nix1
-rw-r--r--sys/ns/default.nix7
-rw-r--r--sys/ns/nsd.nix37
-rw-r--r--sys/ns/rr.nix257
-rw-r--r--sys/ns/zones/README.md1
5 files changed, 303 insertions, 0 deletions
diff --git a/sys/default.nix b/sys/default.nix
index 59a8743..e3d1b11 100644
--- a/sys/default.nix
+++ b/sys/default.nix
@@ -19,6 +19,7 @@ with lib; {
./mail
./mta
./net
+ ./ns
./nspawn
./preset
./seat
diff --git a/sys/ns/default.nix b/sys/ns/default.nix
new file mode 100644
index 0000000..8c02d1d
--- /dev/null
+++ b/sys/ns/default.nix
@@ -0,0 +1,7 @@
+{
+ imports = [
+ ./nsd.nix
+ ./rr.nix
+ ./zones
+ ];
+}
diff --git a/sys/ns/nsd.nix b/sys/ns/nsd.nix
new file mode 100644
index 0000000..46ec1e6
--- /dev/null
+++ b/sys/ns/nsd.nix
@@ -0,0 +1,37 @@
+{ config, lib, ... }:
+with lib; let
+ cfg = config.local.ns.server;
+in
+{
+ options.local.ns.server = {
+ enable = mkEnableOption "nsd authoritative server";
+ };
+
+ config = mkIf cfg.enable {
+ networking.firewall =
+ let
+ inherit (config.services.nsd) port;
+ in
+ {
+ allowedTCPPorts = [ port ];
+ allowedUDPPorts = [ port ];
+ };
+
+ services.nsd = {
+ enable = true;
+
+ ipFreebind = true;
+
+ bind8Stats = true;
+ statistics = 3600;
+
+ tcpCount = 128;
+ tcpTimeout = 30;
+ tcpQueryCount = 128;
+
+ zones = mapAttrs
+ (_: zone: { data = zone.content; })
+ config.local.ns.zones;
+ };
+ };
+}
diff --git a/sys/ns/rr.nix b/sys/ns/rr.nix
new file mode 100644
index 0000000..f1c6aca
--- /dev/null
+++ b/sys/ns/rr.nix
@@ -0,0 +1,257 @@
+{ 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 }: (map
+ (rr: {
+ inherit type;
+ inherit (rr) name ttl;
+
+ data = format 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;
+ };
+
+ 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.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);
+ })
+ ];
+ };
+ }));
+ };
+}
diff --git a/sys/ns/zones/README.md b/sys/ns/zones/README.md
new file mode 100644
index 0000000..37073ba
--- /dev/null
+++ b/sys/ns/zones/README.md
@@ -0,0 +1 @@
+# This directory has been lustrated.