aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/basic.nix185
-rwxr-xr-xtests/common/acme/certs/generate66
-rwxr-xr-xtests/common/acme/certs/generate.ca38
-rwxr-xr-xtests/common/acme/certs/generate.client44
-rw-r--r--tests/common/acme/certs/output/acme.test.cert.pem11
-rw-r--r--tests/common/acme/certs/output/acme.test.key.pem25
-rw-r--r--tests/common/acme/certs/output/acme.test.template5
-rw-r--r--tests/common/acme/certs/output/ca.cert.pem10
-rw-r--r--tests/common/acme/certs/output/ca.key.pem25
-rw-r--r--tests/common/acme/certs/output/ca.template5
-rw-r--r--tests/common/acme/certs/snakeoil-certs.nix13
-rw-r--r--tests/common/acme/client.nix21
-rw-r--r--tests/common/acme/default.nix47
-rw-r--r--tests/common/acme/server.nix91
-rw-r--r--tests/common/dns/client.nix10
-rw-r--r--tests/common/dns/module/default.nix86
-rw-r--r--tests/common/dns/module/dns/default.nix13
-rw-r--r--tests/common/dns/module/dns/types/default.nix16
-rw-r--r--tests/common/dns/module/dns/types/record.nix75
-rw-r--r--tests/common/dns/module/dns/types/records/A.nix19
-rw-r--r--tests/common/dns/module/dns/types/records/AAAA.nix19
-rw-r--r--tests/common/dns/module/dns/types/records/CAA.nix42
-rw-r--r--tests/common/dns/module/dns/types/records/CNAME.nix27
-rw-r--r--tests/common/dns/module/dns/types/records/DKIM.nix75
-rw-r--r--tests/common/dns/module/dns/types/records/DMARC.nix108
-rw-r--r--tests/common/dns/module/dns/types/records/DNAME.nix15
-rw-r--r--tests/common/dns/module/dns/types/records/DNSKEY.nix63
-rw-r--r--tests/common/dns/module/dns/types/records/DS.nix48
-rw-r--r--tests/common/dns/module/dns/types/records/HTTPS.nix5
-rw-r--r--tests/common/dns/module/dns/types/records/MTA-STS.nix42
-rw-r--r--tests/common/dns/module/dns/types/records/MX.nix32
-rw-r--r--tests/common/dns/module/dns/types/records/NS.nix24
-rw-r--r--tests/common/dns/module/dns/types/records/OPENPGPKEY.nix18
-rw-r--r--tests/common/dns/module/dns/types/records/PTR.nix92
-rw-r--r--tests/common/dns/module/dns/types/records/SOA.nix65
-rw-r--r--tests/common/dns/module/dns/types/records/SRV.nix51
-rw-r--r--tests/common/dns/module/dns/types/records/SSHFP.nix39
-rw-r--r--tests/common/dns/module/dns/types/records/SVCB.nix100
-rw-r--r--tests/common/dns/module/dns/types/records/TLSA.nix50
-rw-r--r--tests/common/dns/module/dns/types/records/TXT.nix24
-rw-r--r--tests/common/dns/module/dns/types/records/default.nix43
-rw-r--r--tests/common/dns/module/dns/types/records/dnssec.nix48
-rw-r--r--tests/common/dns/module/dns/types/simple.nix9
-rw-r--r--tests/common/dns/module/dns/types/zone.nix119
-rw-r--r--tests/common/dns/module/dns/util/default.nix76
-rw-r--r--tests/common/dns/server.nix43
46 files changed, 2082 insertions, 0 deletions
diff --git a/tests/basic.nix b/tests/basic.nix
new file mode 100644
index 00000000..7495d093
--- /dev/null
+++ b/tests/basic.nix
@@ -0,0 +1,185 @@
+{pkgs, ...}: {
+ name = "turtle-sync";
+
+ node = {};
+
+ nodes = let
+ atuinSession = "01969ec6b8d07e30a9d2df0911fbfe2a";
+ in {
+ acme = {
+ imports = [
+ ./common/acme/server.nix
+ ./common/dns/client.nix
+ ../nix/module.nix
+ ];
+ };
+ name_server = {nodes, ...}: {
+ imports = [
+ ./common/acme/client.nix
+ ./common/dns/server.nix
+ ../nix/module.nix
+ ];
+
+ vhack.dns.zones = {
+ "turtle-sync.server" = {
+ SOA = {
+ nameServer = "ns";
+ adminEmail = "admin@server.com";
+ serial = 2025012301;
+ };
+ useOrigin = false;
+
+ A = [
+ nodes.server.networking.primaryIPAddress
+ ];
+ AAAA = [
+ nodes.server.networking.primaryIPv6Address
+ ];
+ };
+ };
+ };
+ server = {config, ...}: let
+ turtleCfg = config.services.turtle;
+ in {
+ imports = [
+ ../nix/module.nix
+ ./common/acme/client.nix
+ ./common/dns/client.nix
+ ];
+
+ config = {
+ services = {
+ postgresql.enable = true;
+ turtle = {
+ enable = true;
+ host = "127.0.0.1";
+ database.createLocally = true;
+ };
+ nginx = {
+ enable = true;
+
+ recommendedTlsSettings = true;
+ recommendedOptimisation = true;
+ recommendedGzipSettings = true;
+ recommendedProxySettings = true;
+
+ virtualHosts."turtle-sync.server" = {
+ locations."/" = {
+ proxyPass = "http://${turtleCfg.host}:${toString turtleCfg.port}";
+
+ recommendedProxySettings = true;
+ proxyWebsockets = true;
+ };
+
+ enableACME = true;
+ forceSSL = true;
+ };
+ };
+ };
+ networking.firewall = {
+ allowedTCPPorts = [80 443];
+ };
+ };
+ };
+
+ client1 = {
+ config,
+ pkgs,
+ ...
+ }: {
+ imports = [
+ ../nix/module.nix
+ ./common/acme/client.nix
+ ./common/dns/client.nix
+ ];
+
+ environment.sessionVariables.ATUIN_SESSION = atuinSession;
+ };
+ client2 = {
+ config,
+ pkgs,
+ ...
+ }: {
+ imports = [
+ ../nix/module.nix
+ ./common/acme/client.nix
+ ./common/dns/client.nix
+ ];
+
+ environment.sessionVariables.ATUIN_SESSION = atuinSession;
+ };
+ };
+
+ testScript = {nodes, ...}: let
+ mkSyncConfig = pkgs.writeShellScript "write-turtle-sync-config" ''
+ mkdir --parents ~/.config/atuin/
+ cat << EOF > ~/.config/atuin/config.toml
+
+ [sync]
+ address = "https://turtle-sync.server"
+ user_id_path = "${pkgs.writeText "user-id" "019eb88a-6b51-7e52-b12c-7d30bd8e5928"}"
+ encryption_key_path = "${pkgs.writeText "encryption-key" "3AAgbWsDzL7M00/Mq0LMjsyOCy3MnsypBsyQzKbMywNGzNnMrUBozIINAxdbIiDMhQ=="}"
+ EOF
+ '';
+
+ runCommandAndRecordInTurtle = pkgs.writeShellScript "run-command-and-record-in-turtle" ''
+ # SPDX-SnippetBegin
+ # SPDX-SnippetCopyrightText: 2023 mentalisttraceur (https://github.com/mentalisttraceur)
+ # Source: https://github.com/atuinsh/atuin/issues/1188#issuecomment-1698354107
+ run_and_record_in_turtle()
+ {
+ local id
+ local status
+ local escaped_command="$(printf '%q ' "$@")"
+ id="$(atuin history start -- "$escaped_command")"
+ "$@"
+ status=$?
+ atuin history end --exit $status "$id"
+ return $status
+ }
+ # SPDX-SnippetEnd
+
+ run_and_record_in_turtle "$@"
+ '';
+
+ acme = import ./common/acme {inherit pkgs;};
+ in
+ acme.prepare ["server" "client1" "client2"]
+ # Python
+ ''
+ server.wait_for_unit("turtle.service")
+ server.wait_for_open_port(443)
+
+ # Wait for the server to acquire the acme certificate
+ client1.wait_until_succeeds("curl https://turtle-sync.server")
+
+ with subtest("Setup client syncing"):
+ for client in [client1, client2]:
+ client.succeed("${mkSyncConfig}")
+
+ with subtest("Can generate shell history"):
+ client1.succeed("${runCommandAndRecordInTurtle} echo hi - client 1")
+ client2.succeed("${runCommandAndRecordInTurtle} echo hi - client 2")
+
+ with subtest("Can sync"):
+ for client in [client1, client2]:
+ client.succeed("atuin sync perform --force")
+ client1.succeed("atuin sync perform --force")
+
+
+ with subtest("Have correct tasks"):
+ hist1 = client1.succeed("atuin history list --format '{command}'").strip().split('\n')
+ hist2 = client2.succeed("atuin history list --format '{command}'").strip().split('\n')
+
+ hist1.sort()
+ hist2.sort()
+
+ canonicalHistory = [
+ "echo hi - client 1",
+ "echo hi - client 2"
+ ]
+
+ assert hist1 == hist2, f"The clients don't have the same amount of history items, client1: '{hist1}', client2: '{hist2}'"
+ assert hist1 == canonicalHistory, f"The history is not correct: '{hist1}' vs. '{canonicalHistory}'"
+ '';
+}
diff --git a/tests/common/acme/certs/generate b/tests/common/acme/certs/generate
new file mode 100755
index 00000000..0d6258eb
--- /dev/null
+++ b/tests/common/acme/certs/generate
@@ -0,0 +1,66 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -p gnutls -p dash -i dash --impure
+# shellcheck shell=dash
+
+# For development and testing.
+# Create a CA key and cert, and use that to generate a server key and cert.
+# Creates:
+# ca.key.pem
+# ca.cert.pem
+# server.key.pem
+# server.cert.pem
+
+export SEC_PARAM=ultra
+export EXPIRATION_DAYS=123456
+export ORGANIZATION="Vhack.eu Test Keys"
+export COUNTRY=EU
+export SAN="acme.test"
+export KEY_TYPE="ed25519"
+
+BASEDIR="$(dirname "$0")"
+GENERATION_LOCATION="$BASEDIR/output"
+cd "$BASEDIR" || {
+ echo "(BUG?) No basedir ('$BASEDIR')" 1>&2
+ exit 1
+}
+
+ca=false
+clients=false
+
+usage() {
+ echo "Usage: $0 --ca|--clients"
+ exit 2
+}
+
+if [ "$#" -eq 0 ]; then
+ usage
+fi
+
+for arg in "$@"; do
+ case "$arg" in
+ "--ca")
+ ca=true
+ ;;
+ "--clients")
+ clients=true
+ ;;
+ *)
+ usage
+ ;;
+ esac
+done
+
+[ -d "$GENERATION_LOCATION" ] || mkdir --parents "$GENERATION_LOCATION"
+cd "$GENERATION_LOCATION" || echo "(BUG?) No generation location fould!" 1>&2
+
+[ "$ca" = true ] && ../generate.ca
+
+# Creates:
+# <client_name>.key.pem
+# <client_name>.cert.pem
+#
+[ "$clients" = true ] && ../generate.client "acme.test"
+
+echo "(INFO) Look for the keys at: $GENERATION_LOCATION"
+
+# vim: ft=sh
diff --git a/tests/common/acme/certs/generate.ca b/tests/common/acme/certs/generate.ca
new file mode 100755
index 00000000..92832c54
--- /dev/null
+++ b/tests/common/acme/certs/generate.ca
@@ -0,0 +1,38 @@
+#! /usr/bin/env sh
+
+# Take the correct binary to create the certificates
+CERTTOOL=$(command -v gnutls-certtool 2>/dev/null || command -v certtool 2>/dev/null)
+if [ -z "$CERTTOOL" ]; then
+ echo "ERROR: No certtool found" >&2
+ exit 1
+fi
+
+# Create a CA key.
+$CERTTOOL \
+ --generate-privkey \
+ --sec-param "$SEC_PARAM" \
+ --key-type "$KEY_TYPE" \
+ --outfile ca.key.pem
+
+chmod 600 ca.key.pem
+
+# Sign a CA cert.
+cat <<EOF >ca.template
+country = $COUNTRY
+dns_name = "$SAN"
+expiration_days = $EXPIRATION_DAYS
+organization = $ORGANIZATION
+ca
+EOF
+#state = $STATE
+#locality = $LOCALITY
+
+$CERTTOOL \
+ --generate-self-signed \
+ --load-privkey ca.key.pem \
+ --template ca.template \
+ --outfile ca.cert.pem
+
+chmod 600 ca.cert.pem
+
+# vim: ft=sh
diff --git a/tests/common/acme/certs/generate.client b/tests/common/acme/certs/generate.client
new file mode 100755
index 00000000..5930298a
--- /dev/null
+++ b/tests/common/acme/certs/generate.client
@@ -0,0 +1,44 @@
+#! /usr/bin/env sh
+
+# Take the correct binary to create the certificates
+CERTTOOL=$(command -v gnutls-certtool 2>/dev/null || command -v certtool 2>/dev/null)
+if [ -z "$CERTTOOL" ]; then
+ echo "ERROR: No certtool found" >&2
+ exit 1
+fi
+
+NAME=client
+if [ $# -gt 0 ]; then
+ NAME="$1"
+fi
+
+# Create a client key.
+$CERTTOOL \
+ --generate-privkey \
+ --sec-param "$SEC_PARAM" \
+ --key-type "$KEY_TYPE" \
+ --outfile "$NAME".key.pem
+
+chmod 600 "$NAME".key.pem
+
+# Sign a client cert with the key.
+cat <<EOF >"$NAME".template
+dns_name = "$NAME"
+dns_name = "$SAN"
+expiration_days = $EXPIRATION_DAYS
+organization = $ORGANIZATION
+encryption_key
+signing_key
+EOF
+
+$CERTTOOL \
+ --generate-certificate \
+ --load-privkey "$NAME".key.pem \
+ --load-ca-certificate ca.cert.pem \
+ --load-ca-privkey ca.key.pem \
+ --template "$NAME".template \
+ --outfile "$NAME".cert.pem
+
+chmod 600 "$NAME".cert.pem
+
+# vim: ft=sh
diff --git a/tests/common/acme/certs/output/acme.test.cert.pem b/tests/common/acme/certs/output/acme.test.cert.pem
new file mode 100644
index 00000000..687101d1
--- /dev/null
+++ b/tests/common/acme/certs/output/acme.test.cert.pem
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBjTCCAT+gAwIBAgIUfiDKld3eiPKuFhsaiHpPNmbMJU8wBQYDK2VwMCoxCzAJ
+BgNVBAYTAkVVMRswGQYDVQQKExJWaGFjay5ldSBUZXN0IEtleXMwIBcNMjUwMzAx
+MTEyNjU2WhgPMjM2MzAzMDYxMTI2NTZaMB0xGzAZBgNVBAoTElZoYWNrLmV1IFRl
+c3QgS2V5czAqMAUGAytlcAMhAHYq2cjrfrlslWxvcKjs2cD7THbpmtq+jf/dlrKW
+UEo8o4GBMH8wDAYDVR0TAQH/BAIwADAfBgNVHREEGDAWgglhY21lLnRlc3SCCWFj
+bWUudGVzdDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFN/1UyS0jnC3LoryMIL2
+/6cdsYBBMB8GA1UdIwQYMBaAFLUZcL/zguHlulHg5GYyYhXmVt/6MAUGAytlcANB
+ALz3u7lBreHeVZ0YXrwK3SDwlhWIH/SeUQwbxQlarzR47qu3cwQQ93Y1xjtOdu+h
+hOM/ig3nLGVOT6qL8IsZrQk=
+-----END CERTIFICATE-----
diff --git a/tests/common/acme/certs/output/acme.test.key.pem b/tests/common/acme/certs/output/acme.test.key.pem
new file mode 100644
index 00000000..06195b8c
--- /dev/null
+++ b/tests/common/acme/certs/output/acme.test.key.pem
@@ -0,0 +1,25 @@
+Public Key Info:
+ Public Key Algorithm: EdDSA (Ed25519)
+ Key Security Level: High (256 bits)
+
+curve: Ed25519
+private key:
+ 9d:25:38:89:f2:37:d7:65:41:f5:24:ba:4c:19:fb:0f
+ 86:c8:a3:cf:f7:08:57:69:cc:64:cf:55:2d:8e:99:3e
+
+
+x:
+ 76:2a:d9:c8:eb:7e:b9:6c:95:6c:6f:70:a8:ec:d9:c0
+ fb:4c:76:e9:9a:da:be:8d:ff:dd:96:b2:96:50:4a:3c
+
+
+
+Public Key PIN:
+ pin-sha256:NPwZitkDv4isUmdiicSsM1t1OtYoxqhdvBUnqSc4bFQ=
+Public Key ID:
+ sha256:34fc198ad903bf88ac52676289c4ac335b753ad628c6a85dbc1527a927386c54
+ sha1:dff55324b48e70b72e8af23082f6ffa71db18041
+
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIJ0lOInyN9dlQfUkukwZ+w+GyKPP9whXacxkz1Utjpk+
+-----END PRIVATE KEY-----
diff --git a/tests/common/acme/certs/output/acme.test.template b/tests/common/acme/certs/output/acme.test.template
new file mode 100644
index 00000000..320a1701
--- /dev/null
+++ b/tests/common/acme/certs/output/acme.test.template
@@ -0,0 +1,5 @@
+dns_name = "acme.test"
+dns_name = "acme.test"
+expiration_days = 123456
+organization = Vhack.eu Test Keys
+encryption_key
diff --git a/tests/common/acme/certs/output/ca.cert.pem b/tests/common/acme/certs/output/ca.cert.pem
new file mode 100644
index 00000000..0fa9d144
--- /dev/null
+++ b/tests/common/acme/certs/output/ca.cert.pem
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE-----
+MIIBYDCCARKgAwIBAgIUdhVVcf+NgElqGuutU55FUDBtFVMwBQYDK2VwMCoxCzAJ
+BgNVBAYTAkVVMRswGQYDVQQKExJWaGFjay5ldSBUZXN0IEtleXMwIBcNMjUwMzAx
+MTEyNjU2WhgPMjM2MzAzMDYxMTI2NTZaMCoxCzAJBgNVBAYTAkVVMRswGQYDVQQK
+ExJWaGFjay5ldSBUZXN0IEtleXMwKjAFBgMrZXADIQCkO1LhHINvJjt41JD6UEc4
+ZKKUubB8lKPxSOyTkFBOgqNIMEYwDwYDVR0TAQH/BAUwAwEB/zAUBgNVHREEDTAL
+gglhY21lLnRlc3QwHQYDVR0OBBYEFLUZcL/zguHlulHg5GYyYhXmVt/6MAUGAytl
+cANBAFMFFy5tjuQtp5GVEN6qM50L4lteQuxfhlQqmOOfl06HV6153wJnrlKaTOYO
+t0dKlSqKROMYUYeU39xDp07MLAc=
+-----END CERTIFICATE-----
diff --git a/tests/common/acme/certs/output/ca.key.pem b/tests/common/acme/certs/output/ca.key.pem
new file mode 100644
index 00000000..64263bcb
--- /dev/null
+++ b/tests/common/acme/certs/output/ca.key.pem
@@ -0,0 +1,25 @@
+Public Key Info:
+ Public Key Algorithm: EdDSA (Ed25519)
+ Key Security Level: High (256 bits)
+
+curve: Ed25519
+private key:
+ 82:0d:fc:f0:d6:82:89:63:e5:bc:23:78:ba:98:38:83
+ 09:2d:e0:78:4c:53:92:e3:db:5b:2f:e4:39:ce:96:3d
+
+
+x:
+ a4:3b:52:e1:1c:83:6f:26:3b:78:d4:90:fa:50:47:38
+ 64:a2:94:b9:b0:7c:94:a3:f1:48:ec:93:90:50:4e:82
+
+
+
+Public Key PIN:
+ pin-sha256:jpzYZMOHDPCeSXxfL+YUXgSPcbO9MAs8foGMP5CJiD8=
+Public Key ID:
+ sha256:8e9cd864c3870cf09e497c5f2fe6145e048f71b3bd300b3c7e818c3f9089883f
+ sha1:b51970bff382e1e5ba51e0e466326215e656dffa
+
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIIIN/PDWgolj5bwjeLqYOIMJLeB4TFOS49tbL+Q5zpY9
+-----END PRIVATE KEY-----
diff --git a/tests/common/acme/certs/output/ca.template b/tests/common/acme/certs/output/ca.template
new file mode 100644
index 00000000..a2295d8d
--- /dev/null
+++ b/tests/common/acme/certs/output/ca.template
@@ -0,0 +1,5 @@
+country = EU
+dns_name = "acme.test"
+expiration_days = 123456
+organization = Vhack.eu Test Keys
+ca
diff --git a/tests/common/acme/certs/snakeoil-certs.nix b/tests/common/acme/certs/snakeoil-certs.nix
new file mode 100644
index 00000000..aeb6dfce
--- /dev/null
+++ b/tests/common/acme/certs/snakeoil-certs.nix
@@ -0,0 +1,13 @@
+let
+ domain = "acme.test";
+in {
+ inherit domain;
+ ca = {
+ cert = ./output/ca.cert.pem;
+ key = ./output/ca.key.pem;
+ };
+ "${domain}" = {
+ cert = ./output/. + "/${domain}.cert.pem";
+ key = ./output/. + "/${domain}.key.pem";
+ };
+}
diff --git a/tests/common/acme/client.nix b/tests/common/acme/client.nix
new file mode 100644
index 00000000..2b870e89
--- /dev/null
+++ b/tests/common/acme/client.nix
@@ -0,0 +1,21 @@
+{
+ nodes,
+ lib,
+ ...
+}: let
+ inherit (nodes.acme.test-support.acme) caCert;
+ inherit (nodes.acme.test-support.acme) caDomain;
+in {
+ security = {
+ acme = {
+ acceptTerms = true;
+ defaults = {
+ server = "https://${caDomain}/dir";
+ };
+ };
+
+ pki = {
+ certificateFiles = lib.mkForce [caCert];
+ };
+ };
+}
diff --git a/tests/common/acme/default.nix b/tests/common/acme/default.nix
new file mode 100644
index 00000000..c756a4f1
--- /dev/null
+++ b/tests/common/acme/default.nix
@@ -0,0 +1,47 @@
+{pkgs}: let
+ add_pebble_ca_certs = pkgs.writeShellScript "fetch-and-set-ca" ''
+ set -xe
+
+ # Fetch the randomly generated ca certificate
+ curl https://acme.test:15000/roots/0 > /tmp/ca.crt
+ curl https://acme.test:15000/intermediates/0 >> /tmp/ca.crt
+
+ # Append it to the various system stores
+ # The file paths are from <nixpgks>/modules/security/ca.nix
+ for cert_path in "ssl/certs/ca-certificates.crt" "ssl/certs/ca-bundle.crt" "pki/tls/certs/ca-bundle.crt"; do
+ cert_path="/etc/$cert_path"
+
+ mv "$cert_path" "$cert_path.old"
+ cat "$cert_path.old" > "$cert_path"
+ cat /tmp/ca.crt >> "$cert_path"
+ done
+
+ export NIX_SSL_CERT_FILE=/tmp/ca.crt
+ export SSL_CERT_FILE=/tmp/ca.crt
+
+ # TODO
+ # # P11-Kit trust source.
+ # environment.etc."ssl/trust-source".source = "$${cacertPackage.p11kit}/etc/ssl/trust-source";
+ '';
+in {
+ prepare = clients: extra:
+ # The parens are needed for the syntax highlighting to work.
+ ( # python
+ ''
+ # Start dependencies for the other services
+ acme.start()
+ acme.wait_for_unit("pebble.service")
+ name_server.start()
+ name_server.wait_for_unit("nsd.service")
+
+ # Start actual test
+ start_all()
+
+ with subtest("Add pebble ca key to all services"):
+ for node in [name_server, ${builtins.concatStringsSep "," clients}]:
+ node.wait_until_succeeds("curl https://acme.test:15000/roots/0")
+ node.succeed("${add_pebble_ca_certs}")
+ ''
+ )
+ + extra;
+}
diff --git a/tests/common/acme/server.nix b/tests/common/acme/server.nix
new file mode 100644
index 00000000..997c944a
--- /dev/null
+++ b/tests/common/acme/server.nix
@@ -0,0 +1,91 @@
+# Add this node as acme server.
+# This also needs a DNS server.
+{
+ config,
+ pkgs,
+ lib,
+ ...
+}: let
+ testCerts = import ./certs/snakeoil-certs.nix;
+ inherit (testCerts) domain;
+
+ pebbleConf.pebble = {
+ listenAddress = "0.0.0.0:443";
+ managementListenAddress = "0.0.0.0:15000";
+
+ # The cert and key are used only for the Web Front End (WFE)
+ certificate = testCerts.${domain}.cert;
+ privateKey = testCerts.${domain}.key;
+
+ httpPort = 80;
+ tlsPort = 443;
+ ocspResponderURL = "http://${domain}:4002";
+ strict = true;
+ };
+
+ pebbleConfFile = pkgs.writeText "pebble.conf" (builtins.toJSON pebbleConf);
+in {
+ options.test-support.acme = {
+ caDomain = lib.mkOption {
+ type = lib.types.str;
+ default = domain;
+ readOnly = true;
+ description = ''
+ A domain name to use with the `nodes` attribute to
+ identify the CA server in the `client` config.
+ '';
+ };
+ caCert = lib.mkOption {
+ type = lib.types.path;
+ readOnly = true;
+ default = testCerts.ca.cert;
+ description = ''
+ A certificate file to use with the `nodes` attribute to
+ inject the test CA certificate used in the ACME server into
+ {option}`security.pki.certificateFiles`.
+ '';
+ };
+ };
+
+ config = {
+ networking = {
+ # This has priority 140, because modules/testing/test-instrumentation.nix
+ # already overrides this with priority 150.
+ nameservers = lib.mkOverride 140 ["127.0.0.1"];
+ firewall.allowedTCPPorts = [
+ 80
+ 443
+ 15000
+ 4002
+ ];
+
+ extraHosts = ''
+ 127.0.0.1 ${domain}
+ ${config.networking.primaryIPAddress} ${domain}
+ '';
+ };
+
+ systemd.services = {
+ pebble = {
+ enable = true;
+ description = "Pebble ACME server";
+ wantedBy = ["network.target"];
+ environment = {
+ # We're not testing lego, we're just testing our configuration.
+ # No need to sleep.
+ PEBBLE_VA_NOSLEEP = "1";
+ };
+
+ serviceConfig = {
+ RuntimeDirectory = "pebble";
+ WorkingDirectory = "/run/pebble";
+
+ # Required to bind on privileged ports.
+ AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
+
+ ExecStart = "${pkgs.pebble}/bin/pebble -config ${pebbleConfFile}";
+ };
+ };
+ };
+ };
+}
diff --git a/tests/common/dns/client.nix b/tests/common/dns/client.nix
new file mode 100644
index 00000000..52f32671
--- /dev/null
+++ b/tests/common/dns/client.nix
@@ -0,0 +1,10 @@
+{
+ lib,
+ nodes,
+ ...
+}: {
+ networking.nameservers = lib.mkForce [
+ nodes.name_server.networking.primaryIPAddress
+ nodes.name_server.networking.primaryIPv6Address
+ ];
+}
diff --git a/tests/common/dns/module/default.nix b/tests/common/dns/module/default.nix
new file mode 100644
index 00000000..8f4ad37a
--- /dev/null
+++ b/tests/common/dns/module/default.nix
@@ -0,0 +1,86 @@
+{
+ config,
+ lib,
+ ...
+}: let
+ cfg = config.vhack.dns;
+
+ zones =
+ builtins.mapAttrs (name: value: {
+ data =
+ dns.types.zone.renderToString name value;
+ })
+ cfg.zones;
+
+ dns = import ./dns {inherit lib;};
+
+ ports = let
+ parsePorts = listeners: let
+ splitAddress = addr: lib.splitString "@" addr;
+
+ extractPort = addr: let
+ split = splitAddress addr;
+ in
+ lib.toInt (
+ if (builtins.length split) == 2
+ then builtins.elemAt split 1
+ else "53"
+ );
+ in
+ builtins.map extractPort listeners;
+ in
+ lib.unique (parsePorts cfg.interfaces);
+in {
+ options.vhack.dns = {
+ enable = lib.mkEnableOption "custom dns server";
+
+ openFirewall = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = ''
+ Open the following ports:
+ TCP (${lib.concatStringsSep ", " (map toString ports)})
+ UDP (${lib.concatStringsSep ", " (map toString ports)})
+ '';
+ };
+
+ interfaces = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ description = ''
+ A list of the interfaces to bind to. To select the port add `@` to the end of the
+ interface. The default port is 53.
+ '';
+ 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;
+ verbosity = 4;
+ inherit (cfg) interfaces;
+ inherit zones;
+ };
+
+ networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall ports;
+ networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall ports;
+
+ systemd.services.nsd = {
+ requires = [
+ "network-online.target"
+ ];
+ after = [
+ "network.target"
+ "network-online.target"
+ ];
+ };
+ };
+}
diff --git a/tests/common/dns/module/dns/default.nix b/tests/common/dns/module/dns/default.nix
new file mode 100644
index 00000000..4ce07d8f
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/default.nix b/tests/common/dns/module/dns/types/default.nix
new file mode 100644
index 00000000..ece315fa
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/record.nix b/tests/common/dns/module/dns/types/record.nix
new file mode 100644
index 00000000..e992bf90
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/A.nix b/tests/common/dns/module/dns/types/records/A.nix
new file mode 100644
index 00000000..296943ef
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/AAAA.nix b/tests/common/dns/module/dns/types/records/AAAA.nix
new file mode 100644
index 00000000..4717176a
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/CAA.nix b/tests/common/dns/module/dns/types/records/CAA.nix
new file mode 100644
index 00000000..4b405107
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/CNAME.nix b/tests/common/dns/module/dns/types/records/CNAME.nix
new file mode 100644
index 00000000..095b078c
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/DKIM.nix b/tests/common/dns/module/dns/types/records/DKIM.nix
new file mode 100644
index 00000000..31b2f67e
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/DMARC.nix b/tests/common/dns/module/dns/types/records/DMARC.nix
new file mode 100644
index 00000000..0f10f2c1
--- /dev/null
+++ b/tests/common/dns/module/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 7489
+{
+ 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/tests/common/dns/module/dns/types/records/DNAME.nix b/tests/common/dns/module/dns/types/records/DNAME.nix
new file mode 100644
index 00000000..042ce95c
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/DNSKEY.nix b/tests/common/dns/module/dns/types/records/DNSKEY.nix
new file mode 100644
index 00000000..86ce3a10
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/DS.nix b/tests/common/dns/module/dns/types/records/DS.nix
new file mode 100644
index 00000000..76fac9a3
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/HTTPS.nix b/tests/common/dns/module/dns/types/records/HTTPS.nix
new file mode 100644
index 00000000..6e2ef3df
--- /dev/null
+++ b/tests/common/dns/module/dns/types/records/HTTPS.nix
@@ -0,0 +1,5 @@
+args:
+import ./SVCB.nix args
+// {
+ rtype = "HTTPS";
+}
diff --git a/tests/common/dns/module/dns/types/records/MTA-STS.nix b/tests/common/dns/module/dns/types/records/MTA-STS.nix
new file mode 100644
index 00000000..030490e1
--- /dev/null
+++ b/tests/common/dns/module/dns/types/records/MTA-STS.nix
@@ -0,0 +1,42 @@
+#
+# SPDX-FileCopyrightText: 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+#
+# 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 8461
+{
+ lib,
+ util,
+ ...
+}: let
+ inherit (lib) mkOption types;
+in rec {
+ rtype = "TXT";
+ options = {
+ id = mkOption {
+ type = types.str;
+ example = "20160831085700Z";
+ description = ''
+ A short string used to track policy updates. This string MUST
+ uniquely identify a given instance of a policy, such that senders
+ can determine when the policy has been updated by comparing to the
+ "id" of a previously seen policy. There is no implied ordering of
+ "id" fields between revisions.
+ '';
+ };
+ };
+ dataToString = data: let
+ items =
+ ["v=STSv1"]
+ ++ lib.pipe data [
+ (builtins.intersectAttrs options) # remove garbage list `_module`
+ (lib.filterAttrs (k: v: v != null && v != ""))
+ (lib.mapAttrsToList (k: v: "${k}=${v}"))
+ ];
+ result = lib.concatStringsSep "; " items + ";";
+ in
+ util.writeCharacterString result;
+ nameFixup = name: _self: "_mta-sts.${name}";
+}
diff --git a/tests/common/dns/module/dns/types/records/MX.nix b/tests/common/dns/module/dns/types/records/MX.nix
new file mode 100644
index 00000000..c25b89cf
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/NS.nix b/tests/common/dns/module/dns/types/records/NS.nix
new file mode 100644
index 00000000..ea60a911
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/OPENPGPKEY.nix b/tests/common/dns/module/dns/types/records/OPENPGPKEY.nix
new file mode 100644
index 00000000..1f39cb93
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/PTR.nix b/tests/common/dns/module/dns/types/records/PTR.nix
new file mode 100644
index 00000000..075f82ee
--- /dev/null
+++ b/tests/common/dns/module/dns/types/records/PTR.nix
@@ -0,0 +1,92 @@
+#
+# 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;
+
+ inherit (lib.strings) stringToCharacters splitString;
+
+ reverseIpv4 = input:
+ builtins.concatStringsSep "." (lib.lists.reverseList (splitString "."
+ input));
+
+ reverseIpv6 = input: let
+ split = splitString ":" input;
+ elementLength = builtins.length split;
+
+ reverseString = string:
+ builtins.concatStringsSep "" (lib.lists.reverseList
+ (stringToCharacters string));
+ in
+ reverseString (builtins.concatStringsSep "." (stringToCharacters (builtins.concatStringsSep
+ "" (builtins.map (
+ part: let
+ c = stringToCharacters part;
+ in
+ if builtins.length c == 4
+ then
+ # valid part
+ part
+ else if builtins.length c < 4 && builtins.length c > 0
+ then
+ # leading zeros were elided
+ (builtins.concatStringsSep "" (
+ builtins.map builtins.toString (
+ builtins.genList (_: 0) (4 - (builtins.length c))
+ )
+ ))
+ + part
+ else if builtins.length c == 0
+ then
+ # Multiple full blocks were elided. Only one of these can be in an
+ # IPv6 address, as such we can simply add (8 - (elementLength - 1)) `0000`
+ # blocks. We need to substract one from `elementLength` because
+ # this empty part is included in the `elementLength`.
+ builtins.concatStringsSep "" (builtins.genList (_: "0000") (8 - (elementLength - 1)))
+ else builtins.throw "Impossible"
+ )
+ split))));
+in {
+ rtype = "PTR";
+ options = {
+ name = mkOption {
+ type = simple.types.domain-name;
+ example = "mail2.server.com";
+ description = "The <domain-name> which is defined by the IP.";
+ };
+ ip = {
+ v4 = mkOption {
+ type = lib.types.nullOr lib.types.str;
+ example = "192.168.1.4";
+ description = "The IPv4 address of the host.";
+ default = null;
+ apply = v:
+ if v != null
+ then reverseIpv4 v
+ else v;
+ };
+ v6 = mkOption {
+ type = lib.types.nullOr lib.types.str;
+ example = "192.168.1.4";
+ description = "The IPv6 address of the host.";
+ default = null;
+ apply = v:
+ if v != null
+ then reverseIpv6 v
+ else v;
+ };
+ };
+ };
+ dataToString = {name, ...}: "${name}.";
+ nameFixup = name: self:
+ if self.ip.v6 == null
+ then "${self.ip.v4}.in-addr.arpa"
+ else "${self.ip.v6}.ip6.arpa";
+}
diff --git a/tests/common/dns/module/dns/types/records/SOA.nix b/tests/common/dns/module/dns/types/records/SOA.nix
new file mode 100644
index 00000000..db7436e9
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/SRV.nix b/tests/common/dns/module/dns/types/records/SRV.nix
new file mode 100644
index 00000000..5f558edd
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/SSHFP.nix b/tests/common/dns/module/dns/types/records/SSHFP.nix
new file mode 100644
index 00000000..14098603
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/SVCB.nix b/tests/common/dns/module/dns/types/records/SVCB.nix
new file mode 100644
index 00000000..62cbc3da
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/TLSA.nix b/tests/common/dns/module/dns/types/records/TLSA.nix
new file mode 100644
index 00000000..d92a29b0
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/TXT.nix b/tests/common/dns/module/dns/types/records/TXT.nix
new file mode 100644
index 00000000..d605ce82
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/records/default.nix b/tests/common/dns/module/dns/types/records/default.nix
new file mode 100644
index 00000000..76a86cdd
--- /dev/null
+++ b/tests/common/dns/module/dns/types/records/default.nix
@@ -0,0 +1,43 @@
+#
+# 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"
+ "MTA-STS"
+ ];
+in
+ genAttrs types (t: import (./. + "/${t}.nix") {inherit lib simple util;})
diff --git a/tests/common/dns/module/dns/types/records/dnssec.nix b/tests/common/dns/module/dns/types/records/dnssec.nix
new file mode 100644
index 00000000..648f6762
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/simple.nix b/tests/common/dns/module/dns/types/simple.nix
new file mode 100644
index 00000000..fece2c9b
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/types/zone.nix b/tests/common/dns/module/dns/types/zone.nix
new file mode 100644
index 00000000..44ccb150
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/module/dns/util/default.nix b/tests/common/dns/module/dns/util/default.nix
new file mode 100644
index 00000000..59e661d7
--- /dev/null
+++ b/tests/common/dns/module/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/tests/common/dns/server.nix b/tests/common/dns/server.nix
new file mode 100644
index 00000000..1fb5dadb
--- /dev/null
+++ b/tests/common/dns/server.nix
@@ -0,0 +1,43 @@
+{
+ lib,
+ nodes,
+ ...
+}: {
+ imports = [
+ ./module
+ ];
+
+ networking.nameservers = lib.mkForce [
+ nodes.name_server.networking.primaryIPAddress
+ nodes.name_server.networking.primaryIPv6Address
+ ];
+
+ vhack = {
+ dns = {
+ enable = true;
+ openFirewall = true;
+ interfaces = [
+ nodes.name_server.networking.primaryIPAddress
+ nodes.name_server.networking.primaryIPv6Address
+ ];
+
+ zones = {
+ "acme.test" = {
+ SOA = {
+ nameServer = "ns";
+ adminEmail = "admin@server.com";
+ serial = 2025012301;
+ };
+ useOrigin = false;
+
+ A = [
+ nodes.acme.networking.primaryIPAddress
+ ];
+ AAAA = [
+ nodes.acme.networking.primaryIPv6Address
+ ];
+ };
+ };
+ };
+ };
+}