Raspberry Pi - Pi-Hole and NxF combination. Home DNS and Ads blocking

For this tutorial I'm using Raspberry Pi 4B. I do not explicitly explain how to image the Pi or configure network interfaces her. I'm also not focusing on extra-security configuration for the setup, which is surely possible.

What we're trying to achieve

Pi-Hole is known as a good and easy to use DNS filter for a home network with many ads blocking lists publicly available. Although it helps to get rid of some of the annoying ads and known malware, it lacks advanced functionality and control. Even in a home network you may be tempted to have more control and custom policies.

NXFilter is much more than an ads filter, in fact it's designed to be used in an enterprise environment with many amazing features that allow granular control. It is free, and the Jahaslist for blocking that comes with it is free for home usage (under 25 users configured in the system).
Some of the features include:
- Creating users based on IP or LDAP and assigning different DNS access policies to them, including time quota
- Manage categories and classifiers, easily create your own
- Block by categories
- Malware and botnet detection

We want to daisy chain Pi-Hole and NXfilter to take advantage of both of them in our home network and run all of that on our Raspberry Pi. Also, I'm not a fan of installing everything on a bare metal - it's often more complicated, leads to port conflicts and too much configuration, harder to keep up to date. For this scenario we want to use docker containers and leverage their default security and isolation.


Topology explained

- Configure your Raspberry Pi with a static IP.
- Set Raspberry Pi as the primary and only DNS IP for you LAN network (can be done in DHCP settings on your router/modem or manually on each device).
- Run Pi-Hole on the raspberry Pi and expose DNS port 53 to the client devices.
- Run NxFilter on the raspberry Pi. Configure Pi-Hole to use NxFIlter's container IP as the only upstream DNS
- Configure NxFilter for DOH (DNS over HTTPS). It will use actual internet DNS servers and complete the name resolution.


1. Install docker on Pi:

# Install dependancies
sudo apt-get install vim apt-transport-https ca-certificates software-properties-common -y
 
# Get the docker easy install script 
curl -fsSL get.docker.com -o get-docker.sh && sh get-docker.sh
 
# Allow pi user to run docker commands without sudo 
sudo usermod -aG docker pi  
 
# Add docker to the package update repositories. This will allow to update docker.
# Import the key 
sudo curl https://download.docker.com/linux/raspbian/gpg
 
# Add the repository url to the apt list 
# EDIT FILE: /etc/apt/sources.list  
deb https://download.docker.com/linux/raspbian/ stretch stable 
 
# Verify that repository is working and upgrade packages, if needed
sudo apt-get update
sudo apt-get upgrade 

# Verify that docker is running or start it if it's not:
systemctl status docker.service
 
systemctl start docker.service
systemctl enable docker.service

# Configure a size limit for docker logs 
# EDIT FILE: /etc/docker/daemon.json
 {
    "log-driver": "json-file",
    "log-opts": {
        "max-size": "10m"
    }
 }

  


2. Run Pi-Hole in a docker container and verify that it works.
Ports for this container must be published to the recommended standard values to work properly, as this is what LAN clients will face first: 80, 443, 53.
Update the environment variable TZ to your timezone.


# Create a docker network where we will keep both containers
docker network create dns
# Check which subnet was assigned to the network (e.g. 172.18.0.0). We will use it to set static IPs to the containers.
docker network inspect dns 
. . .
"Subnet": "172.18.0.0/16",
. . .


# Create a folder where we will keep our containers' persistence data
mkdir ~/Documents/DOCKER

