Merge pull request #27683 (add test for ACME)

This is a rebased version of the pull request with small fixes due to
changes in recent master.

Original description from the pull request:

  Currently this is only a very basic test which gets certificates via
  the enableACME option of the nginx module.

  However the main reason why I'm not directly merging and putting this
  up for review is that the complexity here lies in the support-modules
  needed for the test. The support modules are for running a Boulder
  instance along with a DNS resolver (as a separate module).

  For details about the implementation, see the commit messages and the
  comments at the start of the respective support modules.

I'm merging this first of all because other than @abbradar, none of the
other requested reviewers did comment on the changes and second because
the change here is adding a test, so even if the implementation would be
so disgusting and crappy it's better than having no test at all.

The comment of @abbradar was:

  Can't we factor Boulder into a proper package and a NixOS service?
  Maybe not very general purpose for now but still -- putting everything
  into one test seems painful to me.

My objection to this is that the components are heavily patched and some
of them don't even have a release, so I'm not sure whether infesting
pkgs/ with them is really a good idea.

Nevertheless, we can still do that later.

Cc: @fpletz, @domenkozar, @bjornfor
This commit is contained in:
aszlig 2017-09-13 23:30:07 +02:00
commit 62711f4265
No known key found for this signature in database
GPG Key ID: 1DE8E48E57DB5436
4 changed files with 650 additions and 0 deletions

View File

