You are here because you want to run Pi-Hole and Unbound locally on your own laptop so that you have a portable ‘as secure as can be’ internet browsing experience. This eliminates the need to carry around an external raspberry pi / pi-hole device.

I’m going to outline a solution running on an Ubuntu/Mint linux install and we will,

  • Carry out a native install of Unbound on your laptop.
  • Install Pi-Hole as a docker container.

Have tried a few online docker containers of Unbound but they all failed for me, if this changes I will update this guide to run fully on Docker.

Here are the software versions I used in this guide.

  • Unbound Version 1.13.1
  • Pihole Container Version 2023.01.10
  • Linux Mint 21.1 “Vera”
  • Docker version 20.10.23
  • Docker Desktop 4.16.2

Please note that you can only run one virtualisation technology on your laptop at one time, so if you are running VirtualBox you are going to have to shut it down while using any docker containers. The reverse is true too… if you try and start a virtual machine while running docker you are going to have a bad time.

DNS Leak Protection

As briefly mentioned in one of my previous guides, https://rexbytes.com/2022/09/19/pi-hole-a-portable-docker-laptop-solution/ ,
benefiting from the protection that your Pi-hole give you can and does in most cases expose you to DNS leaks.

When running your VPN and Pi-hole setup go ahead and visit https://dnscheck.tools/ or https://www.dnsleaktest.com/ .

You will find that the DNS provider(s) detected are the ones chosen by you in the Pi-hole admin page, not the DNS server provided by your VPN connection. This isn’t really a fault of the pi-hole project, it’s just that pi-hole isn’t designed to also handle your DNS resolving so it asks for an upstream DNS server instead.

The solution is to be your own DNS resolver and install Unbound.

DNS isn’t a perfect technology and just using it someone, something, somewhere will know which websites you are visiting – this is by design.


You can however limit your exposure and maximize your protection, so why bother installing Unbound?

  • Google, Cloudflare and DNS resolving services as a whole may or may not be creating a profile on your web browsing history to sell to marketing companies. Why not just opt out?
  • These same DNS resolver providers are highly centralised and are a very tempting target for hackers, if in the unlikely event hacker access these central DNS records they may gain enough information to target you.
  • In combination with a VPN tunnel you immediately protect yourself in public wifi connections, cafes, airports, and even unsecured home wifi connections at elderly relative homes. Your DNS request become invisible to the local network.
  • Most VPN providers claim to not log DNS queries.

You might stumble upon the topic of DoH, which is DNS over HTTPS, although this would encrypt your DNS requests you could still be at the mercy of an upstream provider as they would still be able to create an advertising profile on your behaviour. Cloudflare for example allows you to connect using DoH requests.

I personally consider Pi-hole, with Unbound over a VPN provided by a VPN provide based in Switzerland the best choice… it is in my opinion better than running DoH. You could run DoH through your VPN buy that would defeat the purpose of a VPN, if you run DOH without a VPN you are still open to threats on local networks because they can see the ip addresses from traffic… https or not.

If you go through all the above… only to then sign in to facebook and other FANG services… well, they will build a profile of your web activity anyway. Not saying it would be a waste of time setting up all the DNS security, but be aware that a secure connection is only as secure as the data you share with the services you think you can trust.

Order of DNS requests

Once fully setup your DNS request route will look like this.

Web browser (or any other web client) > [port:53] Local Docker Pi-Hole >[port:5335] Local Native Unbound Resolver > [automatic] VPN DNS resolver

For further benefits of using your own Unbound DNS resolver consult their documentation.

Install & Configure Unbound

You know how your pi-hole asks if you want google, cloudflare or any other upstream DNS resolver set as your DNS provider?


Well, why not become your own? This in short is what Unbound enables you to do. For a complete description of the Unbound project please read more at the following location. https://www.nlnetlabs.nl/projects/unbound/about/

https://unbound.docs.nlnetlabs.nl/en/latest/getting-started/installation.html

I am on linux mint, so the install is as easy as,

sudo apt update
sudo apt install unbound

This will install Unbound on your system. We will configure Unbound to play nice with your docker pi-hole.

Setting up Unbound

Let’s setup Unbound.

unbound.conf

This is the bare minimum unbound.conf file.

Pi-hole will already be listening for all local DNS requests on port 53 so in this config we will need to set Unbound to listen for requests inbound from the pi-hole on another port, we will use port 5335.

We want to accept connections from our local loopback address, in the case where we don’t want to use a VPN and so we set an interface to 127.0.0.1@5335 . Mainly this allows you to run a dig command to check the state of Unbound from a local native terminal. More on dig later.

We also want to listen to requests from the Docker default bridge network, which for my non-root user is 172.17.0.1@5335

Download a copy of the latest root hints file, https://www.iana.org/domains/root/files , and save it at the following file on your host system, /var/lib/unbound/root.hints You are going to be sending DNS queries direct to the source.

