142 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
		
		
			
		
	
	
			142 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
|   | # 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} | ||
|  |       '';
 | ||
|  |     }; | ||
|  |   }; | ||
|  | } |