Unbound RPZ Guard DIPDAP home

Unbound RPZ Guard

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.

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

Architecture Filtered and unfiltered clients query Unbound. Unbound streams every answer over dnstap to a Go collector that writes SQLite. A Flask web app behind Caddy with a WAF reads SQLite, manages the RPZ lists and controls Unbound. Filtered clients(e.g. the home LAN) Unfiltered clients(servers, localhost) Unboundrecursion · DNSSECtagged RPZ policy · local zone DNS root, TLDsauthoritative servers dnstap collector (Go)classify · attribute · batch SQLite (WAL)query log · lists · rules Web UI (Flask)gunicorn · Unix socket Caddy + WAFTLS · OWASP CRS dnstap reads controls
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

Web security

Built with

PartTechnology
PlatformSmartOS native zone, pkgsrc 2025Q4, SMF
ResolverUnbound 1.24 (built with dnstap)
TelemetryGo: golang-dnstap, miekg/dns, SQLite
Web appPython 3.14, Flask, gunicorn, argon2
Front endCaddy 2.11 with Coraza WAF + OWASP CRS v4
Packagingsigned 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:

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.

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

Architecture Les clients filtrés et non filtrés interrogent Unbound. Unbound envoie chaque réponse par dnstap à un collecteur Go qui écrit dans SQLite. Une application web Flask derrière Caddy et un pare-feu applicatif lit SQLite, gère les listes RPZ et pilote Unbound. Clients filtrés(p. ex. le réseau maison) Clients non filtrés(serveurs, localhost) Unboundrécursion · DNSSECpolitique RPZ · zone locale Racine, TLDserveurs faisant autorité Collecteur dnstap (Go)classe · attribue · regroupe SQLite (WAL)journal · listes · règles Interface web (Flask)gunicorn · socket Unix Caddy + WAFTLS · OWASP CRS dnstap lit pilote
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

Sécurité web

Technologies

PartieTechnologie
PlateformeZone native SmartOS, pkgsrc 2025Q4, SMF
RésolveurUnbound 1.24 (compilé avec dnstap)
TélémétrieGo : golang-dnstap, miekg/dns, SQLite
Application webPython 3.14, Flask, gunicorn, argon2
FaçadeCaddy 2.11 avec pare-feu Coraza + OWASP CRS v4
Paquetspaquets 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 :

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.