@ -214,6 +214,7 @@ in rec {
# Run the tests for each platform. You can run a test by doing
# e.g. nix-build -A tests.login.x86_64-linux, or equivalently,
# nix-build tests/login.nix -A result.
tests.acme = callTest tests/acme.nix {};
tests.avahi = callTest tests/avahi.nix {};
tests.bittorrent = callTest tests/bittorrent.nix {};
tests.blivet = callTest tests/blivet.nix {};

62
nixos/tests/acme.nix Normal file
View File

@ -0,0 +1,62 @@
let
commonConfig = { config, lib, pkgs, nodes, ... }: {
networking.nameservers = [
nodes.letsencrypt.config.networking.primaryIPAddress
];
nixpkgs.overlays = lib.singleton (self: super: {
cacert = super.cacert.overrideDerivation (drv: {
installPhase = (drv.installPhase or "") + ''
cat "${nodes.letsencrypt.config.test-support.letsencrypt.caCert}" \
>> "$out/etc/ssl/certs/ca-bundle.crt"
'';
});
pythonPackages = (super.python.override {
packageOverrides = lib.const (pysuper: {
certifi = pysuper.certifi.overrideDerivation (drv: {
postPatch = (drv.postPatch or "") + ''
cat "${self.cacert}/etc/ssl/certs/ca-bundle.crt" \
> certifi/cacert.pem
'';
});
});
}).pkgs;
});
};
in import ./make-test.nix {
name = "acme";
nodes = {
letsencrypt = ./common/letsencrypt.nix;
webserver = { config, pkgs, ... }: {
imports = [ commonConfig ];
networking.firewall.allowedTCPPorts = [ 80 443 ];
networking.extraHosts = ''
${config.networking.primaryIPAddress} example.com
'';
services.nginx.enable = true;
services.nginx.virtualHosts."example.com" = {
enableACME = true;
forceSSL = true;
locations."/".root = pkgs.runCommand "docroot" {} ''
mkdir -p "$out"
echo hello world > "$out/index.html"
'';
};
};
client = commonConfig;
};
testScript = ''
$letsencrypt->waitForUnit("boulder.service");
startAll;
$webserver->waitForUnit("acme-certificates.target");
$client->succeed('curl https://example.com/ | grep -qF "hello world"');
'';
}

View File

@ -0,0 +1,446 @@
# Fully pluggable module to have Letsencrypt's Boulder ACME service running in
# a test environment.
#
# The certificate for the ACME service is exported as:
#
# config.test-support.letsencrypt.caCert
#
# This value can be used inside the configuration of other test nodes to inject
# the snakeoil certificate into security.pki.certificateFiles or into package
# overlays.
#
# Another value that's needed if you don't use a custom resolver (see below for
# notes on that) is to add the letsencrypt node as a nameserver to every node
# that needs to acquire certificates using ACME, because otherwise the API host
# for letsencrypt.org can't be resolved.
#
# A configuration example of a full node setup using this would be this:
#
# {
# letsencrypt = import ./common/letsencrypt.nix;
#
# example = { nodes, ... }: {
# networking.nameservers = [
# nodes.letsencrypt.config.networking.primaryIPAddress
# ];
# security.pki.certificateFiles = [
# nodes.letsencrypt.config.test-support.letsencrypt.caCert
# ];
# };
# }
#
# By default, this module runs a local resolver, generated using resolver.nix
# from the same directory to automatically discover all zones in the network.
#
# If you do not want this and want to use your own resolver, you can just
# override networking.nameservers like this:
#
# {
# letsencrypt = { nodes, ... }: {
# imports = [ ./common/letsencrypt.nix ];
# networking.nameservers = [
# nodes.myresolver.config.networking.primaryIPAddress
# ];
# };
#
# myresolver = ...;
# }
#
# Keep in mind, that currently only _one_ resolver is supported, if you have
# more than one resolver in networking.nameservers only the first one will be
# used.
#
# Also make sure that whenever you use a resolver from a different test node
# that it has to be started _before_ the ACME service.
{ config, pkgs, lib, ... }:
let
softhsm = pkgs.stdenv.mkDerivation rec {
name = "softhsm-${version}";
version = "1.3.8";
src = pkgs.fetchurl {
url = "https://dist.opendnssec.org/source/${name}.tar.gz";
sha256 = "0flmnpkgp65ym7w3qyg78d3fbmvq3aznmi66rgd420n33shf7aif";
};
configureFlags = [ "--with-botan=${pkgs.botan}" ];
buildInputs = [ pkgs.sqlite ];
};
pkcs11-proxy = pkgs.stdenv.mkDerivation {
name = "pkcs11-proxy";
src = pkgs.fetchFromGitHub {
owner = "SUNET";
repo = "pkcs11-proxy";
rev = "944684f78bca0c8da6cabe3fa273fed3db44a890";
sha256 = "1nxgd29y9wmifm11pjcdpd2y293p0dgi0x5ycis55miy97n0f5zy";
};
postPatch = "patchShebangs mksyscalls.sh";
nativeBuildInputs = [ pkgs.cmake ];
buildInputs = [ pkgs.openssl pkgs.libseccomp ];
};
mkGoDep = { goPackagePath, url ? "https://${goPackagePath}", rev, sha256 }: {
inherit goPackagePath;
src = pkgs.fetchgit { inherit url rev sha256; };
};
goose = let
owner = "liamstask";
repo = "goose";
rev = "8488cc47d90c8a502b1c41a462a6d9cc8ee0a895";
version = "20150116";
in pkgs.buildGoPackage rec {
name = "${repo}-${version}";
src = pkgs.fetchFromBitbucket {
name = "${name}-src";
inherit rev owner repo;
sha256 = "1jy0pscxjnxjdg3hj111w21g8079rq9ah2ix5ycxxhbbi3f0wdhs";
};
goPackagePath = "bitbucket.org/${owner}/${repo}";
subPackages = [ "cmd/goose" ];
extraSrcs = map mkGoDep [
{ goPackagePath = "github.com/go-sql-driver/mysql";
rev = "2e00b5cd70399450106cec6431c2e2ce3cae5034";
sha256 = "085g48jq9hzmlcxg122n0c4pi41sc1nn2qpx1vrl2jfa8crsppa5";
}
{ goPackagePath = "github.com/kylelemons/go-gypsy";
rev = "08cad365cd28a7fba23bb1e57aa43c5e18ad8bb8";
sha256 = "1djv7nii3hy451n5jlslk0dblqzb1hia1cbqpdwhnps1g8hqjy8q";
}
{ goPackagePath = "github.com/lib/pq";
rev = "ba5d4f7a35561e22fbdf7a39aa0070f4d460cfc0";
sha256 = "1mfbqw9g00bk24bfmf53wri5c2wqmgl0qh4sh1qv2da13a7cwwg3";
}
{ goPackagePath = "github.com/mattn/go-sqlite3";
rev = "2acfafad5870400156f6fceb12852c281cbba4d5";
sha256 = "1rpgil3w4hh1cibidskv1js898hwz83ps06gh0hm3mym7ki8d5h7";
}
{ goPackagePath = "github.com/ziutek/mymysql";
rev = "0582bcf675f52c0c2045c027fd135bd726048f45";
sha256 = "0bkc9x8sgqbzgdimsmsnhb0qrzlzfv33fgajmmjxl4hcb21qz3rf";
}
{ goPackagePath = "golang.org/x/net";
url = "https://go.googlesource.com/net";
rev = "10c134ea0df15f7e34d789338c7a2d76cc7a3ab9";
sha256 = "14cbr2shl08gyg85n5gj7nbjhrhhgrd52h073qd14j97qcxsakcz";
}
];
};
boulder = let
owner = "letsencrypt";
repo = "boulder";
rev = "9866abab8962a591f06db457a4b84c518cc88243";
version = "20170510";
in pkgs.buildGoPackage rec {
name = "${repo}-${version}";
src = pkgs.fetchFromGitHub {
name = "${name}-src";
inherit rev owner repo;
sha256 = "170m5cjngbrm36wi7wschqw8jzs7kxpcyzmshq3pcrmcpigrhna1";
};
postPatch = ''
# compat for go < 1.8
sed -i -e 's/time\.Until(\([^)]\+\))/\1.Sub(time.Now())/' \
test/ocsp/helper/helper.go
find test -type f -exec sed -i -e '/libpkcs11-proxy.so/ {
s,/usr/local,${pkcs11-proxy},
}' {} +
sed -i -r \
-e '/^def +install/a \ return True' \
-e 's,exec \./bin/,,' \
test/startservers.py
cat "${snakeOilCa}/ca.key" > test/test-ca.key
cat "${snakeOilCa}/ca.pem" > test/test-ca.pem
'';
goPackagePath = "github.com/${owner}/${repo}";
buildInputs = [ pkgs.libtool ];
};
boulderSource = "${boulder.out}/share/go/src/${boulder.goPackagePath}";
softHsmConf = pkgs.writeText "softhsm.conf" ''
0:/var/lib/softhsm/slot0.db
1:/var/lib/softhsm/slot1.db
'';
snakeOilCa = pkgs.runCommand "snakeoil-ca" {
buildInputs = [ pkgs.openssl ];
} ''
mkdir "$out"
openssl req -newkey rsa:4096 -x509 -sha256 -days 36500 \
-subj '/CN=Snakeoil CA' -nodes \
-out "$out/ca.pem" -keyout "$out/ca.key"
'';
createAndSignCert = fqdn: let
snakeoilCertConf = pkgs.writeText "snakeoil.cnf" ''
[req]
default_bits = 4096
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
[dn]
CN = ${fqdn}
[req_ext]
subjectAltName = DNS:${fqdn}
'';
in pkgs.runCommand "snakeoil-certs-${fqdn}" {
buildInputs = [ pkgs.openssl ];
} ''
mkdir "$out"
openssl genrsa -out "$out/snakeoil.key" 4096
openssl req -new -key "$out/snakeoil.key" \
-config ${lib.escapeShellArg snakeoilCertConf} \
-out snakeoil.csr
openssl x509 -req -in snakeoil.csr -sha256 -set_serial 666 \
-CA "${snakeOilCa}/ca.pem" -CAkey "${snakeOilCa}/ca.key" \
-extfile ${lib.escapeShellArg snakeoilCertConf} \
-out "$out/snakeoil.pem" -days 36500
'';
wfeCerts = createAndSignCert wfeDomain;
wfeDomain = "acme-v01.api.letsencrypt.org";
wfeCertFile = "${wfeCerts}/snakeoil.pem";
wfeKeyFile = "${wfeCerts}/snakeoil.key";
siteCerts = createAndSignCert siteDomain;
siteDomain = "letsencrypt.org";
siteCertFile = "${siteCerts}/snakeoil.pem";
siteKeyFile = "${siteCerts}/snakeoil.key";
# Retrieved via:
# curl -s -I https://acme-v01.api.letsencrypt.org/terms \
# | sed -ne 's/^[Ll]ocation: *//p'
tosUrl = "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf";
tosPath = builtins.head (builtins.match "https?://[^/]+(.*)" tosUrl);
tosFile = pkgs.fetchurl {
url = tosUrl;
sha256 = "08b2gacdz23mzji2pjr1pwnk82a84rzvr36isif7mmi9kydl6wv3";
};
resolver = let
message = "You need to define a resolver for the letsencrypt test module.";
firstNS = lib.head config.networking.nameservers;
in if config.networking.nameservers == [] then throw message else firstNS;
cfgDir = pkgs.stdenv.mkDerivation {
name = "boulder-config";
src = "${boulderSource}/test/config";
nativeBuildInputs = [ pkgs.jq ];
phases = [ "unpackPhase" "patchPhase" "installPhase" ];
postPatch = ''
sed -i -e 's/5002/80/' -e 's/5002/443/' va.json
sed -i -e '/listenAddress/s/:4000/:80/' wfe.json
sed -i -r \
-e ${lib.escapeShellArg "s,http://boulder:4000/terms/v1,${tosUrl},g"} \
-e 's,http://(boulder|127\.0\.0\.1):4000,https://${wfeDomain},g' \
-e '/dnsResolver/s/127\.0\.0\.1:8053/${resolver}:53/' \
*.json
if grep 4000 *.json; then exit 1; fi
# Change all ports from 1909X to 909X, because the 1909X range of ports is
# allocated by startservers.py in order to intercept gRPC communication.
sed -i -e 's/\<1\(909[0-9]\)\>/\1/' *.json
# Patch out all additional issuer certs
jq '. + {ca: (.ca + {Issuers:
[.ca.Issuers[] | select(.CertFile == "test/test-ca.pem")]
})}' ca.json > tmp
mv tmp ca.json
'';
installPhase = "cp -r . \"$out\"";
};
components = {
gsb-test-srv.args = "-apikey my-voice-is-my-passport";
gsb-test-srv.waitForPort = 6000;
gsb-test-srv.first = true;
boulder-sa.args = "--config ${cfgDir}/sa.json";
boulder-wfe.args = "--config ${cfgDir}/wfe.json";
boulder-ra.args = "--config ${cfgDir}/ra.json";
boulder-ca.args = "--config ${cfgDir}/ca.json";
boulder-va.args = "--config ${cfgDir}/va.json";
boulder-publisher.args = "--config ${cfgDir}/publisher.json";
boulder-publisher.waitForPort = 9091;
ocsp-updater.args = "--config ${cfgDir}/ocsp-updater.json";
ocsp-updater.after = [ "boulder-publisher" ];
ocsp-responder.args = "--config ${cfgDir}/ocsp-responder.json";
ct-test-srv = {};
mail-test-srv.args = "--closeFirst 5";
};
commonPath = [ softhsm pkgs.mariadb goose boulder ];
mkServices = a: b: with lib; listToAttrs (concatLists (mapAttrsToList a b));
componentServices = mkServices (name: attrs: let
mkSrvName = n: "boulder-${n}.service";
firsts = lib.filterAttrs (lib.const (c: c.first or false)) components;
firstServices = map mkSrvName (lib.attrNames firsts);
firstServicesNoSelf = lib.remove "boulder-${name}.service" firstServices;
additionalAfter = firstServicesNoSelf ++ map mkSrvName (attrs.after or []);
needsPort = attrs ? waitForPort;
inits = map (n: "boulder-init-${n}.service") [ "mysql" "softhsm" ];
portWaiter = {
name = "boulder-${name}";
value = {
description = "Wait For Port ${toString attrs.waitForPort} (${name})";
after = [ "boulder-real-${name}.service" "bind.service" ];
requires = [ "boulder-real-${name}.service" ];
requiredBy = [ "boulder.service" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
script = let
netcat = "${pkgs.netcat-openbsd}/bin/nc";
portCheck = "${netcat} -z 127.0.0.1 ${toString attrs.waitForPort}";
in "while ! ${portCheck}; do :; done";
};
};
in lib.optional needsPort portWaiter ++ lib.singleton {
name = if needsPort then "boulder-real-${name}" else "boulder-${name}";
value = {
description = "Boulder ACME Component (${name})";
after = inits ++ additionalAfter;
requires = inits;
requiredBy = [ "boulder.service" ];
path = commonPath;
environment.GORACE = "halt_on_error=1";
environment.SOFTHSM_CONF = softHsmConf;
environment.PKCS11_PROXY_SOCKET = "tcp://127.0.0.1:5657";
serviceConfig.WorkingDirectory = boulderSource;
serviceConfig.ExecStart = "${boulder}/bin/${name} ${attrs.args or ""}";
serviceConfig.Restart = "on-failure";
};
}) components;
in {
imports = [ ./resolver.nix ];
options.test-support.letsencrypt.caCert = lib.mkOption {
type = lib.types.path;
description = ''
A certificate file to use with the <literal>nodes</literal> attribute to
inject the snakeoil CA certificate used in the ACME server into
<option>security.pki.certificateFiles</option>.
'';
};
config = {
test-support = {
resolver.enable = let
isLocalResolver = config.networking.nameservers == [ "127.0.0.1" ];
in lib.mkOverride 900 isLocalResolver;
letsencrypt.caCert = "${snakeOilCa}/ca.pem";
};
# This has priority 140, because modules/testing/test-instrumentation.nix
# already overrides this with priority 150.
networking.nameservers = lib.mkOverride 140 [ "127.0.0.1" ];
networking.firewall.enable = false;
networking.extraHosts = ''
127.0.0.1 ${toString [
"sa.boulder" "ra.boulder" "wfe.boulder" "ca.boulder" "va.boulder"
"publisher.boulder" "ocsp-updater.boulder" "admin-revoker.boulder"
"boulder" "boulder-mysql" wfeDomain
]}
${config.networking.primaryIPAddress} ${wfeDomain} ${siteDomain}
'';
services.mysql.enable = true;
services.mysql.package = pkgs.mariadb;
services.nginx.enable = true;
services.nginx.recommendedProxySettings = true;
services.nginx.virtualHosts.${wfeDomain} = {
onlySSL = true;
enableACME = false;
sslCertificate = wfeCertFile;
sslCertificateKey = wfeKeyFile;
locations."/".proxyPass = "http://127.0.0.1:80";
};
services.nginx.virtualHosts.${siteDomain} = {
onlySSL = true;
enableACME = false;
sslCertificate = siteCertFile;
sslCertificateKey = siteKeyFile;
locations.${tosPath}.extraConfig = "alias ${tosFile};";
};
systemd.services = {
pkcs11-daemon = {
description = "PKCS11 Daemon";
after = [ "boulder-init-softhsm.service" ];
before = map (n: "${n}.service") (lib.attrNames componentServices);
wantedBy = [ "multi-user.target" ];
environment.SOFTHSM_CONF = softHsmConf;
environment.PKCS11_DAEMON_SOCKET = "tcp://127.0.0.1:5657";
serviceConfig.ExecStart = let
softhsmLib = "${softhsm}/lib/softhsm/libsofthsm.so";
in "${pkcs11-proxy}/bin/pkcs11-daemon ${softhsmLib}";
};
boulder-init-mysql = {
description = "Boulder ACME Init (MySQL)";
after = [ "mysql.service" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
serviceConfig.WorkingDirectory = boulderSource;
path = commonPath;
script = "${pkgs.bash}/bin/sh test/create_db.sh";
};
boulder-init-softhsm = {
description = "Boulder ACME Init (SoftHSM)";
environment.SOFTHSM_CONF = softHsmConf;
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
serviceConfig.WorkingDirectory = boulderSource;
preStart = "mkdir -p /var/lib/softhsm";
path = commonPath;
script = ''
softhsm --slot 0 --init-token \
--label intermediate --pin 5678 --so-pin 1234
softhsm --slot 0 --import test/test-ca.key \
--label intermediate_key --pin 5678 --id FB
softhsm --slot 1 --init-token \
--label root --pin 5678 --so-pin 1234
softhsm --slot 1 --import test/test-root.key \
--label root_key --pin 5678 --id FA
'';
};
boulder = {
description = "Boulder ACME Server";
after = map (n: "${n}.service") (lib.attrNames componentServices);
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
script = let
ports = lib.range 8000 8005 ++ lib.singleton 80;
netcat = "${pkgs.netcat-openbsd}/bin/nc";
mkPortCheck = port: "${netcat} -z 127.0.0.1 ${toString port}";
checks = "(${lib.concatMapStringsSep " && " mkPortCheck ports})";
in "while ! ${checks}; do :; done";
};
} // componentServices;
};
}

View File

@ -0,0 +1,141 @@
# This module automatically discovers zones in BIND and NSD NixOS
# configurations and creates zones for all definitions of networking.extraHosts
# (except those that point to 127.0.0.1 or ::1) within the current test network
# and delegates these zones using a fake root zone served by a BIND recursive
# name server.
{ config, nodes, pkgs, lib, ... }:
{
options.test-support.resolver.enable = lib.mkOption {
type = lib.types.bool;
default = true;
internal = true;
description = ''
Whether to enable the resolver that automatically discovers zone in the
test network.
This option is <literal>true</literal> by default, because the module
defining this option needs to be explicitly imported.
The reason this option exists is for the
<filename>nixos/tests/common/letsencrypt.nix</filename> module, which
needs that option to disable the resolver once the user has set its own
resolver.
'';
};
config = lib.mkIf config.test-support.resolver.enable {
networking.firewall.enable = false;
services.bind.enable = true;
services.bind.cacheNetworks = lib.mkForce [ "any" ];
services.bind.forwarders = lib.mkForce [];
services.bind.zones = lib.singleton {
name = ".";
file = let
addDot = zone: zone + lib.optionalString (!lib.hasSuffix "." zone) ".";
mkNsdZoneNames = zones: map addDot (lib.attrNames zones);
mkBindZoneNames = zones: map (zone: addDot zone.name) zones;
getZones = cfg: mkNsdZoneNames cfg.services.nsd.zones
++ mkBindZoneNames cfg.services.bind.zones;
getZonesForNode = attrs: {
ip = attrs.config.networking.primaryIPAddress;
zones = lib.filter (zone: zone != ".") (getZones attrs.config);
};
zoneInfo = lib.mapAttrsToList (lib.const getZonesForNode) nodes;
# A and AAAA resource records for all the definitions of
# networking.extraHosts except those for 127.0.0.1 or ::1.
#
# The result is an attribute set with keys being the host name and the
# values are either { ipv4 = ADDR; } or { ipv6 = ADDR; } where ADDR is
# the IP address for the corresponding key.
recordsFromExtraHosts = let
getHostsForNode = lib.const (n: n.config.networking.extraHosts);
allHostsList = lib.mapAttrsToList getHostsForNode nodes;
allHosts = lib.concatStringsSep "\n" allHostsList;
reIp = "[a-fA-F0-9.:]+";
reHost = "[a-zA-Z0-9.-]+";
matchAliases = str: let
matched = builtins.match "[ \t]+(${reHost})(.*)" str;
continue = lib.singleton (lib.head matched)
++ matchAliases (lib.last matched);
in if matched == null then [] else continue;
matchLine = str: let
result = builtins.match "[ \t]*(${reIp})[ \t]+(${reHost})(.*)" str;
in if result == null then null else {
ipAddr = lib.head result;
hosts = lib.singleton (lib.elemAt result 1)
++ matchAliases (lib.last result);
};
skipLine = str: let
rest = builtins.match "[^\n]*\n(.*)" str;
in if rest == null then "" else lib.head rest;
getEntries = str: acc: let
result = matchLine str;
next = getEntries (skipLine str);
newEntry = acc ++ lib.singleton result;
continue = if result == null then next acc else next newEntry;
in if str == "" then acc else continue;
isIPv6 = str: builtins.match ".*:.*" str != null;
loopbackIps = [ "127.0.0.1" "::1" ];
filterLoopback = lib.filter (e: !lib.elem e.ipAddr loopbackIps);
allEntries = lib.concatMap (entry: map (host: {
inherit host;
${if isIPv6 entry.ipAddr then "ipv6" else "ipv4"} = entry.ipAddr;
}) entry.hosts) (filterLoopback (getEntries (allHosts + "\n") []));
mkRecords = entry: let
records = lib.optional (entry ? ipv6) "AAAA ${entry.ipv6}"
++ lib.optional (entry ? ipv4) "A ${entry.ipv4}";
mkRecord = typeAndData: "${entry.host}. IN ${typeAndData}";
in lib.concatMapStringsSep "\n" mkRecord records;
in lib.concatMapStringsSep "\n" mkRecords allEntries;
# All of the zones that are subdomains of existing zones.
# For example if there is only "example.com" the following zones would
# be 'subZones':
#
# * foo.example.com.
# * bar.example.com.
#
# While the following would *not* be 'subZones':
#
# * example.com.
# * com.
#
subZones = let
allZones = lib.concatMap (zi: zi.zones) zoneInfo;
isSubZoneOf = z1: z2: lib.hasSuffix z2 z1 && z1 != z2;
in lib.filter (z: lib.any (isSubZoneOf z) allZones) allZones;
# All the zones without 'subZones'.
filteredZoneInfo = map (zi: zi // {
zones = lib.filter (x: !lib.elem x subZones) zi.zones;
}) zoneInfo;
in pkgs.writeText "fake-root.zone" ''
$TTL 3600
. IN SOA ns.fakedns. admin.fakedns. ( 1 3h 1h 1w 1d )
ns.fakedns. IN A ${config.networking.primaryIPAddress}
. IN NS ns.fakedns.
${lib.concatImapStrings (num: { ip, zones }: ''
ns${toString num}.fakedns. IN A ${ip}
${lib.concatMapStrings (zone: ''
${zone} IN NS ns${toString num}.fakedns.
'') zones}
'') (lib.filter (zi: zi.zones != []) filteredZoneInfo)}
${recordsFromExtraHosts}
'';
};
};
}