# AdGuard Home Module # Provides: Private DNS-over-HTTPS with ClientID-based access control # # Usage: # myModules.adguard = { # enable = true; # domain = "dns.example.com"; # clients = [ # { name = "phone"; idSecret = "adguard_client_phone"; } # ]; # }; { config, lib, pkgs, ... }: let cfg = config.myModules.adguard; in { options.myModules.adguard = { enable = lib.mkEnableOption "AdGuard Home DNS server"; domain = lib.mkOption { type = lib.types.str; example = "dns.example.com"; description = "Public domain name for DoH endpoint"; }; port = lib.mkOption { type = lib.types.port; default = 3000; description = "Internal port for AdGuard HTTP/DoH listener"; }; upstreamDoh = lib.mkOption { type = lib.types.str; default = "https://dns.mullvad.net/dns-query"; description = "Upstream DoH server URL"; }; bootstrapDns = lib.mkOption { type = lib.types.listOf (lib.types.str); default = [ "194.242.2.2" "2a07:e340::2" ]; description = "Bootstrap DNS servers for resolving DoH upstream"; }; filters = lib.mkOption { type = lib.types.listOf (lib.types.submodule { options = { name = lib.mkOption { type = lib.types.str; description = "Friendly name for the filter list"; }; url = lib.mkOption { type = lib.types.str; description = "URL of the filter list (txt format)"; }; enabled = lib.mkOption { type = lib.types.bool; default = true; description = "Whether the filter is enabled"; }; }; }); default = [ { name = "AdGuard DNS filter"; url = "https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt"; } { name = "AdAway Default Blocklist"; url = "https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt"; } { name = "HaGeZi Multi Light"; url = "https://hagezi.github.io/dns-blocklists/wildcard/light.txt"; } { name = "OISD Basic"; url = "https://small.oisd.nl/"; } { name = "Peter Lowe's List"; url = "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=adguard&showintro=0&mimetype=plaintext"; } ]; description = "DNS blocklists to maintain in AdGuard Home"; }; }; config = lib.mkIf cfg.enable { # Generate a temporary JSON file containing the filters for yq to inject systemd.tmpfiles.rules = [ "f /run/adguardhome_filters.json 0644 root root - ${builtins.toJSON { filters = map (f: { inherit (f) name url enabled; }) cfg.filters; }}" ]; services.adguardhome = { enable = true; host = "127.0.0.1"; port = cfg.port; settings = { dns = { bind_hosts = [ "0.0.0.0" ]; port = 5353; upstream_dns = [ cfg.upstreamDoh ]; bootstrap_dns = cfg.bootstrapDns; querylog_enabled = true; querylog_file_enabled = true; statistics_enabled = true; }; tls = { enabled = true; server_name = cfg.domain; certificate_path = "/var/lib/acme/${cfg.domain}/fullchain.pem"; private_key_path = "/var/lib/acme/${cfg.domain}/key.pem"; port_https = 3001; # Prevent conflict with Nginx on port 443 port_dns_over_tls = 853; port_dns_over_quic = 0; allow_unencrypted_doh = false; }; filtering = { protection_enabled = true; filtering_enabled = true; }; safebrowsing = { enabled = false; }; parental = { enabled = false; }; safesearch = { enabled = false; }; log = { file = ""; max_backups = 0; max_size = 100; compress = false; local_time = false; verbose = false; }; }; }; # Inject filters into AdGuardHome.yaml before starting systemd.services.adguardhome = { requires = [ "acme-${cfg.domain}.service" ]; after = [ "acme-${cfg.domain}.service" ]; serviceConfig.SupplementaryGroups = [ "acme" ]; serviceConfig.SystemCallFilter = lib.mkForce []; # Allow yq-go to run its syscalls preStart = lib.mkAfter '' if [ -f /var/lib/private/AdGuardHome/AdGuardHome.yaml ]; then ${pkgs.yq-go}/bin/yq -i '.filters = load("/run/adguardhome_filters.json").filters' /var/lib/private/AdGuardHome/AdGuardHome.yaml fi ''; }; # Open firewall for DoT networking.firewall.allowedTCPPorts = [ 853 ]; networking.firewall.allowedUDPPorts = [ 853 ]; # Nginx configuration (kept to satisfy ACME challenges for DoT certificates) services.nginx.virtualHosts."${cfg.domain}" = { enableACME = true; forceSSL = true; # Block all paths (no DoH or UI exposed via Nginx) locations."/" = { return = "404"; }; }; # Ensure nginx user can access ACME certs users.users.nginx.extraGroups = [ "acme" ]; }; }