{ pkgs, lib, config, ... }:
let
  inherit (config.services) unbound;
  inherit (config.users) users;
  stateDir = "/var/lib/unbound";
in
{
networking.resolvconf.useLocalResolver = true;
services.unbound = {
  enable = true;
  # DOC: https://calomel.org/unbound_dns.html
  extraConfig = ''
    remote-control:
      control-enable: yes
      control-interface: /run/unbound/unbound.socket

    server:
      log-queries: no
      verbosity: 1

      port: 53

      # The file which contains the listing of primary root DNS servers.
      # To be updated once every six months.
      root-hints: /var/lib/unbound/named.root

      # Do no answer id.server and hostname.bind queries.
      hide-identity: yes
      # Do not answer version.server and version.bind queries.
      hide-version: yes

      # Will trust glue only if it is within the servers authority.
      # Harden against out of zone rrsets, to avoid spoofing attempts.
      # Hardening queries multiple name servers for the same data to make
      # spoofing significantly harder and does not mandate dnssec.
      harden-glue: yes

      # Require DNSSEC data for trust-anchored zones, if such data is absent, the
      # zone becomes  bogus.  Harden against receiving dnssec-stripped data. If you
      # turn it off, failing to validate dnskey data for a trustanchor will trigger
      # insecure mode for that zone (like without a trustanchor).  Default on,
      # which insists on dnssec data for trust-anchored zones.
      harden-dnssec-stripped: yes

      # Use 0x20-encoded random bits in the query to foil spoof attempts.
      # http://tools.ietf.org/html/draft-vixie-dnsext-dns0x20-00
      #
      # When Unbound sends a query to a remote server it sends the hostname
      # string in random upper and lower characters. The remote server must
      # resolve the hostname as if all the characters were lower case. The remote
      # server must then send the query back to Unbound in the same random upper
      # and lower characters that Unbound sent. If the characters of the hostname
      # in the response are in the same format as the query then the dns-0x20
      # check is satisfied.
      # Attackers hoping to poison a Unbound DNS cache must therefore guess the
      # mixed-case encoding of the query and the timing of the return dns answer
      # in addition to all other fields required in a DNS poisoning attack.
      # dns-0x20 increases the difficulty of the attack significantly.
      #
      # It may result in maybe 0.4% of domains getting no answers
      # due to no support on the authoritative server side
      use-caps-for-id: yes

      #cache-min-ttl: 3600
      cache-max-ttl: 86400

      # Perform prefetching of close to expired message cache entries.  If a client
      # requests the dns lookup and the TTL of the cached hostname is going to
      # expire in less than 10% of its TTL, unbound will (1st) return the IP of the
      # host to the client and (2nd) pre-fetch the DNS request from the remote DNS server.
      # This method has been shown to increase the amount of cached hits by
      # local clients by 10% on average.
      prefetch: yes

      # Number of threads to create. 1 disables threading.
      # This should equal the number of CPU cores in the machine.
      num-threads: ${toString config.nix.maxJobs}

      # The number of slabs to use for cache and must be a power of 2 times the
      # number of num-threads set above. more slabs reduce lock contention,
      # but fragment memory usage.
      msg-cache-slabs: 8
      rrset-cache-slabs: 8
      infra-cache-slabs: 8
      key-cache-slabs: 8

      # Increase the memory size of the cache. Use roughly twice as much rrset cache
      # memory as you use msg cache memory. Due to malloc overhead, the total memory
      # usage is likely to rise to double (or 2.5x) the total cache memory.
      rrset-cache-size: 32m
      msg-cache-size: 16m

      # buffer size for UDP port 53 incoming (SO_RCVBUF socket option). This sets
      # the kernel buffer larger so that no messages are lost in spikes in the traffic.
      so-rcvbuf: 1m

      # Enforce privacy of these addresses. Strips them away from answers.
      # It may cause DNSSEC validation to additionally mark it as bogus.
      # Protects against 'DNS Rebinding' (uses browser as network proxy).
      #  Only 'private-domain' and 'local-data' names are allowed
      # to have these private addresses. No default.
      private-address: 192.168.0.0/16
      private-address: 172.16.0.0/12
      private-address: 10.0.0.0/8

      # Allow the domain (and its subdomains) to contain private addresses.
      # local-data statements are allowed to contain private addresses too.
      #private-domain: "home.lan"

      # If nonzero, unwanted replies are not only reported in statistics, but also
      # a running total is kept per thread. If it reaches the threshold, a warning
      # is printed and a defensive action is taken, the cache is cleared to flush
      # potential poison out of it.  A suggested value is 10000000, the default is
      # 0 (turned off). calomel.org thinks 10K is a good value.
      unwanted-reply-threshold: 10000

      # IMPORTANT FOR TESTING: If you are testing and setup NSD or BIND on
      # localhost you will want to allow the resolver to send queries to localhost.
      # Make sure to set do-not-query-localhost: yes.
      do-not-query-localhost: yes

      # Should additional section of secure message also be kept clean of unsecure
      # data. Useful to shield the users of this validator from potential bogus
      # data in the additional section. All unsigned data in the additional section
      # is removed from secure messages.
      val-clean-additional: yes
  '';
};
networking.nftables.ruleset = ''
  add rule inet filter fw2net tcp dport 53 skuid ${users.unbound.name} counter accept comment "Unbound"
  add rule inet filter fw2net udp dport 53 skuid ${users.unbound.name} counter accept comment "Unbound"
'';
systemd.services.unbound = {
  serviceConfig = {
    RuntimeDirectory = "unbound";
    RuntimeDirectoryMode = "0700";
    # FIXME: upstream service shouldn't overwrite ExecStopPost
    # so that postStop can be used.
    ExecStopPost = lib.mkForce (pkgs.writeShellScript "unit-script-unbound-post-stop" ''
      ${pkgs.utillinux}/bin/umount ${stateDir}/dev/random
      ${pkgs.utillinux}/bin/umount ${stateDir}/run/unbound
    '');
  };
  preStart = ''
    install -m 444 -o unbound -g nogroup \
     ${unbound/named.root} \
     /var/lib/unbound/named.root
    mkdir -p ${stateDir}/run/unbound
    ${pkgs.utillinux}/bin/mount --bind -n /run/unbound ${stateDir}/run/unbound
  '';
};
}