{
  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: xfr_key
        algorithm: hmac-sha256
        secret: zOYgOgnzx3TGe5J5I/0kxd7gTcxXhLYMEq3Ek3fY37s=
  '';
in
{
  name = "knot";
  meta = with pkgs.lib.maintainers; {
    maintainers = [ hexa ];
  };

  nodes = {
    primary =
      { lib, ... }:
      {
        imports = [ common ];

        # trigger sched_setaffinity syscall
        virtualisation.cores = 2;

        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.settings = {
          server = {
            listen = [
              "0.0.0.0@53"
              "::@53"
            ];
            listen-quic = [
              "0.0.0.0@853"
              "::@853"
            ];
            automatic-acl = true;
          };

          acl.secondary_acl = {
            address = "192.168.0.2";
            key = "xfr_key";
            action = "transfer";
          };

          remote.secondary.address = "192.168.0.2@53";

          template.default = {
            storage = knotZonesEnv;
            notify = [ "secondary" ];
            acl = [ "secondary_acl" ];
            dnssec-signing = true;
            # 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";
          };

          zone = {
            "example.com".file = "example.com.zone";
            "sub.example.com".file = "sub.example.com.zone";
          };

          log.syslog.any = "info";
        };
      };

    secondary =
      { 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.settings = {
          server = {
            automatic-acl = true;
          };

          xdp = {
            listen = [
              "eth1"
            ];
            tcp = true;
          };

          remote.primary = {
            address = "192.168.0.1@53";
            key = "xfr_key";
          };

          remote.primary-quic = {
            address = "192.168.0.1@853";
            key = "xfr_key";
            quic = true;
          };

          template.default = {
            # zonefileless setup
            # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-2
            zonefile-sync = "-1";
            zonefile-load = "none";
            journal-content = "all";
          };

          zone = {
            "example.com" = {
              master = "primary";
              file = "example.com.zone";
            };
            "sub.example.com" = {
              master = "primary-quic";
              file = "sub.example.com.zone";
            };
          };

          log.syslog.any = "debug";
        };
      };
    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
      primary4 = (lib.head nodes.primary.config.networking.interfaces.eth1.ipv4.addresses).address;
      primary6 = (lib.head nodes.primary.config.networking.interfaces.eth1.ipv6.addresses).address;

      secondary4 = (lib.head nodes.secondary.config.networking.interfaces.eth1.ipv4.addresses).address;
      secondary6 = (lib.head nodes.secondary.config.networking.interfaces.eth1.ipv6.addresses).address;
    in
    ''
      import re

      start_all()

      client.wait_for_unit("network.target")
      primary.wait_for_unit("knot.service")
      secondary.wait_for_unit("knot.service")

      for zone in ("example.com.", "sub.example.com."):
          secondary.wait_until_succeeds(
            f"knotc zone-status {zone} | grep -q 'serial: 2019031302'"
          )

      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 ("${primary4}", "${primary6}", "${secondary4}", "${secondary6}"):
          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")

      primary.log(primary.succeed("systemd-analyze security knot.service | grep -v '✓'"))
    '';
}