Copy the following text to ‘/etc/unbound/unbound.conf‘ , backup the origional unbound.conf that came with the fresh install if you wish.

server:
    # If no logfile is specified, syslog is used
    # logfile: "/var/log/unbound/unbound.log"
    verbosity: 1
    interface: 127.0.0.1@5335
    interface: 172.17.0.1@5335
    do-ip4: yes
    do-udp: yes
    do-tcp: yes

    # May be set to yes if you have IPv6 connectivity
    do-ip6: no

    # You want to leave this to no unless you have *native* IPv6. With 6to4 and
    # Terredo tunnels your web browser should favor IPv4 for the same reasons
    prefer-ip6: no

    # Use this only when you downloaded the list of primary root servers!
    # If you use the default dns-root-data package, unbound will find it automatically
    root-hints: "/var/lib/unbound/root.hints"

    # Trust glue only if it is within the server's authority
    harden-glue: yes

    # Require DNSSEC data for trust-anchored zones, if such data is absent, the zone becomes BOGUS
    harden-dnssec-stripped: yes

    # Don't use Capitalization randomization as it known to cause DNSSEC issues sometimes
    # see https://discourse.pi-hole.net/t/unbound-stubby-or-dnscrypt-proxy/9378 for further details
    use-caps-for-id: no

    # Reduce EDNS reassembly buffer size.
    # IP fragmentation is unreliable on the Internet today, and can cause
    # transmission failures when large DNS messages are sent via UDP. Even
    # when fragmentation does work, it may not be secure; it is theoretically
    # possible to spoof parts of a fragmented DNS message, without easy
    # detection at the receiving end. Recently, there was an excellent study
    # >>> Defragmenting DNS - Determining the optimal maximum UDP response size for DNS <<<
    # by Axel Koolhaas, and Tjeerd Slokker (https://indico.dns-oarc.net/event/36/contributions/776/)
    # in collaboration with NLnet Labs explored DNS using real world data from the
    # the RIPE Atlas probes and the researchers suggested different values for
    # IPv4 and IPv6 and in different scenarios. They advise that servers should
    # be configured to limit DNS messages sent over UDP to a size that will not
    # trigger fragmentation on typical network links. DNS servers can switch
    # from UDP to TCP when a DNS response is too big to fit in this limited
    # buffer size. This value has also been suggested in DNS Flag Day 2020.
    edns-buffer-size: 1232

    # Perform prefetching of close to expired message cache entries
    # This only applies to domains that have been frequently queried
    prefetch: yes

    # One thread should be sufficient, can be increased on beefy machines. In reality for most users running on small networks or on a single machine, it should be unnecessary to seek performance enhancement by increasing num-threads above 1.
    num-threads: 1

    # Ensure kernel buffer is large enough to not lose messages in traffic spikes
    # so-rcvbuf: 1m

    # Ensure privacy of local IP ranges
    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    #This is the docker root default bridge network.
    private-address: 172.16.0.0/12
    #This is my non-root docker user bridge network.
    private-address: 172.17.0.0/12
    private-address: 10.0.0.0/8
    private-address: fd00::/8
    private-address: fe80::/10

    remote-control:
        #Yea, this says "remote" , but setting the interface ip address
        #to local loopback enables you to check the Unbound cache and
        #run other checks.
	control-enable: yes
        control-interface: 127.0.0.1
        control-port: 8953

systemctl systemd unbound.service

I’m going to quote an error you will get if you don’t setup unbound.service to wait for docker to start,

error: can’t bind socket: Cannot assign requested address for 172.17.0.1 port 5335

I hope people searching online for help with this common issue will find their way here.

In short, Unbound is looking for a network that doesn’t exist – yet.

Since we are running pihole in a docker container we need to make sure docker desktop is always up and running before your local install of unbound otherwise unbound will not be able to bind to 172.17.0.0 , this means unbound will ALWAYS fail on startup and you will have to manually start it by using ‘systemctl start unbound‘ , to avoid this problem we are going to edit the unbound service file.

As root, open ‘/lib/systemd/system/unbound.service‘ and add ‘After=docker.service‘ in the same place I have.

[Unit]
Description=Unbound DNS server
Documentation=man:unbound(8)
After=network.target
After=docker.service
Before=nss-lookup.target
Wants=nss-lookup.target

[Service]
Type=notify
Restart=on-failure
EnvironmentFile=-/etc/default/unbound
ExecStartPre=-/usr/lib/unbound/package-helper chroot_setup
ExecStartPre=-/usr/lib/unbound/package-helper root_trust_anchor_update
ExecStart=/usr/sbin/unbound -d -p $DAEMON_OPTS
ExecStopPost=-/usr/lib/unbound/package-helper chroot_teardown
ExecReload=+/bin/kill -HUP $MAINPID

