{ config, inputs, lib, pkgs, this, ... }: let cfg = config.nixfiles.modules.unbound; in { options.nixfiles.modules.unbound = { enable = lib.mkEnableOption "Unbound"; domain = lib.mkOption { description = "Domain name sans protocol scheme."; type = lib.types.str; default = config.networking.domain; }; zone = { whitelist = lib.mkOption { description = "List of domains that always will be allowed."; type = with lib.types; listOf str; default = [ ]; }; blacklist = lib.mkOption { description = "List of domains that always will be denied."; type = with lib.types; listOf str; default = [ ]; }; }; }; config = lib.mkIf cfg.enable { ark.directories = [ config.services.unbound.stateDir ]; nixfiles.modules.redis.enable = true; services = { unbound = { enable = true; package = pkgs.unbound-with-systemd.override { withDNSTAP = true; withRedis = true; withTFO = true; }; checkconf = true; settings = { server = { module-config = ''"respip validator iterator"''; interface = with this.wireguard; [ "127.0.0.1" "::1" ipv4.address ipv6.address ]; local-zone = let mapAs = t: map (x: ''"${x}" ${t}''); in ( lib.my.configurations |> lib.mapAttrsToList (x: _: [ ''"${x}.${cfg.domain}" redirect'' ]) |> lib.concatLists ) ++ mapAs "always_transparent" cfg.zone.whitelist ++ mapAs "always_nxdomain" cfg.zone.blacklist; local-data = lib.my.configurations |> lib.mapAttrsToList ( hostname: let domain = "${hostname}.${cfg.domain}"; in attr: (lib.optionals (lib.hasAttr "wireguard" attr) ( let inherit (attr.wireguard) ipv4 ipv6; in [ ''"${domain} 604800 IN A ${ipv4.address}"'' ''"${domain} 604800 IN AAAA ${ipv6.address}"'' ''"${domain}. A ${ipv4.address}"'' ''"${domain}. AAAA ${ipv6.address}"'' ] ++ (lib.optionals (lib.hasAttr "domains" attr) ( attr.domains |> lib.concatMap (domain: [ ''"${domain}. A ${ipv4.address}"'' ''"${domain}. AAAA ${ipv6.address}"'' ]) )) )) ) |> lib.concatLists; local-data-ptr = lib.my.configurations |> lib.mapAttrsToList ( hostname: let domain = "${hostname}.${cfg.domain}"; in attr: (lib.optionals (lib.hasAttr "wireguard" attr) ( let inherit (attr.wireguard) ipv4 ipv6; in [ ''"${ipv4.address} ${domain}"'' ''"${ipv6.address} ${domain}"'' ] ++ (lib.optionals (lib.hasAttr "domains" attr) ( attr.domains |> lib.concatMap (domain: [ ''"${ipv4.address} ${domain}"'' ''"${ipv6.address} ${domain}"'' ]) )) )) ) |> lib.concatLists; private-domain = map (domain: "${domain}.") [ cfg.domain "local" ]; private-address = with config.nixfiles.modules.wireguard; [ ipv4.subnet ipv6.subnet ]; access-control = with config.nixfiles.modules.wireguard; [ "0.0.0.0/0 refuse" "::/0 refuse" "127.0.0.0/8 allow" "::1/128 allow" "${ipv4.subnet} allow" "${ipv6.subnet} allow" ]; cache-min-ttl = 0; cache-max-ttl = 60 * 60 * 24; serve-expired = true; serve-expired-reply-ttl = 0; prefetch = true; prefetch-key = true; hide-identity = false; hide-version = false; extended-statistics = true; log-replies = false; log-tag-queryreply = false; log-local-actions = false; verbosity = 1; }; forward-zone = { name = "."; forward-tls-upstream = true; forward-addr = lib.dns.mkDoT lib.dns.const.quad9.ecs; }; cachedb = with config.services.redis.servers.default; { backend = "redis"; redis-server-host = bind; redis-server-port = port; }; dnstap = { dnstap-enable = true; dnstap-socket-path = "/run/dnstap-unbound/read.sock"; dnstap-send-identity = false; dnstap-send-version = false; dnstap-log-resolver-query-messages = false; dnstap-log-resolver-response-messages = false; dnstap-log-forwarder-query-messages = false; dnstap-log-forwarder-response-messages = false; dnstap-log-client-query-messages = false; dnstap-log-client-response-messages = true; }; rpz = let # https://unbound.docs.nlnetlabs.nl/en/latest/topics/filtering/rpz.html mkRpz = { name, url ? null, zonefile ? null, }: { inherit name; zonefile = lib.mkIf (zonefile != null) zonefile; url = lib.mkIf (url != null) url; rpz-log = true; rpz-log-name = name; }; # https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html#response-policy-zone-options mkLocalZonefile = { name, action, list, }: [ '' $TTL 30 @ SOA localhost. root.localhost. 1 43200 3600 86400 300 NS localhost. '' ] ++ (cfg.zone.${name} |> map (x: "${x} CNAME ${action}")) |> lib.concatLines |> pkgs.writeText "${name}.zone" |> toString; in [ { name = "whitelist"; zonefile = mkLocalZonefile { name = "whitelist"; action = "rpz-passthru."; list = cfg.zone.whitelist; }; } { name = "blacklist"; zonefile = mkLocalZonefile { name = "blacklist"; action = "."; list = cfg.zone.whitelist; }; } { name = "hagezi-ultimate"; url = "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/rpz/ultimate.txt"; } { name = "big-osid"; url = "https://big.oisd.nl/rpz"; } { name = "nsfw-osid"; url = "https://nsfw.oisd.nl/rpz"; } ] |> map mkRpz; }; enableRootTrustAnchor = true; localControlSocketPath = "/run/unbound/control.sock"; }; prometheus.exporters.unbound = { enable = true; listenAddress = "127.0.0.1"; port = 9167; inherit (config.services.unbound) group user; unbound.host = "unix://${config.services.unbound.localControlSocketPath}"; }; }; systemd = let in { services = { unbound = { after = [ "dnstap-unbound.service" ]; requires = [ "dnstap-unbound.service" ]; }; dnstap-unbound = { serviceConfig = { ExecStart = "${lib.getExe pkgs.dnstap} -j -u ${config.services.unbound.settings.dnstap.dnstap-socket-path}"; User = config.services.unbound.user; Group = config.services.unbound.group; RuntimeDirectory = "dnstap-unbound"; }; wantedBy = [ "multi-user.target" ]; }; alloy.reloadTriggers = [ config.environment.etc."alloy/unbound.alloy".source ]; }; }; environment.etc."alloy/unbound.alloy".text = with config.services.prometheus.exporters.unbound; '' prometheus.scrape "unbound" { targets = [ { __address__ = "${listenAddress}:${toString port}", instance = "${config.networking.hostName}", }, ] forward_to = [prometheus.relabel.default.receiver] } ''; boot.kernel.sysctl."net.ipv4.tcp_fastopen" = lib.mkOverride 200 3; topology.nodes.${this.hostname}.services.unbound = { name = "Unbound"; icon = "${inputs.homelab-svg-assets}/assets/unbound.svg"; details.listen.text = config.services.unbound.settings.server.interface |> lib.filter (x: x != "127.0.0.1" && x != "::1") |> map (x: "${x}:53") |> lib.concatLines; }; }; }