Below is a small tutorial on how you can create your own recursive DNS server using Unbound, adding custom records to block ads (plus fakenews, porn and/or social websites), on Apple 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.