Configurer un filtrage Web DNS avec Unbound et des listes RPZ sous Linux
- Mise à jour le 13 sept. 2026
Je cherchais une méthode fiable pour mettre en place un filtrage Web DNS sous GNU/Linux. Bien que des outils comme SquidGuard puissent être utilisés pour filtrer le Web, je les ai trouvés trop complexes à configurer et difficiles à déployer automatiquement sur plusieurs postes de travail, notamment dans les environnements qui ne sont pas gérés par un domaine Active Directory. Au cours de mes recherches, j’ai découvert le pare-feu open source DynFi (https://dynfi.com), qui propose des fonctions de filtrage DNS reposant sur les RPZ (Response Policy Zone) — https://en.wikipedia.org/. J’ai donc approfondi le filtrage basé sur les RPZ et élaboré une solution fonctionnelle utilisant Unbound et les RPZ sous Linux.
- Cette solution propose les fonctionnalités suivantes :
- Blocage de l’accès aux domaines présents dans une liste de blocage DNS prédéfinie
- Redirection des domaines bloqués vers un serveur Web local, afin d’afficher une page de blocage personnalisée pour les connexions HTTP non chiffrées
- Prise en charge de listes de blocage RPZ volumineuses et de politiques de filtrage différentes selon le réseau client
Schéma réseau
Dans cette configuration, un serveur Debian assure à la fois les fonctions de résolveur DNS et de serveur Web. Lorsqu’un client tente de résoudre un domaine présent dans la liste de blocage prédéfinie, Unbound renvoie l’adresse IP du serveur Web local à la place de celle de la destination réelle. Pour les connexions HTTP non chiffrées, le serveur Web peut alors afficher une page personnalisée d’accès bloqué dans le navigateur de l’utilisateur.
Serveur Debian
Comme expliqué précédemment, le serveur Debian exécute deux services essentiels : un serveur Web capable d’afficher une page d’information pour les domaines bloqués en HTTP non chiffré, et un résolveur DNS qui renvoie des réponses DNS standard ou modifiées selon les règles de filtrage. Pour réaliser cette configuration, nous utiliserons micro-httpd, un serveur HTTP léger et minimaliste, ainsi qu’Unbound comme résolveur DNS prenant en charge le filtrage basé sur les RPZ.
micro-httpd
Pour informer les utilisateurs lorsqu’un domaine est bloqué, nous avons besoin d’un serveur Web léger capable de fournir une simple page « Accès interdit ». Pour les connexions HTTP non chiffrées, les domaines bloqués sont redirigés vers ce serveur Web local, qui affiche la page d’information. Nous utiliserons pour cela micro-httpd, un serveur HTTP minimal particulièrement adapté à la diffusion d’une simple page statique.
Installation
- Installez le paquet micro-httpd :
root@host:~# apt install micro-httpd
Configuration
- La configuration du service systemd de micro-httpd se trouve dans
/lib/systemd/system/micro-httpd@.service:
[Unit]
Description=micro-httpd
Documentation=man:micro-httpd(8)
[Service]
User=nobody
Group=www-data
ExecStart=-/usr/sbin/micro-httpd /var/www/html
StandardInput=socket
- La configuration du socket est définie dans
/lib/systemd/system/micro-httpd.socket:
[Unit]
Description=micro-httpd
Documentation=man:micro-httpd(8)
[Socket]
ListenStream=0.0.0.0:80
Accept=true
[Install]
WantedBy=sockets.target
- Créez un fichier HTML simple dans
/var/www/html/index.htmlqui servira de page de blocage personnalisée :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Accès interdit</title>
</head>
<body>
<h1>Accès interdit</h1>
<p>L’accès à ce domaine a été bloqué par la politique de filtrage du réseau.</p>
</body>
</html>
root@host:~# chown -R www-data:www-data /var/www/html
- Si nécessaire, redémarrez le socket micro-httpd pour appliquer les modifications :
root@host:~# systemctl restart micro-httpd.socket
Ouvrez un navigateur Web et accédez à http://192.168.0.200/ pour vérifier que la page de blocage personnalisée s’affiche correctement.
Unbound
Unbound constitue le composant central de notre système de filtrage Web DNS. Il agit comme résolveur DNS et peut renvoyer des réponses modifiées pour les noms de domaine bloqués selon des règles de filtrage prédéfinies. Comme les appareils clients utilisent ce serveur pour la résolution DNS, Unbound peut bloquer, rediriger ou modifier les réponses DNS au moyen de politiques RPZ. Il devient ainsi le point de contrôle central du filtrage par domaine. Dans cette section, nous allons configurer Unbound afin qu’il fonctionne à la fois comme résolveur DNS récursif standard et comme moteur chargé d’appliquer nos politiques de filtrage Web.
Installation
- Installez le paquet Unbound :
root@host:~# apt install unbound
Configuration
- Créez le fichier de configuration
/etc/unbound/unbound.conf.d/rpz.confavec le contenu suivant :
server:
module-config: "respip validator iterator" # Charge les modules nécessaires au traitement des RPZ
interface: 192.168.0.200 # Écoute les requêtes DNS sur l’interface du réseau local
interface: 127.0.0.1 # Écoute les requêtes DNS locales
do-ip4: yes # Active IPv4
do-ip6: no # Désactive IPv6 s’il n’est pas utilisé sur votre réseau
do-udp: yes # Active le DNS via UDP
do-tcp: yes # Active le DNS via TCP
access-control: 0.0.0.0/0 allow # Autorise les requêtes DNS de toute adresse IPv4 pouvant joindre le serveur
# Configuration plus restrictive (recommandée en production) :
# access-control: 127.0.0.0/8 allow
# access-control: 192.168.0.0/24 allow
# access-control: 0.0.0.0/0 refuse
rpz:
name: rpz.std.rocks
zonefile: /etc/unbound/blacklist.zone
- Créez le fichier de zone RPZ
/etc/unbound/blacklist.zone. Pour les besoins du test, nous redirigeronsorange.fr,google.fret leurs sous-domaines vers l’adresse IP locale de la page de blocage :
$TTL 3600
@ IN SOA localhost. root.localhost. (
1 ; Serial
3600 ; Refresh
900 ; Retry
604800 ; Expire
3600 ; Minimum TTL
)
@ IN NS localhost.
orange.fr IN A 192.168.0.200
*.orange.fr IN A 192.168.0.200
google.fr IN A 192.168.0.200
*.google.fr IN A 192.168.0.200
- Vérifiez que la configuration d’Unbound ne contient aucune erreur de syntaxe :
root@host:~# unbound-checkconf
- Si aucune erreur n’est signalée, redémarrez le service Unbound pour appliquer les modifications :
root@host:~# systemctl restart unbound
Poste de travail
- Depuis votre poste de travail, ouvrez un navigateur Web et accédez à
http://www.google.fr. Comme la réponse DNS pointe vers le serveur Web local, la page de blocage personnalisée devrait s’afficher :
- Vous pouvez également contrôler directement le filtrage DNS à l’aide de
nslookupou d’un outil similaire. Les domaineswww.google.fretorange.frdoivent tous deux être résolus vers l’adresse IP locale de la page de blocage,192.168.0.200:
Télécharger et appliquer une liste de blocage
Maintenant que le système de filtrage Web DNS est opérationnel, nous pouvons accroître son efficacité en lui appliquant une véritable liste de blocage. De nombreuses listes RPZ publiques sont disponibles en ligne. Dans cet exemple, nous utiliserons une liste du projet HaGeZi DNS Blocklists : https://github.com/hagezi/dns-blocklists.
- Les entrées RPZ de HaGeZi utilisent généralement le format suivant :
website.to.block CNAME .
- Pour notre page de blocage locale, ces entrées doivent être converties au format suivant :
website.to.block IN A 192.168.0.200
Cette conversion peut être effectuée de plusieurs manières. Dans cet exemple, nous utiliserons l’éditeur de flux sed.
- Commencez par télécharger la liste de blocage RPZ de HaGeZi :
root@host:~# wget https://raw.githubusercontent.com/hagezi/dns-blocklists/main/rpz/multi.txt
- Remplacez ensuite l’action RPZ
CNAME .par défaut par un enregistrementApointant vers le serveur Web local :
root@host:~# sed -i 's/CNAME.*/IN A 192.168.0.200/' multi.txt
Aller plus loin
Pour rendre le système de filtrage plus flexible, vous pouvez utiliser des étiquettes clientes afin d’appliquer différentes politiques RPZ à des réseaux précis ou à des hôtes individuels. Vous pourrez ainsi définir plusieurs niveaux de filtrage selon l’adresse IP source de chaque client DNS.
- Modifiez le fichier de configuration
/etc/unbound/unbound.conf.d/rpz.confpour définir les étiquettes, les attribuer à des clients ou réseaux précis, puis associer chaque étiquette à la zone RPZ correspondante :
server:
module-config: "respip validator iterator" # Charge les modules nécessaires au traitement des RPZ
interface: 192.168.0.200 # Écoute les requêtes DNS sur l’interface du réseau local
interface: 127.0.0.1 # Écoute les requêtes DNS locales
do-ip4: yes # Active IPv4
do-ip6: no # Désactive IPv6 s’il n’est pas utilisé sur votre réseau
do-udp: yes # Active le DNS via UDP
do-tcp: yes # Active le DNS via TCP
access-control: 0.0.0.0/0 allow # Autorise les requêtes DNS de tout hôte IPv4 pouvant joindre le serveur
# Configuration plus restrictive (recommandée en production) :
# access-control: 127.0.0.0/8 allow
# access-control: 192.168.0.0/24 allow
# access-control: 192.168.10.0/24 allow
# access-control: 192.168.20.0/24 allow
# access-control: 0.0.0.0/0 refuse
# Définit les étiquettes de filtrage
define-tag: "social adult dnsbypass"
# Attribue les étiquettes à des réseaux ou des hôtes précis
access-control-tag: 192.168.10.0/24 "social adult dnsbypass"
access-control-tag: 192.168.10.200/32 "social adult"
access-control-tag: 192.168.20.0/24 "adult dnsbypass"
rpz:
name: rpz.social.std.rocks
zonefile: /var/lib/unbound/social_networks/blacklist.zone
tags: "social"
rpz:
name: rpz.adult.std.rocks
zonefile: /var/lib/unbound/adult/blacklist.zone
tags: "adult"
rpz:
name: rpz.dnsbypass.std.rocks
zonefile: /var/lib/unbound/dns_bypass/blacklist.zone
tags: "dnsbypass"
Dans cet exemple :
- Le sous-réseau
192.168.10.0/24reçoit les filtressocial,adultetdnsbypass. - L’hôte
192.168.10.200/32reçoit uniquement les filtressocialetadult. - Le sous-réseau
192.168.20.0/24reçoit les filtresadultetdnsbypass.
Cette méthode permet d’appliquer différentes politiques de filtrage DNS à des hôtes individuels ou à des segments réseau entiers, tout en utilisant le même résolveur Unbound.
Optimisation des performances
Selon le nombre d’utilisateurs, le profil du trafic DNS ou la taille de votre liste de blocage, Unbound peut subir une latence plus élevée ou perdre des requêtes lors des pics de trafic. Dans cette section, nous examinerons plusieurs statistiques et options de configuration susceptibles d’améliorer ses performances.
- Commencez par vérifier les performances actuelles à l’aide de la commande
unbound-control:
root@host:~# unbound-control stats_noreset | grep -E "total.num.queries|total.recursion.time.avg|total.requestlist.avg|cache|requestlist.avg|requestlist.max|requestlist.exceeded"
thread0.num.cachehits=29387234
thread0.num.cachemiss=25838180
thread0.requestlist.avg=2.6
thread0.requestlist.max=518
thread0.requestlist.exceeded=251851
total.num.queries=55225414
total.num.queries_ip_ratelimited=0
total.num.cachehits=29387234
total.num.cachemiss=25838180
total.requestlist.avg=2.62538
total.recursion.time.avg=0.047619
Ces statistiques fournissent des informations utiles sur l’efficacité avec laquelle votre instance Unbound traite les requêtes DNS.
La valeur total.num.cachehits indique le nombre de requêtes auxquelles le cache a directement répondu, tandis que total.num.cachemiss représente les requêtes absentes du cache et qui ont donc nécessité un traitement récursif supplémentaire. Dans cet exemple, environ 53 % des requêtes ont reçu une réponse depuis le cache. La possibilité d’améliorer ce taux dépend de plusieurs facteurs, notamment du profil du trafic DNS, de la durée de vie (TTL) des enregistrements et de la quantité de mémoire allouée au cache.
La valeur total.recursion.time.avg est d’environ 48 ms. Elle représente le temps moyen consacré à la résolution des requêtes qui ont nécessité un traitement récursif, ce qui constitue déjà un bon résultat.
Les statistiques de la liste des requêtes sont particulièrement utiles pour repérer les surcharges temporaires :
thread0.requestlist.avg: nombre moyen de requêtes récursives en attente. Dans cet exemple, la valeur2.6est faible et ne révèle aucune surcharge continue.thread0.requestlist.max: nombre maximal de requêtes récursives simultanément en attente. La valeur518montre que le résolveur a dû gérer une concurrence nettement plus élevée durant les pics de trafic, mais cette valeur ne suffit pas à elle seule à révéler un problème.thread0.requestlist.exceeded: nombre de requêtes qui n’ont pas pu être ajoutées à la liste parce que sa capacité était dépassée. La valeur251851confirme que le résolveur n’a pas pu traiter toutes les requêtes reçues pendant certaines périodes de forte charge.
Dans ce cas, le serveur ne semble pas continuellement surchargé. Toutefois, la valeur non nulle de requestlist.exceeded confirme que sa capacité de traitement a été dépassée à un moment donné. Cette situation peut se produire lors de pics de trafic temporaires ou lorsque le résolveur n’est pas configuré pour gérer suffisamment de requêtes simultanées.
Autre observation importante : seul thread0 apparaît dans les statistiques, alors que le serveur utilisé dans cet exemple dispose de quatre cœurs de processeur. L’augmentation du nombre de threads de travail d’Unbound constitue donc une piste d’optimisation évidente.
- À partir de ces observations, vous pouvez optimiser la configuration en modifiant
/etc/unbound/unbound.conf.d/rpz.confet en ajoutant les directives suivantes sousserver:. Adaptez les valeurs à votre matériel et à votre charge de travail ; dans cet exemple, le serveur dispose d’un processeur à 4 cœurs et de 8 Go de RAM :
server:
# Utilise les cœurs de processeur disponibles
num-threads: 4
# Augmente la capacité du cache DNS
msg-cache-size: 512m
rrset-cache-size: 1g
# Améliore la répartition des requêtes UDP entre les threads de travail
so-reuseport: yes
# Augmente le tampon de réception UDP pour mieux absorber les pics de trafic
# Les limites des tampons de sockets du système peuvent également devoir être augmentées
so-rcvbuf: 4m
# Actualise les enregistrements fréquemment demandés avant leur expiration
prefetch: yes
# Précharge les clés DNSSEC lorsque la validation DNSSEC est activée
prefetch-key: yes
# Augmente le nombre de requêtes récursives simultanées pour les environnements très sollicités
# Vérifiez que la version d’Unbound et les limites de descripteurs de fichiers du système acceptent ces valeurs
outgoing-range: 8192
num-queries-per-thread: 4096
# Améliore la disponibilité DNS lorsque les serveurs faisant autorité sont temporairement indisponibles
serve-expired: yes
# Facultatif : conserve un peu plus longtemps les enregistrements à très faible TTL afin de réduire la récursion
# Évitez les valeurs trop élevées, car cette option remplace les TTL définis par les serveurs faisant autorité
# cache-min-ttl: 300
Après avoir appliqué les modifications, redémarrez Unbound afin de recharger tous les paramètres d’optimisation :
root@host:~# systemctl restart unbound
Réinitialisez ensuite les compteurs de statistiques. La commande unbound-control stats affiche les statistiques actuelles, puis remet immédiatement les compteurs à zéro :
root@host:~# unbound-control stats
Après plusieurs heures d’utilisation normale, consultez de nouveau les statistiques :
root@host:~# unbound-control stats_noreset | grep -E "total.num.queries|total.recursion.time.avg|total.requestlist.avg|cache|requestlist.avg|requestlist.max|requestlist.exceeded"
Surveillez particulièrement la valeur requestlist.exceeded. Dans l’idéal, elle doit rester à 0 ou à une valeur proche. Vérifiez également que total.recursion.time.avg reste stable ou diminue, puis comparez le taux de réponses fournies par le cache avant et après les modifications afin de déterminer si l’augmentation du cache et les paramètres de préchargement apportent un bénéfice mesurable.
Dépannage
Lorsque vous utilisez des listes RPZ contenant plusieurs centaines de milliers d’entrées, le démarrage d’Unbound peut être beaucoup plus long, car la zone RPZ doit être analysée et chargée en mémoire. Dans certains cas, systemd peut arrêter le service si le démarrage dépasse le délai d’attente configuré.
- Exemple d’erreur de délai d’attente lors du démarrage d’Unbound :
root@host:~# systemctl restart unbound
Job for unbound.service failed because a timeout was exceeded.
See "systemctl status unbound.service" and "journalctl -xeu unbound.service" for details.
- Commencez par vérifier la configuration d’Unbound et la syntaxe de la zone RPZ :
root@host:~# unbound-checkconf
- Si la configuration est valide, consultez les journaux du service pour vérifier que l’échec provient bien d’un dépassement du délai de démarrage :
root@host:~# journalctl -u unbound.service -b
- (Facultatif) Définissez votre éditeur de texte préféré. Par exemple, pour utiliser
vim:
root@host:~# export EDITOR=vim
- Si les journaux confirment un dépassement du délai de démarrage, modifiez la configuration de surcharge de
unbound.service:
root@host:~# systemctl edit unbound.service
- Ajoutez les lignes suivantes pour augmenter le délai de démarrage d’Unbound :
### Editing /etc/systemd/system/unbound.service.d/override.conf
### Anything between here and the comment below will become the new contents of the file
[Service]
TimeoutStartSec=300
TimeoutStopSec=300
### Lines below this comment will be discarded
### /lib/systemd/system/unbound.service
# [Unit]
# Description=Unbound DNS server
# Documentation=man:unbound(8)
# After=network.target
# Before=nss-lookup.target
# Wants=nss-lookup.target
#
# [Service]
# Type=notify
# Restart=on-failure
# EnvironmentFile=-/etc/default/unbound
# ExecStartPre=-/usr/libexec/unbound-helper chroot_setup
# ExecStartPre=-/usr/libexec/unbound-helper root_trust_anchor_update
# ExecStart=/usr/sbin/unbound -d -p $DAEMON_OPTS
# ExecStopPost=-/usr/libexec/unbound-helper chroot_teardown
# ExecReload=+/bin/kill -HUP $MAINPID
#
# [Install]
# WantedBy=multi-user.target
- Si l’échec était causé par le délai de démarrage, le service Unbound devrait désormais démarrer correctement :
root@host:~# systemctl restart unbound