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

aszlig 62711f42 50cf2a71

+650
+1
nixos/release.nix
··· 214 214 # Run the tests for each platform. You can run a test by doing 215 215 # e.g. ‘nix-build -A tests.login.x86_64-linux’, or equivalently, 216 216 # ‘nix-build tests/login.nix -A result’. 217 + tests.acme = callTest tests/acme.nix {}; 217 218 tests.avahi = callTest tests/avahi.nix {}; 218 219 tests.bittorrent = callTest tests/bittorrent.nix {}; 219 220 tests.blivet = callTest tests/blivet.nix {};
+62
nixos/tests/acme.nix
··· 1 + let 2 + commonConfig = { config, lib, pkgs, nodes, ... }: { 3 + networking.nameservers = [ 4 + nodes.letsencrypt.config.networking.primaryIPAddress 5 + ]; 6 + 7 + nixpkgs.overlays = lib.singleton (self: super: { 8 + cacert = super.cacert.overrideDerivation (drv: { 9 + installPhase = (drv.installPhase or "") + '' 10 + cat "${nodes.letsencrypt.config.test-support.letsencrypt.caCert}" \ 11 + >> "$out/etc/ssl/certs/ca-bundle.crt" 12 + ''; 13 + }); 14 + 15 + pythonPackages = (super.python.override { 16 + packageOverrides = lib.const (pysuper: { 17 + certifi = pysuper.certifi.overrideDerivation (drv: { 18 + postPatch = (drv.postPatch or "") + '' 19 + cat "${self.cacert}/etc/ssl/certs/ca-bundle.crt" \ 20 + > certifi/cacert.pem 21 + ''; 22 + }); 23 + }); 24 + }).pkgs; 25 + }); 26 + }; 27 + 28 + in import ./make-test.nix { 29 + name = "acme"; 30 + 31 + nodes = { 32 + letsencrypt = ./common/letsencrypt.nix; 33 + 34 + webserver = { config, pkgs, ... }: { 35 + imports = [ commonConfig ]; 36 + networking.firewall.allowedTCPPorts = [ 80 443 ]; 37 + 38 + networking.extraHosts = '' 39 + ${config.networking.primaryIPAddress} example.com 40 + ''; 41 + 42 + services.nginx.enable = true; 43 + services.nginx.virtualHosts."example.com" = { 44 + enableACME = true; 45 + forceSSL = true; 46 + locations."/".root = pkgs.runCommand "docroot" {} '' 47 + mkdir -p "$out" 48 + echo hello world > "$out/index.html" 49 + ''; 50 + }; 51 + }; 52 + 53 + client = commonConfig; 54 + }; 55 + 56 + testScript = '' 57 + $letsencrypt->waitForUnit("boulder.service"); 58 + startAll; 59 + $webserver->waitForUnit("acme-certificates.target"); 60 + $client->succeed('curl https://example.com/ | grep -qF "hello world"'); 61 + ''; 62 + }
+446
nixos/tests/common/letsencrypt.nix
··· 1 + # Fully pluggable module to have Letsencrypt's Boulder ACME service running in 2 + # a test environment. 3 + # 4 + # The certificate for the ACME service is exported as: 5 + # 6 + # config.test-support.letsencrypt.caCert 7 + # 8 + # This value can be used inside the configuration of other test nodes to inject 9 + # the snakeoil certificate into security.pki.certificateFiles or into package 10 + # overlays. 11 + # 12 + # Another value that's needed if you don't use a custom resolver (see below for 13 + # notes on that) is to add the letsencrypt node as a nameserver to every node 14 + # that needs to acquire certificates using ACME, because otherwise the API host 15 + # for letsencrypt.org can't be resolved. 16 + # 17 + # A configuration example of a full node setup using this would be this: 18 + # 19 + # { 20 + # letsencrypt = import ./common/letsencrypt.nix; 21 + # 22 + # example = { nodes, ... }: { 23 + # networking.nameservers = [ 24 + # nodes.letsencrypt.config.networking.primaryIPAddress 25 + # ]; 26 + # security.pki.certificateFiles = [ 27 + # nodes.letsencrypt.config.test-support.letsencrypt.caCert 28 + # ]; 29 + # }; 30 + # } 31 + # 32 + # By default, this module runs a local resolver, generated using resolver.nix 33 + # from the same directory to automatically discover all zones in the network. 34 + # 35 + # If you do not want this and want to use your own resolver, you can just 36 + # override networking.nameservers like this: 37 + # 38 + # { 39 + # letsencrypt = { nodes, ... }: { 40 + # imports = [ ./common/letsencrypt.nix ]; 41 + # networking.nameservers = [ 42 + # nodes.myresolver.config.networking.primaryIPAddress 43 + # ]; 44 + # }; 45 + # 46 + # myresolver = ...; 47 + # } 48 + # 49 + # Keep in mind, that currently only _one_ resolver is supported, if you have 50 + # more than one resolver in networking.nameservers only the first one will be 51 + # used. 52 + # 53 + # Also make sure that whenever you use a resolver from a different test node 54 + # that it has to be started _before_ the ACME service. 55 + { config, pkgs, lib, ... }: 56 + 57 + let 58 + softhsm = pkgs.stdenv.mkDerivation rec { 59 + name = "softhsm-${version}"; 60 + version = "1.3.8"; 61 + 62 + src = pkgs.fetchurl { 63 + url = "https://dist.opendnssec.org/source/${name}.tar.gz"; 64 + sha256 = "0flmnpkgp65ym7w3qyg78d3fbmvq3aznmi66rgd420n33shf7aif"; 65 + }; 66 + 67 + configureFlags = [ "--with-botan=${pkgs.botan}" ]; 68 + buildInputs = [ pkgs.sqlite ]; 69 + }; 70 + 71 + pkcs11-proxy = pkgs.stdenv.mkDerivation { 72 + name = "pkcs11-proxy"; 73 + 74 + src = pkgs.fetchFromGitHub { 75 + owner = "SUNET"; 76 + repo = "pkcs11-proxy"; 77 + rev = "944684f78bca0c8da6cabe3fa273fed3db44a890"; 78 + sha256 = "1nxgd29y9wmifm11pjcdpd2y293p0dgi0x5ycis55miy97n0f5zy"; 79 + }; 80 + 81 + postPatch = "patchShebangs mksyscalls.sh"; 82 + 83 + nativeBuildInputs = [ pkgs.cmake ]; 84 + buildInputs = [ pkgs.openssl pkgs.libseccomp ]; 85 + }; 86 + 87 + mkGoDep = { goPackagePath, url ? "https://${goPackagePath}", rev, sha256 }: { 88 + inherit goPackagePath; 89 + src = pkgs.fetchgit { inherit url rev sha256; }; 90 + }; 91 + 92 + goose = let 93 + owner = "liamstask"; 94 + repo = "goose"; 95 + rev = "8488cc47d90c8a502b1c41a462a6d9cc8ee0a895"; 96 + version = "20150116"; 97 + 98 + in pkgs.buildGoPackage rec { 99 + name = "${repo}-${version}"; 100 + 101 + src = pkgs.fetchFromBitbucket { 102 + name = "${name}-src"; 103 + inherit rev owner repo; 104 + sha256 = "1jy0pscxjnxjdg3hj111w21g8079rq9ah2ix5ycxxhbbi3f0wdhs"; 105 + }; 106 + 107 + goPackagePath = "bitbucket.org/${owner}/${repo}"; 108 + subPackages = [ "cmd/goose" ]; 109 + extraSrcs = map mkGoDep [ 110 + { goPackagePath = "github.com/go-sql-driver/mysql"; 111 + rev = "2e00b5cd70399450106cec6431c2e2ce3cae5034"; 112 + sha256 = "085g48jq9hzmlcxg122n0c4pi41sc1nn2qpx1vrl2jfa8crsppa5"; 113 + } 114 + { goPackagePath = "github.com/kylelemons/go-gypsy"; 115 + rev = "08cad365cd28a7fba23bb1e57aa43c5e18ad8bb8"; 116 + sha256 = "1djv7nii3hy451n5jlslk0dblqzb1hia1cbqpdwhnps1g8hqjy8q"; 117 + } 118 + { goPackagePath = "github.com/lib/pq"; 119 + rev = "ba5d4f7a35561e22fbdf7a39aa0070f4d460cfc0"; 120 + sha256 = "1mfbqw9g00bk24bfmf53wri5c2wqmgl0qh4sh1qv2da13a7cwwg3"; 121 + } 122 + { goPackagePath = "github.com/mattn/go-sqlite3"; 123 + rev = "2acfafad5870400156f6fceb12852c281cbba4d5"; 124 + sha256 = "1rpgil3w4hh1cibidskv1js898hwz83ps06gh0hm3mym7ki8d5h7"; 125 + } 126 + { goPackagePath = "github.com/ziutek/mymysql"; 127 + rev = "0582bcf675f52c0c2045c027fd135bd726048f45"; 128 + sha256 = "0bkc9x8sgqbzgdimsmsnhb0qrzlzfv33fgajmmjxl4hcb21qz3rf"; 129 + } 130 + { goPackagePath = "golang.org/x/net"; 131 + url = "https://go.googlesource.com/net"; 132 + rev = "10c134ea0df15f7e34d789338c7a2d76cc7a3ab9"; 133 + sha256 = "14cbr2shl08gyg85n5gj7nbjhrhhgrd52h073qd14j97qcxsakcz"; 134 + } 135 + ]; 136 + }; 137 + 138 + boulder = let 139 + owner = "letsencrypt"; 140 + repo = "boulder"; 141 + rev = "9866abab8962a591f06db457a4b84c518cc88243"; 142 + version = "20170510"; 143 + 144 + in pkgs.buildGoPackage rec { 145 + name = "${repo}-${version}"; 146 + 147 + src = pkgs.fetchFromGitHub { 148 + name = "${name}-src"; 149 + inherit rev owner repo; 150 + sha256 = "170m5cjngbrm36wi7wschqw8jzs7kxpcyzmshq3pcrmcpigrhna1"; 151 + }; 152 + 153 + postPatch = '' 154 + # compat for go < 1.8 155 + sed -i -e 's/time\.Until(\([^)]\+\))/\1.Sub(time.Now())/' \ 156 + test/ocsp/helper/helper.go 157 + 158 + find test -type f -exec sed -i -e '/libpkcs11-proxy.so/ { 159 + s,/usr/local,${pkcs11-proxy}, 160 + }' {} + 161 + 162 + sed -i -r \ 163 + -e '/^def +install/a \ return True' \ 164 + -e 's,exec \./bin/,,' \ 165 + test/startservers.py 166 + 167 + cat "${snakeOilCa}/ca.key" > test/test-ca.key 168 + cat "${snakeOilCa}/ca.pem" > test/test-ca.pem 169 + ''; 170 + 171 + goPackagePath = "github.com/${owner}/${repo}"; 172 + buildInputs = [ pkgs.libtool ]; 173 + }; 174 + 175 + boulderSource = "${boulder.out}/share/go/src/${boulder.goPackagePath}"; 176 + 177 + softHsmConf = pkgs.writeText "softhsm.conf" '' 178 + 0:/var/lib/softhsm/slot0.db 179 + 1:/var/lib/softhsm/slot1.db 180 + ''; 181 + 182 + snakeOilCa = pkgs.runCommand "snakeoil-ca" { 183 + buildInputs = [ pkgs.openssl ]; 184 + } '' 185 + mkdir "$out" 186 + openssl req -newkey rsa:4096 -x509 -sha256 -days 36500 \ 187 + -subj '/CN=Snakeoil CA' -nodes \ 188 + -out "$out/ca.pem" -keyout "$out/ca.key" 189 + ''; 190 + 191 + createAndSignCert = fqdn: let 192 + snakeoilCertConf = pkgs.writeText "snakeoil.cnf" '' 193 + [req] 194 + default_bits = 4096 195 + prompt = no 196 + default_md = sha256 197 + req_extensions = req_ext 198 + distinguished_name = dn 199 + [dn] 200 + CN = ${fqdn} 201 + [req_ext] 202 + subjectAltName = DNS:${fqdn} 203 + ''; 204 + in pkgs.runCommand "snakeoil-certs-${fqdn}" { 205 + buildInputs = [ pkgs.openssl ]; 206 + } '' 207 + mkdir "$out" 208 + openssl genrsa -out "$out/snakeoil.key" 4096 209 + openssl req -new -key "$out/snakeoil.key" \ 210 + -config ${lib.escapeShellArg snakeoilCertConf} \ 211 + -out snakeoil.csr 212 + openssl x509 -req -in snakeoil.csr -sha256 -set_serial 666 \ 213 + -CA "${snakeOilCa}/ca.pem" -CAkey "${snakeOilCa}/ca.key" \ 214 + -extfile ${lib.escapeShellArg snakeoilCertConf} \ 215 + -out "$out/snakeoil.pem" -days 36500 216 + ''; 217 + 218 + wfeCerts = createAndSignCert wfeDomain; 219 + wfeDomain = "acme-v01.api.letsencrypt.org"; 220 + wfeCertFile = "${wfeCerts}/snakeoil.pem"; 221 + wfeKeyFile = "${wfeCerts}/snakeoil.key"; 222 + 223 + siteCerts = createAndSignCert siteDomain; 224 + siteDomain = "letsencrypt.org"; 225 + siteCertFile = "${siteCerts}/snakeoil.pem"; 226 + siteKeyFile = "${siteCerts}/snakeoil.key"; 227 + 228 + # Retrieved via: 229 + # curl -s -I https://acme-v01.api.letsencrypt.org/terms \ 230 + # | sed -ne 's/^[Ll]ocation: *//p' 231 + tosUrl = "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf"; 232 + tosPath = builtins.head (builtins.match "https?://[^/]+(.*)" tosUrl); 233 + 234 + tosFile = pkgs.fetchurl { 235 + url = tosUrl; 236 + sha256 = "08b2gacdz23mzji2pjr1pwnk82a84rzvr36isif7mmi9kydl6wv3"; 237 + }; 238 + 239 + resolver = let 240 + message = "You need to define a resolver for the letsencrypt test module."; 241 + firstNS = lib.head config.networking.nameservers; 242 + in if config.networking.nameservers == [] then throw message else firstNS; 243 + 244 + cfgDir = pkgs.stdenv.mkDerivation { 245 + name = "boulder-config"; 246 + src = "${boulderSource}/test/config"; 247 + nativeBuildInputs = [ pkgs.jq ]; 248 + phases = [ "unpackPhase" "patchPhase" "installPhase" ]; 249 + postPatch = '' 250 + sed -i -e 's/5002/80/' -e 's/5002/443/' va.json 251 + sed -i -e '/listenAddress/s/:4000/:80/' wfe.json 252 + sed -i -r \ 253 + -e ${lib.escapeShellArg "s,http://boulder:4000/terms/v1,${tosUrl},g"} \ 254 + -e 's,http://(boulder|127\.0\.0\.1):4000,https://${wfeDomain},g' \ 255 + -e '/dnsResolver/s/127\.0\.0\.1:8053/${resolver}:53/' \ 256 + *.json 257 + if grep 4000 *.json; then exit 1; fi 258 + 259 + # Change all ports from 1909X to 909X, because the 1909X range of ports is 260 + # allocated by startservers.py in order to intercept gRPC communication. 261 + sed -i -e 's/\<1\(909[0-9]\)\>/\1/' *.json 262 + 263 + # Patch out all additional issuer certs 264 + jq '. + {ca: (.ca + {Issuers: 265 + [.ca.Issuers[] | select(.CertFile == "test/test-ca.pem")] 266 + })}' ca.json > tmp 267 + mv tmp ca.json 268 + ''; 269 + installPhase = "cp -r . \"$out\""; 270 + }; 271 + 272 + components = { 273 + gsb-test-srv.args = "-apikey my-voice-is-my-passport"; 274 + gsb-test-srv.waitForPort = 6000; 275 + gsb-test-srv.first = true; 276 + boulder-sa.args = "--config ${cfgDir}/sa.json"; 277 + boulder-wfe.args = "--config ${cfgDir}/wfe.json"; 278 + boulder-ra.args = "--config ${cfgDir}/ra.json"; 279 + boulder-ca.args = "--config ${cfgDir}/ca.json"; 280 + boulder-va.args = "--config ${cfgDir}/va.json"; 281 + boulder-publisher.args = "--config ${cfgDir}/publisher.json"; 282 + boulder-publisher.waitForPort = 9091; 283 + ocsp-updater.args = "--config ${cfgDir}/ocsp-updater.json"; 284 + ocsp-updater.after = [ "boulder-publisher" ]; 285 + ocsp-responder.args = "--config ${cfgDir}/ocsp-responder.json"; 286 + ct-test-srv = {}; 287 + mail-test-srv.args = "--closeFirst 5"; 288 + }; 289 + 290 + commonPath = [ softhsm pkgs.mariadb goose boulder ]; 291 + 292 + mkServices = a: b: with lib; listToAttrs (concatLists (mapAttrsToList a b)); 293 + 294 + componentServices = mkServices (name: attrs: let 295 + mkSrvName = n: "boulder-${n}.service"; 296 + firsts = lib.filterAttrs (lib.const (c: c.first or false)) components; 297 + firstServices = map mkSrvName (lib.attrNames firsts); 298 + firstServicesNoSelf = lib.remove "boulder-${name}.service" firstServices; 299 + additionalAfter = firstServicesNoSelf ++ map mkSrvName (attrs.after or []); 300 + needsPort = attrs ? waitForPort; 301 + inits = map (n: "boulder-init-${n}.service") [ "mysql" "softhsm" ]; 302 + portWaiter = { 303 + name = "boulder-${name}"; 304 + value = { 305 + description = "Wait For Port ${toString attrs.waitForPort} (${name})"; 306 + after = [ "boulder-real-${name}.service" "bind.service" ]; 307 + requires = [ "boulder-real-${name}.service" ]; 308 + requiredBy = [ "boulder.service" ]; 309 + serviceConfig.Type = "oneshot"; 310 + serviceConfig.RemainAfterExit = true; 311 + script = let 312 + netcat = "${pkgs.netcat-openbsd}/bin/nc"; 313 + portCheck = "${netcat} -z 127.0.0.1 ${toString attrs.waitForPort}"; 314 + in "while ! ${portCheck}; do :; done"; 315 + }; 316 + }; 317 + in lib.optional needsPort portWaiter ++ lib.singleton { 318 + name = if needsPort then "boulder-real-${name}" else "boulder-${name}"; 319 + value = { 320 + description = "Boulder ACME Component (${name})"; 321 + after = inits ++ additionalAfter; 322 + requires = inits; 323 + requiredBy = [ "boulder.service" ]; 324 + path = commonPath; 325 + environment.GORACE = "halt_on_error=1"; 326 + environment.SOFTHSM_CONF = softHsmConf; 327 + environment.PKCS11_PROXY_SOCKET = "tcp://127.0.0.1:5657"; 328 + serviceConfig.WorkingDirectory = boulderSource; 329 + serviceConfig.ExecStart = "${boulder}/bin/${name} ${attrs.args or ""}"; 330 + serviceConfig.Restart = "on-failure"; 331 + }; 332 + }) components; 333 + 334 + in { 335 + imports = [ ./resolver.nix ]; 336 + 337 + options.test-support.letsencrypt.caCert = lib.mkOption { 338 + type = lib.types.path; 339 + description = '' 340 + A certificate file to use with the <literal>nodes</literal> attribute to 341 + inject the snakeoil CA certificate used in the ACME server into 342 + <option>security.pki.certificateFiles</option>. 343 + ''; 344 + }; 345 + 346 + config = { 347 + test-support = { 348 + resolver.enable = let 349 + isLocalResolver = config.networking.nameservers == [ "127.0.0.1" ]; 350 + in lib.mkOverride 900 isLocalResolver; 351 + letsencrypt.caCert = "${snakeOilCa}/ca.pem"; 352 + }; 353 + 354 + # This has priority 140, because modules/testing/test-instrumentation.nix 355 + # already overrides this with priority 150. 356 + networking.nameservers = lib.mkOverride 140 [ "127.0.0.1" ]; 357 + networking.firewall.enable = false; 358 + 359 + networking.extraHosts = '' 360 + 127.0.0.1 ${toString [ 361 + "sa.boulder" "ra.boulder" "wfe.boulder" "ca.boulder" "va.boulder" 362 + "publisher.boulder" "ocsp-updater.boulder" "admin-revoker.boulder" 363 + "boulder" "boulder-mysql" wfeDomain 364 + ]} 365 + ${config.networking.primaryIPAddress} ${wfeDomain} ${siteDomain} 366 + ''; 367 + 368 + services.mysql.enable = true; 369 + services.mysql.package = pkgs.mariadb; 370 + 371 + services.nginx.enable = true; 372 + services.nginx.recommendedProxySettings = true; 373 + services.nginx.virtualHosts.${wfeDomain} = { 374 + onlySSL = true; 375 + enableACME = false; 376 + sslCertificate = wfeCertFile; 377 + sslCertificateKey = wfeKeyFile; 378 + locations."/".proxyPass = "http://127.0.0.1:80"; 379 + }; 380 + services.nginx.virtualHosts.${siteDomain} = { 381 + onlySSL = true; 382 + enableACME = false; 383 + sslCertificate = siteCertFile; 384 + sslCertificateKey = siteKeyFile; 385 + locations.${tosPath}.extraConfig = "alias ${tosFile};"; 386 + }; 387 + 388 + systemd.services = { 389 + pkcs11-daemon = { 390 + description = "PKCS11 Daemon"; 391 + after = [ "boulder-init-softhsm.service" ]; 392 + before = map (n: "${n}.service") (lib.attrNames componentServices); 393 + wantedBy = [ "multi-user.target" ]; 394 + environment.SOFTHSM_CONF = softHsmConf; 395 + environment.PKCS11_DAEMON_SOCKET = "tcp://127.0.0.1:5657"; 396 + serviceConfig.ExecStart = let 397 + softhsmLib = "${softhsm}/lib/softhsm/libsofthsm.so"; 398 + in "${pkcs11-proxy}/bin/pkcs11-daemon ${softhsmLib}"; 399 + }; 400 + 401 + boulder-init-mysql = { 402 + description = "Boulder ACME Init (MySQL)"; 403 + after = [ "mysql.service" ]; 404 + serviceConfig.Type = "oneshot"; 405 + serviceConfig.RemainAfterExit = true; 406 + serviceConfig.WorkingDirectory = boulderSource; 407 + path = commonPath; 408 + script = "${pkgs.bash}/bin/sh test/create_db.sh"; 409 + }; 410 + 411 + boulder-init-softhsm = { 412 + description = "Boulder ACME Init (SoftHSM)"; 413 + environment.SOFTHSM_CONF = softHsmConf; 414 + serviceConfig.Type = "oneshot"; 415 + serviceConfig.RemainAfterExit = true; 416 + serviceConfig.WorkingDirectory = boulderSource; 417 + preStart = "mkdir -p /var/lib/softhsm"; 418 + path = commonPath; 419 + script = '' 420 + softhsm --slot 0 --init-token \ 421 + --label intermediate --pin 5678 --so-pin 1234 422 + softhsm --slot 0 --import test/test-ca.key \ 423 + --label intermediate_key --pin 5678 --id FB 424 + softhsm --slot 1 --init-token \ 425 + --label root --pin 5678 --so-pin 1234 426 + softhsm --slot 1 --import test/test-root.key \ 427 + --label root_key --pin 5678 --id FA 428 + ''; 429 + }; 430 + 431 + boulder = { 432 + description = "Boulder ACME Server"; 433 + after = map (n: "${n}.service") (lib.attrNames componentServices); 434 + wantedBy = [ "multi-user.target" ]; 435 + serviceConfig.Type = "oneshot"; 436 + serviceConfig.RemainAfterExit = true; 437 + script = let 438 + ports = lib.range 8000 8005 ++ lib.singleton 80; 439 + netcat = "${pkgs.netcat-openbsd}/bin/nc"; 440 + mkPortCheck = port: "${netcat} -z 127.0.0.1 ${toString port}"; 441 + checks = "(${lib.concatMapStringsSep " && " mkPortCheck ports})"; 442 + in "while ! ${checks}; do :; done"; 443 + }; 444 + } // componentServices; 445 + }; 446 + }
+141
nixos/tests/common/resolver.nix
··· 1 + # This module automatically discovers zones in BIND and NSD NixOS 2 + # configurations and creates zones for all definitions of networking.extraHosts 3 + # (except those that point to 127.0.0.1 or ::1) within the current test network 4 + # and delegates these zones using a fake root zone served by a BIND recursive 5 + # name server. 6 + { config, nodes, pkgs, lib, ... }: 7 + 8 + { 9 + options.test-support.resolver.enable = lib.mkOption { 10 + type = lib.types.bool; 11 + default = true; 12 + internal = true; 13 + description = '' 14 + Whether to enable the resolver that automatically discovers zone in the 15 + test network. 16 + 17 + This option is <literal>true</literal> by default, because the module 18 + defining this option needs to be explicitly imported. 19 + 20 + The reason this option exists is for the 21 + <filename>nixos/tests/common/letsencrypt.nix</filename> module, which 22 + needs that option to disable the resolver once the user has set its own 23 + resolver. 24 + ''; 25 + }; 26 + 27 + config = lib.mkIf config.test-support.resolver.enable { 28 + networking.firewall.enable = false; 29 + services.bind.enable = true; 30 + services.bind.cacheNetworks = lib.mkForce [ "any" ]; 31 + services.bind.forwarders = lib.mkForce []; 32 + services.bind.zones = lib.singleton { 33 + name = "."; 34 + file = let 35 + addDot = zone: zone + lib.optionalString (!lib.hasSuffix "." zone) "."; 36 + mkNsdZoneNames = zones: map addDot (lib.attrNames zones); 37 + mkBindZoneNames = zones: map (zone: addDot zone.name) zones; 38 + getZones = cfg: mkNsdZoneNames cfg.services.nsd.zones 39 + ++ mkBindZoneNames cfg.services.bind.zones; 40 + 41 + getZonesForNode = attrs: { 42 + ip = attrs.config.networking.primaryIPAddress; 43 + zones = lib.filter (zone: zone != ".") (getZones attrs.config); 44 + }; 45 + 46 + zoneInfo = lib.mapAttrsToList (lib.const getZonesForNode) nodes; 47 + 48 + # A and AAAA resource records for all the definitions of 49 + # networking.extraHosts except those for 127.0.0.1 or ::1. 50 + # 51 + # The result is an attribute set with keys being the host name and the 52 + # values are either { ipv4 = ADDR; } or { ipv6 = ADDR; } where ADDR is 53 + # the IP address for the corresponding key. 54 + recordsFromExtraHosts = let 55 + getHostsForNode = lib.const (n: n.config.networking.extraHosts); 56 + allHostsList = lib.mapAttrsToList getHostsForNode nodes; 57 + allHosts = lib.concatStringsSep "\n" allHostsList; 58 + 59 + reIp = "[a-fA-F0-9.:]+"; 60 + reHost = "[a-zA-Z0-9.-]+"; 61 + 62 + matchAliases = str: let 63 + matched = builtins.match "[ \t]+(${reHost})(.*)" str; 64 + continue = lib.singleton (lib.head matched) 65 + ++ matchAliases (lib.last matched); 66 + in if matched == null then [] else continue; 67 + 68 + matchLine = str: let 69 + result = builtins.match "[ \t]*(${reIp})[ \t]+(${reHost})(.*)" str; 70 + in if result == null then null else { 71 + ipAddr = lib.head result; 72 + hosts = lib.singleton (lib.elemAt result 1) 73 + ++ matchAliases (lib.last result); 74 + }; 75 + 76 + skipLine = str: let 77 + rest = builtins.match "[^\n]*\n(.*)" str; 78 + in if rest == null then "" else lib.head rest; 79 + 80 + getEntries = str: acc: let 81 + result = matchLine str; 82 + next = getEntries (skipLine str); 83 + newEntry = acc ++ lib.singleton result; 84 + continue = if result == null then next acc else next newEntry; 85 + in if str == "" then acc else continue; 86 + 87 + isIPv6 = str: builtins.match ".*:.*" str != null; 88 + loopbackIps = [ "127.0.0.1" "::1" ]; 89 + filterLoopback = lib.filter (e: !lib.elem e.ipAddr loopbackIps); 90 + 91 + allEntries = lib.concatMap (entry: map (host: { 92 + inherit host; 93 + ${if isIPv6 entry.ipAddr then "ipv6" else "ipv4"} = entry.ipAddr; 94 + }) entry.hosts) (filterLoopback (getEntries (allHosts + "\n") [])); 95 + 96 + mkRecords = entry: let 97 + records = lib.optional (entry ? ipv6) "AAAA ${entry.ipv6}" 98 + ++ lib.optional (entry ? ipv4) "A ${entry.ipv4}"; 99 + mkRecord = typeAndData: "${entry.host}. IN ${typeAndData}"; 100 + in lib.concatMapStringsSep "\n" mkRecord records; 101 + 102 + in lib.concatMapStringsSep "\n" mkRecords allEntries; 103 + 104 + # All of the zones that are subdomains of existing zones. 105 + # For example if there is only "example.com" the following zones would 106 + # be 'subZones': 107 + # 108 + # * foo.example.com. 109 + # * bar.example.com. 110 + # 111 + # While the following would *not* be 'subZones': 112 + # 113 + # * example.com. 114 + # * com. 115 + # 116 + subZones = let 117 + allZones = lib.concatMap (zi: zi.zones) zoneInfo; 118 + isSubZoneOf = z1: z2: lib.hasSuffix z2 z1 && z1 != z2; 119 + in lib.filter (z: lib.any (isSubZoneOf z) allZones) allZones; 120 + 121 + # All the zones without 'subZones'. 122 + filteredZoneInfo = map (zi: zi // { 123 + zones = lib.filter (x: !lib.elem x subZones) zi.zones; 124 + }) zoneInfo; 125 + 126 + in pkgs.writeText "fake-root.zone" '' 127 + $TTL 3600 128 + . IN SOA ns.fakedns. admin.fakedns. ( 1 3h 1h 1w 1d ) 129 + ns.fakedns. IN A ${config.networking.primaryIPAddress} 130 + . IN NS ns.fakedns. 131 + ${lib.concatImapStrings (num: { ip, zones }: '' 132 + ns${toString num}.fakedns. IN A ${ip} 133 + ${lib.concatMapStrings (zone: '' 134 + ${zone} IN NS ns${toString num}.fakedns. 135 + '') zones} 136 + '') (lib.filter (zi: zi.zones != []) filteredZoneInfo)} 137 + ${recordsFromExtraHosts} 138 + ''; 139 + }; 140 + }; 141 + }