Below is a small tutorial on how you can create your own recursive DNS server using Unbound, adding custom records to block ads (and fakenews, porn or social websites), on macOS. Also, you can use DNS over TLS if needed/wanted.

Installation

Unbound is already in Homebrew so installing it is just a matter of running:

$ brew install unbound

Configuration

Find a free unique id for the unbound user in the 1-500 range (reserved for system accounts). For example, 444.

$ dscl . -list /Groups PrimaryGroupID | grep 444
$ dscl . -list /Users PrimaryGroupID | grep 444

If you have no output for those two commands, you can proceed to actually create the user and group.

$ sudo dscl . -create /Groups/_unbound
$ sudo dscl . -create /Groups/_unbound PrimaryGroupID 444
$ sudo dscl . -create /Users/_unbound
$ sudo dscl . -create /Users/_unbound RecordName _unbound unbound
$ sudo dscl . -create /Users/_unbound RealName "Unbound DNS server"
$ sudo dscl . -create /Users/_unbound UniqueID 444
$ sudo dscl . -create /Users/_unbound PrimaryGroupID 444
$ sudo dscl . -create /Users/_unbound UserShell /usr/bin/false
$ sudo dscl . -create /Users/_unbound Password '*'
$ sudo dscl . -create /Groups/_unbound GroupMembership _unbound

Fetch the root key required for DNSSEC validation:

$ sudo unbound-anchor -a /usr/local/etc/unbound/root.key

Create the certificates needed:

$ sudo unbound-control-setup -d /usr/local/etc/unbound

Here is a single-line command that will download the StevenBlack hosts list (fakenews + gambling + porn + social, so keep that in mind), convert it for unbound and save it in /usr/local/etc/unbound/zone-block-general.conf. Unbound will respond with NXDOMAIN to all the domains in this list.

$ (curl --silent https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn-social/hosts | grep '^0\.0\.0\.0' | sort) | awk '{print "local-zone: \""$2"\" refuse"}' > /usr/local/etc/unbound/zone-block-general.conf

Let’s configure unbound as an authoritative, validating, recursive caching DNS server. The lines below go to your /usr/local/etc/unbound/unbound.conf:

server:
	# log verbosity
	verbosity: 1
	interface: 127.0.0.1
	access-control: 127.0.0.1/8 allow
	chroot: ""
	username: "_unbound"
	auto-trust-anchor-file: "/usr/local/etc/unbound/root.key"
	# answer DNS queries on this port
	port: 53
	# enable IPV4
	do-ip4: yes
	# disable IPV6
	do-ip6: no
	# enable UDP
	do-udp: yes
	# enable TCP, you could disable this if not needed, UDP is quicker
	do-tcp: yes
	# which client IPs are allowed to make (recursive) queries to this server
	access-control: 10.0.0.0/8 allow
	access-control: 127.0.0.0/8 allow
	access-control: 192.168.0.0/16 allow
	root-hints: "/usr/local/etc/unbound/root.hints"
	# do not answer id.server and hostname.bind queries
	hide-identity: yes
	# do not answer version.server and version.bind queries
	hide-version: yes
	# will trust glue only if it is within the servers authority
	harden-glue: yes
	# require DNSSEC data for trust-anchored zones, if such data
	# is absent, the zone becomes  bogus
	harden-dnssec-stripped: yes
	# use 0x20-encoded random bits in the query to foil spoof attempts
	use-caps-for-id: yes
	# the time to live (TTL) value lower bound, in seconds
	cache-min-ttl: 3600
	# the time to live (TTL) value cap for RRsets and messages in the cache
	cache-max-ttl: 86400
	# perform prefetching of close to expired message cache entries
	prefetch: yes
	num-threads: 4
	msg-cache-slabs: 8
	rrset-cache-slabs: 8
	infra-cache-slabs: 8
	key-cache-slabs: 8
	rrset-cache-size: 256m
	msg-cache-size: 128m
	so-rcvbuf: 1m
	private-address: 192.168.0.0/16
	private-address: 172.16.0.0/12
	private-address: 10.0.0.0/8
	private-domain: "home.lan"
	unwanted-reply-threshold: 10000
	val-clean-additional: yes
	# additional blocklist (Steven Black hosts file, read above)
	include: /usr/local/etc/unbound/zone-block-general.conf
remote-control:
	control-enable: yes
	control-interface: 127.0.0.1
	server-key-file: "/usr/local/etc/unbound/unbound_server.key"
	server-cert-file: "/usr/local/etc/unbound/unbound_server.pem"
	control-key-file: "/usr/local/etc/unbound/unbound_control.key"
	control-cert-file: "/usr/local/etc/unbound/unbound_control.pem"

If you want to use DNS over TLS, you can forward requests to a TLS-capable recursive server, for example Cloudflare (1.1.1.1) or Quad9 (9.9.9.9). Add the lines below to your /usr/local/etc/unbound.conf:

forward-zone:
	name:"."
	# use Quad9
	forward-addr:9.9.9.9@853
	# or Cloudflare
	# forward-addr:1.1.1.1@853
	forward-ssl-upstream:yes

The unbound process needs read and write permissions for the configuration directory, use staff as group so the user can use unbound-control:

$ sudo chown -R _unbound:staff /usr/local/etc/unbound
$ sudo chmod 640 /usr/local/etc/unbound/*

If you want to start unbound at boot, you need to create the /Library/LaunchDaemons/net.unbound.plist file and place those lines in it:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
	<dict>
		<key>Label</key>
		<string>net.unbound</string>
		<key>ProgramArguments</key>
		<array>
			<string>/usr/local/sbin/unbound</string>
			<string>-d</string>
			<string>-c</string>
			<string>/usr/local/etc/unbound/unbound.conf</string>
		</array>
		<key>KeepAlive</key>
		<true/>
		<key>RunAtLoad</key>
		<true/>
	</dict>
</plist>

Start the Unbound daemon:

$ sudo launchctl load /Library/LaunchDaemons/net.unbound.plist

Stop (when needed) the Unbound daemon:

$ sudo launchctl unload /Library/LaunchDaemons/net.unbound.plist

Set your local DNS server as default for the Wi-Fi connection:

$ networksetup -setdnsservers Wi-Fi 127.0.0.1

Check if DNS was set and everything is ok:

$ networksetup -getdnsservers Wi-Fi
127.0.0.1

Testing DNSSEC for your new DNS resolver is easy, using dig:

$ dig org. SOA +dnssec @127.0.0.1                                                         
; <<>> DiG 9.10.6 <<>> org. SOA +dnssec @127.0.0.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 8381
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 7, ADDITIONAL: 1
...

The ad flag is short for Authenticated Data and means DNSSEC is working.