# Start Pi-Hole. Use a static IP in the 'dns' network subnet (here 172.18.0.2). Note that I'm not publishing DHCP port 67, since I don't use Pi-Hole dhcp.
docker run -d \
    --name pihole \
    --network dns \
    --ip 172.18.0.2 \
    -p 53:53/tcp -p 53:53/udp \
    -p 80:80 \
    -p 443:443 \
    -e TZ="America/Chicago" \
    -v "/home/pi/Documents/DOCKER/etc-pihole/:/etc/pihole/" \
    -v "/home/pi/Documents/DOCKER/etc-dnsmasq.d/:/etc/dnsmasq.d/" \
    --dns=127.0.0.1 --dns=1.1.1.1 \
    --restart=unless-stopped \
    --hostname pi.hole \
    -e VIRTUAL_HOST="pi.hole" \
    -e PROXY_LOCATION="pi.hole" \
    pihole/pihole:latest
# Verify container status (Up, healthy):
docker ps -a 

IMAGE                  CREATED         STATUS                PORTS                                                                                                  
pihole/pihole:latest   25 hours ago    Up 24 hours (healthy)  0.0.0.0:53->53/udp, 0.0.0.0:53->53/tcp, 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp


# See container logs for troubleshooting, if required. You will also find the random Web Interface password here.
docker logs pihole

# Login to the Pi-Hole web interface and verify that it's working. You should be able to login with the web interface password found in docker logs. Alternatively change the password: docker exec -it pihole_container_name pihole -a -p 

http://<RaspPI IP ADDRESS>/admin/
# Test DNS from one of your client machines against PiHole
nslookup google.com <RaspPI IP ADDRESS>


3. NXFilter (NxF)
Now that the Pi-Hole is working, we're ready to start the NxF. We will put NxF container in the same docker network as Pi-Hole. This way pi-hole can access NXFilter's dns port without them being published on the host. At the same time both will be isolated from the Docker default network. NXFilter Web interface ports shall be published to something that do not overlap with Pi-Hole's web interface (e.g. change 80 to 8080, change 443 to 8081)

At the moment of writing I could not find a well-documented and working docker image for ARM, so I made my own Dockerfile to build the image:  https://github.com/maksokami/docker-nxfilter  

# Build the docker image
git clone https://github.com/maksokami/docker-nxfilter.git
docker image built -t maksokami/nxfilter .



# Start NXFilter. Use a static IP in the 'dns' network subnet (here 172.18.0.3).

docker run -dt \
   --name nxfilter \
   --network dns \
   --ip 172.18.0.3 \
   -p 8080:80 \
   -p 8081:443 \
   -v "/home/pi/Documents/DOCKER/nxfilter-conf":/nxfilter/conf \
   -v "/home/pi/Documents/DOCKER/nxfilter-db":/nxfilter/db \
   -v "/home/pi/Documents/DOCKER/nxfilter-log":/nxfilter/log \
   -e TZ="America/Chicago" \
   --restart=unless-stopped \
    maksokami/nxfilter

# Verify container status (Up, healthy):
docker ps -a 
IMAGE CREATED STATUS PORTS
maksokami/nxfilter 2 days ago Up 25 hours 0.0.0.0:8080->80/tcp, 0.0.0.0:8081->443/tcp
 # See container logs for troubleshooting, if required.
docker logs nxfilter 

# Go to the NxF web interface and verify that it's working. Default credentials: admin/admin
https://<RaspPI IP>:8081/admin

# For additional troubleshooting you can publish DNS ports as well and test them separately from Pi-Hole:

#   Add extra parameters to the docker run:
    docker run . . . -p 8053:53/tcp -p 8053:53/udp . . .
# Test NxF DNS functionality (without Pi-Hole) from a client machine with linux or even from the PI.
 dig @<RaspPI IP> -p 8053 google.com A

 
4. Enable the chain
Now all you need to enable the daisy-chaining is to configure Pi-Hole to use NxF's container IP ( 172.18.0.3) as the only upstream DNS.
Go to Pi-Hole Settings -> DNS. Configure "Upstream DNS server" to be 172.18.0.3. Disable any other DNS, including DOH (DNS over HTTPS). You should configure DOH in NxF instead.



You will be able to observe some blocking happening at Pi-Hole container, and additional blocking and your custom policies applied at NxF container.

Pi-Hole:
 
NxF: