From 1a2a4bd2bd99b3c73a405d22340bf157312efa9d Mon Sep 17 00:00:00 2001 From: alexion Date: Wed, 21 Oct 2026 18:51:29 -0400 Subject: [PATCH] feat(nixos): migrate AdGuard to Pikachu --- configs/nixos/guests/adguard.nix | 51 ++++ configs/nixos/hosts/pikachu/default.nix | 8 + .../nixos/modules/services/adguardhome.nix | 277 ++++++++++++++++++ .../adguard-pikachu-migration-design-tree.md | 110 +++++++ 4 files changed, 446 insertions(+) create mode 100644 configs/nixos/guests/adguard.nix create mode 100644 configs/nixos/modules/services/adguardhome.nix create mode 100644 docs/00 INBOX/ai/adguard-pikachu-migration-design-tree.md diff --git a/configs/nixos/guests/adguard.nix b/configs/nixos/guests/adguard.nix new file mode 100644 index 0000000..c04d337 --- /dev/null +++ b/configs/nixos/guests/adguard.nix @@ -0,0 +1,51 @@ +{ + config, + lib, + registry, + ... +}: +let + cfg = config.guests.adguard; +in +{ + options.guests.adguard.adminPasswordHash = lib.mkOption { + type = lib.types.str; + description = "Bcrypt verifier for the AdGuard Home administrator password."; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.address != null; + message = "guests.adguard.address must identify the AdGuard Home guest."; + } + ]; + + registry.homepage.${config.networking.hostName}.adguard = { + tab = "System"; + section = "Network"; + name = "AdGuard Home"; + description = "Internal DNS"; + icon = "adguard-home.svg"; + href = "http://${cfg.address}"; + }; + + guests.adguard = { + interior = { + modules.services.adguardhome = { + enable = true; + inherit (cfg) adminPasswordHash; + reverseProxyAddress = registry.address.${config.networking.hostName}."reverse-proxy"; + }; + + services.resolved.settings.Resolve.DNSStubListener = false; + }; + + limits = { + memory = lib.mkDefault "256M"; + cpu = lib.mkDefault "100%"; + tasksMax = lib.mkDefault 128; + }; + }; + }; +} diff --git a/configs/nixos/hosts/pikachu/default.nix b/configs/nixos/hosts/pikachu/default.nix index 2726f84..2b81737 100644 --- a/configs/nixos/hosts/pikachu/default.nix +++ b/configs/nixos/hosts/pikachu/default.nix @@ -29,6 +29,14 @@ registry.address.pikachu.self = "10.23.10.102"; + guests.adguard = { + enable = true; + vlan = 20; + mac = "bc:24:11:91:60:bf"; + address = "10.23.20.33"; + adminPasswordHash = "$2b$12$wariglnrCvJOnh4b8KnB9eDX4RdQRaaTZtyXMUOilaYSn7xTR0g1e"; + }; + guests.valheim = { enable = true; vlan = 20; diff --git a/configs/nixos/modules/services/adguardhome.nix b/configs/nixos/modules/services/adguardhome.nix new file mode 100644 index 0000000..9301d04 --- /dev/null +++ b/configs/nixos/modules/services/adguardhome.nix @@ -0,0 +1,277 @@ +{ config, lib, ... }: +let + cfg = config.modules.services.adguardhome; +in +{ + options.modules.services.adguardhome = { + enable = lib.mkEnableOption "AdGuard Home"; + + adminPasswordHash = lib.mkOption { + type = lib.types.str; + description = "Bcrypt verifier for the AdGuard Home administrator password."; + }; + + reverseProxyAddress = lib.mkOption { + type = lib.types.str; + description = "IP address of the guest that handles internal HTTP requests."; + }; + }; + + config = lib.mkIf cfg.enable { + services.adguardhome = { + enable = true; + mutableSettings = false; + openFirewall = true; + settings = { + http = { + pprof = { + port = 6060; + enabled = false; + }; + address = "0.0.0.0:80"; + session_ttl = "720h"; + }; + users = [ + { + name = config.user.name; + password = cfg.adminPasswordHash; + } + ]; + auth_attempts = 5; + block_auth_min = 15; + http_proxy = ""; + language = ""; + theme = "auto"; + + dns = { + bind_hosts = [ "0.0.0.0" ]; + port = 53; + anonymize_client_ip = false; + ratelimit = 20; + ratelimit_subnet_len_ipv4 = 24; + ratelimit_subnet_len_ipv6 = 56; + ratelimit_whitelist = [ ]; + refuse_any = true; + upstream_dns = [ + "https://dns.cloudflare.com/dns-query" + "tls://1.1.1.1" + "https://dns.google/dns-query" + "tls://8.8.8.8" + "https://dns10.quad9.net/dns-query" + "tls://9.9.9.9" + ]; + upstream_dns_file = ""; + bootstrap_dns = [ + "9.9.9.10" + "149.112.112.10" + "2620:fe::10" + "2620:fe::fe:10" + ]; + fallback_dns = [ ]; + upstream_mode = "parallel"; + fastest_timeout = "1s"; + allowed_clients = [ ]; + disallowed_clients = [ ]; + blocked_hosts = [ + "version.bind" + "id.server" + "hostname.bind" + ]; + trusted_proxies = [ + "127.0.0.0/8" + "::1/128" + ]; + cache_enabled = true; + cache_size = 4194304; + cache_ttl_min = 0; + cache_ttl_max = 0; + cache_optimistic = false; + cache_optimistic_answer_ttl = "30s"; + cache_optimistic_max_age = "12h"; + bogus_nxdomain = [ ]; + aaaa_disabled = false; + enable_dnssec = false; + edns_client_subnet = { + custom_ip = ""; + enabled = false; + use_custom = false; + }; + max_goroutines = 300; + handle_ddr = true; + ipset = [ ]; + ipset_file = ""; + bootstrap_prefer_ipv6 = false; + upstream_timeout = "10s"; + private_networks = [ ]; + use_private_ptr_resolvers = true; + local_ptr_upstreams = [ ]; + use_dns64 = false; + dns64_prefixes = [ ]; + serve_http3 = false; + use_http3_upstreams = false; + serve_plain_dns = true; + hostsfile_enabled = true; + pending_requests.enabled = true; + }; + + tls = { + enabled = false; + server_name = ""; + force_https = false; + port_https = 443; + port_dns_over_tls = 853; + port_dns_over_quic = 853; + port_dnscrypt = 0; + dnscrypt_config_file = ""; + allow_unencrypted_doh = false; + certificate_chain = ""; + private_key = ""; + certificate_path = ""; + private_key_path = ""; + strict_sni_check = false; + }; + + querylog = { + dir_path = ""; + ignored = [ ]; + interval = "2160h"; + size_memory = 1000; + enabled = true; + ignored_enabled = false; + file_enabled = true; + }; + + statistics = { + dir_path = ""; + ignored = [ ]; + interval = "24h"; + enabled = true; + ignored_enabled = false; + }; + + filters = [ + { + enabled = true; + url = "https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt"; + name = "AdGuard DNS filter"; + id = 1; + } + { + enabled = false; + url = "https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt"; + name = "AdAway Default Blocklist"; + id = 2; + } + ]; + whitelist_filters = [ ]; + user_rules = [ ]; + + dhcp = { + enabled = false; + interface_name = ""; + local_domain_name = "lan"; + dhcpv4 = { + gateway_ip = ""; + subnet_mask = ""; + range_start = ""; + range_end = ""; + lease_duration = 86400; + icmp_timeout_msec = 1000; + options = [ ]; + }; + dhcpv6 = { + range_start = ""; + lease_duration = 86400; + ra_slaac_only = false; + ra_allow_slaac = false; + }; + }; + + filtering = { + blocking_ipv4 = ""; + blocking_ipv6 = ""; + blocked_services = { + schedule.time_zone = "Local"; + ids = [ ]; + }; + protection_disabled_until = null; + safe_search = { + enabled = false; + bing = true; + duckduckgo = true; + ecosia = true; + google = true; + pixabay = true; + yandex = true; + youtube = true; + }; + blocking_mode = "default"; + parental_block_host = "family-block.dns.adguard.com"; + safebrowsing_block_host = "standard-block.dns.adguard.com"; + rewrites = [ + { + domain = "*.home"; + answer = "10.23.20.32"; + enabled = true; + } + { + domain = "*.alexion.dev"; + answer = cfg.reverseProxyAddress; + enabled = true; + } + { + domain = "home"; + answer = cfg.reverseProxyAddress; + enabled = true; + } + ]; + safe_fs_patterns = [ "/opt/AdGuardHome/userfilters/*" ]; + safebrowsing_cache_size = 1048576; + safesearch_cache_size = 1048576; + parental_cache_size = 1048576; + cache_time = 30; + filters_update_interval = 24; + blocked_response_ttl = 10; + filtering_enabled = true; + rewrites_enabled = true; + parental_enabled = false; + safebrowsing_enabled = false; + protection_enabled = true; + }; + + clients = { + runtime_sources = { + whois = true; + arp = true; + rdns = true; + dhcp = true; + hosts = true; + }; + persistent = [ ]; + }; + + log = { + enabled = true; + file = ""; + max_backups = 0; + max_size = 100; + max_age = 3; + compress = false; + local_time = false; + verbose = false; + }; + + os = { + group = ""; + user = ""; + rlimit_nofile = 0; + }; + }; + }; + + networking.firewall = { + allowedTCPPorts = [ 53 ]; + allowedUDPPorts = [ 53 ]; + }; + }; +} diff --git a/docs/00 INBOX/ai/adguard-pikachu-migration-design-tree.md b/docs/00 INBOX/ai/adguard-pikachu-migration-design-tree.md new file mode 100644 index 0000000..75e292c --- /dev/null +++ b/docs/00 INBOX/ai/adguard-pikachu-migration-design-tree.md @@ -0,0 +1,110 @@ +# AdGuard migration to Pikachu: design tree + +## Research facts + +- The pinned Nixpkgs revision provides `adguardhome` version `0.107.79`. +- NixOS provides the `services.adguardhome` module. +- The module runs AdGuard Home as a hardened dynamic systemd user. +- It stores state in `/var/lib/AdGuardHome`. +- It grants `CAP_NET_BIND_SERVICE` for DNS port 53. +- It grants `CAP_NET_RAW` when AdGuard Home DHCP is enabled. +- The module supports an arbitrary YAML configuration through `settings`. +- The module supports fully declarative operation with `mutableSettings = false`. +- In fully declarative operation, the module replaces `/var/lib/AdGuardHome/AdGuardHome.yaml` on each service start. +- Fully declarative operation requires `dns.bootstrap_dns` as a list. +- Declarative settings pass an AdGuard configuration check during the build. +- `mutableSettings = true` merges declared settings into UI changes at service start. +- `mutableSettings = false` replaces the state configuration on every service start. +- `openFirewall` opens only the web interface port. +- The migration must explicitly open DNS ports 53/TCP and 53/UDP when clients need them. +- Pikachu has address `10.23.10.102` on its untagged network. +- Pikachu carries VLAN 20 and creates bridge `br-vlan20`. +- The guest framework runs each guest in its own systemd-nspawn container. +- Each guest attaches to a host VLAN bridge and receives an address through DHCP. +- Pikachu currently configures only VLAN 20 for guests. +- The repository has no AdGuard configuration, Proxmox inventory, or DHCP-provider configuration. +- The current DHCP provider remains unestablished. +- The current Proxmox AdGuard container uses MAC `bc:24:11:91:60:bf`. +- The current Proxmox AdGuard container uses address `10.23.20.33` on VLAN 20. +- This environment has no Proxmox SSH target or local Proxmox control utility. +- The current AdGuard configuration listens on all guest addresses at DNS port 53. +- The current AdGuard configuration has no allowed-client or disallowed-client restrictions. +- The current AdGuard configuration uses Cloudflare, Google, and Quad9 encrypted upstreams in parallel. +- The current AdGuard configuration disables DNSSEC. +- The current AdGuard configuration defines three local rewrites. +- The repository configures no host backup system or monitoring system. +- Hosts update through explicit `nixos-rebuild switch --flake` commands. +- The working tree has unrelated operator changes. + +## Service tradeoffs + +| Service | NixOS support | Domain filtering | Administration UI | DHCP | Encrypted DNS | Main tradeoff | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| AdGuard Home | `services.adguardhome`; checked YAML and mutable or immutable state | Native block lists, rules, and per-client policy | Native | Native | Upstream and client-facing encrypted DNS | Direct migration with the closest state and feature match. UI state conflicts with fully declarative operation unless configuration is immutable. | +| Blocky | `services.blocky`; checked declarative YAML | Native lists, allow lists, rewrites, and per-client groups | No UI; REST API and metrics only | No | Encrypted upstreams and client-facing DoH, DoT, and DoQ | Strong Git-managed configuration and low operational surface. It lacks an interactive administration interface and DHCP. | +| Pi-hole | `services.pihole-ftl` plus `services.pihole-web`; declarative TOML, lists, and dashboard | Native gravity lists, allow lists, regex rules, and groups | Native | Native | Use a companion for encrypted upstream DNS | Familiar filtering product with a dashboard. The migration changes product data and uses more coupled services. | +| Technitium DNS Server | `services.technitium-dns-server`; service and firewall options only | Native block lists and DNS applications | Native | Native | Upstream and client-facing encrypted DNS | Broadest DNS feature set, including authoritative DNS. NixOS does not expose its runtime configuration declaratively. | +| Unbound | `services.unbound`; structured server and free-form settings | Local zones or response-policy zones require custom policy management | No | No | DNS-over-TLS support | Mature validating recursive resolver. Filtering and user operations require separate design. | +| dnscrypt-proxy | `services.dnscrypt-proxy`; declarative TOML | Local block and allow rules, without a list-management UI | No | No | Encrypted upstream DNS, including DNSCrypt and DoH | Strong upstream privacy and resolver selection. It is not a full network filtering appliance. | +| Knot Resolver | `services.kresd`; structured listener settings and extra Lua | Custom policy modules | No | No | Client-facing DoH and TLS listeners | Flexible high-performance recursive resolver. Filtering requires resolver expertise and custom policy. | +| SmartDNS | `services.smartdns`; free-form settings | Domain and address rules | No NixOS-integrated UI | No | Encrypted upstream and client-facing DNS | Optimizes upstream selection and latency. It offers less complete filtering management than dedicated blockers. | + +`Native` means that the product provides the feature without a companion service. + +## Decision tree + +- **D1 — DNS service product** [settled] + - Retain AdGuard Home on Pikachu. + - Constraint: Preserve AdGuard Home as the DNS service product. + - **D2A — AdGuard Home role** [settled] + - Provides DNS filtering and resolution only. + - Constraint: The existing DHCP provider continues to assign addresses and DHCP options. + - **D3A — Execution placement** [settled] + - Runs in a dedicated guest on Pikachu's existing VLAN 20. + - Constraint: Isolate AdGuard Home from the Pikachu host. + - **D4A — Guest network identity** [settled] + - Reuses MAC `bc:24:11:91:60:bf` and address `10.23.20.33` on VLAN 20. + - Constraint: Stop the Proxmox container before the guest starts. + - **D5A — Network exposure** [settled] + - Listens on all guest addresses at port 53. + - Accepts requests from every reachable internal device. + - **D6A — Resolver policy** [settled] + - Preserves the existing upstream resolvers and parallel resolver mode. + - Preserves disabled DNSSEC and the three local rewrites. + - **D7A — Configuration authority** [settled] + - Uses fully declarative Nix configuration. + - Constraint: Set `services.adguardhome.mutableSettings = false`. + - Constraint: Declare `dns.bootstrap_dns` as a list. + - Constraint: Web UI changes do not persist. + - Constraint: Match existing service behavior except for the administrator password. + - **D8A — Migration data** [settled] + - Migrate functional DNS configuration only. + - Constraint: Do not migrate query history or statistics. + - **D9A — Operations** [settled] + - Declares a newly generated bcrypt verifier in Nix. + - Constraint: Keep the plaintext password outside the repository and Nix store. + - Constraint: The verifier is readable from the Nix store. + - **D10A — Cutover and rollback** [settled] + - Uses a planned atomic cutover with rollback. + - Stop the Proxmox container before starting the guest. + - Validate the guest before restoring the Proxmox container during rollback. + - **D11A — Runtime-state backup** [settled] + - Do not back up rebuildable AdGuard Home runtime state. + - **D12A — Monitoring** [settled] + - Do not add DNS availability monitoring. + - **D13A — Upgrade ownership** [settled] + - The normal flake update and explicit NixOS rebuild workflow owns upgrades. + - Replace AdGuard Home. [not selected] + - **D2B — Replacement product** [not applicable] + - Choose Blocky, Pi-hole, Technitium, or a composed resolver. + +## Frontier + +None. + +## Sources + +- Pinned Nixpkgs module: `nixos/modules/services/networking/adguardhome.nix`. +- Pinned Nixpkgs modules: `blocky.nix`, `pihole-ftl.nix`, `pihole-web.nix`, `technitium-dns-server.nix`, `unbound.nix`, `dnscrypt-proxy.nix`, `knot-resolver.nix`, and `smartdns.nix`. +- AdGuard Home README, release `v0.107.79`. +- Technitium DNS Server README, current upstream. -- 2.47.3