ctucx.git: dns.nix

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

commit ee218b2c231997c8190ac4a0d52bb7b95634c97d
parent a891c4ad70457e080a32891ea2b004b5c05afad5
Author: Kirill Elagin <kirelagin@gmail.com>
Date: Fri, 9 Apr 2021 00:31:46 -0400

Track option types more accurately

Turns out, strings in DNS zones have size limitations. In particular,
domain names cannot be longer than 255 octets. Some other strings, such
as data in TXT records, are not limited in length, but the STRING
LITERALS used in zone files are limited to 255 octets.

* Annotate each record file with a reference to the corresponding RFC.
* Add a new option type (lib.dns.types.domain-name), which is a string
  limited to 255 characters.
* Use the new `domain-name` type instead of `str` where appropriate.
* Add a helper util that breaks a string of aribtrary length into
  multiple string literals, each no longer than 255 characters.
* Use the new helper where appropriate (I hope).
17 files changed, 108 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,26 @@
+# 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], such that each output str is <= 255 bytes
+  # (RFC 1035, 3.3)
+  writeCharacterString = s:
+    if stringLength s <= 255
+    then ''"${s}"''
+    else concatMapStringsSep " " (x: ''"${x}"'') (splitInGroupsOf 255 s);
+
+in {
+  inherit writeCharacterString;
+}