Skip to content

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