ctucx.git: dns.nix

fork of https://github.com/kirelagin/dns.nix

commit f12fd1e1c39a98b53b930153666170a272c02679
parent f85bebe9a2ffd1836477606708b542aa73d0d536
Author: Kirill Elagin <kirelagin@gmail.com>
Date: Fri, 9 Apr 2021 08:22:40 -0400

Merge branch 'string-length'
17 files changed, 110 insertions(+), 22 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -14,6 +14,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Changed
+
+- Options that correspond to domain names are now limited to 255 characters
+  (as required by the standard). This change is not breaking, since,
+  even though the restriction was not represented in option types,
+  using a longer string would not work anyway.
+- Fix: character-string data of arbitrary length is now correctly split
+  into string literals each of which is no longer than 255 characters,
+  as required by the zone file format specification.
+
 
 ## [1.0.0]
 
diff --git a/dns/default.nix b/dns/default.nix
@@ -7,8 +7,14 @@
 { lib }:
 
 let
-  types = import ./types { inherit lib; };
-  combinators = import ./combinators.nix { inherit lib; };
+  dnslib = {
+    util = import ./util { inherit lib; };
+    inherit types;
+  };
+  types = import ./types { lib = lib'; };
+  lib' = lib // { dns = dnslib; };
+
+  combinators = import ./combinators.nix { lib = lib'; };
 
   evalZone = name: zone:
     (lib.evalModules {
diff --git a/dns/types/default.nix b/dns/types/default.nix
@@ -7,6 +7,7 @@
 { lib }:
 
 {
+  inherit (import ./simple.nix { inherit lib; }) domain-name;
   inherit (import ./zone.nix { inherit lib; }) zone subzone;
   record = import ./record.nix { inherit lib; };
   records = import ./records { inherit lib; };
diff --git a/dns/types/records/CAA.nix b/dns/types/records/CAA.nix
@@ -4,6 +4,8 @@
 # SPDX-License-Identifier: MPL-2.0 or MIT
 #
 
+# RFC 8659
+
 { lib }:
 
 let

@@ -25,7 +27,7 @@ in
       description = "One of the defined property tags";
     };
     value = mkOption {
-      type = types.str;
+      type = types.str;  # section 4.1.1: not limited in length
       example = "ca.example.net";
       description = "Value of the property";
     };
diff --git a/dns/types/records/CNAME.nix b/dns/types/records/CNAME.nix
@@ -4,10 +4,12 @@
 # SPDX-License-Identifier: MPL-2.0 or MIT
 #
 
+# RFC 1035, 3.3.1
+
 { lib }:
 
 let
-  inherit (lib) mkOption types;
+  inherit (lib) dns mkOption;
 
 in
 

@@ -15,7 +17,7 @@ in
   rtype = "CNAME";
   options = {
     cname = mkOption {
-      type = types.str;
+      type = dns.types.domain-name;
       example = "www.test.com";
       description = "A <domain-name> which specifies the canonical or primary name for the owner. The owner name is an alias";
     };
diff --git a/dns/types/records/DKIM.nix b/dns/types/records/DKIM.nix
@@ -7,10 +7,12 @@
 # This is a “fake” record type, not actually part of DNS.
 # It gets compiled down to a TXT record.
 
+# RFC 6376
+
 { lib }:
 
 let
-  inherit (lib) mkOption types;
+  inherit (lib) dns mkOption types;
 
 in
 

@@ -69,7 +71,8 @@ rec {
         (lib.filterAttrs (k: _v: k != "selector"))
         (lib.mapAttrsToList (k: v: "${k}=${v}"))
       ];
-    in ''"${lib.concatStringsSep "; " items + ";"}"'';
+      result = lib.concatStringsSep "; " items + ";";
+    in dns.util.writeCharacterString result;
   nameFixup = name: self:
     "${self.selector}._domainkey.${name}";
 }
diff --git a/dns/types/records/DMARC.nix b/dns/types/records/DMARC.nix
@@ -7,10 +7,12 @@
 # This is a “fake” record type, not actually part of DNS.
 # It gets compiled down to a TXT record.
 
+# RFC 7208
+
 { lib }:
 
 let
-  inherit (lib) mkOption types;
+  inherit (lib) dns mkOption types;
 
 in
 

@@ -98,7 +100,8 @@ rec {
         (lib.filterAttrs (_k: v: v != null && v != ""))
         (lib.mapAttrsToList (k: v: "${k}=${v}"))
       ];
-    in ''"${lib.concatStringsSep "; " items + ";"}"'';
+      result = lib.concatStringsSep "; " items + ";";
+    in dns.util.writeCharacterString result;
   nameFixup = name: _self:
     "_dmarc.${name}";
 }
diff --git a/dns/types/records/DNSKEY.nix b/dns/types/records/DNSKEY.nix
@@ -2,7 +2,10 @@
 #
 # SPDX-License-Identifier: MPL-2.0 or MIT
 
+# RFC 4034, 2
+
 { lib }:
+
 let
   inherit (builtins) isInt split;
   inherit (lib) concatStrings flatten mkOption types;
diff --git a/dns/types/records/DS.nix b/dns/types/records/DS.nix
@@ -2,7 +2,10 @@
 #
 # SPDX-License-Identifier: MPL-2.0 or MIT
 
+# RFC 4034, 5
+
 { lib }:
+
 let
   inherit (lib) mkOption types;
 
diff --git a/dns/types/records/MX.nix b/dns/types/records/MX.nix
@@ -4,10 +4,12 @@
 # SPDX-License-Identifier: MPL-2.0 or MIT
 #
 
+# RFC 1035, 3.3.9
+
 { lib }:
 
 let
-  inherit (lib) mkOption types;
+  inherit (lib) dns mkOption types;
 
 in
 

@@ -20,7 +22,7 @@ in
       description = "The preference given to this RR among others at the same owner. Lower values are preferred";
     };
     exchange = mkOption {
-      type = types.str;
+      type = dns.types.domain-name;
       example = "smtp.example.com.";
       description = "A <domain-name> which specifies a host willing to act as a mail exchange for the owner name";
     };
diff --git a/dns/types/records/NS.nix b/dns/types/records/NS.nix
@@ -4,10 +4,12 @@
 # SPDX-License-Identifier: MPL-2.0 or MIT
 #
 
+# RFC 1035, 3.3.11
+
 { lib }:
 
 let
-  inherit (lib) mkOption types;
+  inherit (lib) dns mkOption;
 
 in
 

@@ -15,7 +17,7 @@ in
   rtype = "NS";
   options = {
     nsdname = mkOption {
-      type = types.str;
+      type = dns.types.domain-name;
       example = "ns2.example.com";
       description = "A <domain-name> which specifies a host which should be authoritative for the specified class and domain";
     };
diff --git a/dns/types/records/PTR.nix b/dns/types/records/PTR.nix
@@ -4,10 +4,12 @@
 # SPDX-License-Identifier: MPL-2.0 or MIT
 #
 
+# RFC 1035, 3.3.12
+
 { lib }:
 
 let
-  inherit (lib) mkOption types;
+  inherit (lib) dns mkOption;
 
 in
 

@@ -15,7 +17,7 @@ in
   rtype = "PTR";
   options = {
     ptrdname = mkOption {
-      type = types.str;
+      type = dns.types.domain-name;
       example = "4-3-2-1.dynamic.example.com.";
       description = "A <domain-name> which points to some location in the domain name space";
     };
diff --git a/dns/types/records/SOA.nix b/dns/types/records/SOA.nix
@@ -4,11 +4,13 @@
 # SPDX-License-Identifier: MPL-2.0 or MIT
 #
 
+# RFC 1035, 3.3.13
+
 { lib }:
 
 let
   inherit (lib) concatStringsSep removeSuffix replaceStrings;
-  inherit (lib) mkOption types;
+  inherit (lib) dns mkOption types;
 
 in
 

@@ -16,12 +18,12 @@ in
   rtype = "SOA";
   options = {
     nameServer = mkOption {
-      type = types.str;
+      type = dns.types.domain-name;
       example = "ns1.example.com";
       description = "The <domain-name> of the name server that was the original or primary source of data for this zone. Don't forget the dot at the end!";
     };
     adminEmail = mkOption {
-      type = types.str;
+      type = dns.types.domain-name;
       example = "admin@example.com";
       description = "An email address of the person responsible for this zone. (Note: in traditional zone files you are supposed to put a dot instead of `@` in your address; you can use `@` with this module and it is recommended to do so. Also don't put the dot at the end!)";
       apply = s: replaceStrings ["@"] ["."] (removeSuffix "." s);
diff --git a/dns/types/records/SRV.nix b/dns/types/records/SRV.nix
@@ -4,10 +4,12 @@
 # SPDX-License-Identifier: MPL-2.0 or MIT
 #
 
+# RFC 2782
+
 { lib }:
 
 let
-  inherit (lib) mkOption types;
+  inherit (lib) dns mkOption types;
 
 in
 

@@ -42,7 +44,7 @@ in
       description = "The port on this target host of this service";
     };
     target = mkOption {
-      type = types.str;
+      type = dns.types.domain-name;
       example = "";
       description = "The domain name of the target host";
     };
diff --git a/dns/types/records/TXT.nix b/dns/types/records/TXT.nix
@@ -4,10 +4,12 @@
 # SPDX-License-Identifier: MPL-2.0 or MIT
 #
 
+# RFC 1035, 3.3.14
+
 { lib }:
 
 let
-  inherit (lib) mkOption types;
+  inherit (lib) dns mkOption types;
 
 in
 

@@ -20,6 +22,6 @@ in
       description = "Arbitrary information";
     };
   };
-  dataToString = {data, ...}: ''"${data}"'';
+  dataToString = { data, ... }: dns.util.writeCharacterString data;
   fromString = data: { inherit data; };
 }
diff --git a/dns/types/simple.nix b/dns/types/simple.nix
@@ -0,0 +1,15 @@
+# SPDX-FileCopyrightText: 2021 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+
+{ lib }:
+
+let
+  inherit (builtins) stringLength;
+
+in
+
+{
+  # RFC 1035, 3.1
+  domain-name = lib.types.addCheck lib.types.str (s: stringLength s <= 255);
+}
diff --git a/dns/util/default.nix b/dns/util/default.nix
@@ -0,0 +1,28 @@
+# SPDX-FileCopyrightText: 2021 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+
+{ lib }:
+
+let
+  inherit (builtins) genList stringLength substring;
+  inherit (lib.strings) concatMapStringsSep;
+
+  # : int -> str -> [str], such that each output str is <= n bytes
+  splitInGroupsOf = n: s:
+    let
+      groupCount = (stringLength s - 1) / n + 1;
+    in genList (i: substring (i * n) n s) groupCount;
+
+  # : str -> str
+  # Prepares a Nix string to be written to a zone file as a character-string
+  # literal: breaks it into chunks of 255 (per RFC 1035, 3.3) and encloses
+  # each chunk in quotation marks.
+  writeCharacterString = s:
+    if stringLength s <= 255
+    then ''"${s}"''
+    else concatMapStringsSep " " (x: ''"${x}"'') (splitInGroupsOf 255 s);
+
+in {
+  inherit writeCharacterString;
+}