diff options
Diffstat (limited to 'nixos/tests/common/resolver.nix')
-rw-r--r-- | nixos/tests/common/resolver.nix | 141 |
1 files changed, 141 insertions, 0 deletions
diff --git a/nixos/tests/common/resolver.nix b/nixos/tests/common/resolver.nix new file mode 100644 index 000000000000..a1901c5c8167 --- /dev/null +++ b/nixos/tests/common/resolver.nix @@ -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} + ''; + }; + }; +} |