Tuto : son propre résolveur DNS avec dnsmasq + Unbound
Sommaire
- Pourquoi monter son propre résolveur
- Architecture
- Prérequis et installation
- Configuration d'Unbound
- Configuration de dnsmasq
- /etc/dnsmasq.conf
- /etc/dnsmasq.d/lan.conf
- /etc/dnsmasq.hosts/statiques
- /etc/dnsmasq.dhcp/reservations
- /etc/resolv.conf du serveur
- Tests et vérifications
- Maintenance, blocage pub et sauvegarde
- Blocage pub/trackers avec OISD
- Sauvegarde
- Crontab root
- Rotation des logs
- Pièges courants
Pourquoi monter son propre résolveur
Un résolveur récursif local (Unbound) derrière un serveur DNS/DHCP de LAN (dnsmasq) supprime toute dépendance à un résolveur public, valide DNSSEC lui-même et bloque la pub pour tout le réseau. Le tout tourne sur une petite VM ou un Raspberry Pi.
Les résolveurs publics deviennent fragiles en France :
- OpenDNS (Cisco) a coupé son service en France en juin 2024, après des injonctions de blocage anti-piratage.
- Quad9 refuse de bloquer des domaines de streaming sportif visés par une décision du tribunal judiciaire de Paris (13 mai 2026) et envisage de cesser de servir les internautes français face à une astreinte réclamée par beIN Sports (JustGeek, TorrentFreak).
- Mullvad ferme ses DNS chiffrés publics le 2 novembre 2026 (annonce Mullvad).
Avec Unbound en mode récursif, vos requêtes partent directement vers les serveurs racine puis les serveurs faisant autorité : aucun intermédiaire ne voit l'ensemble de votre trafic DNS, et aucun ne peut disparaître du jour au lendemain.
Architecture
dnsmasq fait face au réseau, Unbound reste caché derrière lui sur localhost.
dnsmasq répond seul aux noms locaux, aux domaines bloqués et à ce qu'il a en cache ; tout le reste part vers Unbound, qui interroge directement les serveurs racine puis les serveurs faisant autorité et valide les signatures DNSSEC.
Prérequis et installation
Ce tuto a été réalisé sur Debian 13 (Trixie) dans une VM Proxmox (1 vCPU, 512 Mo de RAM suffisent). Valeurs d'exemple utilisées partout ci-dessous, à adapter :
| Élément | Valeur d'exemple |
|---|---|
| Réseau LAN | 192.168.1.0/24 |
| Box / passerelle | 192.168.1.1 |
| Serveur DNS (IP fixe) | 192.168.1.53 |
| Interface réseau | eth0 (souvent ens18 sous Proxmox) |
| Domaine local | maison.lan |
| Plage DHCP | 192.168.1.10 → 192.168.1.149 |
- Donnez une IP fixe au serveur (dans
/etc/network/interfacesou via NetworkManager), hors de la plage DHCP. - Installez les paquets :
apt update
apt install dnsmasq unbound dns-root-data dnsutils
- Vérifiez que rien n'occupe déjà le port 53 (
ss -lntup | grep :53). Sur une distribution qui activesystemd-resolved(Ubuntu notamment), désactivez son stub listener (DNSStubListener=nodans/etc/systemd/resolved.conf) avant de lancer dnsmasq. - Désactivez le DHCP de votre box : dnsmasq devient le seul serveur DHCP du réseau. Deux DHCP sur le même LAN = baux incohérents.
Configuration d'Unbound
Unbound écoute uniquement sur 127.0.0.1:5335 : seul dnsmasq lui parle, le port 53 reste libre pour dnsmasq. Le fichier principal Debian (/etc/unbound/unbound.conf) ne fait qu'inclure /etc/unbound/unbound.conf.d/*.conf, on n'y touche pas.
Récupérez d'abord la liste des serveurs racine :
curl -sf -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
chown unbound:unbound /var/lib/unbound/root.hints
Puis créez /etc/unbound/unbound.conf.d/local.conf :
server:
# Écoute locale uniquement, port dédié
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-udp: yes
do-tcp: yes
# Ne rien divulguer sur le serveur
hide-identity: yes
hide-version: yes
# Performance : précharge les entrées populaires avant expiration
prefetch: yes
num-threads: 2
cache-min-ttl: 300
cache-max-ttl: 86400
# Résolution récursive directe depuis la racine
root-hints: "/var/lib/unbound/root.hints"
# Logs sobres en production
verbosity: 1
# Seul localhost (donc dnsmasq) peut interroger Unbound
access-control: 127.0.0.1/32 allow
Deux fichiers fournis par Debian sont à garder tels quels :
root-auto-trust-anchor-file.confactive la validation DNSSEC (auto-trust-anchor-file: "/var/lib/unbound/root.key"), la clé racine étant mise à jour automatiquement.remote-control.confactiveunbound-controlvia le socket/run/unbound.ctl(utile pour vider le cache ou lire les stats).
Note : cache-min-ttl: 300 garde en cache 5 minutes minimum même les enregistrements à TTL court. C'est plus rapide, mais un basculement d'IP côté serveur (CDN, failover) peut mettre jusqu'à 5 minutes à être vu. Mettez 0 si cela vous gêne.
Vérifiez et démarrez :
unbound-checkconf
systemctl enable --now unbound
dig @127.0.0.1 -p 5335 debian.org +short
Configuration de dnsmasq
dnsmasq répond aux clients sur le port 53, résout les noms locaux, sert le DHCP et transmet le reste à Unbound. La config est découpée en quatre fichiers.
/etc/dnsmasq.conf
# ===== INTERFACE =====
interface=eth0
bind-interfaces
listen-address=127.0.0.1,192.168.1.53
# Fichiers additionnels (seuls les *.conf sont lus)
conf-dir=/etc/dnsmasq.d/,*.conf
# ===== DNS =====
# Domaine local : les noms courts deviennent nom.maison.lan
domain=maison.lan
local=/maison.lan/
expand-hosts
# Ne jamais transmettre à Internet les noms sans point
# ni les reverse des plages privées
domain-needed
bogus-priv
# Upstream unique : Unbound en local
server=127.0.0.1#5335
# Transmet le bit AD (DNSSEC validé par Unbound) aux clients
proxy-dnssec
# Ignorer /etc/resolv.conf (évite les boucles)
no-resolv
# Cache
cache-size=10000
# Logs (pratique pour le debug, pensez à la rotation)
log-queries
log-facility=/var/log/dnsmasq.log
log-async
log-dhcp
# ===== DHCP =====
dhcp-range=192.168.1.10,192.168.1.149,1h
# Option 3 : passerelle
dhcp-option=3,192.168.1.1
# Option 6 : DNS poussé aux clients = ce serveur
dhcp-option=6,192.168.1.53
# Option 15 : domaine de recherche
dhcp-option=15,maison.lan
# Ce dnsmasq est LE DHCP du réseau
dhcp-authoritative
dhcp-leasefile=/var/lib/misc/dnsmasq.leases
/etc/dnsmasq.d/lan.conf
Les hôtes statiques et réservations DHCP vivent dans des dossiers dédiés : dnsmasq les relit automatiquement à chaque modification, sans redémarrage.
# Enregistrements DNS statiques
hostsdir=/etc/dnsmasq.hosts
# Réservations DHCP
dhcp-hostsdir=/etc/dnsmasq.dhcp
# Option : couper le DNS d'un appareil marqué "bloque"
#dhcp-option=tag:bloque,option:dns-server,0.0.0.0
/etc/dnsmasq.hosts/statiques
Format /etc/hosts : pour les machines à IP fixe (hyperviseur, NAS, reverse proxy…). Vous pouvez aussi y forcer un nom public vers une IP locale (split-horizon), par ex. service.mondomaine.fr vers votre reverse proxy.
192.168.1.53 dns.maison.lan dns
192.168.1.150 pve.maison.lan pve
192.168.1.152 proxy.maison.lan proxy
192.168.1.250 nas.maison.lan nas
192.168.1.152 service.mondomaine.fr
/etc/dnsmasq.dhcp/reservations
# <MAC>,<hostname>,<IP>[,durée_bail]
AA:BB:CC:00:00:01,raspberrypi,192.168.1.63
AA:BB:CC:00:00:02,imprimante,192.168.1.20
AA:BB:CC:00:00:03,pc-bureau,192.168.1.25
# Bloquer un appareil (avec la ligne tag:bloque de lan.conf)
#AA:BB:CC:DD:EE:FF,set:bloque
/etc/resolv.conf du serveur
Le serveur lui-même passe par dnsmasq :
nameserver 127.0.0.1
Vous pouvez ajouter un second nameserver public en secours, mais le serveur l'utilisera alors parfois sans passer par Unbound.
Vérifiez et démarrez :
dnsmasq --test
systemctl enable --now dnsmasq
systemctl restart dnsmasq
Tests et vérifications
Cinq commandes suffisent à valider toute la chaîne, à lancer depuis un poste du LAN (ou le serveur).
# 1. Résolution Internet via dnsmasq → Unbound
dig @192.168.1.53 debian.org
# 2. DNSSEC validé : le flag "ad" doit apparaître dans la réponse
dig @192.168.1.53 debian.org +dnssec | grep flags
# 3. DNSSEC cassé volontairement : doit répondre SERVFAIL
dig @192.168.1.53 dnssec-failed.org
# 4. Nom local
dig @192.168.1.53 nas.maison.lan +short
# 5. Le cache fonctionne : la 2e requête doit afficher ~0 ms
dig @192.168.1.53 wikipedia.org | grep "Query time"
Côté client, après renouvellement du bail DHCP, vérifiez que le DNS reçu est bien 192.168.1.53 (ipconfig /all, resolvectl status ou scutil --dns sur macOS).
Pour suivre le trafic :
tail -f /var/log/dnsmasq.log # requêtes et DHCP
unbound-control stats_noreset | grep total # stats Unbound
cat /var/lib/misc/dnsmasq.leases # baux en cours
Maintenance, blocage pub et sauvegarde
Trois tâches cron gardent le serveur à jour sans intervention : liste de blocage quotidienne, root hints mensuels, sauvegarde nocturne.
Blocage pub/trackers avec OISD
OISD publie une liste au format dnsmasq natif (dnsmasq ≥ 2.86). La variante small bloque environ 58 000 domaines sans casser les sites. Script /usr/local/bin/update-adblock.sh : téléchargement, contrôle du contenu, test de syntaxe, rollback automatique en cas d'échec.
#!/bin/bash
# Mise à jour blocklist OISD small pour dnsmasq (>= 2.86, format dnsmasq2)
set -u
export PATH="/usr/sbin:/usr/bin:/sbin:/bin"
URL="https://small.oisd.nl/dnsmasq2"
DEST="/etc/dnsmasq.d/adblock.conf"
BACKUP="/etc/dnsmasq.d/.adblock.conf.bak" # ne finit pas par .conf => ignoré par conf-dir
LOG="/var/log/adblock.log"
MIN_LINES=1000
log() { echo "$1 $(date '+%F %T') $2"; }
TMP=$(mktemp /var/tmp/adblock.XXXXXX) || { log "ERR" "mktemp échoué"; exit 1; }
trap 'rm -f "$TMP"' EXIT
# Téléchargement
if ! wget -q --timeout=30 --tries=3 -O "$TMP" "$URL"; then
log "ERR" "Échec téléchargement"; exit 1
fi
# Vérification du contenu (compte les vraies règles)
COUNT=$(grep -c '^local=/' "$TMP")
if [ "$COUNT" -lt "$MIN_LINES" ]; then
log "ERR" "Fichier suspect ($COUNT règles)"; exit 1
fi
# Rien à faire si la liste n'a pas changé
if [ -f "$DEST" ] && cmp -s "$TMP" "$DEST"; then
log "INFO" "Liste inchangée ($COUNT domaines)"; exit 0
fi
# Sauvegarde puis remplacement
[ -f "$DEST" ] && cp -p "$DEST" "$BACKUP"
install -m 644 "$TMP" "$DEST"
rollback() {
log "ERR" "$1 — rollback"
if [ -f "$BACKUP" ]; then cp -p "$BACKUP" "$DEST"; else rm -f "$DEST"; fi
systemctl restart dnsmasq
exit 1
}
# Test de la conf COMPLÈTE (principale + conf-dir + nouvelle liste)
dnsmasq --test 2>>"$LOG" || rollback "Syntaxe invalide"
# Restart obligatoire : un reload (SIGHUP) ne relit pas conf-dir
systemctl restart dnsmasq || rollback "Restart dnsmasq échoué"
sleep 2
systemctl is-active --quiet dnsmasq || rollback "dnsmasq inactif après restart"
log "OK" "Adblock : $COUNT domaines bloqués"
Test : dig @192.168.1.53 doubleclick.net +short doit renvoyer une réponse vide (NXDOMAIN).
Sauvegarde
Archivez au minimum : /etc/dnsmasq.conf, /etc/dnsmasq.d, /etc/dnsmasq.hosts, /etc/dnsmasq.dhcp, /var/lib/misc/dnsmasq.leases, /etc/unbound/unbound.conf*, /etc/hosts, /etc/resolv.conf, vos scripts et la crontab root. Avec cp -a --parents puis tar -czf, les chemins complets sont conservés ; la restauration tient en une ligne :
tar -xzf dns-backup_AAAA-MM-JJ.tar.gz -C / --exclude=./_meta
dnsmasq --test && systemctl restart dnsmasq
unbound-checkconf && systemctl restart unbound
L'archive peut ensuite être copiée sur un NAS (montage NFS, rétention 30 jours par exemple).
Script complet /usr/local/bin/backup-dns.sh : archive locale dans /var/backups/dns, copie sur le NAS (montage NFS automatique si besoin), rotation à 30 jours des deux côtés, et un fichier _meta/INFOS.txt qui rappelle la procédure de restauration.
#!/bin/bash
# =====================================================
# Backup DNS - serveur dnsmasq/unbound vers un NAS (NFS)
# Sauvegarde : dnsmasq (conf, dnsmasq.d, hostsdir, dhcp-hostsdir, baux)
# + unbound + /etc/hosts + resolv.conf + logrotate
# + scripts perso + crontab + infos système
#
# Les fichiers sont archivés avec leur chemin complet.
# Restauration : tar -xzf <archive> -C / --exclude=./_meta
# =====================================================
# ===== CONFIGURATION =====
NAS_SERVER="192.168.1.250"
NAS_EXPORT="/volume1/backup"
NAS_ROOT="/mnt/nas"
NAS_MOUNT="${NAS_ROOT}/backupdns"
LOCAL_BACKUP_DIR="/var/backups/dns"
RETENTION_DAYS=30
HOSTNAME=$(hostname)
DATE=$(date +%Y-%m-%d_%H-%M-%S)
ARCHIVE_NAME="${HOSTNAME}_dns-backup_${DATE}.tar.gz"
LOG_FILE="/var/log/backup-dns.log"
TMP_DIR="/tmp/dns-backup-$$"
# Fichiers et dossiers à sauvegarder (chemins absolus)
BACKUP_PATHS=(
# dnsmasq
/etc/dnsmasq.conf
/etc/dnsmasq.d
/etc/dnsmasq.hosts
/etc/dnsmasq.dhcp
/var/lib/misc/dnsmasq.leases
# unbound
/etc/unbound/unbound.conf
/etc/unbound/unbound.conf.d
# système
/etc/hosts
/etc/resolv.conf
/etc/logrotate.d/dnsmasq-lan
# scripts perso
/usr/local/bin/backup-dns.sh
/usr/local/bin/update-adblock.sh
)
# ===== FONCTIONS =====
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
log "===== Début du backup DNS ====="
# ===== PRÉPARATION =====
mkdir -p "$LOCAL_BACKUP_DIR"
mkdir -p "$TMP_DIR/_meta"
# ===== COLLECTE DES FICHIERS =====
log "Collecte des fichiers de configuration..."
for path in "${BACKUP_PATHS[@]}"; do
if [ -e "$path" ]; then
if cp -a --parents "$path" "$TMP_DIR/" 2>>"$LOG_FILE"; then
log " OK $path"
else
log " ERR $path (échec copie)"
fi
else
log " WARN $path introuvable"
fi
done
# Crontab root
crontab -l > "$TMP_DIR/_meta/crontab-root.txt" 2>/dev/null && log " OK crontab root"
# Infos système (utile pour restauration)
{
echo "===== INFOS BACKUP ====="
echo "Date : $(date)"
echo "Hostname : $HOSTNAME"
echo "IP : $(hostname -I)"
echo "OS : $(lsb_release -d 2>/dev/null | cut -f2)"
echo "Kernel : $(uname -r)"
echo ""
echo "===== RESTAURATION ====="
echo "tar -xzf ${ARCHIVE_NAME} -C / --exclude=./_meta"
echo "crontab _meta/crontab-root.txt"
echo "dnsmasq --test && systemctl restart dnsmasq"
echo "unbound-checkconf && systemctl restart unbound"
echo ""
echo "===== VERSION DNSMASQ ====="
dnsmasq --version 2>/dev/null | head -3
echo ""
echo "===== STATUS DNSMASQ ====="
systemctl status dnsmasq --no-pager 2>/dev/null | head -10
echo ""
echo "===== STATUS UNBOUND ====="
systemctl status unbound --no-pager 2>/dev/null | head -10
} > "$TMP_DIR/_meta/INFOS.txt"
log " OK INFOS.txt"
# ===== CRÉATION DE L'ARCHIVE =====
log "Création de l'archive..."
if tar -czf "${LOCAL_BACKUP_DIR}/${ARCHIVE_NAME}" -C "$TMP_DIR" . 2>>"$LOG_FILE"; then
SIZE=$(du -h "${LOCAL_BACKUP_DIR}/${ARCHIVE_NAME}" | cut -f1)
log "Archive locale créée : ${ARCHIVE_NAME} (${SIZE})"
else
log "Échec création archive"
exit 1
fi
# ===== VÉRIFICATION DU MONTAGE NFS =====
NAS_OK=0
if ! mountpoint -q "$NAS_ROOT"; then
log "NAS non monté, tentative de montage..."
mkdir -p "$NAS_ROOT"
mount -t nfs "${NAS_SERVER}:${NAS_EXPORT}" "$NAS_ROOT" 2>>"$LOG_FILE"
if mountpoint -q "$NAS_ROOT"; then
log "NAS monté avec succès"
NAS_OK=1
else
log "Impossible de monter le NAS. Backup local uniquement."
fi
else
log "NAS déjà monté"
NAS_OK=1
fi
# ===== COPIE VERS NAS =====
if [ "$NAS_OK" = "1" ]; then
mkdir -p "$NAS_MOUNT"
if cp "${LOCAL_BACKUP_DIR}/${ARCHIVE_NAME}" "${NAS_MOUNT}/" 2>>"$LOG_FILE"; then
log "Archive copiée sur NAS : ${NAS_MOUNT}/${ARCHIVE_NAME}"
else
log "Échec copie vers NAS"
NAS_OK=0
fi
fi
# ===== ROTATION LOCALE =====
DELETED_LOCAL=$(find "$LOCAL_BACKUP_DIR" -name "${HOSTNAME}_dns-backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete -print | wc -l)
log "Local : $DELETED_LOCAL ancien(s) supprimé(s) (> ${RETENTION_DAYS}j)"
# ===== ROTATION NAS =====
if [ "$NAS_OK" = "1" ]; then
DELETED_NAS=$(find "$NAS_MOUNT" -name "${HOSTNAME}_dns-backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete -print | wc -l)
log "NAS : $DELETED_NAS ancien(s) supprimé(s) (> ${RETENTION_DAYS}j)"
fi
# ===== RÉSUMÉ =====
TOTAL_LOCAL=$(ls -1 "$LOCAL_BACKUP_DIR"/${HOSTNAME}_dns-backup_*.tar.gz 2>/dev/null | wc -l)
log "Backups locaux : $TOTAL_LOCAL"
if [ "$NAS_OK" = "1" ]; then
TOTAL_NAS=$(ls -1 "$NAS_MOUNT"/${HOSTNAME}_dns-backup_*.tar.gz 2>/dev/null | wc -l)
log "Backups NAS : $TOTAL_NAS"
fi
log "===== Fin du backup DNS ====="
exit 0
Pour le montage NFS, installez nfs-common et autorisez l'IP du serveur DNS dans les droits NFS du dossier partagé côté NAS.
Crontab root
# Sauvegarde de la configuration DNS
0 3 * * * /usr/local/bin/backup-dns.sh >/dev/null 2>&1
# Mise à jour de la blocklist OISD
0 9 * * * /usr/local/bin/update-adblock.sh >> /var/log/adblock.log 2>&1
# Mise à jour mensuelle des root hints
0 0 1 * * curl -sf -o /var/lib/unbound/root.hints.tmp https://www.internic.net/domain/named.root && mv /var/lib/unbound/root.hints.tmp /var/lib/unbound/root.hints && systemctl reload unbound
Rotation des logs
log-queries écrit une ligne par requête : sans rotation, le fichier grossit vite. /etc/logrotate.d/dnsmasq-lan :
/var/log/dnsmasq.log /var/log/adblock.log {
weekly
rotate 4
compress
missingok
notifempty
postrotate
systemctl kill -s USR2 dnsmasq.service 2>/dev/null || true
endscript
}
Pièges courants
- IPv6 : si votre box annonce son propre DNS IPv6 (RDNSS dans les Router Advertisements), les clients peuvent le préférer et contourner dnsmasq. Désactivez cette option dans la box, ou faites annoncer le serveur par dnsmasq (
enable-ra,dhcp-range=::,constructor:eth0,ra-stateless). - DNS en dur sur les appareils : certains objets connectés ou navigateurs (DNS-over-HTTPS) ignorent le DNS du DHCP. Désactivez DoH dans le navigateur ou bloquez le port 53 sortant sur le routeur, sauf depuis le serveur.
- Box qui garde son DHCP actif : symptôme typique, des appareils avec la box comme DNS. Vérifiez qu'elle est bien coupée.
- dnsmasq ne démarre pas après reboot : avec
bind-interfaces, l'interface doit exister au démarrage. Préférezbind-dynamicsi l'IP arrive tard.