Setting Up a FreeBSD DNS Adblocker

I've come across a few sites that will instruct you with how to setup an adblock dns server on FreeBSD. Unfortunately, many of them are either out of date, over complicated, request referral clicks, or aren't whole. Now, I'm not saying that mine will be any better, but, here we go.

The DNS service I help run use both Ubuntu with Bind9 and FreeBSD with Unbound. In this post, we will talk about setting up a FreeBSD-based DNS adblocker running Unbound.

UPDATE: If you're interested in adding DNSCrypt to this server, I've added a post on setting up DNSCrypt with this Unbound configuration:

Why FreeBSD?

FreeBSD is generally known to have a better network stack, separation between the base and ports, fine grained update control, use of ZFS, and no systemd (trolling). There are thousands of posts out there that are about "Why FreeBSD?" This post is about setting up an adblocker..let's move on.

Specs/Our Setup

In this post, we're going to be using a FreeBSD droplet on Digital Ocean.

  • Digital Ocean Node:
    • $5/month droplet
    • FreeBSD 11 zfs
    • IPv6 Enabled (optional - used with our service)

Note: You will need to have an SSH key setup!

All you need now is a few minutes of free time.

Initial Setup/Portsnap

Once we've got a droplet running, we'll want to do a few things:

Ensure FreeBSD is updated:

sudo freebsd-update fetch install

To install Unbound, we're going to use portsnap. Before we can do that, however, we'll need to extract portsnap and make sure it's updated.

Extract Portsnap:

portsnap extract

Update Portsnap:

portsnap fetch update

Setting Up Unbound

Now that we've updated FreeBSD and setup portsnap, we're ready to finally ready start configuring our server! While we are using porsnap, you are free to use something like pkg_add (sudo pkg_add -i unbound).

Install wget (optionally, you can use the fetch command; this will be used for getting root hints later):

cd /usr/ports/ftp/wget

sudo make install clean


Now that wget has been installed, it's time to install Unbound:

cd /usr/ports/dns/unbound

sudo make install clean


NOTE: After installing Unbound, I restarted my droplet..just in case.

Now we will want to start the Unbound service to ensure that all files are created/things are working:

sudo service unbound start

Kill the service:

sudo service unbound stop

Configuring Unbound

Awesome. We have now installed Unbound! Next step is to configure the server. We'll want to cd into /usr/local/etc/unbound. During the install I elected to not install any example files. Either way, I started with a blank unbound.conf file.

First off, let's start by adding root hints. This file will contain the current root DNS servers.

wget ftp://FTP.INTERNIC.NET/domain/named.cache -O /usr/local/etc/unbound/root.hints

Now that we've downloaded the root hints file, it's time to configure Unbound. Below is an example configuration that I've made (read the comments):

More on the first two includes later...

## Unbound config file
    # Zone file
    include: /usr/local/etc/unbound/
    # List of valid clients 
    include: /usr/local/etc/unbound/users.conf
    # Verbosity to zero - we don't log
    verbosity: 0
    use-syslog: no
    # Specify interfaces
    interface: ::0
    # Our root hints file
    root-hints: /usr/local/etc/unbound/root.hints

    # Hide/block identity and version
    hide-identity: yes
    hide-version: yes
    # Trust glue only if it is within the servers authority.
    harden-glue: yes
    # Require DNSSEC data for trust-anchored zones
    harden-dnssec-stripped: yes
    # Use 0x20-encoded random bits in the query to help prevent spoofs
    use-caps-for-id: yes
    # Specify caching TTLs
    cache-min-ttl: 3600
    cache-max-ttl: 86400
    # Perform prefetching of close to expired message cache entries.
    prefetch: yes

    # Do not allow localhost to use the forwarder
    do-not-query-localhost: yes
    # Specify servers for forwarding to

I wrote this config late at night. Let me know if I'm missing something.

Zones and Access Controls

Remember those first two include statements in our config file? The first include that we had is our zone file/the file that will specify a list of ad networks which will also specify what actions to take with them. The second file will specify a list of allowed clients/IPs that are allowed to query our DNS server. By default, Unbound denies any requests except for localhost

Our configuration is going to simply do an inform_deny operation on queries that match anything in our zone file. Where can you get a list of ad networks?

Great, we have hosts...Now what? You'll want to ensure that you've properly formatted this list. This looks like:

local-zone: "" inform_deny  

Save the file a and you are done with zones.

Since we have now created the zone file which will specify domains to block, we will now setup our accepted client/IP list. Create a file called users.conf. Similarly to ACLs in BIND, we will specify the IP with the corresponding CIDR value. With Unbound, this setting is added with an "allow" option at the end.

Example allowed IP in users.conf:

access-control: 123.123.123./24 allow  

Save the file and that's it!


Final step on the server is to start the service.

sudo service unbound start

Now that we're up and running, perform a query from an allowed client and check to make sure we're resolving. If you need to query via localhost on the server for testing, enable do-not-query-localhost in the unbound.conf file.

protoxin :: ~ » dig @OURSERVERIP

; <<>> DiG 9.8.3-P1 <<>> @SERVERIP
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16940
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;            IN  A

;; ANSWER SECTION:        3600    IN  A

;; Query time: 121 msec
;; WHEN: Sun Feb 13 02:41:31 2017
;; MSG SIZE  rcvd: 44

protoxin :: ~ »

Look at that, we are resolving!

Zone Options

It is important to note that in this setup, we are simply denying the request if it contains a specified zone. While using this option is fine, there is another option that can be pursued. Should you be interested in having something similar to the Pi-Hole project, you can change the zone settings from:

local-zone: "" inform_deny  


local-zone: " A OUR.SERVER.IP"  

Keep in mind that by doing this, you will most likely want to setup something like lighttpd. This is easy to setup on either the same server that is running the DNS service, or a completely separate droplet.

Until next time.


Join EFF!