rss logo

Configurer un filtrage Web DNS avec Unbound et des listes RPZ sous Linux

Logo d’Unbound

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.

💡 Remarque : Le projet Pi-hole propose un mécanisme similaire de filtrage DNS reposant sur de grandes listes de blocage, ainsi qu’une interface d’administration Web.

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.

💡 Recommandation : Sur votre passerelle, bloquez les connexions sortantes vers le port 53 TCP/UDP pour les réseaux clients, tout en autorisant le serveur Unbound à effectuer ses requêtes DNS. Vous empêcherez ainsi les postes de travail de contourner le résolveur local en interrogeant directement des serveurs DNS externes.

🚨 Remarque : Pour les sites HTTPS, le navigateur affiche généralement une erreur de certificat TLS à la place de la page de blocage personnalisée, car le serveur Web local ne peut pas fournir de certificat valide pour le domaine tiers bloqué. Il s’agit d’une limite inhérente à la redirection DNS.

Schéma réseau montrant comment le filtrage DNS d’Unbound redirige un domaine bloqué vers un serveur Web local au lieu de résoudre sa véritable destination
Processus de filtrage DNS avec Unbound : un domaine bloqué est redirigé vers un serveur Web local au lieu d’être résolu vers sa véritable destination.

Serveur Debian

Logo de 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.html qui 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.conf avec 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

🚨 Sécurité : La directive access-control: 0.0.0.0/0 allow rend Unbound accessible à tout hôte IPv4 capable de joindre le serveur. En production, limitez l’accès aux seuls réseaux de confiance afin de ne pas exposer un résolveur DNS récursif ouvert.

💡 Remarque : Le filtrage DNS fonctionne au niveau du nom de domaine. Il ne peut ni inspecter ni filtrer des URL, des chemins ou le contenu de pages individuelles comme example.com/category/page.html.

  • Créez le fichier de zone RPZ /etc/unbound/blacklist.zone. Pour les besoins du test, nous redirigerons orange.fr, google.fr et 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 :
Firefox affichant la page personnalisée Accès interdit après la redirection d’un domaine bloqué vers le serveur Web local
Page de blocage personnalisée affichée dans Firefox après la redirection d’un domaine bloqué par Unbound vers le serveur Web local en HTTP.

🚨 Remarque : Ce test utilise le protocole HTTP non chiffré. Si vous tentez d’accéder à https://www.google.fr, le navigateur affiche normalement une erreur de certificat TLS à la place de la page de blocage personnalisée, car le serveur local ne possède pas de certificat valide pour www.google.fr.

  • Vous pouvez également contrôler directement le filtrage DNS à l’aide de nslookup ou d’un outil similaire. Les domaines www.google.fr et orange.fr doivent tous deux être résolus vers l’adresse IP locale de la page de blocage, 192.168.0.200 :
Résultat de nslookup sous Windows montrant que les domaines bloqués sont résolus vers l’adresse IP locale 192.168.0.200
Vérification du filtrage DNS avec nslookup : les domaines bloqués sont 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.

💡 Remarque : HaGeZi fournit un format RPZ compatible avec Unbound. Par défaut, les domaines bloqués utilisent l’action RPZ CNAME ., qui renvoie une réponse NXDOMAIN aux requêtes DNS correspondantes. Dans notre configuration, nous souhaitons plutôt que les domaines bloqués soient résolus vers le serveur Web local. Nous allons donc remplacer cette action par un enregistrement A pointant vers 192.168.0.200.

  • 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 enregistrement A pointant vers le serveur Web local :
root@host:~# sed -i 's/CNAME.*/IN A 192.168.0.200/' multi.txt

💡 Remarque : Le blocage des connexions sortantes vers le port 53 TCP/UDP empêche les clients d’utiliser des résolveurs DNS externes classiques. Vous pouvez également bloquer le DNS-over-TLS (DoT) et le DNS-over-QUIC (DoQ), qui utilisent généralement le port 853. Il est plus difficile d’empêcher l’utilisation du DNS-over-HTTPS (DoH), car il emprunte le trafic HTTPS standard sur le port 443. Des listes de blocage appliquées aux postes clients et des politiques de navigateur administrées peuvent contribuer à limiter ce type de contournement. Vous trouverez ici une liste de serveurs DoH publics : https://github.com/crypt0rr/public-doh-servers.

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.conf pour 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/24 reçoit les filtres social, adult et dnsbypass.
  • L’hôte 192.168.10.200/32 reçoit uniquement les filtres social et adult.
  • Le sous-réseau 192.168.20.0/24 reçoit les filtres adult et dnsbypass.

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 valeur 2.6 est faible et ne révèle aucune surcharge continue.
  • thread0.requestlist.max : nombre maximal de requêtes récursives simultanément en attente. La valeur 518 montre 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 valeur 251851 confirme 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.conf et en ajoutant les directives suivantes sous server:. 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

💡 Remarque : Les tailles de cache configurées ne représentent pas la quantité totale de mémoire que peut consommer Unbound. Les structures de données internes et le surcoût d’allocation augmentent l’utilisation réelle de la mémoire. Les valeurs du cache doivent donc toujours être adaptées à la quantité de RAM disponible sur le serveur.

💡 Remarque : Les directives outgoing-range et num-queries-per-thread déterminent le nombre de requêtes récursives qu’Unbound peut traiter simultanément. Les valeurs telles que 8192 et 4096 sont destinées aux environnements fortement sollicités et ne doivent pas être augmentées inutilement. Assurez-vous que votre version d’Unbound et les limites de descripteurs de fichiers du système d’exploitation les prennent en charge.

💡 Remarque : La définition de so-rcvbuf: 4m peut nécessiter d’augmenter la taille maximale du tampon de réception du système d’exploitation. Sous Linux, vérifiez la limite actuelle avec sysctl net.core.rmem_max.

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