# 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"; }; clients = lib.mkOption { type = lib.types.listOf (lib.types.submodule { options = { name = lib.mkOption { type = lib.types.str; description = "Friendly name for client device"; }; idSecret = lib.mkOption { type = lib.types.str; description = "SOPS secret name containing the ClientID"; }; }; }); default = [ ]; description = "List of clients with their ClientID secrets"; }; }; config = lib.mkIf cfg.enable { # Ensure Podman is enabled myModules.podman.enable = true; # AdGuard Home Container virtualisation.oci-containers.containers."adguard" = { image = "docker.io/adguard/adguardhome:latest"; ports = [ "127.0.0.1:${toString cfg.port}:3000/tcp" ]; extraOptions = [ "--tmpfs=/tmp" ]; volumes = [ "${config.sops.templates."adguardhome.yaml".path}:/opt/adguardhome/conf/AdGuardHome.yaml:ro" "adguard-data:/opt/adguardhome/work" ]; }; # SOPS template for AdGuard configuration sops.templates."adguardhome.yaml" = { content = '' http: address: 0.0.0.0:3000 dns: upstream_dns: - ${cfg.upstreamDoh} bootstrap_dns: ${lib.concatStringsSep "\n " (map (d: "- ${d}") cfg.bootstrapDns)} querylog_enabled: true querylog_file_enabled: true statistics_enabled: true clients: persistent: ${lib.concatStringsSep "\n " ( map (client: '' - name: ${client.name} ids: - ${config.sops.placeholder.${client.idSecret}} '') cfg.clients )} 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 ''; }; # Auto-declare SOPS secrets for each client sops.secrets = lib.mkMerge ( map (client: { ${client.idSecret} = { }; }) cfg.clients ); # Nginx configuration for DoH endpoint services.nginx.virtualHosts."${cfg.domain}" = { enableACME = true; forceSSL = true; # Regex location to match /dns-query and /dns-query/{clientId} locations."~ ^/dns-query" = { proxyPass = "http://127.0.0.1:${toString cfg.port}"; extraConfig = '' proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # DoH uses POST with application/dns-message proxy_pass_request_body on; proxy_set_header Content-Type "application/dns-message"; # Buffer settings for DNS queries proxy_buffers 8 16k; proxy_buffer_size 32k; ''; }; # Block all other paths including admin UI locations."/" = { return = "404"; }; }; # Ensure nginx user can access ACME certs users.users.nginx.extraGroups = [ "acme" ]; }; }