DNS Privacy Stack — Architecture
Architecture¶
Traffic Flow¶
flowchart TD
subgraph LAN["LAN Devices"]
L[Laptop / Phone / IoT]
end
subgraph Router["GL.iNet Router"]
DHCP["DHCP Server<br/>Option 6: <YOUR_SERVER_IP>"]
end
subgraph Server["Server (<YOUR_SERVER_IP>)"]
AG["AdGuard Home<br/>0.0.0.0:53<br/>Docker, host network"]
UB["Unbound<br/>127.0.0.1:5335<br/>systemd service"]
end
subgraph Upstream["Encrypted Upstream (port 853)"]
MV["Mullvad DNS<br/>194.242.2.2"]
Q9["Quad9<br/>9.9.9.9"]
end
subgraph Local["Local Resolution"]
RT["Router<br/><YOUR_ROUTER_IP>"]
end
L -->|"DNS query"| DHCP
DHCP -->|"<YOUR_SERVER_IP>:53"| AG
AG -->|"127.0.0.1:5335"| UB
UB -->|"TLS encrypted<br/>port 853"| MV
UB -->|"TLS encrypted<br/>port 853"| Q9
UB -->|".lan / .ts.net<br/>plaintext, LAN only"| RT
Component Responsibilities¶
| Layer | Component | Runs As | Listens On | Responsibility |
|---|---|---|---|---|
| 1 | Router DHCP | OpenWrt service | N/A | Tells clients to use <YOUR_SERVER_IP> as DNS |
| 2 | AdGuard Home | Docker container (host network) | 0.0.0.0:53 |
Ad blocking, DNS rewrites, access control |
| 3 | Unbound | systemd service | 127.0.0.1:5335 |
Caching, DoT encryption, local domain routing |
| 4 | Mullvad/Quad9 | External service | *:853 |
Actual DNS resolution (no logging) |
Network Topology¶
flowchart TD
subgraph Internet
ISP["ISP Gateway<br/><i>Hijacks port 53<br/>Cannot touch port 853</i>"]
end
subgraph Router["GL.iNet Router — <YOUR_ROUTER_IP>"]
DHCP["DHCP Server<br/>Hands out DNS=<YOUR_SERVER_IP><br/>force_dns=0"]
end
subgraph Server["Homelab Server — <YOUR_SERVER_IP>"]
AG["AdGuard Home<br/>0.0.0.0:53 (Docker)<br/>Filtering + Ad Blocking"]
UB["Unbound<br/>127.0.0.1:5335 (systemd)<br/>Caching + Encryption"]
end
subgraph Upstream["Privacy Resolvers (port 853, TLS)"]
MV["Mullvad DNS<br/>Sweden"]
Q9["Quad9<br/>Switzerland"]
end
ISP --- Router
Router -->|"LAN <YOUR_SUBNET>/24"| Server
AG -->|"127.0.0.1:5335"| UB
UB -->|"DoT (TLS)"| MV
UB -->|"DoT (TLS)"| Q9
Port Map¶
| Port | Protocol | Direction | Purpose |
|---|---|---|---|
| 53 | UDP/TCP | Inbound (LAN) | AdGuard receives DNS queries |
| 5335 | UDP/TCP | Localhost only | Unbound receives from AdGuard |
| 853 | TCP+TLS | Outbound | Unbound → Mullvad/Quad9 (encrypted) |
| 8091 | HTTP | Inbound (LAN) | AdGuard web UI |
Local Domain Routing¶
Not all queries should leave the network. Local domains are forwarded to the router instead of going over DoT:
| Domain | Forward To | Why |
|---|---|---|
*.lan |
<YOUR_ROUTER_IP> |
Local network hostnames |
*.ts.net |
<YOUR_ROUTER_IP> |
Tailscale MagicDNS |
*.example.com |
AdGuard DNS rewrite | Custom domain → local IP |
This is configured in Unbound via forward-zone entries and private-domain / domain-insecure directives.
What Your ISP Sees¶
| Without This Stack | With This Stack |
|---|---|
| Every domain you visit (plaintext) | Connections to 194.242.2.2:853 and 9.9.9.9:853 |
| Can hijack/redirect queries | Cannot intercept (wrong port, encrypted) |
| Can inject fake responses | Cannot modify (TLS integrity) |
| Full browsing history via DNS | Only that you use Mullvad/Quad9 DNS |
Caching Layers¶
Queries pass through two caching layers for maximum performance:
flowchart LR
Q["Query: google.com"] --> AGC{"AdGuard Cache<br/>50,000 entries<br/>min TTL 300s<br/>optimistic: true"}
AGC -->|"HIT"| R1["0ms response"]
AGC -->|"MISS"| UBC{"Unbound Cache<br/>64MB rrset + 32MB msg<br/>serve-expired: yes<br/>prefetch: yes"}
UBC -->|"HIT"| R2["0-1ms response"]
UBC -->|"MISS or STALE"| DoT["DoT to Mullvad<br/>~40ms"]
UBC -->|"STALE + client-timeout"| R3["0ms stale response<br/>(refresh in background)"]
DoT --> R4["~40ms response"]
The serve-expired-client-timeout: 1800 setting in Unbound and cache_optimistic: true in AdGuard ensure that clients never wait for upstream resolution — they get a cached (possibly stale) answer immediately while the fresh answer is fetched in the background.
Previous: 01-fundamentals | Next: 03-setup