Skip to content

Commit ca3b091

Browse files
committed
articles
1 parent 239ec91 commit ca3b091

2 files changed

Lines changed: 386 additions & 0 deletions

File tree

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
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+
```
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# 1. Install Chrony & Stop the old time service
2+
sudo apt-get update
3+
sudo apt-get install chrony -y
4+
sudo systemctl stop systemd-timesyncd
5+
sudo systemctl disable systemd-timesyncd
6+
sudo systemctl enable chrony
7+
8+
# 2. Write the "Boot Proof" Config
9+
sudo bash -c 'cat > /etc/chrony/chrony.conf <<EOF
10+
# --- BOOT PROOF NTP SERVERS (IPs) ---
11+
# Google Public NTP (IPs solve the "1970" boot loop)
12+
server 216.239.35.0 iburst
13+
server 216.239.35.4 iburst
14+
15+
# Cloudflare Public NTP
16+
server 162.159.200.1 iburst
17+
server 162.159.200.123 iburst
18+
19+
# --- OFFICIAL FINNISH AUTHORITIES ---
20+
# VTT MIKES
21+
server time.mikes.fi iburst
22+
server time1.mikes.fi iburst
23+
server time2.mikes.fi iburst
24+
25+
# DNA & FUNET
26+
server ntp.dnainternet.fi iburst
27+
server ntp.funet.fi iburst
28+
29+
# --- CORE SETTINGS ---
30+
driftfile /var/lib/chrony/chrony.drift
31+
makestep 1.0 3
32+
rtcsync
33+
34+
# --- ACCESS CONTROL ---
35+
# Allow local network to sync from this server
36+
allow 10.0.0.0/8
37+
38+
# Listen on all interfaces
39+
bindcmdaddress 0.0.0.0
40+
41+
# --- LOGGING ---
42+
logdir /var/log/chrony
43+
EOF'
44+
45+
# 3. Restart Chrony to apply
46+
sudo systemctl restart chrony
47+
48+
# verify the sources
49+
chronyc sources -v

0 commit comments

Comments
 (0)