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/hostsdomain 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.
# Increase kernel UDP buffer sizes required by quic-go for DNS-over-QUIC (DoQ)
net.core.rmem_max = 7500000
net.core.wmem_max = 7500000
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.
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:
# 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:
# 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