diff options
author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-02-23 18:34:58 +0100 |
---|---|---|
committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-03-09 13:44:17 +0100 |
commit | 116c55f3308efc8e6c0a35556404ab59539a6a99 (patch) | |
tree | 96c1bc14ea5f4b2704ed039898bde83406d52cb4 /modules/by-name | |
parent | tests/email: Test the mvp (diff) | |
download | nixos-server-116c55f3308efc8e6c0a35556404ab59539a6a99.zip |
{modules,tests}/dns: Init
Most of the dns module was taken from: <https://github.com/nix-community/dns.nix>
Diffstat (limited to 'modules/by-name')
29 files changed, 1291 insertions, 0 deletions
diff --git a/modules/by-name/dn/dns/dns/default.nix b/modules/by-name/dn/dns/dns/default.nix new file mode 100644 index 0000000..4ce07d8 --- /dev/null +++ b/modules/by-name/dn/dns/dns/default.nix @@ -0,0 +1,13 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{lib}: let + util = import ./util {inherit lib;}; + types = import ./types {inherit lib util;}; +in { + inherit + types + ; +} diff --git a/modules/by-name/dn/dns/dns/types/default.nix b/modules/by-name/dn/dns/dns/types/default.nix new file mode 100644 index 0000000..ece315f --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/default.nix @@ -0,0 +1,16 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{ + lib, + util, +}: let + simple = {types = import ./simple.nix {inherit lib;};}; +in { + record = import ./record.nix {inherit lib util;}; + records = import ./records {inherit lib util simple;}; + + zone = import ./zone.nix {inherit lib util simple;}; +} diff --git a/modules/by-name/dn/dns/dns/types/record.nix b/modules/by-name/dn/dns/dns/types/record.nix new file mode 100644 index 0000000..e992bf9 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/record.nix @@ -0,0 +1,75 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# SPDX-FileCopyrightText: 2021 Naïm Favier <n@monade.li> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{lib, ...}: let + inherit (lib) hasSuffix isString mkOption removeSuffix types; + + recordType = rsubt: let + submodule = types.submodule { + options = + { + class = mkOption { + type = types.enum ["IN"]; + default = "IN"; + example = "IN"; + description = "Resource record class. Only IN is supported"; + }; + ttl = mkOption { + type = types.nullOr types.ints.unsigned; # TODO: u32 + default = null; + example = 300; + description = "Record caching duration (in seconds)"; + }; + } + // rsubt.options; + }; + in + ( + if rsubt ? fromString + then types.either types.str + else lib.id + ) + submodule; + + # name == "@" : use unqualified domain name + writeRecord = name: rsubt: data: let + data' = + if isString data && rsubt ? fromString + then + # add default values for the record type + (recordType rsubt).merge [] [ + { + file = ""; + value = rsubt.fromString data; + } + ] + else data; + name' = let + fname = rsubt.nameFixup or (n: _: n) name data'; + in + if name == "@" + then name + else if (hasSuffix ".@" name) + then removeSuffix ".@" fname + else "${fname}."; + inherit (rsubt) rtype; + in + lib.concatStringsSep " " (with data'; + [ + name' + ] + ++ lib.optionals (ttl != null) [ + (toString ttl) + ] + ++ [ + class + rtype + (rsubt.dataToString data') + ]); +in { + inherit recordType; + inherit writeRecord; +} diff --git a/modules/by-name/dn/dns/dns/types/records/A.nix b/modules/by-name/dn/dns/dns/types/records/A.nix new file mode 100644 index 0000000..296943e --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/A.nix @@ -0,0 +1,19 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{lib, ...}: let + inherit (lib) mkOption types; +in { + rtype = "A"; + options = { + address = mkOption { + type = types.str; + example = "26.3.0.103"; + description = "IP address of the host"; + }; + }; + dataToString = {address, ...}: address; + fromString = address: {inherit address;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/AAAA.nix b/modules/by-name/dn/dns/dns/types/records/AAAA.nix new file mode 100644 index 0000000..4717176 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/AAAA.nix @@ -0,0 +1,19 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{lib, ...}: let + inherit (lib) mkOption types; +in { + rtype = "AAAA"; + options = { + address = mkOption { + type = types.str; + example = "4321:0:1:2:3:4:567:89ab"; + description = "IPv6 address of the host"; + }; + }; + dataToString = {address, ...}: address; + fromString = address: {inherit address;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/CAA.nix b/modules/by-name/dn/dns/dns/types/records/CAA.nix new file mode 100644 index 0000000..4b40510 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/CAA.nix @@ -0,0 +1,42 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 8659 +{lib, ...}: let + inherit (lib) mkOption types; +in { + rtype = "CAA"; + options = { + issuerCritical = mkOption { + type = types.bool; + example = true; + description = '' + If set to '1', indicates that the corresponding property tag + MUST be understood if the semantics of the CAA record are to be + correctly interpreted by an issuer + ''; + }; + tag = mkOption { + type = types.enum ["issue" "issuewild" "iodef"]; + example = "issue"; + description = "One of the defined property tags"; + }; + value = mkOption { + type = types.str; # section 4.1.1: not limited in length + example = "ca.example.net"; + description = "Value of the property"; + }; + }; + dataToString = { + issuerCritical, + tag, + value, + ... + }: ''${ + if issuerCritical + then "128" + else "0" + } ${tag} "${value}"''; +} diff --git a/modules/by-name/dn/dns/dns/types/records/CNAME.nix b/modules/by-name/dn/dns/dns/types/records/CNAME.nix new file mode 100644 index 0000000..095b078 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/CNAME.nix @@ -0,0 +1,27 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 1035, 3.3.1 +{ + lib, + simple, + ... +}: let + inherit (lib) mkOption; +in { + rtype = "CNAME"; + options = { + cname = mkOption { + type = simple.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. + ''; + }; + }; + dataToString = {cname, ...}: "${cname}"; + fromString = cname: {inherit cname;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/DKIM.nix b/modules/by-name/dn/dns/dns/types/records/DKIM.nix new file mode 100644 index 0000000..31b2f67 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/DKIM.nix @@ -0,0 +1,75 @@ +# +# SPDX-FileCopyrightText: 2020 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# This is a “fake” record type, not actually part of DNS. +# It gets compiled down to a TXT record. +# RFC 6376 +{ + lib, + util, + ... +}: let + inherit (lib) mkOption types; +in rec { + rtype = "TXT"; + options = { + selector = mkOption { + type = types.str; + example = "mail"; + description = "DKIM selector name"; + }; + h = mkOption { + type = types.listOf types.str; + default = []; + example = ["sha1" "sha256"]; + description = "Acceptable hash algorithms. Empty means all of them"; + apply = lib.concatStringsSep ":"; + }; + k = mkOption { + type = types.nullOr types.str; + default = "rsa"; + example = "rsa"; + description = "Key type"; + }; + n = mkOption { + type = types.str; + default = ""; + example = "Just any kind of arbitrary notes."; + description = "Notes that might be of interest to a human"; + }; + p = mkOption { + type = types.str; + example = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYtIxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhitdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB"; + description = "Public-key data (base64)"; + }; + s = mkOption { + type = types.listOf (types.enum ["*" "email"]); + default = ["*"]; + example = ["email"]; + description = "Service Type"; + apply = lib.concatStringsSep ":"; + }; + t = mkOption { + type = types.listOf (types.enum ["y" "s"]); + default = []; + example = ["y"]; + description = "Flags"; + apply = lib.concatStringsSep ":"; + }; + }; + dataToString = data: let + items = + ["v=DKIM1"] + ++ lib.pipe data [ + (builtins.intersectAttrs options) # remove garbage list `_module` + (lib.filterAttrs (_k: v: v != null && v != "")) + (lib.filterAttrs (k: _v: k != "selector")) + (lib.mapAttrsToList (k: v: "${k}=${v}")) + ]; + result = lib.concatStringsSep "; " items + ";"; + in + util.writeCharacterString result; + nameFixup = name: self: "${self.selector}._domainkey.${name}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/DMARC.nix b/modules/by-name/dn/dns/dns/types/records/DMARC.nix new file mode 100644 index 0000000..e67dd12 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/DMARC.nix @@ -0,0 +1,108 @@ +# +# SPDX-FileCopyrightText: 2020 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# This is a “fake” record type, not actually part of DNS. +# It gets compiled down to a TXT record. +# RFC 7208 +{ + lib, + util, + ... +}: let + inherit (lib) mkOption types; +in rec { + rtype = "TXT"; + options = { + adkim = mkOption { + type = types.enum ["relaxed" "strict"]; + default = "relaxed"; + example = "strict"; + description = "DKIM Identifier Alignment mode"; + apply = builtins.substring 0 1; + }; + aspf = mkOption { + type = types.enum ["relaxed" "strict"]; + default = "relaxed"; + example = "strict"; + description = "SPF Identifier Alignment mode"; + apply = builtins.substring 0 1; + }; + fo = mkOption { + type = types.listOf (types.enum ["0" "1" "d" "s"]); + default = ["0"]; + example = ["0" "1" "s"]; + description = "Failure reporting options"; + apply = lib.concatStringsSep ":"; + }; + p = mkOption { + type = types.enum ["none" "quarantine" "reject"]; + example = "quarantine"; + description = "Requested Mail Receiver policy"; + }; + pct = mkOption { + type = types.ints.between 0 100; + default = 100; + example = 30; + description = "Percentage of messages to which the DMARC policy is to be applied"; + apply = builtins.toString; + }; + rf = mkOption { + type = types.listOf (types.enum ["afrf"]); + default = ["afrf"]; + example = ["afrf"]; + description = "Format to be used for message-specific failure reports"; + apply = lib.concatStringsSep ":"; + }; + ri = mkOption { + type = types.ints.unsigned; # FIXME: u32 + default = 86400; + example = 12345; + description = "Interval requested between aggregate reports"; + apply = builtins.toString; + }; + rua = mkOption { + type = types.oneOf [types.str (types.listOf types.str)]; + default = []; + example = "mailto:dmarc+rua@example.com"; + description = "Addresses to which aggregate feedback is to be sent"; + apply = val: + # FIXME: need to encode commas in URIs + if builtins.isList val + then lib.concatStringsSep "," val + else val; + }; + ruf = mkOption { + type = types.listOf types.str; + default = []; + example = ["mailto:dmarc+ruf@example.com" "mailto:another+ruf@example.com"]; + description = "Addresses to which message-specific failure information is to be reported"; + apply = val: + # FIXME: need to encode commas in URIs + if builtins.isList val + then lib.concatStringsSep "," val + else val; + }; + sp = mkOption { + type = types.nullOr (types.enum ["none" "quarantine" "reject"]); + default = null; + example = "quarantine"; + description = "Requested Mail Receiver policy for all subdomains"; + }; + }; + dataToString = data: let + # The specification could be more clear on this, but `v` and `p` MUST + # be the first two tags in the record. + items = + ["v=DMARC1; p=${data.p}"] + ++ lib.pipe data [ + (builtins.intersectAttrs options) # remove garbage list `_module` + (lib.filterAttrs (k: v: v != null && v != "" && k != "p")) + (lib.mapAttrsToList (k: v: "${k}=${v}")) + ]; + result = lib.concatStringsSep "; " items + ";"; + in + util.writeCharacterString result; + nameFixup = name: _self: "_dmarc.${name}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/DNAME.nix b/modules/by-name/dn/dns/dns/types/records/DNAME.nix new file mode 100644 index 0000000..042ce95 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/DNAME.nix @@ -0,0 +1,15 @@ +# RFC 6672 +{lib, ...}: let + inherit (lib) dns mkOption; +in { + rtype = "DNAME"; + options = { + dname = mkOption { + type = dns.types.domain-name; + example = "www.test.com"; + description = "A <domain-name> which provides redirection from a part of the DNS name tree to another part of the DNS name tree"; + }; + }; + dataToString = {dname, ...}: "${dname}"; + fromString = dname: {inherit dname;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/DNSKEY.nix b/modules/by-name/dn/dns/dns/types/records/DNSKEY.nix new file mode 100644 index 0000000..86ce3a1 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/DNSKEY.nix @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2020 Aluísio Augusto Silva Gonçalves <https://aasg.name> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# RFC 4034, 2 +{lib, ...}: let + inherit (builtins) isInt split; + inherit (lib) concatStrings flatten mkOption types; + + dnssecOptions = import ./dnssec.nix {inherit lib;}; + inherit (dnssecOptions) mkDNSSECAlgorithmOption; +in { + rtype = "DNSKEY"; + options = { + flags = mkOption { + description = "Flags pertaining to this RR."; + type = types.either types.ints.u16 (types.submodule { + options = { + zoneSigningKey = mkOption { + description = "Whether this RR holds a zone signing key (ZSK)."; + type = types.bool; + default = false; + }; + secureEntryPoint = mkOption { + type = types.bool; + description = '' + Whether this RR holds a secure entry point. + In general, this means the key is a key-signing key (KSK), as opposed to a zone-signing key. + ''; + default = false; + }; + }; + }); + apply = value: + if isInt value + then value + else + ( + if value.zoneSigningKey + then 256 + else 0 + ) + + ( + if value.secureEntryPoint + then 1 + else 0 + ); + }; + algorithm = mkDNSSECAlgorithmOption { + description = "Algorithm of the key referenced by this RR."; + }; + publicKey = mkOption { + type = types.str; + description = "Base64-encoded public key."; + apply = value: concatStrings (flatten (split "[[:space:]]" value)); + }; + }; + dataToString = { + flags, + algorithm, + publicKey, + ... + }: "${toString flags} 3 ${toString algorithm} ${publicKey}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/DS.nix b/modules/by-name/dn/dns/dns/types/records/DS.nix new file mode 100644 index 0000000..76fac9a --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/DS.nix @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: 2020 Aluísio Augusto Silva Gonçalves <https://aasg.name> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# RFC 4034, 5 +{lib, ...}: let + inherit (lib) mkOption types; + + dnssecOptions = import ./dnssec.nix {inherit lib;}; + inherit (dnssecOptions) mkRegisteredNumberOption mkDNSSECAlgorithmOption; + + mkDSDigestTypeOption = args: + mkRegisteredNumberOption { + registryName = "Delegation Signer (DS) Resource Record (RR) Type Digest Algorithms"; + numberType = types.ints.u8; + # These mnemonics are unofficial, unlike the DNSSEC algorithm ones. + mnemonics = { + "sha-1" = 1; + "sha-256" = 2; + "gost" = 3; + "sha-384" = 4; + }; + }; +in { + rtype = "DS"; + options = { + keyTag = mkOption { + description = "Tag computed over the DNSKEY referenced by this RR to identify it."; + type = types.ints.u16; + }; + algorithm = mkDNSSECAlgorithmOption { + description = "Algorithm of the key referenced by this RR."; + }; + digestType = mkDSDigestTypeOption { + description = "Type of the digest given in the `digest` attribute."; + }; + digest = mkOption { + description = "Digest of the DNSKEY referenced by this RR."; + type = types.strMatching "[[:xdigit:]]+"; + }; + }; + dataToString = { + keyTag, + algorithm, + digestType, + digest, + ... + }: "${toString keyTag} ${toString algorithm} ${toString digestType} ${digest}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/HTTPS.nix b/modules/by-name/dn/dns/dns/types/records/HTTPS.nix new file mode 100644 index 0000000..6e2ef3d --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/HTTPS.nix @@ -0,0 +1,5 @@ +args: +import ./SVCB.nix args +// { + rtype = "HTTPS"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/MX.nix b/modules/by-name/dn/dns/dns/types/records/MX.nix new file mode 100644 index 0000000..c25b89c --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/MX.nix @@ -0,0 +1,32 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 1035, 3.3.9 +{ + lib, + simple, + ... +}: let + inherit (lib) mkOption types; +in { + rtype = "MX"; + options = { + preference = mkOption { + type = types.ints.u16; + example = 10; + description = "The preference given to this RR among others at the same owner. Lower values are preferred"; + }; + exchange = mkOption { + type = simple.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"; + }; + }; + dataToString = { + preference, + exchange, + ... + }: "${toString preference} ${exchange}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/NS.nix b/modules/by-name/dn/dns/dns/types/records/NS.nix new file mode 100644 index 0000000..ea60a91 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/NS.nix @@ -0,0 +1,24 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 1035, 3.3.11 +{ + lib, + simple, + ... +}: let + inherit (lib) mkOption; +in { + rtype = "NS"; + options = { + nsdname = mkOption { + type = simple.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"; + }; + }; + dataToString = {nsdname, ...}: "${nsdname}"; + fromString = nsdname: {inherit nsdname;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/OPENPGPKEY.nix b/modules/by-name/dn/dns/dns/types/records/OPENPGPKEY.nix new file mode 100644 index 0000000..1f39cb9 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/OPENPGPKEY.nix @@ -0,0 +1,18 @@ +# RFC7929 +{ + lib, + util, + ... +}: let + inherit (lib) mkOption types; +in { + rtype = "OPENPGPKEY"; + options = { + data = mkOption { + type = types.str; + }; + }; + + dataToString = {data, ...}: util.writeCharacterString data; + fromString = data: {inherit data;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/PTR.nix b/modules/by-name/dn/dns/dns/types/records/PTR.nix new file mode 100644 index 0000000..32a1913 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/PTR.nix @@ -0,0 +1,24 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 1035, 3.3.12 +{ + lib, + simple, + ... +}: let + inherit (lib) mkOption; +in { + rtype = "PTR"; + options = { + ptrdname = mkOption { + type = simple.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"; + }; + }; + dataToString = {ptrdname, ...}: "${ptrdname}"; + fromString = ptrdname: {inherit ptrdname;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/SOA.nix b/modules/by-name/dn/dns/dns/types/records/SOA.nix new file mode 100644 index 0000000..db7436e --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/SOA.nix @@ -0,0 +1,65 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 1035, 3.3.13 +{ + lib, + simple, + ... +}: let + inherit (lib) concatStringsSep removeSuffix replaceStrings; + inherit (lib) mkOption types; +in { + rtype = "SOA"; + options = { + nameServer = mkOption { + type = simple.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 = simple.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); + }; + serial = mkOption { + type = types.ints.unsigned; # TODO: u32 + example = 20; + description = "Version number of the original copy of the zone"; + }; + refresh = mkOption { + type = types.ints.unsigned; # TODO: u32 + default = 24 * 60 * 60; + example = 7200; + description = "Time interval before the zone should be refreshed"; + }; + retry = mkOption { + type = types.ints.unsigned; # TODO: u32 + default = 10 * 60; + example = 600; + description = "Time interval that should elapse before a failed refresh should be retried"; + }; + expire = mkOption { + type = types.ints.unsigned; # TODO: u32 + default = 10 * 24 * 60 * 60; + example = 3600000; + description = "Time value that specifies the upper limit on the time interval that can elapse before the zone is no longer authoritative"; + }; + minimum = mkOption { + type = types.ints.unsigned; # TODO: u32 + default = 60; + example = 60; + description = "Minimum TTL field that should be exported with any RR from this zone"; + }; + }; + dataToString = data @ { + nameServer, + adminEmail, + ... + }: let + numbers = map toString (with data; [serial refresh retry expire minimum]); + in "${nameServer} ${adminEmail}. (${concatStringsSep " " numbers})"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/SRV.nix b/modules/by-name/dn/dns/dns/types/records/SRV.nix new file mode 100644 index 0000000..5f558ed --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/SRV.nix @@ -0,0 +1,51 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 2782 +{ + lib, + simple, + ... +}: let + inherit (lib) mkOption types; +in { + rtype = "SRV"; + options = { + service = mkOption { + type = types.str; + example = "foobar"; + description = "The symbolic name of the desired service. Do not add the underscore!"; + }; + proto = mkOption { + type = types.str; + example = "tcp"; + description = "The symbolic name of the desired protocol. Do not add the underscore!"; + }; + priority = mkOption { + type = types.ints.u16; + default = 0; + example = 0; + description = "The priority of this target host"; + }; + weight = mkOption { + type = types.ints.u16; + default = 100; + example = 20; + description = "The weight field specifies a relative weight for entries with the same priority. Larger weights SHOULD be given a proportionately higher probability of being selected"; + }; + port = mkOption { + type = types.ints.u16; + example = 9; + description = "The port on this target host of this service"; + }; + target = mkOption { + type = simple.types.domain-name; + example = ""; + description = "The domain name of the target host"; + }; + }; + dataToString = data: with data; "${toString priority} ${toString weight} ${toString port} ${target}"; + nameFixup = name: self: "_${self.service}._${self.proto}.${name}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/SSHFP.nix b/modules/by-name/dn/dns/dns/types/records/SSHFP.nix new file mode 100644 index 0000000..1409860 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/SSHFP.nix @@ -0,0 +1,39 @@ +# RFC 4255 +{lib, ...}: let + inherit (lib) mkOption types; + inherit (builtins) attrNames; + algorithm = { + "rsa" = 1; + "dsa" = 2; + "ecdsa" = 3; # RFC 6594 + "ed25519" = 4; # RFC 7479 / RFC 8709 + "ed448" = 6; # RFC 8709 + }; + mode = { + "sha1" = 1; + "sha256" = 2; # RFC 6594 + }; +in { + rtype = "SSHFP"; + options = { + algorithm = mkOption { + example = "ed25519"; + type = types.enum (attrNames algorithm); + apply = value: algorithm.${value}; + }; + fingerprintType = mkOption { + example = "sha256"; + type = types.enum (attrNames mode); + apply = value: mode.${value}; + }; + fingerprint = mkOption { + type = types.str; + }; + }; + dataToString = { + algorithm, + fingerprintType, + fingerprint, + ... + }: "${toString algorithm} ${toString fingerprintType} ${fingerprint}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/SVCB.nix b/modules/by-name/dn/dns/dns/types/records/SVCB.nix new file mode 100644 index 0000000..62cbc3d --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/SVCB.nix @@ -0,0 +1,100 @@ +# rfc9460 +{lib, ...}: let + inherit + (lib) + concatStringsSep + filter + isInt + isList + mapAttrsToList + mkOption + types + ; + + mkSvcParams = params: + concatStringsSep " " ( + filter (s: s != "") ( + mapAttrsToList ( + name: value: + if value + then name + else if isList value + then "${name}=${concatStringsSep "," value}" + else if isInt value + then "${name}=${builtins.toString value}" + else "" + ) + params + ) + ); +in { + rtype = "SVCB"; + options = { + svcPriority = mkOption { + example = 1; + type = types.ints.u16; + }; + targetName = mkOption { + example = "."; + type = types.str; + }; + mandatory = mkOption { + example = ["ipv4hint"]; + default = null; + type = types.nullOr (types.nonEmptyListOf types.str); + }; + alpn = mkOption { + example = ["h2"]; + default = null; + type = types.nullOr (types.nonEmptyListOf types.str); + }; + no-default-alpn = mkOption { + example = true; + default = false; + type = types.bool; + }; + port = mkOption { + example = 443; + default = null; + type = types.nullOr types.port; + }; + ipv4hint = mkOption { + example = ["127.0.0.1"]; + default = null; + type = types.nullOr (types.nonEmptyListOf types.str); + }; + ipv6hint = mkOption { + example = ["::1"]; + default = null; + type = types.nullOr (types.nonEmptyListOf types.str); + }; + ech = mkOption { + type = types.nullOr types.str; + default = null; + }; + }; + dataToString = { + svcPriority, + targetName, + mandatory ? null, + alpn ? null, + no-default-alpn ? null, + port ? null, + ipv4hint ? null, + ipv6hint ? null, + ech ? null, + ... + }: "${toString svcPriority} ${targetName} ${ + mkSvcParams { + inherit + alpn + ech + ipv4hint + ipv6hint + mandatory + no-default-alpn + port + ; + } + }"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/TLSA.nix b/modules/by-name/dn/dns/dns/types/records/TLSA.nix new file mode 100644 index 0000000..d92a29b --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/TLSA.nix @@ -0,0 +1,50 @@ +# RFC 6698 +{lib, ...}: let + inherit (lib) mkOption types; + inherit (builtins) attrNames; + + certUsage = { + "pkix-ta" = 0; + "pkix-ee" = 1; + "dane-ta" = 2; + "dane-ee" = 3; + }; + selectors = { + "cert" = 0; + "spki" = 1; + }; + match = { + "exact" = 0; + "sha256" = 1; + "sha512" = 2; + }; +in { + rtype = "TLSA"; + options = { + certUsage = mkOption { + example = "dane-ee"; + type = types.enum (attrNames certUsage); + apply = value: certUsage.${value}; + }; + selector = mkOption { + example = "spki"; + type = types.enum (attrNames selectors); + apply = value: selectors.${value}; + }; + matchingType = mkOption { + example = "sha256"; + type = types.enum (attrNames match); + apply = value: match.${value}; + }; + certificate = mkOption { + type = types.str; + }; + }; + dataToString = { + certUsage, + selector, + matchingType, + certificate, + ... + }: "${toString certUsage} ${toString selector} ${toString matchingType} ${certificate}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/TXT.nix b/modules/by-name/dn/dns/dns/types/records/TXT.nix new file mode 100644 index 0000000..d605ce8 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/TXT.nix @@ -0,0 +1,24 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 1035, 3.3.14 +{ + lib, + util, + ... +}: let + inherit (lib) mkOption types; +in { + rtype = "TXT"; + options = { + data = mkOption { + type = types.str; + example = "favorite drink=orange juice"; + description = "Arbitrary information"; + }; + }; + dataToString = {data, ...}: util.writeCharacterString data; + fromString = data: {inherit data;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/default.nix b/modules/by-name/dn/dns/dns/types/records/default.nix new file mode 100644 index 0000000..b6f6270 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/default.nix @@ -0,0 +1,42 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{ + lib, + util, + simple, +}: let + inherit (lib.attrsets) genAttrs; + + types = [ + "A" + "AAAA" + "CAA" + "CNAME" + "DNAME" + "MX" + "NS" + "SOA" + "SRV" + "TXT" + "PTR" + + # DNSSEC types + "DNSKEY" + "DS" + + # DANE types + "SSHFP" + "TLSA" + "OPENPGPKEY" + "SVCB" + "HTTPS" + + # Pseudo types + "DKIM" + "DMARC" + ]; +in + genAttrs types (t: import (./. + "/${t}.nix") {inherit lib simple util;}) diff --git a/modules/by-name/dn/dns/dns/types/records/dnssec.nix b/modules/by-name/dn/dns/dns/types/records/dnssec.nix new file mode 100644 index 0000000..648f676 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/dnssec.nix @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: 2020 Aluísio Augusto Silva Gonçalves <https://aasg.name> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +{lib}: let + inherit (builtins) attrNames isInt removeAttrs; + inherit (lib) mkOption types; +in rec { + mkRegisteredNumberOption = { + registryName, + numberType, + mnemonics, + } @ args: + mkOption + { + type = + types.either numberType (types.enum (attrNames mnemonics)) + // { + name = "registeredNumber"; + description = "number in IANA registry '${registryName}'"; + }; + apply = value: + if isInt value + then value + else mnemonics.${value}; + } + // removeAttrs args ["registryName" "numberType" "mnemonics"]; + + mkDNSSECAlgorithmOption = args: + mkRegisteredNumberOption { + registryName = "Domain Name System Security (DNSSEC) Algorithm Numbers"; + numberType = types.ints.u8; + mnemonics = { + "dsa" = 3; + "rsasha1" = 5; + "dsa-nsec3-sha1" = 6; + "rsasha1-nsec3-sha1" = 7; + "rsasha256" = 8; + "rsasha512" = 10; + "ecc-gost" = 12; + "ecdsap256sha256" = 13; + "ecdsap384sha384" = 14; + "ed25519" = 15; + "ed448" = 16; + "privatedns" = 253; + "privateoid" = 254; + }; + }; +} diff --git a/modules/by-name/dn/dns/dns/types/simple.nix b/modules/by-name/dn/dns/dns/types/simple.nix new file mode 100644 index 0000000..fece2c9 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/simple.nix @@ -0,0 +1,9 @@ +# 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/modules/by-name/dn/dns/dns/types/zone.nix b/modules/by-name/dn/dns/dns/types/zone.nix new file mode 100644 index 0000000..44ccb15 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/zone.nix @@ -0,0 +1,119 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# SPDX-FileCopyrightText: 2021 Naïm Favier <n@monade.li> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{ + lib, + util, + simple, +}: let + inherit (builtins) filter removeAttrs; + inherit + (lib) + concatMapStringsSep + concatStringsSep + mapAttrs + mapAttrsToList + optionalString + ; + inherit (lib) mkOption literalExample types; + + inherit (import ./record.nix {inherit lib;}) recordType writeRecord; + + rsubtypes = import ./records {inherit lib util simple;}; + rsubtypes' = removeAttrs rsubtypes ["SOA"]; + + subzoneOptions = + { + subdomains = mkOption { + type = types.attrsOf subzone; + default = {}; + example = { + www = { + A = [{address = "1.1.1.1";}]; + }; + staging = { + A = [{address = "1.0.0.1";}]; + }; + }; + description = "Records for subdomains of the domain"; + }; + } + // mapAttrs (n: t: + mkOption { + type = types.listOf (recordType t); + default = []; + # example = [ t.example ]; # TODO: any way to auto-generate an example for submodule? + description = "List of ${n} records for this zone/subzone"; + }) + rsubtypes'; + + subzone = types.submodule { + options = subzoneOptions; + }; + + writeSubzone = name: zone: let + groupToString = pseudo: subt: + concatMapStringsSep "\n" (writeRecord name subt) zone."${pseudo}"; + groups = mapAttrsToList groupToString rsubtypes'; + groups' = filter (s: s != "") groups; + + writeSubzone' = subname: writeSubzone "${subname}.${name}"; + sub = concatStringsSep "\n\n" (mapAttrsToList writeSubzone' zone.subdomains); + in + concatStringsSep "\n\n" groups' + + optionalString (sub != "") ("\n\n" + sub); + zone = types.submodule ({name, ...}: { + options = + { + useOrigin = mkOption { + type = types.bool; + default = false; + description = "Wether to use $ORIGIN and unqualified name or fqdn when exporting the zone."; + }; + + TTL = mkOption { + type = types.ints.unsigned; + default = 24 * 60 * 60; + example = literalExample "60 * 60"; + description = "Default record caching duration. Sets the $TTL variable"; + }; + SOA = mkOption rec { + type = recordType rsubtypes.SOA; + example = + { + ttl = 24 * 60 * 60; + } + // type.example; + description = "SOA record"; + }; + } + // subzoneOptions; + }); + renderToString = name: { + useOrigin, + TTL, + SOA, + ... + } @ zone: + if useOrigin + then '' + $ORIGIN ${name}. + $TTL ${toString TTL} + + ${writeRecord "@" rsubtypes.SOA SOA} + + ${writeSubzone "@" zone} + '' + else '' + $TTL ${toString TTL} + + ${writeRecord name rsubtypes.SOA SOA} + + ${writeSubzone name zone} + ''; +in { + inherit zone subzone renderToString; +} diff --git a/modules/by-name/dn/dns/dns/util/default.nix b/modules/by-name/dn/dns/dns/util/default.nix new file mode 100644 index 0000000..59e661d --- /dev/null +++ b/modules/by-name/dn/dns/dns/util/default.nix @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: 2021 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +{lib}: let + inherit + (builtins) + concatStringsSep + genList + stringLength + substring + ; + inherit + (lib.strings) + concatMapStrings + concatMapStringsSep + fixedWidthString + splitString + stringToCharacters + ; + inherit (lib.lists) filter reverseList; + + /* + Split a string into byte chunks, such that each output String is less then or equal to + `n` bytes. + + # Type + + splitInGroupsOf :: Integer -> String -> [String] + + # Arguments + + n + : The number of bytes to put into each String. + + s + : The String to split. + */ + 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); + + # : str -> str, with length 4 (zeros are padded to the left) + align4Bytes = fixedWidthString 4 "0"; + + # : int -> str -> str + # Expands "" to 4n zeros and aligns the rest on 4 bytes + align4BytesOrExpand = n: v: + if v == "" + then (fixedWidthString (4 * n) "0" "") + else align4Bytes v; + + # : str -> [ str ] + # Returns the record of the ipv6 as a list + mkRecordAux = v6: let + splitted = splitString ":" v6; + n = 8 - builtins.length (filter (x: x != "") splitted); + in + stringToCharacters (concatMapStrings (align4BytesOrExpand n) splitted); + + # : str -> str + # Returns the reversed record of the ipv6 + mkReverseRecord = v6: + concatStringsSep "." (reverseList (mkRecordAux v6)) + ".ip6.arpa"; +in { + inherit writeCharacterString mkReverseRecord; +} diff --git a/modules/by-name/dn/dns/module.nix b/modules/by-name/dn/dns/module.nix new file mode 100644 index 0000000..6611f8c --- /dev/null +++ b/modules/by-name/dn/dns/module.nix @@ -0,0 +1,45 @@ +{ + config, + lib, + ... +}: let + cfg = config.vhack.dns; + + zones = lib.debug.traceValSeqN 2 ( + builtins.mapAttrs (name: value: { + data = + dns.types.zone.renderToString name value; + }) + (lib.debug.traceValSeqN 4 cfg.zones) + ); + + dns = import ./dns {inherit lib;}; +in { + options.vhack.dns = { + enable = lib.mkEnableOption "custom dns server"; + + interfaces = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = '' + A list of the interfaces to bind to. + ''; + example = [ + "192.168.1.3" + "2001:db8:1::3" + ]; + }; + + zones = lib.mkOption { + type = lib.types.attrsOf dns.types.zone.zone; + description = "DNS zones"; + }; + }; + + config = lib.mkIf cfg.enable { + services.nsd = { + enable = true; + inherit (cfg) interfaces; + inherit zones; + }; + }; +} |