OpenBSD Home Server - Blocklist Set Up 07 Sep 2020 ------------------------------------------------------------------------ Before getting started with blocklists, I should mention something that I'd forgotten in the last article. The DHCP server on AT&T's gateway sets the nameserver in /etc/resolve.conf to itself. If you want to use unbound on the OpenBSD server, you can set up dhclient to ignore the setting from the gateway by adding this line to /etc/dhclient.conf: supersede domain-name-server 127.0.0.1; Then run dhclient to apply the changes. Now on to domain blocking. Preventing a domain name from being resolved by unbound is simple. Just add a local-zone for that domain to the main "server" section in the config file, with "always_nxdomain" as the type. For example: server: ... local-zone: "example.com" always_nxdomain ... Then every DNS query for "example.com" will return NXDOMAIN, indicating that the domain doesn't exist. If you have a lot of domains to block, then it makes sense to put all of the local-zone declarations into their own file, and include it in the main unbound.conf: server: ... include: /var/unbound/etc/blocklist.conf ... The next task is to populate blocklist.conf with known advertising and malware servers. A good source of those domain lists is Steven Black's hosts file[0]. However, it's not quite in the right format. Besides comments, each line looks like this: 0.0.0.0 bad-domain.name If that line appears in /etc/hosts, then the local DNS resolver will return '0.0.0.0' as the A record for bad-domain.name, which effectively blocks that domain for local users. However, we want Unbound to block the domain for every client on the local network. So we need a bit of scripting to transform the hosts line into an unbound.conf local-zone declaration. Awk is a good tool for this. 1: awk '{ OFS = "" } 2: NF == 2 && $1 == "0.0.0.0" && $2 != "0.0.0.0" { 3: print "local-zone: \"", $2, "\" always_nxdomain" 4: }' hosts > blocklist.conf Line 1: Invoke Awk, and set the output field separator to "". That way the print command won't add unwanted spaces. Line 2: Match lines with 2 white-space separated fields, where the first field is "0.0.0.0". That omits blank lines and comments in the hosts file. Also make sure the second field isn't "0.0.0.0", because when I was testing this out that line seemed to cause some issues. Line 3: Print a local-zone declaration for the domain name that was in the second field. Line 4: End the awk script, pass in the hosts file, and write the output to blocklist.conf. The hosts file gets updated somewhat frequently, so it'd be convenient if we could automate downloading and deploying new versions on a regular basis. To do that, we need a script, a non-priviledged user to run that script, and a cron job to run it every night. This approach is very similar to the one used by Jordan Geoghegan's unbound-adblock[1], but somewhat simplified. First, create the user, and an initial (empty) blocklist with the right permissions: adduser -s /sbin/nologin _adblock install -m 644 -o _adblock -g wheel /dev/null \ /var/unbound/etc/blocklist.conf Next, we need a script: --- update-adblock.sh #!/bin/sh BLOCKLIST_URL='https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts' TMPFILE=$(mktemp) OUTFILE=/var/unbound/etc/blocklist.conf cleanup() { rm -f "$TMPFILE" } quit() { cleanup && exit } abort() { [ -n "${1}"] && echo "${1}" 1>&2 cleanup exit 1 } trap quit EXIT trap quit INT # Download the blocklist to a temporary file. ftp -o - "$BLOCKLIST_URL" > "$TMPFILE" || abort "Couldn't download blocklist." # Convert all '0.0.0.0' host lines into local-zone sections. awk '{ OFS = "" } NF == 2 && $1 == "0.0.0.0" && $2 != "0.0.0.0" { print "local-zone: \"", $2, "\" always_nxdomain" }' "$TMPFILE" > "$OUTFILE" || abort "Couldn't create $OUTFILE." doas rcctl restart unbound cleanup ... Install the script in /usr/local/bin: install -m 755 -o root -g bin update-adblock.sh \ /usr/local/bin/update-adblock.sh The last thing the script does before cleaning up temporary files is restarting unbound so the changes take effect. For that to work, we need to add the following line to /etc/doas.conf: permit nopass _adblock cmd rcctl args restart unbound The line gives the _adblock user permission to run rcctl with exactly the arguments listed. To test that everything is working as expected, try runnning the script as the _adblock user: doas -u _adblock /bin/sh /user/local/bin/update-adblock.sh If all goes well, you should have a /var/unbound/etc/blocklist.conf file with ~56k lines, and trying to resolve one of the blocked domains should return a status of NXDOMAIN. dig @127.0.0.1 zmedia.com ; <<>> dig 9.10.8-P1 <<>> zmedia.com ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 47040 ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;zmedia.com. IN A ;; Query time: 0 msec ;; SERVER: 127.0.0.1#53(127.0.0.1) ;; WHEN: Mon Sep 07 12:29:48 CDT 2020 ;; MSG SIZE rcvd: 39 If that doesn't work, make sure you're including blocklist.conf in the server section of unbound.conf, as mentioned way back at the beginning of this article. Now to automate updates, edit the _adblock user's cronttab: crontab -u _adblock -e And add this line: @midnight /bin/sh /usr/local/bin/update-adblock.sh Now new versions of the blocklist should be generated every night at midnight. [0]: https://github.com/StevenBlack/hosts [1]: https://www.geoghegan.ca/unbound-adblock.html