A self-hosted recursive DNS resolver that validates DNSSEC and blocks ads, trackers and
malware with Response Policy Zones, built for SmartOS / illumos native zones. Think of a Pi-hole or
AdGuard Home, but with Unbound doing everything natively and no upstream DNS provider in the middle.
Full recursion from the DNS root, no forwarders
DNSSEC validation
Native RPZ block and allow lists
Per-network filtering policy
Every answer logged with the list that blocked it
Why
Most home and small-office ad blockers forward every question to a big public resolver. RPZ Guard asks
the authoritative servers itself, checks their DNSSEC signatures, and applies blocking inside the
resolver, using the standard RPZ format that public feeds
such as OISD already publish. Nothing leaves your network that the DNS itself does not require, and
the policy is enforced in the same process that answers the query.
How it fits together
Each piece runs as its own unprivileged user under SMF, the illumos service manager.
Features
Resolver
Unbound with respip validator iterator: full recursion from the root, DNSSEC validation, QNAME minimisation, 0x20 randomisation, aggressive NSEC, hardened defaults.
Split-horizon local zone: local host names answer locally, everything else falls through to the public DNS.
Rebinding protection for private address ranges.
Blocking
Any number of RPZ block feeds and allow feeds, plus a local allowlist. Allow lists always win.
Per-network policy with Unbound tags: filter the home network, leave servers and the resolver itself unfiltered.
Lists refresh daily and reload in place, with no restart and no cache loss.
Visibility
Every answer is streamed over dnstap and classified as allowed, blocked or allowlisted, with the list responsible.
Dashboard: queries per hour, % blocked, top blocked domains, top lists and top clients.
Searchable live query log with a one-click Allow button.
Management
Add, enable, update or remove feeds from the browser. Errors are shown per list.
Start, restart, flush the cache, or clear a failed state of the DNS service, using illumos RBAC instead of root.
A command-line tool for the same tasks, plus scheduled maintenance.
Design highlights
Feeds are never loaded as downloaded
A downloaded list is parsed by a strict streaming parser that keeps only plain domain triggers (exact names and
*. wildcards), refuses $INCLUDE and other directives, and counts anything malformed. The
app then writes its own minimal zone file, so a broken or hostile feed cannot inject records into the
resolver. Updates are refused if the list is empty, has too many bad lines, or suddenly shrinks by more than half.
Every configuration change is checked with unbound-checkconf first and rolled back if Unbound
rejects it. The DNS service must never be left broken by a bad feed.
Knowing why a name was blocked
Unbound's telemetry stream does not say which policy matched. RPZ Guard asks Unbound to clear the
recursion available flag on blocked answers. A genuine "no such domain" answer always has it set,
so the collector can tell a block from a real NXDOMAIN with certainty. It then names the list by walking an
in-memory index of all rules in exactly the order Unbound evaluates them. That index reloads within seconds
of any list change.
DNS never waits for logging
The Go collector reads Frame Streams from a Unix socket, buffers the records, and writes them to SQLite in
batches (every 250 ms or 500 records) in WAL mode. If logging ever falls behind, records are counted and
dropped. Resolution is never slowed down.
Least privilege everywhere
Unbound, the collector, the web app and the web server each run as a separate user with a trimmed illumos privilege set. The collector cannot even open a network socket.
The web app reaches Unbound only through its local control socket, and the service manager only through a single RBAC authorization.
All external commands use fixed argument lists, never a shell.
Web security
TLS 1.2/1.3 in front, the OWASP Core Rule Set (Coraza WAF) and per-client rate limits before any request reaches the app.
The admin UI answers only on the networks you choose; everything else gets 403.
argon2id password hashing, login rate limiting, CSRF tokens plus same-origin checks, __Host- session cookies (Secure, HttpOnly, SameSite=Strict).
A strict Content-Security-Policy with no inline scripts. Data is inserted as text, never as HTML.
Built with
Part
Technology
Platform
SmartOS native zone, pkgsrc 2025Q4, SMF
Resolver
Unbound 1.24 (built with dnstap)
Telemetry
Go: golang-dnstap, miekg/dns, SQLite
Web app
Python 3.14, Flask, gunicorn, argon2
Front end
Caddy 2.11 with Coraza WAF + OWASP CRS v4
Packaging
signed pkgsrc binary packages
In production it runs comfortably in a 4 GB zone: with the full OISD Big list loaded (about 460,000 rules) Unbound uses under 400 MB of memory.
Lessons from illumos
A few things that cost us time, in case they save you some:
chmod 2750 does not set the set-group-ID bit on a directory; only chmod g+s does.
An SMF method that runs as a user without a home directory fails with exit status 96 unless the manifest sets working_directory.
gunicorn re-owns its Unix socket after binding, which overrides a set-group-ID directory. Hand it over in a when_ready hook, using the file_chown_self privilege.
Connecting to a Unix socket needs both read and write permission.
The pure-Go SQLite driver has no illumos port; the cgo driver works well.
In a Caddyfile, a top-level error runs after handle. Wrap the handlers in a matcher instead.
License
Unbound RPZ Guard is free software under the GNU Affero General Public License v3 (AGPL-3.0).
It builds on Unbound by NLnet Labs and on public RPZ feeds such as OISD.
Unbound RPZ Guard
Un résolveur DNS récursif auto-hébergé qui valide DNSSEC et bloque les publicités, les
traqueurs et les logiciels malveillants à l'aide de zones de politique de réponse (RPZ), conçu pour les
zones natives SmartOS / illumos. C'est l'équivalent d'un Pi-hole ou d'un AdGuard Home, mais Unbound fait
tout lui-même, sans fournisseur DNS en amont.
Récursion complète depuis la racine, sans redirecteur
Validation DNSSEC
Listes de blocage et d'autorisation RPZ natives
Filtrage par réseau
Chaque réponse journalisée avec la liste qui l'a bloquée
Pourquoi
La plupart des bloqueurs de publicité pour la maison ou le petit bureau transmettent chaque question à un
grand résolveur public. RPZ Guard interroge lui-même les serveurs faisant autorité, vérifie leurs signatures
DNSSEC et applique le blocage dans le résolveur, au format standard RPZ
que publient déjà des listes publiques comme OISD. Rien ne quitte votre réseau sans que le DNS lui-même
l'exige, et la politique est appliquée par le processus même qui répond.
Architecture
Chaque composant tourne sous son propre utilisateur non privilégié, géré par SMF, le gestionnaire de services d'illumos.
Fonctionnalités
Résolveur
Unbound avec respip validator iterator : récursion complète depuis la racine, validation DNSSEC, minimisation des QNAME, randomisation 0x20, NSEC agressif, réglages durcis.
Zone locale à horizon partagé : les noms locaux répondent localement, le reste passe au DNS public.
Protection contre le rebinding pour les plages d'adresses privées.
Blocage
Autant de listes RPZ de blocage et d'autorisation que voulu, plus une liste locale d'autorisation. L'autorisation l'emporte toujours.
Politique par réseau grâce aux étiquettes d'Unbound : filtrer le réseau maison, laisser les serveurs et le résolveur lui-même sans filtre.
Les listes se mettent à jour chaque jour et se rechargent à chaud, sans redémarrage ni perte du cache.
Visibilité
Chaque réponse est transmise par dnstap et classée autorisée, bloquée ou sur liste d'autorisation, avec la liste responsable.
Tableau de bord : requêtes par heure, % bloqué, domaines les plus bloqués, listes et clients les plus actifs.
Journal des requêtes en direct, avec recherche et un bouton Autoriser en un clic.
Gestion
Ajouter, activer, mettre à jour ou supprimer des listes depuis le navigateur. Les erreurs sont affichées par liste.
Démarrer, redémarrer, vider le cache ou effacer un état d'échec du service DNS, grâce au RBAC d'illumos plutôt qu'à root.
Un outil en ligne de commande pour les mêmes tâches, plus la maintenance planifiée.
Points de conception
Les listes ne sont jamais chargées telles quelles
Une liste téléchargée est analysée par un analyseur strict qui ne garde que les déclencheurs de domaine
simples (noms exacts et jokers *.), refuse $INCLUDE et les autres directives et
compte tout ce qui est mal formé. L'application écrit ensuite son propre fichier de zone minimal :
une liste défectueuse ou malveillante ne peut donc rien injecter dans le résolveur. Une mise à jour est
refusée si la liste est vide, contient trop de lignes invalides ou rapetisse soudain de plus de moitié.
Chaque changement de configuration est d'abord vérifié par unbound-checkconf et annulé si
Unbound le rejette. Le service DNS ne doit jamais être cassé par une mauvaise liste.
Savoir pourquoi un nom a été bloqué
Le flux de télémétrie d'Unbound n'indique pas quelle politique s'est appliquée. RPZ Guard demande à Unbound
d'effacer l'indicateur récursion disponible sur les réponses bloquées. Une vraie réponse « domaine
inexistant » l'a toujours, ce qui permet au collecteur de distinguer à coup sûr un blocage d'un vrai
NXDOMAIN. Il retrouve ensuite la liste en parcourant un index en mémoire de toutes les règles, exactement
dans l'ordre d'évaluation d'Unbound. Cet index se recharge quelques secondes après tout changement.
Le DNS n'attend jamais la journalisation
Le collecteur Go lit les Frame Streams sur un socket Unix, met les enregistrements en tampon et les écrit
dans SQLite par lots (toutes les 250 ms ou 500 enregistrements), en mode WAL. Si la journalisation prend du
retard, les enregistrements sont comptés et abandonnés ; la résolution n'est jamais ralentie.
Moindre privilège partout
Unbound, le collecteur, l'application web et le serveur web tournent chacun sous un utilisateur distinct, avec un jeu de privilèges illumos réduit. Le collecteur ne peut même pas ouvrir de socket réseau.
L'application web ne parle à Unbound que par son socket de contrôle local, et au gestionnaire de services que par une seule autorisation RBAC.
Toutes les commandes externes utilisent des arguments fixes, jamais un shell.
Sécurité web
TLS 1.2/1.3 en façade, l'OWASP Core Rule Set (pare-feu Coraza) et des limites de débit par client avant qu'une requête n'atteigne l'application.
L'interface d'administration ne répond que sur les réseaux choisis ; tout le reste reçoit 403.
Mots de passe hachés avec argon2id, limitation des connexions, jetons CSRF et vérification de même origine, cookies de session __Host- (Secure, HttpOnly, SameSite=Strict).
Une politique de sécurité du contenu stricte, sans script en ligne ; les données sont insérées comme du texte, jamais comme du HTML.
Technologies
Partie
Technologie
Plateforme
Zone native SmartOS, pkgsrc 2025Q4, SMF
Résolveur
Unbound 1.24 (compilé avec dnstap)
Télémétrie
Go : golang-dnstap, miekg/dns, SQLite
Application web
Python 3.14, Flask, gunicorn, argon2
Façade
Caddy 2.11 avec pare-feu Coraza + OWASP CRS v4
Paquets
paquets binaires pkgsrc signés
En production, une zone de 4 Go suffit largement : avec la liste OISD Big complète (environ 460 000 règles), Unbound utilise moins de 400 Mo de mémoire.
Leçons d'illumos
Quelques pièges qui nous ont coûté du temps, au cas où ils vous en feraient gagner :
chmod 2750 ne pose pas le bit set-group-ID sur un répertoire ; seul chmod g+s le fait.
Une méthode SMF exécutée par un utilisateur sans répertoire personnel échoue avec le code 96, sauf si le manifeste définit working_directory.
gunicorn change le propriétaire de son socket Unix après le bind, ce qui annule un répertoire set-group-ID. Cédez-le dans un crochet when_ready, avec le privilège file_chown_self.
Se connecter à un socket Unix exige les droits de lecture et d'écriture.
Le pilote SQLite en Go pur n'existe pas pour illumos ; le pilote cgo fonctionne bien.
Dans un Caddyfile, un error de premier niveau s'exécute après handle. Enveloppez plutôt les gestionnaires dans un filtre.
Licence
Unbound RPZ Guard est un logiciel libre sous licence publique générale GNU Affero v3 (AGPL-3.0).
Il repose sur Unbound de NLnet Labs et sur des listes RPZ publiques comme OISD.