[Install]
WantedBy=multi-user.target

Unbound will now patiently wait in line for docker to start, if docker fails to start, unbound will probably attempt to start anyway as systemd After and Before ordering is only a suggestion. Keep this in mind when troubleshooting.

Configure Pi-hole

I am assuming you have followed the linux instructions on setting up pihole on your laptop, which can be found here…

Make sure you also pay attention to the port53 conflict in the above linked guide.

We need to modify the docker-compose.yml file to enable our pi-hole ( which is running in a container, so is in an internal docker default bridge network) to make requests to your host computer which is running Unbound natively.

To allow communication between the docker bridge network and services on your host we use a ‘magic ip-address’, this ip address is activated by adding the extra_hosts option with the value ‘host.docker.internal:host-gateway‘. The ip address 192.168.65.2 can now be referenced by any container in mentioned in your docker-compose.yml file to contact your host. This ip-address is internal to docker and is a constant.

version: "3"

services:
  pihole:
    container_name: pihole
    image: pihole/pihole:2023.01.10
    extra_hosts:
      - "host.docker.internal:host-gateway"
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "80:80/tcp"
    environment:
      TZ: 'America/Chicago'
      WEBPASSWORD: 'choose a password'
      WEB_UID: 999
      PIHOLE_DNS_: 192.168.65.2#5335
      DNSSEC: 'true'
      DNSMASQ_LISTENING: local
    volumes:
      - './etc-pihole:/etc/pihole'
      - './etc-dnsmasq.d:/etc/dnsmasq.d'
    restart: unless-stopped

Notice the pihole environment variables we set.

WEB_UID: 999 is the docker container workaround we mentioned in the setup guide to enable writting to the pihole database, for when you want to add addlists and black and white lists.

PIHOLE_DNS_: takes advantage of dockers magic ip address and routes all your DNS requests to port 5335 on your local host.
This enables your dockerized pihole to talk with your host install of Unbound.

Graphically inside the pihole admin page it will look like this,

Notice, when you check your stats inside pi-hole you will see ‘host.docker.internal#5335‘ but if you try using that string as a custom DNS server in the above settings section pihole will reject it… strange. Hmmm. Stick with ‘192.168.65.2#5335

DNSMASQ_LISTENING: local, this will only allow local DNS requests – requests from your laptop.

DNSSEC: ‘true’, will enable validation of DNS replies and cache DNSSEC data.

Network Settings

I set my wifi and vpn networks DNS to the local server ip address, the following images look the same… but one is a screen shot of my wifi settings, the other from my VPN. Both direct DNS queries to the containerized pi-hole listening on the default DNS port 53.

Dig for DNS

Start up your docker container and lets do some dig queries,

DNS query from the host computer

ubuntu@goodboy:~$ dig google.com @127.0.0.1 -p 5335

; <<>> DiG 9.18.1-1ubuntu1.2-Ubuntu <<>> google.com @127.0.0.1 -p 5335
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5766
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;google.com.			IN	A

;; ANSWER SECTION:
google.com.		300	IN	A	172.217.169.46

;; Query time: 463 msec
;; SERVER: 127.0.0.1#5335(127.0.0.1) (UDP)
;; WHEN: Sun Jan 29 16:31:34 CET 2023
;; MSG SIZE  rcvd: 55

DNS query from inside the pi-hole

Enter your pihole container,

ubuntu@goodboy:~$ docker container exec -it pihole bash

Remember we are going to have to use the magic ip address to route our dig query to host local loopback.

root@38843fae6f36:/# dig google @192.168.65.2 -p 5335

; <<>> DiG 9.16.33-Debian <<>> google @192.168.65.2 -p 5335
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 48392
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;google.				IN	A

;; AUTHORITY SECTION:
google.			900	IN	SOA	ns-tld1.charlestonroadregistry.com. cloud-dns-hostmaster.google.com. 1 21600 3600 259200 900

;; Query time: 152 msec
;; SERVER: 192.168.65.2#5335(192.168.65.2)
;; WHEN: Sun Jan 29 09:44:28 CST 2023
;; MSG SIZE  rcvd: 133

DNS leak test

Try this yourself, visit,

Unbound Issues

Problem: Unbound stopped resolving DNS queries intermittently

I have noticed that if you leave your computer on for a long period Unbound stops resolving, which also causes secondary issues such as a VPN connection being terminated… unusual.

I’ve not found a solid fix except for the following.

Restart your Unbound using,

ubuntu@goodboy:~$ sudo systemctl restart unbound

this will make sure unbound resumes resolving DNS queries.

This though is inconvenient if you are using your machine to provide this service to a household. Instead add it to your root crontab, my choice is to run this restart command every half hour.

root@goodboy:~# crontab -e

Then add the following line.

*/30 * * * * /usr/bin/systemctl restart unbound

