Voilà plusieurs années que j’utilise un bloqueur de publicités et de traqueurs sur mon réseau local à la maison.
Jusqu’à présent, j’utilisais Pi-Hole mais lors de la refonte de mon infrastrucure (article à venir) j’ai également étudié l’idée d’utiliser AdGuard Home car il offre quelques fonctionnalités supplémentaires (DoH, DoT, contrôle parental pour ne citer que ceux là …).
Mais une architecture touchant le DNS est souvent très sensible (“It’s always DNS”) et nécessite donc un peu de haute-disponibilité.

DNS

Fonctionnement d’AdGuard Home (et de Pi-Hole)

AdGuard Home et Pi-hole sont des logiciels qui agissent comme des bloqueurs de publicités sur un réseau domestique.

En filtrant les requêtes DNS, ils empêchent les publicités et les traqueurs de se charger. Cela améliore non seulement l’expérience de navigation en rendant les pages plus rapides, mais cela peut aussi augmenter la confidentialité des utilisateurs. Les 2 fonctionnent avec des listes de serveurs de publicités mises à jour quotidiennement et permettent d’ajouter des listes qui vous sont propres.

Pour que cela fonctionne, il va donc falloir déployer 1 de ces logiciels mais aussi forcer les périphériques de votre réseau à les utiliser (je ne rentrerai pas les détails car cela dépend de votre fournisseur d’accès et de votre “box internet”).

Déploiement d’AdGuard Home avec une Quadlet Podman

Dans la mesure du possible, je déploie tous les services dont j’ai besoin en utilisant des conteneurs avec podman. Outre le fait qu’il fonctionne facilement en mode rootless (c’est mieux pour la sécurité), une des fonctionnalités que j’apprécie est le système de Quadlets.
Les Quadlets permettent en gros de lancer des conteneurs comme des services SystemD (je vous entends les gens qui n’aimaient pas systemd …).

Ma Quadlet pour AdGuard Home ressemble au fichier suivant ; les plus observateurs auront vu que dans ce cas précis, ce conteneur s’éxécute en mode rootfull (c’est une simplification que j’ai prise pour publier simplement le service sur le port 53).

# /etc/containers/systemd/adguardhome/adguardhome.container
[Unit]
Description=AdGuardHome Quadlet
After=local-fs.target

[Container]
AutoUpdate=registry
Environment="TZ=Europe/Paris"
Image=docker.io/adguard/adguardhome:latest
DNS=9.9.9.9
DNS=2620:fe::9
Label="traefik.enable=true"
Label=traefik.docker.network=systemd-traefik-net
Label="traefik.http.routers.adguardhome.rule=Host(`adguard.$DOMAIN`)"
Label="traefik.http.routers.adguardhome.entrypoints=websecure"
Label="traefik.http.routers.adguardhome.tls.certresolver=MYRESOLVER"
Label="traefik.http.services.adguardhome.loadbalancer.server.port=3000"
Label="traefik.http.routers.dns.rule=Host(`dns.$DOMAIN`)"
Label="traefik.http.routers.dns.entrypoints=websecure"
Label="traefik.http.routers.dns.tls.certresolver=MYRESOLVER"
PublishPort=53:53
PublishPort=53:53/udp
PublishPort=853:853
Volume=adguardhome-config.volume:/opt/adguardhome/conf
Volume=adguardhome-work.volume:/opt/adguardhome/work

[Install]
# Start by default on boot
WantedBy=multi-user.target default.target

[Service]
Restart=always
TimeoutStartSec=60

Remarque : les lignes Label= ne sont utiles que parce que j’utilise traefik pour exposer mes services sur mon réseau.

On autorise les connexions sur notre firewall :

$ sudo firewall-cmd --add-service=dns --permanent --zone=public

