Zum Inhalt

Encrypted Network Filtering: AdGuard Home Go Engine, Native DoH/DoT/DoQ & Adblock Syntax

Executive Summary: While traditional DNS sinkholes operate exclusively over unencrypted UDP/TCP port 53, modern multi-VLAN and mobile architectures require native encrypted DNS termination (DNS-over-HTTPS, DNS-over-TLS, and DNS-over-QUIC) alongside rich Adblock-style (||example.org^$important) rule evaluation. This guide details the production deployment of AdGuard Home, written in memory-safe Go, covering native TLS/QUIC listener configuration, optimistic DNS caching, client-subnet EDNS0 (ECS) passthrough, and declarative YAML configuration management.

  • Native DoH, DoT & DoQ Server


    Terminate DNS-over-QUIC (quic://:853), DNS-over-TLS (tls://:853), and DNS-over-HTTPS (https://:443/dns-query) directly inside a single static Go binary.

  • Adblock-Syntax Rule Engine


    Enforce granular modifiers ($important, $dnstype=HTTPS, $client='192.168.30.0/24', $dnsrewrite) far beyond simple /etc/hosts domain lists.

  • Optimistic Cache & Parallel Upstreams


    Serve expired TTL records immediately while refreshing upstream resolution asynchronously in the background for sub-millisecond median latency.

  • Per-Client Identity via ClientID


    Identify roaming mobile devices outside the LAN via dedicated DoH/DoT URLs (https://dns.example.com/dns-query/laptop-nejat).


1. Architecture: Single-Binary Go Concurrency & Encrypted Transports

AdGuard Home compiles its DNS resolver (dnsproxy), encrypted transport listeners, DHCP server, and React administration UI into a single zero-dependency Go binary. Unlike traditional setups that require Nginx or stunnel to terminate TLS/HTTPS for DNS clients, AdGuard Home handles X.509 certificates and QUIC (UDP/853) streams natively.

%%{init: {'flowchart': {'wrappingWidth': 600, 'subGraphTitleMargin': {'top': 15, 'bottom': 15}}}}%%
flowchart TB
    subgraph Clients["Internal & Roaming Endpoints"]
        LAN["LAN Clients<br/>UDP/TCP :53"]
        Mobile["Roaming iOS / Android<br/>DoT/DoQ :853 & DoH :443"]
    end

    subgraph AGHCore["AdGuard Home (Go Runtime)"]
        Listeners["Unified Listener Pool<br/>53 / 853 (TLS+QUIC) / 443"]
        RuleEngine["Adblock Rule Engine<br/>$dnstype / $client / $rewrite"]
        OptCache["Optimistic LRU Cache<br/>Async TTL Refresh"]
    end

    subgraph Upstreams["Upstream Resolution Tier"]
        Unbound["Local Unbound<br/>127.0.0.1:5335"]
        Quad9DoQ["Encrypted Upstream<br/>quic://dns.quad9.net:853"]
    end

    LAN -->|"Plain DNS"| Listeners
    Mobile -->|"ClientID TLS/QUIC"| Listeners
    Listeners --> RuleEngine
    RuleEngine <-->|"Cache Hit (<1ms)"| OptCache
    OptCache -->|"Local Zones"| Unbound
    OptCache -->|"Parallel/Load-Balanced"| Quad9DoQ

AdGuard Home vs. Pi-hole v6 Architectural Comparison

Capability AdGuard Home Pi-hole v6 (pihole-FTL)
Core Runtime Memory-safe Go static binary C (dnsmasq + Civetweb + SQLite3)
Inbound DoH / DoT / DoQ Built-in Native (supports ClientID routing) Requires external reverse proxy for DoH/DoT; no native DoQ
Outbound Encrypted DNS Built-in DoH, DoT, DoQ, and DNSCrypt Requires unbound or cloudflared helper daemon
Filtering Syntax Full Adblock-style (||domain^$dnstype=AAAA,client=...) Hosts format + Domain lists + POSIX ERE Regex
Configuration Storage Declarative single file (AdGuardHome.yaml) Declarative pihole.toml + gravity.db (SQLite)

2. Hardened Systemd / Container Deployment

When deploying AdGuard Home on Debian/Ubuntu, grant the binary CAP_NET_BIND_SERVICE so it can bind privileged ports (53, 443, 853) as an unprivileged system user (adguard), or run it inside an isolated container with increased UDP buffer limits for QUIC.

/etc/sysctl.d/99-adguard-quic.conf
# Increase kernel UDP buffer sizes required by quic-go for DNS-over-QUIC (DoQ)
net.core.rmem_max = 7500000
net.core.wmem_max = 7500000
/opt/adguardhome/compose.yaml
services:
  adguardhome:
    image: adguard/adguardhome:latest
    container_name: adguardhome
    restart: unless-stopped
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "853:853/tcp"
      - "853:853/udp"
      - "127.0.0.1:3000:3000/tcp"
    volumes:
      - ./work:/opt/adguardhome/work:Z
      - ./conf:/opt/adguardhome/conf:Z
      - /etc/letsencrypt:/etc/letsencrypt:ro
    cap_add:
      - NET_BIND_SERVICE
      - NET_RAW

3. Production Declarative Configuration (AdGuardHome.yaml)

Below is a hardened production AdGuardHome.yaml excerpt implementing parallel upstream querying, optimistic caching, rate limiting against DNS amplification attacks, and native TLS/QUIC termination.

/opt/adguardhome/conf/AdGuardHome.yaml
http:
  address: 127.0.0.1:3000
  session_ttl: 720h

dns:
  bind_hosts:
    - 0.0.0.0
    - "::"
  port: 53
  anonymize_client_ip: false
  ratelimit: 300
  ratelimit_whitelist:
    - 192.168.10.0/24
  refuse_any: true

  # Upstream DNS Strategy: Local Unbound + Fallback Encrypted DoQ
  upstream_dns:
    - 127.0.0.1:5335
    - "[/lan.nejat-hakan.de/]127.0.0.1:5335"
    - "[/10.168.192.in-addr.arpa/]127.0.0.1:5335"
  upstream_mode: load_balance
  fastest_timeout: 1s

  # High-Performance Optimistic LRU Cache
  cache_size: 33554432
  cache_ttl_min: 300
  cache_ttl_max: 86400
  cache_optimistic: true

  enable_dnssec: true
  blocking_mode: null_ip
  blocked_response_ttl: 60

tls:
  enabled: true
  server_name: dns.nejat-hakan.de
  force_https: true
  port_https: 443
  port_dns_over_tls: 853
  port_dns_over_quic: 853
  certificate_path: /etc/letsencrypt/live/dns.nejat-hakan.de/fullchain.pem
  private_key_path: /etc/letsencrypt/live/dns.nejat-hakan.de/privkey.pem
  strict_sni_check: true

Securing Publicly Exposed DoT/DoQ Servers Against Open Resolver Abuse

If you expose port 853 (DoT/DoQ) or 443 (/dns-query) on a public VPS for your mobile devices, never expose UDP/TCP port 53 to 0.0.0.0/0. Furthermore, configure allowed_clients under the dns: section of AdGuardHome.yaml to restrict encrypted resolution exclusively to known ClientID identifiers (e.g., iphone-nejat, macbook-pro), preventing unauthorized third parties from using your server.


4. Mastering AdGuard Filtering Syntax & Custom Modifiers

AdGuard Home's rule engine supports granular modifiers that go far beyond standard hosts files:

Production Custom Filtering Rules in AdGuard Home
# 1. Block domain and all subdomains across all record types
||telemetry.microsoft.com^

# 2. Override any blocklist to force-allow a critical domain ($important)
@@||login.microsoftonline.com^$important

# 3. Split-Horizon DNS Rewrite: Return internal LAN IP for internal domain queries
||vault.nejat-hakan.de^$dnsrewrite=NOERROR;A;192.168.10.25
||vault.nejat-hakan.de^$dnsrewrite=NOERROR;AAAA;fd88:cafe:10::25

# 4. Block HTTPS/SVCB records (Type 65) to prevent browsers from auto-upgrading to ECH/DoH
||*$dnstype=HTTPS,client=192.168.30.0/24

# 5. Block social video streaming only on the Guest VLAN (192.168.40.0/24)
||tiktok.com^$client='192.168.40.0/24'
||googlevideo.com^$client='192.168.40.0/24'

5. Verification & Encrypted Transport Testing

Use doggo or kdig (from knot-dnsutils) to verify standard DNS, DNS-over-TLS (DoT), DNS-over-HTTPS (DoH), and DNS-over-QUIC (DoQ) responses:

Verifying DoT, DoH, and Split-Horizon Rewrites
# 1. Test DNS-over-TLS (DoT) on Port 853 via kdig
kdig -d @dns.nejat-hakan.de +tls-ca +tls-host=dns.nejat-hakan.de nejat-hakan.de A

# 2. Test Split-Horizon $dnsrewrite rule
dig @127.0.0.1 vault.nejat-hakan.de A +short
# Expected output: 192.168.10.25

# 3. Inspect AdGuard Home service health and configuration syntax
sudo AdGuardHome --check-config -c /opt/adguardhome/conf/AdGuardHome.yaml