It's Always DNS. Filtering With PiHole and Podman

The current state of the web is poor… at best. Ads and tracking are out of control and have moved from just being a minor annoyance to being a real detriment to my user experience, whether in a browser or an app. Worse the tools to filter these things are getting worse. They’re woefully underpowered for mobile and on desktops it seems that the filtering tools that they did have is being scaled back. We’re not too far away from the bad old days when more trusting relatives would have dozens of spyware toolbars installed in their browsers.

I’ve long been a fan of filtering at the DNS level. The approach that I’ll outline here is similar to what our devices did at Luma. It is not perfect by any means, but if it is setup well then it provides a chokepoint that has a solid return on investment for the amount of time and effort that it takes to standup. It isn’t going to keep any sophisticated actors at bay, but it does demonstrably improve performance and has a good chance of reducing the headaches of administrating a network and the systems on it for a connected family.

As part of doing this I took the opportunity to explore some new tools. I didn’t get too involved in the “containerize and orchestrate all the things” workflow over the last few years. Not to deny it’s value at all, it just wasn’t something that impacted my day to day very much. My teams use Docker and Kubernetes everyday, but for the most part it has been setup long ago and “just works” so I haven’t found myself having to dig into it at more than a superficial level. Theres been a lot of movement in the space, and I’ve been excited to see podman maturing, so that is what I chose to work with. It is mostly a 1:1 drop in for Docker, but also provides some network orchestration-ish functionality that will be nice for us here. Plus I like the easy path to get it into systemd and to bootstrap up a full kubernetes deployment if I ever felt the need.

My host system is an older Intel Nuc with 4Gb of memory, and 4 cores running at 2.4ghz. I’ve got Ubuntu 20.04, a Long Term Support release, loaded onto it. It’s not a powerhouse system but it gets the job done and is a useful small form factor server great for home use.

For the local DNS resolver I turned to pihole, originally meant to run on raspberry pi’s, it’s lightweight, fast, has great defaults and an official container image which is nice.

For upstream DNS I’m going to start with using Cloudflare’s For Families. Since pihole’s default lists are only focused on ads and tracking it’s a nice extra layer to have for some amount of filtering for unwanted content and spyware/malware. I plan to followup later changing upstream DNS to use Cloudflare for Team over HTTPS to be able to do some customization and have some more privacy from the larger world while giving up some to cloudflare. 🤷‍♂️ everything is a tradeoff and for the moment Cloudflare seems to have their interests aligned with mine on having “cleaner” networks from an adware/spyware perspective.

First steps, get podman installed.

sudo aptitude install podman

Next, we create our podman pod. This gives us a namespace for our pihole and future containers to operate in. We’ll expose the typical DNS ports of 53/tcp and 53/udp along with forwarding port 8053/tcp to go to 80/tcp within the pod to have access to the pihole dashboard.

% podman pod create \
    --name dns-filtering \
    -p 8053:80/tcp \
    -p 53:53/tcp \
    -p 53:53/udp 

Now it’s time to get our pihole container created. As you can see in the projects README there are a lot of parameters available. I’ve narrowed it down to needing just these few to get the functionality I’m looking for.

% podman run -dt \
        --name=pihole \
       --pod dns-filtering \
        -e TZ=America/New_York \
        -e WEBPASSWORD=password \
        -e PIHOLE_DNS_=; \
        -v pihole:/etc/pihole \
        -v dnsmasq:/etc/dnsmasq.d \

With the container started we can run a few tests from our server’s command line.

Verifying that a well known tracking domain is being denied:

% dig @

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

; EDNS: version: 0, flags:; udp: 4096
;		IN	A


;; Query time: 16 msec
;; WHEN: Mon Sep 20 14:49:51 EDT 2021
;; MSG SIZE  rcvd: 60

And verifying that one of Cloudflare’s category test domains is being denied as well:

% dig @

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

; EDNS: version: 0, flags:; udp: 4096
;	IN	A


;; Query time: 24 msec
;; WHEN: Mon Sep 20 14:51:43 EDT 2021
;; MSG SIZE  rcvd: 93

All that is looking good. Next we could just set the address of our server, in my case, to be the DNS server in all DHCP leases that my DHCP server hands out. But, a lot of devices these days like to bypass that and use either own DNS settings, often to enable exactly the kind of behavior that we’re trying to limit with this project. So we’ve got to do a little more to capture and redirect all the DNS requests on the network.

I plan to upgrade to a pfsense system at some point in the future, but for now my Edgerouter Lite is doing a serviceable job, so we’ll open up its interface and create some rules.

First setting up a source rule to masquerade all DNS traffic on my local network, and another to send all packets bound for port 53 that aren’t already going to to there.

Doing the same thing using iptables or similar tools should be relatively straightforward.

Now that that is done, using the dashboard I can see the traffic flowing to and through the pihole from its dashboard:

The last thing we need to do is make sure that our system keeps running when the server is rebooted. This is where podman’s systemd integration comes in very handy.

Create the systemd files:

# podman generate systemd --files --name dns-filtering

Copy the newly created files to the proper location on your system, in my case it’s /etc/systemd/system/, and then enable the pod service.

# systemctl enable pod-dns-filtering.service
Created symlink /etc/systemd/system/ → /etc/systemd/system/pod-dns-filtering.service.
Created symlink /etc/systemd/system/ → /etc/systemd/system/pod-dns-filtering.service.

And start the service

# systemctl start pod-dns-filtering

# systemctl status pod-dns-filtering.service
● pod-dns-filtering.service - Podman pod-dns-filtering.service
     Loaded: loaded (/etc/systemd/system/pod-dns-filtering.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2021-09-20 16:12:19 EDT; 4s ago
       Docs: man:podman-generate-systemd(1)
    Process: 2400 ExecStart=/usr/bin/podman start 3cd04bbb7d61-infra (code=exited, status=0/SUCCESS)
   Main PID: 2587 (conmon)
      Tasks: 2 (limit: 4519)
     Memory: 6.5M
     CGroup: /system.slice/pod-dns-filtering.service
             └─2587 /usr/libexec/podman/conmon --api-version 1 -c c0c4beabd8c476d5542a07e374146f7eed2c03e541d795ab796b8a95a17574a6 -u c0c4beabd8c476d5542a07e374146f7eed2c03e541d795>

Sep 20 16:12:18 nuc1 systemd[1]: Starting Podman pod-dns-filtering.service...
Sep 20 16:12:19 nuc1 podman[2400]: 2021-09-20 16:12:19.7351867 -0400 EDT m=+0.988834551 container init c0c4beabd8c476d5542a07e374146f7eed2c03e541d795ab796b8a95a17574a6 (image=k8s.g>
Sep 20 16:12:19 nuc1 podman[2400]: 2021-09-20 16:12:19.749873837 -0400 EDT m=+1.003521701 container start c0c4beabd8c476d5542a07e374146f7eed2c03e541d795ab796b8a95a17574a6 (image=k8>
Sep 20 16:12:19 nuc1 podman[2400]: 3cd04bbb7d61-infra
Sep 20 16:12:19 nuc1 systemd[1]: Started Podman pod-dns-filtering.service.

And with that I’m done for now and I can feel a little more confident about the domains and accompanying networks that devices on our network will be exposed to and enjoy the speed boost by pihole quickly slapping down those requests without the round trip time.