summary refs log tree commit diff
path: root/nixos/tests/knot.nix
blob: 8bab917a351e39bcb37ab518a0ed600ffd97c00c (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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import ./make-test-python.nix ({ pkgs, lib, ...} :
let
  common = {
    networking.firewall.enable = false;
    networking.useDHCP = false;
  };
  exampleZone = pkgs.writeTextDir "example.com.zone" ''
      @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
      @       NS      ns1
      @       NS      ns2
      ns1     A       192.168.0.1
      ns1     AAAA    fd00::1
      ns2     A       192.168.0.2
      ns2     AAAA    fd00::2
      www     A       192.0.2.1
      www     AAAA    2001:DB8::1
      sub     NS      ns.example.com.
  '';
  delegatedZone = pkgs.writeTextDir "sub.example.com.zone" ''
      @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
      @       NS      ns1.example.com.
      @       NS      ns2.example.com.
      @       A       192.0.2.2
      @       AAAA    2001:DB8::2
  '';

  knotZonesEnv = pkgs.buildEnv {
    name = "knot-zones";
    paths = [ exampleZone delegatedZone ];
  };
  # DO NOT USE pkgs.writeText IN PRODUCTION. This put secrets in the nix store!
  tsigFile = pkgs.writeText "tsig.conf" ''
    key:
      - id: slave_key
        algorithm: hmac-sha256
        secret: zOYgOgnzx3TGe5J5I/0kxd7gTcxXhLYMEq3Ek3fY37s=
  '';
in {
  name = "knot";
  meta = with pkgs.stdenv.lib.maintainers; {
    maintainers = [ hexa ];
  };


  nodes = {
    master = { lib, ... }: {
      imports = [ common ];
      networking.interfaces.eth1 = {
        ipv4.addresses = lib.mkForce [
          { address = "192.168.0.1"; prefixLength = 24; }
        ];
        ipv6.addresses = lib.mkForce [
          { address = "fd00::1"; prefixLength = 64; }
        ];
      };
      services.knot.enable = true;
      services.knot.extraArgs = [ "-v" ];
      services.knot.keyFiles = [ tsigFile ];
      services.knot.extraConfig = ''
        server:
            listen: 0.0.0.0@53
            listen: ::@53

        acl:
          - id: slave_acl
            address: 192.168.0.2
            key: slave_key
            action: transfer

        remote:
          - id: slave
            address: 192.168.0.2@53

        template:
          - id: default
            storage: ${knotZonesEnv}
            notify: [slave]
            acl: [slave_acl]
            dnssec-signing: on
            # Input-only zone files
            # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3
            # prevents modification of the zonefiles, since the zonefiles are immutable
            zonefile-sync: -1
            zonefile-load: difference
            journal-content: changes
            # move databases below the state directory, because they need to be writable
            journal-db: /var/lib/knot/journal
            kasp-db: /var/lib/knot/kasp
            timer-db: /var/lib/knot/timer

        zone:
          - domain: example.com
            file: example.com.zone

          - domain: sub.example.com
            file: sub.example.com.zone

        log:
          - target: syslog
            any: info
      '';
    };

    slave = { lib, ... }: {
      imports = [ common ];
      networking.interfaces.eth1 = {
        ipv4.addresses = lib.mkForce [
          { address = "192.168.0.2"; prefixLength = 24; }
        ];
        ipv6.addresses = lib.mkForce [
          { address = "fd00::2"; prefixLength = 64; }
        ];
      };
      services.knot.enable = true;
      services.knot.keyFiles = [ tsigFile ];
      services.knot.extraArgs = [ "-v" ];
      services.knot.extraConfig = ''
        server:
            listen: 0.0.0.0@53
            listen: ::@53

        acl:
          - id: notify_from_master
            address: 192.168.0.1
            action: notify

        remote:
          - id: master
            address: 192.168.0.1@53
            key: slave_key

        template:
          - id: default
            master: master
            acl: [notify_from_master]
            # zonefileless setup
            # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-2
            zonefile-sync: -1
            zonefile-load: none
            journal-content: all
            # move databases below the state directory, because they need to be writable
            journal-db: /var/lib/knot/journal
            kasp-db: /var/lib/knot/kasp
            timer-db: /var/lib/knot/timer

        zone:
          - domain: example.com
            file: example.com.zone

          - domain: sub.example.com
            file: sub.example.com.zone

        log:
          - target: syslog
            any: info
      '';
    };
    client = { lib, nodes, ... }: {
      imports = [ common ];
      networking.interfaces.eth1 = {
        ipv4.addresses = [
          { address = "192.168.0.3"; prefixLength = 24; }
        ];
        ipv6.addresses = [
          { address = "fd00::3"; prefixLength = 64; }
        ];
      };
      environment.systemPackages = [ pkgs.knot-dns ];
    };
  };

  testScript = { nodes, ... }: let
    master4 = (lib.head nodes.master.config.networking.interfaces.eth1.ipv4.addresses).address;
    master6 = (lib.head nodes.master.config.networking.interfaces.eth1.ipv6.addresses).address;

    slave4 = (lib.head nodes.slave.config.networking.interfaces.eth1.ipv4.addresses).address;
    slave6 = (lib.head nodes.slave.config.networking.interfaces.eth1.ipv6.addresses).address;
  in ''
    import re

    start_all()

    client.wait_for_unit("network.target")
    master.wait_for_unit("knot.service")
    slave.wait_for_unit("knot.service")


    def test(host, query_type, query, pattern):
        out = client.succeed(f"khost -t {query_type} {query} {host}").strip()
        client.log(f"{host} replied with: {out}")
        assert re.search(pattern, out), f'Did not match "{pattern}"'


    for host in ("${master4}", "${master6}", "${slave4}", "${slave6}"):
        with subtest(f"Interrogate {host}"):
            test(host, "SOA", "example.com", r"start of authority.*noc\.example\.com\.")
            test(host, "A", "example.com", r"has no [^ ]+ record")
            test(host, "AAAA", "example.com", r"has no [^ ]+ record")

            test(host, "A", "www.example.com", r"address 192.0.2.1$")
            test(host, "AAAA", "www.example.com", r"address 2001:db8::1$")

            test(host, "NS", "sub.example.com", r"nameserver is ns\d\.example\.com.$")
            test(host, "A", "sub.example.com", r"address 192.0.2.2$")
            test(host, "AAAA", "sub.example.com", r"address 2001:db8::2$")

            test(host, "RRSIG", "www.example.com", r"RR set signature is")
            test(host, "DNSKEY", "example.com", r"DNSSEC key is")
  '';
})