diff --git a/hosts/delta/acme.nix b/hosts/delta/acme.nix index 7d60150..c3b905b 100644 --- a/hosts/delta/acme.nix +++ b/hosts/delta/acme.nix @@ -2,10 +2,11 @@ security.acme.certs."caspervk.net" = { domain = "*.caspervk.net"; reloadServices = [ - "unbound.service" + "kresd@1.service" + "kresd@2.service" ]; }; users.groups.acme.members = [ - "unbound" + "knot-resolver" ]; } diff --git a/hosts/delta/default.nix b/hosts/delta/default.nix index 2b784fe..c6c7144 100644 --- a/hosts/delta/default.nix +++ b/hosts/delta/default.nix @@ -5,8 +5,8 @@ ../../modules/server ./acme.nix ./hardware.nix + ./knot-resolver.nix ./network.nix - ./unbound.nix ]; networking.hostName = "delta"; diff --git a/hosts/delta/knot-resolver.nix b/hosts/delta/knot-resolver.nix new file mode 100644 index 0000000..63ac112 --- /dev/null +++ b/hosts/delta/knot-resolver.nix @@ -0,0 +1,79 @@ +{ + config, + pkgs, + ... +}: { + # Knot Resolver is a minimalistic implementation of a caching validating DNS + # resolver. Modular architecture keeps the core tiny and efficient, and it + # provides a state-machine like API for extensions. + # https://knot-resolver.readthedocs.io/en/stable/index.html + # + # Test resolver: + # > nix shell nixpkgs#knot-dns + # > kdig -d @dns.caspervk.net example.com + # > kdig -d +https @dns.caspervk.net example.com + # > kdig -d +tls @dns.caspervk.net example.com + # + # Connect to control socket: + # > nix shell nixpkgs#socat -c sudo socat readline UNIX-CONNECT:/run/knot-resolver/control/1 + # >> help() + # https://knot-resolver.readthedocs.io/en/stable/daemon-scripting.html + services.kresd = { + enable = true; + # For maximum performance, there should be as many kresd processes as there + # are available CPU threads. + # https://knot-resolver.readthedocs.io/en/stable/systemd-multiinst.html + instances = 2; + # Explicitly listen to DNS/DoH/DoT on the external interface(s). This + # allows systemd-resolved to listen on localhost as on every other system. + listenPlain = [ + "159.69.4.2:53" + "[2a01:4f8:1c0c:70d1::1]:53" + ]; + listenTLS = [ + "159.69.4.2:853" + "[2a01:4f8:1c0c:70d1::1]:853" + ]; + listenDoH = [ + "159.69.4.2:443" + "[2a01:4f8:1c0c:70d1::1]:443" + ]; + extraConfig = '' + -- TLS certificate for DoT and DoH + -- https://knot-resolver.readthedocs.io/en/stable/daemon-bindings-net_tlssrv.html + net.tls( + "${config.security.acme.certs."caspervk.net".directory}/fullchain.pem", + "${config.security.acme.certs."caspervk.net".directory}/key.pem" + ) + -- Cache is stored in /var/cache/knot-resolver, which is mounted as + -- tmpfs. Allow using 90% of the partition for caching. + -- https://knot-resolver.readthedocs.io/en/stable/daemon-bindings-cache.html + cache.size = math.floor(cache.fssize() * 0.9) + -- The predict module helps to keep the cache hot by prefetching + -- records. Any time the resolver answers with records that are about to + -- expire, they get refreshed. + -- https://knot-resolver.readthedocs.io/en/stable/modules-predict.html + modules.load("predict") + -- Block spam and advertising domains + -- https://knot-resolver.readthedocs.io/en/stable/modules-policy.html#response-policy-zones + policy.add( + policy.rpz( + policy.ANSWER({ [kres.type.A] = {rdata=kres.str2ip("0.0.0.0"), ttl = 600} }), + "${pkgs.runCommand "stevenblack-blocklist-rpz" {} ''grep '^0\.0\.0\.0' ${pkgs.stevenblack-blocklist}/hosts | awk '{print $2 " 600 IN CNAME .\n*." $2 " 600 IN CNAME ."}' > $out''}" + ) + ) + -- Test domain to verify DNS server is being used + policy.add( + policy.domains( + policy.ANSWER({ [kres.type.A] = {rdata = kres.str2ip("192.0.2.0"), ttl = 5} }), + policy.todnames({"test.dns.caspervk.net"}) + ) + ) + ''; + }; + + networking.firewall = { + allowedTCPPorts = [443 853]; + allowedUDPPorts = [53]; + }; +} diff --git a/hosts/delta/unbound.nix b/hosts/delta/unbound.nix deleted file mode 100644 index 51cb91d..0000000 --- a/hosts/delta/unbound.nix +++ /dev/null @@ -1,104 +0,0 @@ -{ - config, - pkgs, - ... -}: { - # Unbound is a validating, recursive, caching DNS resolver. It is designed to - # be fast and lean and incorporates modern features based on open standards. - # https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html - # > nix shell nixpkgs#knot-dns - # > kdig -d @dns.caspervk.net example.com - # > kdig -d +https @dns.caspervk.net example.com - # > kdig -d +tls @dns.caspervk.net example.com - services.unbound = { - enable = true; - # Enable `unbound-control` to view stats stats etc. - localControlSocketPath = "/run/unbound/unbound.ctl"; - # Don't mess with resolvconf - resolveLocalQueries = false; - settings = { - server = { - # Explicitly listen to DNS/DoH/DoT on the external interface(s). This - # allows systemd-resolved to listen on localhost as on every other - # system. Default is to listen to DNS on localhost only. - interface = [ - "159.69.4.2@53" - "159.69.4.2@443" - "159.69.4.2@853" - "2a01:4f8:1c0c:70d1::1@53" - "2a01:4f8:1c0c:70d1::1@443" - "2a01:4f8:1c0c:70d1::1@853" - ]; - # Allow access from all netblocks. Default is to allow localhost only. - access-control = [ - "0.0.0.0/0 allow" - "::0/0 allow" - ]; - # Provide DNS-over-TLS or DNS-over-HTTPS service - tls-service-key = "${config.security.acme.certs."caspervk.net".directory}/key.pem"; - tls-service-pem = "${config.security.acme.certs."caspervk.net".directory}/fullchain.pem"; - # Enable global ratelimiting of queries accepted per IP address. This - # does not seem to impact TCP/DoH/DoT queries. Tested by adding +tcp, - # +https, or +tls to the following (run it twice to warm up cache): - # > seq 100 | xargs -n1 -I% dig @dns.caspervk.net %.example.net - ip-ratelimit = 25; - # Use 0x20-encoded random bits in the query to foil spoof attempts. - # This perturbs the lowercase and uppercase of query names sent to - # authority servers and checks if the reply still has the correct - # casing. - use-caps-for-id = true; - # Increase cache-hit ratio by serving old responses from the cache: - # Before trying to resolve, Unbound will also consider expired cached - # records as possible answers. If such a record is found it is - # immediately returned to the client, Unbound then continues resolving - # and hopefully updating the cached record. Used together with - # prefetch, Unbound tries to update a cached record (after first - # replying to the client) when the current TTL is within 10% of the - # original TTL value. Although prefetching comes with a small penalty - # of ~10% in traffic and load from the extra upstream queries, the - # cache is kept up-to-date, at least for popular queries. - # - # Using serve-expired with prefetch is "highly recommended in order to - # try and keep an updated cache". The following allows Unbound to: - # - prioritize (expired) cached replies, - # - keep the cache fairly up-to-date, and - # - in the likelihood that an expired record needs to be served (e.g., - # rare query, issue with upstream resolving), make sure that the - # record is not older than the specified limit. - # https://unbound.docs.nlnetlabs.nl/en/latest/topics/core/serve-stale.html - prefetch = true; - serve-expired = true; - serve-expired-ttl = 14400; # 4 hours - # Fetch the DNSKEYs earlier in the validation process, when a - # DS record is encountered. This lowers the latency of requests. - prefetch-key = true; - # 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 that is entered into the configuration. - # https://unbound.docs.nlnetlabs.nl/en/latest/topics/core/performance.html - rrset-cache-size = "512m"; - msg-cache-size = "256m"; - # Testing domain - local-zone = ["\"test.dns.caspervk.net.\" redirect"]; - local-data = ["\"test.dns.caspervk.net. A 192.0.2.0\""]; - include = [ - ( - # The awk magic is from - # https://deadc0de.re/articles/unbound-blocking-ads.html which is - # linked from the StevenBlack GitHub. - # https://nixos.org/manual/nixpkgs/stable/#trivial-builder-runCommand - builtins.toString (pkgs.runCommand "stevenblack-blocklist-unbound" {} '' - grep '^0\.0\.0\.0' ${pkgs.stevenblack-blocklist}/hosts | awk '{if (NR==1) {print "server:"} print " local-zone: \""$2"\" redirect\n local-data: \""$2" A 0.0.0.0\""}' > $out - '') - ) - ]; - }; - }; - }; - - networking.firewall = { - allowedTCPPorts = [443 853]; - allowedUDPPorts = [53]; - }; -}