I recently started using Unbound and wanted to experiment with the RPZ feature, only alluded to once on this forum as far as I can tell.
Install unbound-daemon, unbound-control-setup and unbound-control from opkg or apk, depending on your OpenWrt release.
opkg install unbound-daemon unbound-control-setup unbound-control
apk add unbound-daemon unbound-control-setup unbound-control
Configure Unbound to your liking, referring to the README on Github. Then, come back and setup RPZs for ad-blocking.
Create your local RPZ file for a manual allowlist and manual blocklist.
cat <<'EOF' >/etc/unbound/rpz-local.conf
$TTL 3600
@ SOA localhost. root.localhost. 1774045021 14400 3600 86400 3600
NS localhost.
;
; Domains to be allowed will use "CNAME rpz-passthru."
browser-intake-datadoghq.com CNAME rpz-passthru.
*.browser-intake-datadoghq.com CNAME rpz-passthru.
; Domains to be blocked will use "CNAME ."
ads.example.com CNAME .
*.ads.example.com CNAME .
EOF
In this example, 1774045021 is the serial number that Unbound will use to recognize an update is available for the RPZ zone. When you modify this file in the future, update this value with the current epoch time (e.g. date +%s), or manually increment the number using your own scheme.
Update /etc/unbound/unbound_ext.conf to define these RPZs to Unbound.
##############################################################################
# Extended user clauses added to the end of the UCI generated 'unbound.conf'
#
# Put your own forward:, view:, stub:, or remote-control: clauses here. This
# file is appended to the end of 'unbound.conf' with an include: statement.
# Notice that it is not part of the server: clause. Use 'unbound_srv.conf' to
# place custom option statements in the server: clause.
##############################################################################
# Local rpz file will override subsequent RPZs due to earlier positioning in this
# config file.
rpz:
name: rpz-local
zonefile: rpz-local.conf
# Hagezi example using HTTPS transfer. See other Hagezi lists at:
# https://github.com/hagezi/dns-blocklists/tree/main/rpz
rpz:
name: rpz-hagezi-multi-light
zonefile: hagezi-multi-light.rpz
url: https://raw.githubusercontent.com/hagezi/dns-blocklists/refs/heads/main/rpz/light.txt
# rpz-log: yes
# rpz-log-name: hagezi-light
# IPFire DBL example using AXFR/IXFR. See all available IPFire blocklists at:
# https://www.ipfire.org/dbl/how-to-use
rpz:
name: malware.rpz.ipfire.org
primary: xfr.dbl.ipfire.org
zonefile: malware.rpz.ipfire.org.zone
# rpz-log: yes
# rpz-log-name: ipfire-malware
When the unbound service starts, it will copy all *.conf files from /etc/unbound/ to the chroot directory /var/lib/unbound/, which is why the rpz-local file is suffixed with .conf instead of .rpz.
service unbound start
Check if unbound started successfully.
logread -e unbound
Check the rpz definitions are visible in the /var/lib/unbound/ directory.
cat /var/lib/unbound/unbound_ext.conf
ls -l /var/lib/unbound/*rpz*
Use unbound-control to view the rpz zones are loaded.
unbound-control list_auth_zones
If you add a domain to the rpz-local.conf file, you must force a reload of local zone file with:
unbound-control auth_zone_reload rpz-local
N.B. Because of the chroot, you must edit the "live" file in chroot /var/lib/unbound/rpz-local.conf and /etc/unbound/rpz-local.conf (for persistence). Edit in one location, and cp to the other.
Unbound will periodically refresh the remote RPZ zones based on the refresh interval defined in the zone files. For Hagezi, this is every 12 hours. For IPFire, it is every hour (3600 seconds). There is rarely any need to restart Unbound to update a blocklist, unless you are adding or removing an rpz in the unbound_ext.conf file.
If you want to force a check for update of an AXFR/IXFR rpz (i.e. IPFire DBL), you can run:
unbound-control auth_zone_transfer malware.rpz.ipfire.org
If you want to pause DNS blocking, you can disable an rpz temporarily:
unbound-control rpz_disable rpz-hagezi-multi-light
Re-enable with:
unbound-control rpz_enable rpz-hagezi-multi-light
If you wish to enable logging of hits from the blocklists, you can uncomment the rpz-log: lines from the unbound_ext.conf file. By default, the logs will go to the system log so you may wish to define a separate log file on a USB drive for persistent storage and later review. Refer to the unbound.conf man pages for how to use the logfile: option, added to /etc/unbound/unbound_srv.conf. Logging isn't covered in this post.
In summary, I like this solution for the simple fact that the blocklists can be updated without having to restart the DNS server and disrupt the existing cache. There are also no cron jobs to manage periodic list updates. Reporting can be implemented through careful activation of the rpz-log feature.
You are invited to try it out and see if it works well for you. Unbound isn't for everyone, and the RAM required could be too much for some older routers. You can have Unbound as a recursive resolver, plain forwarder, or DNS-over-TLS forwarder. RPZs will work in all those modes:
- Block ads and malware locally, and perform recursive lookups to root and authoritative DNS servers using unencrypted port 53.
- Block ads locally, and forward other queries to Quad9 or Cloudflare Security for malware protection using DoT port 853.
Feedback welcome.