|
| 1 | +--- |
| 2 | +author: Centurio |
| 3 | +title: "Run AdGuardHome With Keepalived" |
| 4 | +date: 2025-10-05T07:26:30+02:00 |
| 5 | +categories: |
| 6 | +- Linux |
| 7 | +- Raspberry Pi |
| 8 | +tags: |
| 9 | +- AdGuardHome |
| 10 | +- keepalived |
| 11 | +--- |
| 12 | +# Introduction |
| 13 | +I've used to run Pi-Hole at home, changed then to AdGuardHome, then to AdGuardHome DNS and today I'm back again at at combination of AdGuardHome and AdGuardHome DNS as my primary DNS services at home. The reason for this is I finally found a solution to keep two AdGuardHome installations in sync and also keeping them highly available. Today I want to document how I did that. |
| 14 | + |
| 15 | + |
| 16 | +# Installation |
| 17 | +I'm using two Raspberry Pis for this setup. A Pi 5 8GB and a Pi 4 2GB. The Pi 5 serves also as a docker host and therefore gets the job to sync between the AdGuardHome installations using AdGuardHome-sync. Here's a schema of the current setup: |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +I'll leave the installation and setup of AdGuardHome and refer to [the official documentation](https://github.com/AdguardTeam/AdGuardHome?tab=readme-ov-file#automated-install-linux-and-mac) for this part. [AdGuardHome-sync](https://github.com/bakito/adguardhome-sync) is running as a docker container, using the two IPs of the AdGuardHome UIs (192.168.100.18 and 192.168.100.19). |
| 22 | +I'm running Raspbian on my Raspberry Pis and had to install for keeaplived only two packages, using `sudo apt-get install keepalived libipset13`. For the healthcheck I'm using netcat, which I've installed using `sudo apt-get install netcat-openbsd`. |
| 23 | + |
| 24 | +# Configuration |
| 25 | +The configuration consists of several parts working together. |
| 26 | + |
| 27 | +## Health check |
| 28 | +On each of the two machines two scripts have to be added as `/usr/local/bin/check_adguard.sh`. These scripts act as a health script that keepalived uses to detect the availablity of the service. |
| 29 | + |
| 30 | +On pi5-1: |
| 31 | +```bash |
| 32 | +#!/bin/bash |
| 33 | +UI_URL="http://192.168.100.18:853/" |
| 34 | +DNS_TEST="192.168.3.53" |
| 35 | +DNS_TEST_v6="fdd6:e6df:9a26:3::53" |
| 36 | + |
| 37 | +# 1. Check UI |
| 38 | +if ! curl -fs --max-time 2 "$UI_URL" > /dev/null; then |
| 39 | + echo "AdGuardHome UI on Port 853 not reachable" |
| 40 | + exit 1 |
| 41 | +fi |
| 42 | + |
| 43 | +# 2. Check DNS IPv4 |
| 44 | +if ! nc -zu -w2 "$DNS_TEST" 53 > /dev/null 2>&1; then |
| 45 | + echo "AdGuardHome DNS IPv4 not responding" |
| 46 | + exit 1 |
| 47 | +fi |
| 48 | + |
| 49 | +# 3. Check DNS IPv6 |
| 50 | +if ! nc -6zu -w2 "$DNS_TEST_v6" 53 > /dev/null 2>&1; then |
| 51 | + echo "AdGuardHome DNS IPv6 not responding" |
| 52 | + exit 1 |
| 53 | +fi |
| 54 | + |
| 55 | +exit 0 |
| 56 | +``` |
| 57 | + |
| 58 | +On pi4-1: |
| 59 | +```bash |
| 60 | +#!/bin/bash |
| 61 | +UI_URL="http://192.168.100.19:853/" |
| 62 | +DNS_TEST="192.168.3.54" |
| 63 | +DNS_TEST_v6="fdd6:e6df:9a26:3::54" |
| 64 | + |
| 65 | +# 1. Check UI |
| 66 | +if ! curl -fs --max-time 2 "$UI_URL" > /dev/null; then |
| 67 | + echo "AdGuardHome UI on Port 853 not reachable" |
| 68 | + exit 1 |
| 69 | +fi |
| 70 | + |
| 71 | +# 2. Check DNS IPv4 |
| 72 | +if ! nc -zu -w2 "$DNS_TEST" 53 > /dev/null 2>&1; then |
| 73 | + echo "AdGuardHome DNS IPv4 not responding" |
| 74 | + exit 1 |
| 75 | +fi |
| 76 | + |
| 77 | +# 3. Check DNS IPv6 |
| 78 | +if ! nc -6zu -w2 "$DNS_TEST_v6" 53 > /dev/null 2>&1; then |
| 79 | + echo "AdGuardHome DNS IPv6 not responding" |
| 80 | + exit 1 |
| 81 | +fi |
| 82 | + |
| 83 | +exit 0 |
| 84 | +``` |
| 85 | + |
| 86 | +Make the script executable on both machines using `sudo chmod +x /usr/local/bin/check_adguard.sh`. |
| 87 | + |
| 88 | +Please note that I'm also checking the IPv6 connectivity. Reason for using netcat instead of e.g. dig is, that dig would cause frequent entries in AdGuardHome log, which annoyed me a lot. dig will provice the reliable results though, since you're doing DNS requests and can verify it is really working. It is even possible to create filter rules to avoid these requests showing up in your logs and statistics, but then I'm also excluding other local requests to the DNS so I'm ok with using netcat instead. |
| 89 | + |
| 90 | +## keepalived configuration |
| 91 | +Now we'll have to add the configuration for keepalived. |
| 92 | + |
| 93 | +On pi5-1: |
| 94 | +```bash |
| 95 | +global_defs { |
| 96 | + script_user root |
| 97 | + enable_script_security |
| 98 | +} |
| 99 | + |
| 100 | +vrrp_script chk_adguard { |
| 101 | + script "/usr/local/bin/check_adguard.sh" |
| 102 | + interval 5 |
| 103 | + weight -20 |
| 104 | +} |
| 105 | + |
| 106 | +vrrp_instance VI_DNS { |
| 107 | + state MASTER |
| 108 | + interface eth0.3 |
| 109 | + virtual_router_id 51 |
| 110 | + priority 100 |
| 111 | + advert_int 1 |
| 112 | + authentication { |
| 113 | + auth_type PASS |
| 114 | + auth_pass WENyvutQ |
| 115 | + } |
| 116 | + virtual_ipaddress { |
| 117 | + 192.168.3.190/24 |
| 118 | + } |
| 119 | + track_script { |
| 120 | + chk_adguard |
| 121 | + } |
| 122 | +} |
| 123 | + |
| 124 | +vrrp_instance VI_UI { |
| 125 | + state MASTER |
| 126 | + interface eth0 |
| 127 | + virtual_router_id 52 |
| 128 | + priority 100 |
| 129 | + advert_int 1 |
| 130 | + authentication { |
| 131 | + auth_type PASS |
| 132 | + auth_pass WENyvutQ |
| 133 | + } |
| 134 | + virtual_ipaddress { |
| 135 | + 192.168.100.190/24 |
| 136 | + } |
| 137 | + track_script { |
| 138 | + chk_adguard |
| 139 | + } |
| 140 | +} |
| 141 | +``` |
| 142 | + |
| 143 | +On pi4-1: |
| 144 | +```bash |
| 145 | +global_defs { |
| 146 | + script_user root |
| 147 | + enable_script_security |
| 148 | +} |
| 149 | + |
| 150 | +vrrp_script chk_adguard { |
| 151 | + script "/usr/local/bin/check_adguard.sh" |
| 152 | + interval 5 |
| 153 | + weight -20 |
| 154 | +} |
| 155 | + |
| 156 | +vrrp_instance VI_DNS { |
| 157 | + state MASTER |
| 158 | + interface eth0.3 |
| 159 | + virtual_router_id 51 |
| 160 | + priority 90 |
| 161 | + advert_int 1 |
| 162 | + authentication { |
| 163 | + auth_type PASS |
| 164 | + auth_pass WENyvutQ |
| 165 | + } |
| 166 | + virtual_ipaddress { |
| 167 | + 192.168.3.190/24 |
| 168 | + } |
| 169 | + track_script { |
| 170 | + chk_adguard |
| 171 | + } |
| 172 | +} |
| 173 | + |
| 174 | +vrrp_instance VI_UI { |
| 175 | + state MASTER |
| 176 | + interface eth0 |
| 177 | + virtual_router_id 52 |
| 178 | + priority 90 |
| 179 | + advert_int 1 |
| 180 | + authentication { |
| 181 | + auth_type PASS |
| 182 | + auth_pass WENyvutQ |
| 183 | + } |
| 184 | + virtual_ipaddress { |
| 185 | + 192.168.100.190/24 |
| 186 | + } |
| 187 | + track_script { |
| 188 | + chk_adguard |
| 189 | + } |
| 190 | +} |
| 191 | +``` |
| 192 | + |
| 193 | +It is important to put the block for the check `vrrp_script chk_adguard` in the front of the actual configuration, otherwise keepalived will complain and won't find it. The `auth_pass` should not exceed 8 characters and shouldn't be too complex. I've documented here only a randomly created password. You should change it to your needs. |
| 194 | + |
| 195 | +The priority of the pi4-1 is set to 90, so by default this is the secondary instance. |
| 196 | + |
| 197 | +Now restart the keepalived service using `sudo systemctl restart keepalived.service` on both machines. You can check the current state using `sudo systemctl status keepalived.service` so you'll see who is primary and who is secondary. The instance with the highest weight is the primary, and the health script reduces the weight by minus 20 if there's somekind of error. |
| 198 | + |
| 199 | +## AdGuardHome |
| 200 | +I've had to change the `/opt/AdGuardHome/AdGuardHome.yaml` to bind the process to all available IPs, otherwise the process isn't able to bound to the Virtual IP (VIP) 192.168.100.190 and 192.168.3.190. The relevant parts are: |
| 201 | + |
| 202 | +```yaml |
| 203 | +http: |
| 204 | + address: 0.0.0.0:853 |
| 205 | +dns: |
| 206 | + bind_hosts: |
| 207 | + - 0.0.0.0 |
| 208 | + port: 53 |
| 209 | +``` |
| 210 | +
|
| 211 | +Do a restart of AdGuardHome after this, using `sudo systemctl restart AdGuardHome.service` on both machines. |
| 212 | + |
| 213 | +## Router |
| 214 | +Check if you're able to login to AdGuardHome on the VIP for the UI, e.g. http://192.168.100.190:853. If that is working, try to query the DNS under it's VIP, e.g.: |
| 215 | + |
| 216 | +```bash |
| 217 | +dig google.com @192.168.3.190 |
| 218 | +
|
| 219 | +; <<>> DiG 9.10.6 <<>> google.com @192.168.3.190 |
| 220 | +;; global options: +cmd |
| 221 | +;; Got answer: |
| 222 | +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 37929 |
| 223 | +;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 |
| 224 | +
|
| 225 | +;; OPT PSEUDOSECTION: |
| 226 | +; EDNS: version: 0, flags:; udp: 4096 |
| 227 | +;; QUESTION SECTION: |
| 228 | +;google.com. IN A |
| 229 | +
|
| 230 | +;; ANSWER SECTION: |
| 231 | +google.com. 37 IN A 142.250.179.142 |
| 232 | +
|
| 233 | +;; Query time: 36 msec |
| 234 | +;; SERVER: 192.168.3.190#53(192.168.3.190) |
| 235 | +;; WHEN: Sun Oct 05 08:33:24 CEST 2025 |
| 236 | +;; MSG SIZE rcvd: 55 |
| 237 | +``` |
| 238 | + |
| 239 | +# Problems with IPv6 |
| 240 | +What's currently unresolved is to set a IPv6 address for the DNS. It looks like the Rasbpian version of keepalived doesn't support this out of the box. There are workarounds for this but I don't know if I really want to do this for now, as it doesn't offer me any benefits. |
| 241 | + |
| 242 | +# Conclusion |
| 243 | +It was quite simple to get this configuration running and I'm happy that the fallback works this good. I don't have to fear system restarts that block my complete network until completed startup and at the same time I'm having more control over the DNS requests in my home network. |
| 244 | +With the help of this setup I was able to identify: |
| 245 | + * Tasmota devices can be configured to use a local ntp server instead of making requests to the outside |
| 246 | + * My Feinstaubsensor is unable to change it's ntp server |
| 247 | + * many requests are now cached and doesn't rely that much on external requests, speeding up page loading |
0 commit comments