Ad Blocking w/Raspberry Pi

Image of a Raspberry Pi
Photo by Harrison Broadbent on Unsplash

I’ve used different technologies to block ads for a long time. I remember my first computer used Proxomitron to great success in the early web. (HTTPS wasn’t much of a thing back then which made MITM proxies a lot easier to set up!)

My Pi-Hole recently started acting more and more strangely, so I decided it was time to start fresh – and document it this time. I have three goals for this project:

  1. Have fine control for some devices (my TV) to prevent it from spying on me too much
  2. Block ads for all devices, especially for ones where it’s inconvenient to add a per-device blocker
  3. Hide DNS queries from my ISP (Comcast) so they have a harder time spying on me.

With all of these goals, I am well aware that nothing is foolproof. For example, SNI would allow my ISP to do deep-packet-inspection to guesstimate what sites I’m visiting. However, 1) I’m pretty sure they don’t and 2) making things harder is a worthy endeavor.

Here’s how we’re going to accomplish the goal:

  1. Each device that connects to the WiFi is provided with DNS servers. Instead of Comcast, we’ll change them to point to the IP address of my Raspberry Pi (with a static IP address)
  2. The computer asks the Pi, on port 53, for the DNS info of some website (or some ad server)
  3. The Raspberry Pi is running Pi-Hole which checks to see if the DNS request is for a known ad server. If so, it returns a “Not found” response
  4. Otherwise, the Pi-Hole turns around and asks another DNS server, running on the same Pi, on port 5321 for the real DNS info
  5. This DNS server is a local caching DNS server, provided by Unbound. Unbound is configured to ask Cloudflare, via DNS over TLS (DoT) to prevent Comcast from knowing much about the request.
  6. Cloudflare returns the result & DNSSEC information if available. This is cached in Unbound, and then forwarded to the Pi-Hole
  7. The Pi-Hole validates the DNSSEC certificate, and if valid, forwards it back to the device.

End result: All DNS requests that leave the network are encrypted over TLS. All responses have their DNSSEC response verified. Known ads servers are blocked at the DNS layer. This comes at the cost of 2 extra DNS servers running on the Pi. Also, if the Pi goes down, then so does the internet.

Installing Raspberry Pi OS (Raspbian)

Since the Pi is headless, we need to prepare it to connect to the WiFi before booting. First, figure out your wpa_passphrase by going to http://jorisvr.nl/wpapsk.html (or running wpa_passphrase locally). This especially helps if there are funky characters in the SSID or the password. Then create a file wpa_supplicant.conf in the root directory of the SD card, with the following content:

country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
ssid="YOUR SSID HERE"
scan_ssid=1
psk=YOUR_WPA_PASSPHRASE_HERE
key_mgmt=WPA-PSK
}

For whatever reason, this file is extremely picky with formatting, so you might need to play around with it to work properly. Also run touch ssh on the root directory as well, to enable SSH access.

If all goes well, a few minutes after booting the Pi, we should have SSH access. The first thing we can do is to run passwd and change the pi account password from the default raspberry. Next, let’s get automatic updates going:

sudo apt-get update && apt-get install unattended-upgrades
echo 'Unattended-Upgrade::Origins-Pattern {
//      Fix missing Rasbian sources.
        "origin=Debian,codename=${distro_codename},label=Debian";
        "origin=Debian,codename=${distro_codename},label=Debian-Security";
        "origin=Raspbian,codename=${distro_codename},label=Raspbian";
        "origin=Raspberry Pi Foundation,codename=${distro_codename},label=Raspberry Pi Foundation";
};' | sudo tee /etc/apt/apt.conf.d/51unattended-upgrades-raspbian

Next we can harden up the SSH account to use publickey only: This is /etc/ssh/sshd_config. After changing, you can use sudo service ssh restart and make sure it still connects on a new shell.

