{ config, lib, ... }: let cfg = config.services.crab-hole; in { options.services.crab-hole = { port = lib.mkOption { type = lib.types.port; default = 8080; description = "Port for the crab-hole API to listen on."; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Whether to open the firewall for the crab-hole API port."; }; listen = lib.mkOption { type = lib.types.str; default = "0.0.0.0"; description = "Address for the crab-hole API to listen on."; }; show_doc = lib.mkEnableOption "OpenAPI documentation (loads content from third party websites)"; downstreams = { host = { enable = lib.mkEnableOption "host downstream DNS server accessible from network on all interfaces"; port = lib.mkOption { type = lib.types.port; default = 53; description = "Port for the host downstream DNS server to listen on."; }; openFirewall = lib.mkEnableOption "automatic port forwarding for the host downstream"; disableSystemdResolved = lib.mkOption { type = lib.types.bool; default = true; description = "Whether to automatically disable systemd-resolved when using port 53. Set to false if you want to handle the conflict manually."; }; }; }; extraDownstreams = lib.mkOption { type = lib.types.listOf (lib.types.submodule { options = { protocol = lib.mkOption { type = lib.types.enum ["udp" "tcp" "tls" "https" "quic"]; description = "Protocol for the downstream server."; }; listen = lib.mkOption { type = lib.types.str; description = "Address to listen on for downstream connections."; }; port = lib.mkOption { type = lib.types.port; description = "Port to listen on for downstream connections."; }; }; }); default = []; description = "List of additional downstream DNS server configurations."; }; upstreams = { cloudFlare = { enable = lib.mkEnableOption "Cloudflare DNS over TLS upstream servers (1.1.1.1 and 1.0.0.1)"; }; }; extraUpstreams = lib.mkOption { type = lib.types.listOf (lib.types.submodule { options = { socket_addr = lib.mkOption { type = lib.types.str; description = "Socket address of the upstream DNS server (e.g., \"1.1.1.1:853\" or \"[2606:4700:4700::1111]:853\")."; }; protocol = lib.mkOption { type = lib.types.enum ["udp" "tcp" "tls" "https" "quic"]; description = "Protocol to use for upstream DNS queries."; }; }; }); default = []; description = "List of additional upstream DNS server configurations."; }; blocklists = { ad_malware = { enable = lib.mkEnableOption "Host file for blocking ads and malware"; url = lib.mkOption { type = lib.types.str; default = "http://sbc.io/hosts/hosts"; description = "URL of the ad and malware blocklist host file"; }; }; }; extraBlocklists = lib.mkOption { type = lib.types.listOf lib.types.str; default = []; description = "Additional blocklist URLs to be added to the configuration"; }; }; config = lib.mkIf cfg.enable { # Assertions for proper configuration assertions = [ { assertion = !(cfg.downstreams.host.enable && cfg.downstreams.host.port == 53 && config.services.resolved.enable && cfg.downstreams.host.disableSystemdResolved); message = "crab-hole host downstream cannot use port 53 while systemd-resolved is enabled. Either disable systemd-resolved or use a different port."; } { assertion = !(cfg.downstreams.host.enable && cfg.downstreams.host.port == 53 && !cfg.downstreams.host.disableSystemdResolved && config.services.resolved.enable); message = "crab-hole host downstream is configured to use port 53 but systemd-resolved is still enabled and disableSystemdResolved is false. Set disableSystemdResolved = true or manually disable systemd-resolved."; } ]; # Automatically disable systemd-resolved if using port 53 services.resolved.enable = lib.mkIf (cfg.downstreams.host.enable && cfg.downstreams.host.port == 53 && cfg.downstreams.host.disableSystemdResolved) (lib.mkForce false); # Configure DNS nameservers when disabling systemd-resolved networking.nameservers = lib.mkIf (cfg.downstreams.host.enable && cfg.downstreams.host.port == 53 && cfg.downstreams.host.disableSystemdResolved) (lib.mkDefault ["127.0.0.1" "1.1.1.1" "8.8.8.8"]); services.crab-hole.settings = lib.mkMerge [ { api = { port = cfg.port; listen = cfg.listen; show_doc = cfg.show_doc; }; downstream = cfg.extraDownstreams; upstream.name_servers = cfg.extraUpstreams; blocklist.lists = cfg.extraBlocklists; } (lib.mkIf cfg.blocklists.ad_malware.enable { blocklist.lists = [cfg.blocklists.ad_malware.url]; }) (lib.mkIf cfg.downstreams.host.enable { downstream = [ { protocol = "udp"; listen = "0.0.0.0"; port = cfg.downstreams.host.port; } ]; }) (lib.mkIf cfg.upstreams.cloudFlare.enable { upstream.name_servers = [ { socket_addr = "1.1.1.1:853"; protocol = "tls"; tls_dns_name = "1dot1dot1dot1.cloudflare-dns.com"; trust_nx_responses = false; } { socket_addr = "1.0.0.1:853"; protocol = "tls"; tls_dns_name = "1dot1dot1dot1.cloudflare-dns.com"; trust_nx_responses = false; } { socket_addr = "[2606:4700:4700::1111]:853"; protocol = "tls"; tls_dns_name = "1dot1dot1dot1.cloudflare-dns.com"; trust_nx_responses = false; } { socket_addr = "[2606:4700:4700::1001]:853"; protocol = "tls"; tls_dns_name = "1dot1dot1dot1.cloudflare-dns.com"; trust_nx_responses = false; } ]; }) ]; # Open firewall if requested networking.firewall = lib.mkMerge [ (lib.mkIf cfg.openFirewall { allowedTCPPorts = [cfg.port]; }) (lib.mkIf (cfg.downstreams.host.enable && cfg.downstreams.host.openFirewall) { allowedUDPPorts = [cfg.downstreams.host.port]; }) ]; }; }