Here is what my full crontab looks like,

# Edit this file to introduce tasks to be run by cron.
# 
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
# 
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
# 
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
# 
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
# 
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
# 
# For more information see the manual pages of crontab(5) and cron(8)
# 
# m h  dom mon dow   command
# Unbound seems to stop resolving after some time, so let's restart the service every 30 mins.

*/30 * * * * /usr/bin/systemctl restart unbound

This solution works for me, Unbound has been serving DNS queries successfully for a long period of time.

If you know of any other way other than my workaround please let me know in the comments.

Thanks.

Problem: AppArmor denies Unbound process form opening log file.

Now this one is a weird one… upgrade after upgrade on linux mint I have had no issues.
The most recent upgrade Unbound services don’t start, and the first you know about it is
that you are not able to visit webpages.

For a quick fix you can run,

ubuntu@goodboy:~$ sudo systemctl restart unbound

but this gets really tiring.

The unbound log is at /var/log/unbound/unbound.log

Let’s open the apparmor file for unbound.

ubuntu@goodboy:~$ sudo nano /etc/apparmor.d/usr.sbin.unbound

It looks like there is no entry for the log file!

# Author: Simon Deziel
# vim:syntax=apparmor
#include <tunables/global>

/usr/sbin/unbound flags=(attach_disconnected) {
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/openssl>

# chown the PID/Unix control socket
capability chown,
# chmod the Unix control socket
capability fowner,
capability fsetid,

# added to abstractions/nameservices in Apparmor 2.12
/var/lib/sss/mc/initgroups r,

capability net_bind_service,
capability setgid,
capability setuid,
capability sys_chroot,
capability sys_resource,

# root hints from dns-data-root
/usr/share/dns/root.* r,

# non-chrooted paths
/etc/unbound/** r,
owner /etc/unbound/*.key* rw,
audit deny /etc/unbound/unbound_control.{key,pem} rw,
audit deny /etc/unbound/unbound_server.key w,

# chrooted paths
/var/lib/unbound/** r,
owner /var/lib/unbound/** rw,
audit deny /var/lib/unbound/**/unbound_control.{key,pem} rw,
audit deny /var/lib/unbound/**/unbound_server.key w,

/usr/sbin/unbound mr,

/{,var/}run/systemd/notify w,
/{,var/}run/{unbound/,}unbound.pid rw,

# Unix control socket
/{,var/}run/unbound.ctl rw,

#include <local/usr.sbin.unbound>
}

Just add the following in your apparmor file. Note the log directory for your system.

# Allow Unbound to create the log file
/var/log/unbound/unbound.log rw,
/var/log/unbound/* rw,

# Allow Unbound to read the log file
/var/log/unbound/unbound.log r,

Reload the AppArmor profiles using,

sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.unbound

VPN KillSwitch Software

If you are using the following software I have made available,

this setup can still be used with the VPN killswitch option as it is tun+ interface based.

If you want the –protect setting without using a vpn I have created the –punboundpi switch, which will run your laptop in protect mode and take in to account unbound running on port 5335 and pi running locally on port 53.

For example, if you are connecting to a VPN router you may just want normal related/established network protection.

Enjoy!

-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-N DOCKER
-N DOCKER-ISOLATION-STAGE-1
-N DOCKER-ISOLATION-STAGE-2
-N DOCKER-USER
-N RB_I_RELATED_AND_ESTABLISHED
-N RB_O_RELATED_AND_ESTABLISHED
-A INPUT -j RB_I_RELATED_AND_ESTABLISHED
-A INPUT -m state --state INVALID -j DROP
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A FORWARD -m state --state INVALID -j DROP
-A OUTPUT -j RB_O_RELATED_AND_ESTABLISHED
-A OUTPUT -m state --state INVALID -j DROP
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN
-A RB_I_RELATED_AND_ESTABLISHED -p udp -m udp --dport 5335 -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT
-A RB_I_RELATED_AND_ESTABLISHED -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A RB_I_RELATED_AND_ESTABLISHED -i lo -p udp -m udp --dport 53 -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT
-A RB_I_RELATED_AND_ESTABLISHED -p tcp -m conntrack --ctstate NEW,ESTABLISHED -m multiport --dports 22,80,443 -j ACCEPT
-A RB_O_RELATED_AND_ESTABLISHED -p udp -m udp --sport 5335 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A RB_O_RELATED_AND_ESTABLISHED -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT
-A RB_O_RELATED_AND_ESTABLISHED -i lo -p udp -m udp --sport 53 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A RB_O_RELATED_AND_ESTABLISHED -p tcp -m conntrack --ctstate NEW,RELATED,ESTABLISHED -m multiport --sports 22,80,443 -j ACCEPT
2 thoughts on “Portable Laptop Pi-Hole and Unbound DNS resolver”

Leave a Reply