Cela fait, il nous reste un peu de configuration/adpatation à nos besoins et cela se fait directement dans l’interface d’AdGuard Home (qui se retrouve exposée sur https://adguard.$DOMAIN dans mon cas).
Par défaut, AdGuard Home utilise les serveurs DNS de Quad9 en upstream. Quad9 est (je les cite) une fondation Suisse dont le but est de fournir un Internet plus sûr et plus robuste pour tout le monde.
Vous pouvez bien évidemment utiliser ceux que vous voulez mais pour ma part j’ai décidé de les utiliser en privilégiant leur version DoH (DNS over HTTPS) https://dns.quad9.net/dns-query.
Les détails des différentes options disponibles se trouve ici.

Bon travail … mais

Nous avons maintenant notre service fonctionnel mais testons son fonctionnement.
AdGuard est démarré sur la machine ayant l’adresse ip 192.168.1.200 :

$ dig @192.168.1.200 ad.doubleclick.net +short
0.0.0.0

Bingo ! AdGuard Home nous répond 0.0.0.0 qui évidemment ne sera pas joignable !

On reconfigure sa box Internet pour utiliser cette adresse IP sur tout le réseau et tout va bien, nous pouvons aller dormir … Ah oui ? … Il se passe quoi si la machine qui héberge AdGuard Home est arrêtée ou reboot ou brûle ou … est au minimum inaccessible ?
Ce sont toutes les machines/laptops/smartphones/tablettes qui ne fonctionnent plus car elles ne peuvent plus faire de résolution DNS !

Internet is down

"Ca marche plus Internet !!!!" (quelqu’un dans la maison)

Keepalived à la rescousse

Evidémment si on a une seule machine faisant tourner ce service c’est vite embêtant et on voit tout de suite le SPOF (Single Point Of Failure).
Dans mon cas, en plus du (vieux) NUC sur lequel je déploie mes services, j’ai aussi 1 Raspberry Pi qui peut tout à fait prendre le relais pour ce service.
Néanmoins, hors de question de devoir reconfigurer mon réseau pour que toutes les machines utilisent une nouvelle adresse IP.
L’idée va être d’utiliser une adresse IP flottante qui ira là où le service est actif (avec une préférence pour le NUC).
Et la bonne nouvelle, c’est que Keepalived est tout à fait en mesure de faire çà mais aussi qu’il est disponible sur toutes les distributions Linux.

Mise en place de Keepalived

Après avoir déployé AdGuard Home sur une 2ème machine et avoir installé Keepalived (dnf install keepalived par exemple), on va devoir le configurer.
Tout d’abord, autorisons les connexions du protocol vrrp (le protocole utilisé par keepalived) sur notre firewall :

$ sudo firewall-cmd --add-protocol=vrrp --permanent --zone=public

Plusieurs modes de fonctionnement sont disponibles mais dans mon cas, je veux basculer automatiquement une adresse IP d’un serveur à un autre avec une préférence pour un serveur si le service est démarré sur les deux.

Ma machine principale (le NUC) aura le rôle de MASTER et l’autre (le Raspberry Pi) SLAVE dans cette configuration.

Sur le serveur MASTER :

# /etc/keepalived/keepalived.conf
global_defs {
    enable_script_security
    script_user root
}

vrrp_script chk_adguardhome {
       script "/usr/libexec/keepalived/chk_adguardhome"
       interval 2                      # check every 2 seconds
}

vrrp_instance adguardhome {
        interface bridge0
        state MASTER
        virtual_router_id 50
        priority 200
        authentication {
                auth_type PASS
                auth_pass s0me_random_p@ssw0rd
        }
        virtual_ipaddress {
                192.168.1.3/24 dev bridge0
        }

        track_script {
                chk_adguardhome
        }

}

Sur le serveur SLAVE :

# /etc/keepalived/keepalived.conf
global_defs {
    enable_script_security
    script_user root
}

vrrp_script chk_adguardhome {
       script '/usr/libexec/keepalived/chk_adguardhome'
       interval 2                      # check every 2 seconds
}

vrrp_instance adguardhome {
        interface eth0
        state BACKUP
        virtual_router_id 50
        priority 100
        authentication {
                auth_type PASS
                auth_pass s0me_random_p@ssw0rd
        }
        virtual_ipaddress {
                192.168.1.3/24 dev eth0
        }

        track_script {
                chk_adguardhome
        }
}

Les points notables :

  • state : MASTER ou SLAVE
  • priority : le serveur avec la plus haute priorité gagne
  • virtual_ipaddress : où on définit l’adresse IP qui va se déplacer
  • le script défini par chk_adguardhome : c’est ce script qui va vérifier l’état du service. Si le script renvoie 0, adguardhome est démarré sinon, il est arrêté.
    Dans mon cas avec une Quadlet, le script est celui-ci :
# /usr/libexec/keepalived/chk_adguardhome
#!/bin/sh

/usr/bin/systemctl is-active --quiet adguardhome

Est-ce qu’on peut les synchroniser ?

Le problème avec cet architecture est que la plupart du temps, 1 seul serveur va être mis à jour. Lorsque l’on modifie la configuration sur un serveur (ajout de filtres par exemple), il va falloir le faire sur le 2ème aussi.
Là encore, bonne nouvelle car il existe un utilitaire (adguardhome-sync) qui va permettre de synchroniser une instance vers 1 ou des réplicas.

Sur le serveur principal, on va juste ajouter une nouvelle Quadlet:

# /etc/containers/systemd/adguardhome/adguardhome-sync.container
[Unit]
Description=AdGuardHome Sync Quadlet
After=local-fs.target

[Container]
AutoUpdate=registry
Environment="TZ=Europe/Paris"
Image=ghcr.io/bakito/adguardhome-sync
Label="traefik.enable=true"
Label="traefik.docker.network=systemd-traefik-net"
Label="traefik.http.routers.adguardhome-sync.rule=Host(`adguard-sync.$DOMAIN`)"
Label="traefik.http.routers.adguardhome-sync.entrypoints=websecure"
Label="traefik.http.routers.adguardhome-sync.tls.certresolver=MYRESOLVER"
Label="traefik.http.services.adguardhome-sync.loadbalancer.server.port=8080"
Volume=./adguardhome-sync.yaml:/config/adguardhome-sync.yaml

[Install]
# Start by default on boot
WantedBy=multi-user.target default.target

[Service]
Restart=always
TimeoutStartSec=60

On configure le service via le fichier adguardhome-sync.yaml où on va lui indiquer la source et destination(s) :

# adguardhome-sync.yaml
# cron expression to run in daemon mode. (default; "" = runs only once)
cron: "0 */2 * * *"

# runs the synchronisation on startup
runOnStart: true

# If enabled, the synchronisation task will not fail on single errors, but will log the errors and continue
continueOnError: false

origin:
  # url of the origin instance
  url: https://adguard.$DOMAIN
  # apiPath: define an api path if other than "/control"
  # insecureSkipVerify: true # disable tls check
  username: admin
  password: Y0ur_Aw3s0me_p@ssw0RD!
  # cookie: Origin-Cookie-Name=CCCOOOKKKIIIEEE

# replicas instances
replicas:
  # url of the replica instance
  - url: https://adguard-slave.$DOMAIN
    username: admin
    password: Y0ur_Aw3s0me_p@ssw0RD!
    # cookie: Replica1-Cookie-Name=CCCOOOKKKIIIEEE

# Configure the sync API server, disabled if api port is 0
api:
  # Port, default 8080
  port: 8080
  # if username and password are defined, basic auth is applied to the sync API
  username: admin
  password: ANOth3er_Aw3s0me_p@ssw0RD!
  # enable api dark mode
  darkMode: true

    # enable metrics on path '/metrics' (api port must be != 0)
    # metrics:
    # enabled: true
  # scrapeInterval: 30s
  # queryLogLimit: 10000

# Configure sync features; by default all features are enabled.
features:
  generalSettings: true
  queryLogConfig: true
  statsConfig: true
  clientSettings: true
  services: true
  filters: true
  dhcp:
    serverConfig: true
    staticLeases: true
  dns:
    serverConfig: true
    accessLists: true
    rewrites: true

On démarre le service et voilà, notre 2ème serveur AdGuard Home sera automatiquement et régulièrement mis à jour !

Bonus : fonctionnement hors du réseau local

Avoir le serveur DNS qui filtre localement c’est bien mais si vous sortez de votre maison, votre Smartphone va se retrouver en 4G/5G et donc subir toutes les publicités et autres traqueurs.

Pour remédier à ça, j’ai décidé de me connecter systématiquement en VPN chez moi et donc d’hériter d’AdGuard Home. J’ai utilisé Wireguard disponible nativement sur ma Box Internet (merci Free) mais tout autre solution vous permettant un accès VPN fonctionnera.
On peut même décider de ne faire passer QUE la résolution DNS à travers ce VPN.

Conclusion

Avec tout cela, on a maintenant une architecture DNS filtrant les publicités, redondante et permettant à notre DNS de répondre dans (presque) tous les cas et qui de surcroît nous ajoute une couche de confidentialité et de sécurité grâche aux serveurs DoH de Quad9 !
J’espère que vous aurez trouvé l’article utile.

Remerciements

Un grand merci à Nico pour ces commentaires pendant la relecture !

Références