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</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}