#	$OpenBSD: sshd_config,v 1.103 2018/04/09 20:41:22 tj Exp $

# This is the sshd server system-wide configuration file.  See
# sshd_config(5) for more information.

# This sshd was compiled with PATH=/usr/bin:/bin:/usr/sbin:/sbin

# The strategy used for options in the default sshd_config shipped with
# OpenSSH is to specify options with their default value where
# possible, but leave them commented.  Uncommented options override the
# default value.

LogLevel INFO
PermitRootLogin no
StrictModes yes
MaxAuthTries 6
MaxSessions 10
PubkeyAuthentication yes
PasswordAuthentication no
AuthenticationMethods publickey
PermitEmptyPasswords no
AllowUsers pi
ChallengeResponseAuthentication no

UsePAM yes

X11Forwarding yes
PrintMotd no

AcceptEnv LANG LC_*

Subsystem	sftp	/usr/lib/openssh/sftp-server

Installing Unbound

Now, to setup Unbound. I mostly followed this guide and this config.

First, run sudo apt-get install unbound dnsutils. Then, add the following to /etc/unbound/unbound.conf.d/pihole.conf

server:
  verbosity: 0
  access-control: 127.0.0.1 allow
  aggressive-nsec: yes
  cache-max-ttl: 14400
  cache-min-ttl: 1200
  do-ip4: yes
  do-ip6: yes
  do-tcp: yes
  hide-identity: yes
  hide-version: yes
  interface: 0.0.0.0
  interface: ::0
  pidfile: /var/run/local_unbound.pid
  port: 5321
  prefetch: yes
  rrset-roundrobin: yes
  so-reuseport: yes
  tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"
  use-caps-for-id: yes
  username: unbound

 # forward-addr format must be ip "@" port number "#" followed by the valid public hostname
 # in order for unbound to use the tls-cert-bundle to validate the dns server certificate.
 forward-zone:
   name: "."
   forward-tls-upstream: yes
   forward-addr: 1.0.0.1@853#one.one.one.one
   forward-addr: 1.1.1.1@853#one.one.one.one

Save the file, and then let’s start Unbound: sudo service unbound restart. Now we can test that it works with something like dig terminal.space @127.0.0.1 -p 5321. Note that you’re looking for more than just a wall of text, you want to be sure to get an IP address back in the Answer section like so:

;; ANSWER SECTION:
terminal.space.		1200	IN	A	74.208.92.166

Once that’s working, now it’s time to set up the Pi’s networking. First, stop Unbound from mucking with the local DNS resolver with sudo systemctl status unbound-resolvconf.service. Then, open up /etc/dhcpcd.conf and add whatever static IP settings you need. Here are mine:

interface wlan0
        static ip_address=192.168.1.86/24
        static routers=192.168.1.1
        static domain_name_servers=1.1.1.1 1.0.0.1

The domain_name_servers part is important because the router is going to point back at the current device for the network’s DNS. So if we don’t hardcode it here, the Pi can get jammed up if the local DNS stops working for whatever reason. Once that’s set, you can reload the settings with sudo systemctl restart dhcpcd.

That Pi-Hole life

Next, it’s time to install the Pi-Hole properly. Run curl -sSL https://install.pi-hole.net | bash and follow the installation steps. You can always change the settings in the admin UI later. After installation, change the web password with pihole -a -p. Lastly, go to the web settings (http://192.168.1.86/admin in my case) and change the upstream DNS settings to point to 127.0.0.1#5321, and enable DNSSEC on the Pi.

Web settings for Pi-Hole showing the DNS server pointing to 127.0.0.1#5321

It’s important to uncheck all of the other servers, as we don’t want any leakage. Now, we can test that the PiHole works with dig terminal.space @127.0.0.1 (note that without the -p option) and making sure that works.

And that’s a wrap

Now that everything works, you just need to change the DHCP settings on your router to point to the Pi-Hole & it’s all good to go. Take that, ads!