summary refs log tree commit diff
path: root/nixos/tests/common/resolver.nix
blob: 09a74de20faaef6c2b9806257fc9c403a5b818c2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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/acme/server</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}
      '';
    };
  };
}