|
| 1 | +# DNS: The AdGuard Home & Unbound Stack |
| 2 | +In this section, we break down the DNS architecture of the Homelab. The goal was to move from a standard setup to a "Resilient & Private" stack that eliminates random timeouts, handles high-load traffic without choking, and correctly resolves local device names. |
| 3 | + |
| 4 | +<br/> |
| 5 | + |
| 6 | + |
| 7 | +**Key Takeaways** |
| 8 | + |
| 9 | +* What this HIDES: Your full DNS query log from Google, Cloudflare, and your ISP's DNS Server. (Unless Unbound fails to respond for 10 seconds, it will use the Emergency Fallback) |
| 10 | + |
| 11 | +* What this DOES NOT HIDE: Your IP traffic from your ISP. (You would need a VPN for that). |
| 12 | + |
| 13 | +* What this PROTECTS: You are protected from DNS Censorship, DNS Spoofing, and "Typo-Squatting" ads often injected by ISPs. |
| 14 | + |
| 15 | +<br> |
| 16 | + |
| 17 | +## 1. Architecture Overview |
| 18 | + |
| 19 | +### The "Split-Brain" DNS Model |
| 20 | + |
| 21 | +This system relies on a Decoupled Architecture: |
| 22 | + |
| 23 | +1. The Shield (AdGuard Home): Handles request filtering, blocklists, and client management. It acts as the "doorman." |
| 24 | + |
| 25 | +2. The Resolver (Unbound): Handles the actual "phone book" lookup. It talks directly to the global Root Servers, avoiding the use of Google (8.8.8.8) or your ISP's DNS servers for resolution. |
| 26 | + |
| 27 | +### Visual Diagram |
| 28 | + |
| 29 | +```mermaid |
| 30 | +flowchart TD |
| 31 | + %% -- Subgraph: The Network Clients -- |
| 32 | + subgraph LAN ["🏠 Home Network (VLANs)"] |
| 33 | + Client1[("📱 Phone / PC")] |
| 34 | + Client2[("📺 IoT / TV")] |
| 35 | + end |
| 36 | +
|
| 37 | + %% -- Subgraph: The Router -- |
| 38 | + subgraph Router ["UniFi Dream Machine (UDM Pro)"] |
| 39 | + DHCP[(" DHCP Server ")] |
| 40 | + end |
| 41 | +
|
| 42 | + %% -- Subgraph: High Availability Cluster -- |
| 43 | + subgraph DNS_Cluster ["⚡ DNS High Availability Cluster"] |
| 44 | + direction TB |
| 45 | + |
| 46 | + subgraph Node1 ["Raspberry Pi 63 (Primary)"] |
| 47 | + AGH1["🛡️ AdGuard Home (:53)"] |
| 48 | + Unbound1["🧠 Unbound (:5335)"] |
| 49 | + AGH1 -->|Recursive Query| Unbound1 |
| 50 | + end |
| 51 | +
|
| 52 | + subgraph Node2 ["Raspberry Pi 62 (Secondary)"] |
| 53 | + AGH2["🛡️ AdGuard Home (:53)"] |
| 54 | + Unbound2["🧠 Unbound (:5335)"] |
| 55 | + AGH2 -->|Recursive Query| Unbound2 |
| 56 | + end |
| 57 | + end |
| 58 | +
|
| 59 | + %% -- Subgraph: The Internet -- |
| 60 | + subgraph Internet ["🌍 The Internet"] |
| 61 | + RootServers["ROOT Servers <br> (ICANN / Verisign)"] |
| 62 | + Quad9["🚑 Public Servers <br> (Emergency Fallback)"] |
| 63 | + end |
| 64 | +
|
| 65 | + %% -- Connections -- |
| 66 | + %% 1. DHCP Assignment |
| 67 | + DHCP -- "Assigns DNS 1: Pi 63 <br> Assigns DNS 2: Pi 62" --> LAN |
| 68 | +
|
| 69 | + %% 2. Client Requests (Load Balanced by Client OS) |
| 70 | + LAN -- "DNS Query (Primary)" --> AGH1 |
| 71 | + LAN -- "DNS Query (Backup)" --> AGH2 |
| 72 | +
|
| 73 | + %% 3. Unbound Recursive Lookup (Privacy Path) |
| 74 | + Unbound1 -- "Direct Recursive Lookup <br> (No Middleman)" --> RootServers |
| 75 | + Unbound2 -- "Direct Recursive Lookup <br> (No Middleman)" --> RootServers |
| 76 | +
|
| 77 | + %% 4. The Safety Net (Fail-Safe) - FIXED QUOTES BELOW |
| 78 | + AGH1 -.->|"Timeout > Safety Net"| Quad9 |
| 79 | + AGH2 -.->|"Timeout > Safety Net"| Quad9 |
| 80 | +
|
| 81 | + %% Styling |
| 82 | + style LAN fill:#3471eb,stroke:#01579b,stroke-width:2px |
| 83 | + style DNS_Cluster fill:#2deb49,stroke:#01579b,stroke-width:2px |
| 84 | + style Quad9 fill:#610c06,stroke:#b71c1c,stroke-dasharray: 5 5 |
| 85 | + style RootServers fill:#066114,stroke:#2e7d32 |
| 86 | +``` |
| 87 | +<br/> |
| 88 | +<br/> |
| 89 | + |
| 90 | +## 2. Component Breakdown |
| 91 | + |
| 92 | +### A. The Hardware Level (High Availability) |
| 93 | + |
| 94 | +We utilize **two physical Raspberry Pis** (10.0.0.63 and 10.0.0.62). |
| 95 | + |
| 96 | +* Why: If one Pi crashes, updates, or needs a reboot, the internet does not stop. |
| 97 | + |
| 98 | +* Distribution: The UniFi UDM Pro is configured via DHCP to hand out both IP addresses to every device on the network. |
| 99 | + |
| 100 | +* Client Logic: Modern devices automatically failover to the second IP if the first one doesn't answer (~1000ms), ensuring seamless uptime. |
| 101 | + |
| 102 | +### B. The Software Stack |
| 103 | +Each Pi runs an identical Docker stack: |
| 104 | + |
| 105 | +**1. AdGuard Home (The Frontend)** |
| 106 | + |
| 107 | +* Role: Blocks Ads, Trackers, and Malware. |
| 108 | + |
| 109 | +* Load Balancing: Functionally sends 100% of traffic to the local Unbound container (via "Load Balance" mode). |
| 110 | + |
| 111 | +* Safety Net: Configured with an Emergency Fallback (9.9.9.9) that activates only if Unbound fails to respond for 10 seconds. |
| 112 | + |
| 113 | +**2. Unbound (The Backend)** |
| 114 | + |
| 115 | +* Role: Recursive Resolver. |
| 116 | + |
| 117 | +* Privacy: It bypasses Centralized DNS Loggers (like Google or Cloudflare). No single company creates a profile of your DNS history. |
| 118 | + |
| 119 | +* Nuance: Your ISP can still see the destination IPs and SNI (Server Name Indication) of sites you visit, but they cannot manipulate or spoof your DNS results. |
| 120 | + |
| 121 | +* Security: Handles DNSSEC validation locally. If a site signature is fake, Unbound returns SERVFAIL before AdGuard even sees the IP. |
| 122 | + |
| 123 | +### C. The "Boot-Loop" Protection (NTP) |
| 124 | + |
| 125 | +* **Problem:** Pis lack an RTC battery. If they reboot without internet, they revert to 1970, breaking DNSSEC validation (deadlock). |
| 126 | + |
| 127 | +* **Solution:** Chrony is configured to use Raw IP Addresses (Google/Cloudflare NTP) instead of hostnames. This ensures the Pi gets the correct time immediately at boot, allowing Unbound to start securely. |
| 128 | + |
| 129 | +<br/> |
| 130 | +<br/> |
| 131 | + |
| 132 | + |
| 133 | + |
| 134 | + |
| 135 | +<br/> |
| 136 | +<br/> |
| 137 | + |
| 138 | +## Preparing the Ubuntu Server |
| 139 | + |
| 140 | +If you just try to start the Adguard docker container you will probably run in to few problems: |
| 141 | + |
| 142 | + |
| 143 | + |
| 144 | +!!! warning |
| 145 | + Failed to bind port 0.0.0.0:53/tcp: Error starting userland proxy: listen tcp4 0.0.0.0:53: bind: address already in use |
| 146 | + |
| 147 | +!!! danger |
| 148 | + DNS Port 53 being already in use. This is because the operating system has it reserved. |
| 149 | + |
| 150 | +**Edit etc/systemd/resolved.conf** |
| 151 | +```bash |
| 152 | +sudo nano /etc/systemd/resolved.conf |
| 153 | + |
| 154 | +# Uncomment DNSStubListener and set it to no |
| 155 | +DNSStubListener=no |
| 156 | +``` |
| 157 | +Press CTRL + x and then press y to save the changes |
| 158 | + |
| 159 | +## Prepare Docker Directories |
| 160 | + |
| 161 | +```bash |
| 162 | +cd .. |
| 163 | +cd docker |
| 164 | +sudo mkdir adguard |
| 165 | + |
| 166 | +sudo mkdir adguard/conf |
| 167 | +sudo mkdir adguard/work |
| 168 | + |
| 169 | +#sudo touch adguard/confdir/AdGuardHome.yaml |
| 170 | +sudo chmod -R 0700 adguard/work |
| 171 | +sudo chmod -R 0700 adguard/conf |
| 172 | + |
| 173 | +sudo chown -R evis:docker adguard/work |
| 174 | +sudo chown -R evis:docker adguard/conf |
| 175 | +``` |
| 176 | + |
| 177 | +<br/> |
| 178 | +<br/> |
| 179 | + |
| 180 | +## Unbound Portainer Stack |
| 181 | + |
| 182 | +```yaml |
| 183 | +## NETWORKS ## |
| 184 | +networks: |
| 185 | + ## dns_net network between Adguard Home and Unbound |
| 186 | + dns_net: |
| 187 | + external: |
| 188 | + name: dns_net |
| 189 | + driver: bridge # Using bridge driver for possible VPN solution |
| 190 | + ipam: |
| 191 | + config: |
| 192 | + - subnet: 172.23.0.0/16 |
| 193 | + |
| 194 | + |
| 195 | +## SERVICES ## |
| 196 | +services: |
| 197 | + |
| 198 | + unbound: |
| 199 | + container_name: unbound |
| 200 | + image: mvance/unbound-rpi:latest # Raspberry Pi version |
| 201 | + #image: mvance/unbound:latest # Non ARM version |
| 202 | + networks: |
| 203 | + dns_net: |
| 204 | + ipv4_address: 172.23.0.8 |
| 205 | + environment: |
| 206 | + - TZ=Europe/Helsinki |
| 207 | + volumes: |
| 208 | + - /docker/unbound:/opt/unbound/etc/unbound |
| 209 | + ports: |
| 210 | + - 5053:53/tcp |
| 211 | + - 5053:53/udp |
| 212 | + healthcheck: |
| 213 | + test: ["NONE"] |
| 214 | + restart: unless-stopped |
| 215 | +``` |
| 216 | +
|
| 217 | +<br/> |
| 218 | +<br/> |
| 219 | +
|
| 220 | +## Adguard Portainer Stack |
| 221 | +
|
| 222 | +```yaml |
| 223 | +networks: |
| 224 | + dns_net: |
| 225 | + name: dns_net |
| 226 | + external: true |
| 227 | + driver: bridge |
| 228 | + ipam: |
| 229 | + config: |
| 230 | + - subnet: 172.23.0.0/16 |
| 231 | + proxy: |
| 232 | + external: true |
| 233 | + |
| 234 | + |
| 235 | +services: |
| 236 | + |
| 237 | + adguardhome: |
| 238 | + image: adguard/adguardhome |
| 239 | + container_name: adguard-home |
| 240 | + security_opt: |
| 241 | + - no-new-privileges:true |
| 242 | + networks: |
| 243 | + dns_net: |
| 244 | + ipv4_address: 172.23.0.7 |
| 245 | + proxy: |
| 246 | + ports: |
| 247 | + # DNScrypt: |
| 248 | + - 5443:5443/udp |
| 249 | + - 5443:5443/tcp |
| 250 | + # DNS-over-QUIC: |
| 251 | + - 8853:8853/udp |
| 252 | + - 853:853/udp |
| 253 | + - 784:784/udp |
| 254 | + # DNS-over-TLS |
| 255 | + - 853:853/tcp |
| 256 | + # Home Admin Panel & HTTPS/DNS-over-HTTPS: |
| 257 | + - 3000:3000/tcp |
| 258 | + - 443:443/udp |
| 259 | + #- 443:443/tcp |
| 260 | + - 8080:8080/tcp |
| 261 | + # DHCP server: |
| 262 | + #- 68:68/udp |
| 263 | + #- 67:67/udp |
| 264 | + # Plain DNS: |
| 265 | + - 53:53/udp |
| 266 | + - 53:53/tcp |
| 267 | + environment: |
| 268 | + - TZ=Europe/Helsinki |
| 269 | + restart: always |
| 270 | + volumes: |
| 271 | + - /docker/adguard/conf:/opt/adguardhome/conf |
| 272 | + - /docker/adguard/work:/opt/adguardhome/work |
| 273 | + |
| 274 | +``` |
| 275 | +<br/> |
| 276 | +<br/> |
| 277 | + |
| 278 | +## AdGuard Home settings |
| 279 | + |
| 280 | +### DNS Settings |
| 281 | + |
| 282 | + |
| 283 | + |
| 284 | +| Feature | Setting | Status / Comment | |
| 285 | +|------------------------|---------------------------------------------------|------------------| |
| 286 | +| **Upstream DNS Servers** | | | |
| 287 | +| <br/> | | | |
| 288 | +| Upstream DNS Servers | [/10.in-addr.arpa/]10.0.0.1<br> unbound:53 | Rule: force local names to Router. <br> Everything else to Unbound. | |
| 289 | +| Upstream Mode | Load Balancing | Using Load-balancing instead of "Parallel requests." Parallel requests would be useless with a single Unbound instance. Functionally is zero right now. |
| 290 | +| Fallback DNS servers | https://dns.quad9.net/dns-query <br> https://dns.google/dns-query | Encrypted (DoH) safety net. If Unbound dies, these keep internet alive if Unbound crashes. Unbound times out, AdGuard immediately routes traffic to public DNS servers | |
| 291 | +| Bootstrap DNS | 127.0.0.11 <br> 9.9.9.9 <br> 1.1.1.1 <br> 8.8.8.8 | Configuration Safety. Essential for startup connectivity. With current setup only 127.0.0.11 is used for local Docker container lookups | |
| 292 | +| Private DNS Servers | 10.0.0.1 | Pointing to Router (UDM Pro). AdGuard explicitly asks the UDM Pro for local lookups | |
| 293 | +| Use Private DNS Resolvers | ✅ Checked | Crucial. Tells AdGuard to actually use the Router (UDM Pro) IP above for the lookups. | |
| 294 | +| Enable Reverse Resolving of clients' IP addresses| ✅ Checked | Allows you to see names in logs (from UDM Pro). | |
| 295 | +| Upstream timeout | 10 | Crucial for Unbound. Recursive lookups take longer (walking the chain from Root servers). 10s prevents AdGuard from giving up prematurely on "cold" queries. | |
| 296 | +| <br/> | | | |
| 297 | +| **DNS Sserver configuration** | | | |
| 298 | +| | <br/> | | |
| 299 | +| Rate Limit | 0 | Crucial. Unchecks the “limit” to prevent lag on heavy sites. | |
| 300 | +| Enable DNSSEC | ❌ Unchecked | DNSSEC is handled by Unbound. Prevents packet fragmentation errors. | |
| 301 | +| EDNS Client Subnet | ❌ Unchecked | Prevents “buffer size” errors. | |
| 302 | +| Disable resolving of IPv6 addresses | ✅ Check this box | (Missed in Screenshot) Forces IPv4 for snappier browsing. | |
| 303 | +| Blocking Mode | Null IP | Returns 0.0.0.0 for ads (Safest method). | |
| 304 | +| <br/> | | | |
| 305 | +| **DNS cache configuration** | | | |
| 306 | +| | <br/> | | |
| 307 | +| Enable Cache | ✅ Enabled | Enabled for speed. | |
| 308 | +| Cache size | 10000000 | Set size to 10MB for speed. | |
| 309 | +| Override minimum TTL | 300 | Tells AdGuard: "If a site says 'Ask me again in 30 seconds', ignore it and remember the answer for 5 minutes." This drastically quiets down your network traffic. | |
| 310 | +| Override maximum TTL | 86400 | (86400s = 1 day) Refreshes data every day. | |
| 311 | +| Optimistic Cache | ❌ Disabled | Ensures data freshness. | |
| 312 | + |
| 313 | +<br/> |
| 314 | +<br/> |
| 315 | + |
| 316 | +## Performance |
| 317 | + |
| 318 | +Because Unbound does the heavy lifting of traversing the DNS tree itself (instead of offloading it to a giant like Google), the performance metrics look different than a standard ISP setup. |
| 319 | + |
| 320 | +| Metric | Target Value | Comment | |
| 321 | +| --- | --- | --- | |
| 322 | +| Average Processing Time | ~40–60 ms | This is the "Privacy Tax." It is slightly higher than using Cloudflare directly (20ms), but it means you own the data. | |
| 323 | +| Cache Hit Speed | < 1 ms | Once a site is visited, AdGuard/Unbound serves it instantly from RAM. | |
| 324 | +| Cold Lookup (New Site) | 100–200 ms | The first time you visit a rare site, Unbound must query the Root Servers. This happens once, then it is cached. | |
| 325 | + |
| 326 | +<br/> |
| 327 | +<br/> |
| 328 | + |
| 329 | +## Troubleshooting & Testing |
| 330 | + |
| 331 | +### The "Client Name" Test |
| 332 | +We ran this command to confirm the UDM Pro was correctly resolving local names: |
| 333 | + |
| 334 | +```bash |
| 335 | +dig -x 10.0.0.189 @10.0.0.63 |
| 336 | + |
| 337 | +``` |
0 commit comments