Ad blocking on my home network

  • Post by Rob Kenis
  • Jan 18, 2024
post-thumb

The internet is full of ads and trackers, while I just want to browse in peace with a bit of privacy.

Where it all began

A long time ago, I was an avid Chrome user. Most of the websites I was visiting, worked fine and I didn’t really mind the data I was sending to Google and other companies. Until one day, I was getting recommendations on Facebook for products I didn’t even want, but I searched for briefly.

I took 2 steps to prevent this from happening again:

  • I switched to Firefox, which in my opinion was more privacy focused since it is not backed by Google.
  • I installed AdGuard and Privacy Badger in an attempt to block ads and trackers.

This worked great for me…until I switched over to my phone.

Network Ad Blocking

I did not want to set up ad blocking on each device I used, or even worse, the devices my parents used. So I started looking for a solution that covers the whole network at home. I stumbled upon Pi-hole, which seemed to be a great solution. I installed it on a Raspberry Pi, set the IP as the DNS server for all devices in the network and I was done.

Automation using Ansible

This worked great for a while, but I wanted to improve on the setup of the Raspberry Pi. I called upon Ansible and automated the process of installing a Pi-Hole with Docker.

- name: Make sure the pihole container is created and running
  docker_container:
    name: "pihole"
    image: pihole/pihole
    state: 'started'
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "67:67/udp"
      - "80:80/tcp"
    volumes:
      - '{{ docker_dir }}/{{ container_name }}/pihole:/etc/pihole'
      - '{{ docker_dir }}/{{ container_name }}/dnsmasq.d:/etc/dnsmasq.d'
    env:
      "TZ": "Europe/Brussels"
    labels:
    restart_policy: unless-stopped

Automatic updates

Automating the deployment of an application feels great, but getting automatic updates is even better. For this, I used Watchtower, an application that checks your Docker containers for new image versions and restarts the container when a new image is available. A fun side effect of this, is that Watchtower will also keep itself up to date.

---
- name: Make sure the Watchtower container is created and running
  docker_container:
    name: "watchtower"
    image: "containrrr/watchtower"
    pull: yes
    state: 'started'
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    restart_policy: unless-stopped

Introducing Kubernetes

I was happy with my setup, but I wanted to learn more about Kubernetes. I decided to move my Pi-Hole setup to a Kubernetes cluster. I used k3s to set up a single node cluster on my Raspberry Pi. I used ArgoCD as my GitOps tool to deploy my applications to the cluster. And I won’t dive much further into this, the setup deserves a post by itself.

AdGuard Home

Through the years, another tool has caught my attention. Mostly because of the slow release cycle of Pi-Hole, but also because of the features it offers. I’m talking about AdGuard Home.

To install AdGuard Home on a Kubernetes cluster, I used following manifest, which will do following things:

  • run the latest version of AdGuard Home
  • create a named port for the web interface on port 80 and DNS on port 53
  • mount 2 persistent volumes for the configuration and workspace

I would have loved to put this in a collapsible block, but I can’t get it to work unfortunately.

  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: adguard-home
    labels:
      app: adguard-home
  spec:
    replicas: 1
    revisionHistoryLimit: 1
    selector:
      matchLabels:
        app: adguard-home
    template:
      metadata:
        labels:
          app: adguard-home
      spec:
        serviceAccountName: adguard-home
        containers:
          - name: adguard-home
            image: 'adguard/adguardhome:v0.107.43'
            ports:
              - containerPort: 80
                protocol: TCP
                name: web
              - containerPort: 53
                protocol: UDP
                name: dns-udp
              - containerPort: 53
                protocol: TCP
                name: dns-tcp
            env:
              - name: TZ
                value: "Europe/Brussels"
            volumeMounts:
              - name: configuration
                mountPath: /opt/adguardhome/conf
              - name: workspace
                mountPath: /opt/adguardhome/work
        volumes:
          - name: configuration
            persistentVolumeClaim:
              claimName: adguard-home-config
          - name: workspace
            persistentVolumeClaim:
              claimName: adguard-home

Next to the AdGuard deployment, I decided to run a deployment for acaranta/adguard-exporter which will run a Prometheus exporter so I can get AdGuard metrics into Grafana.

Grafana Dashboard for AdGuard Home

On the road

This setup works fantastic for me, because I get ad blocking on all devices in my home network. But that only works when I’m at home, so I need a setup to get the same benefits when I’m away from home.

WireGuard to the rescue! I have 2 servers running WireGuard, 1 on AWS acting as a server to which all peers connect. And one running on a server in my home network, acting as a peer. This way, I don’t need to expose any ports to the internet, but all traffic needs to pass to an isolated VM on AWS first.

On AWS, I am running WireGuard with following configuration:

[interface]
Address    = 192.168.3.254/24
ListenPort = 51820
PrivateKey = {{ wireguard_private_key }}

# Forward/ MASQUERADE outgoing connections (Internet)
PostUp   = iptables -t filter -A FORWARD -j ACCEPT -i %i ! -o %i
PostDown = iptables -t filter -D FORWARD -j ACCEPT -i %i ! -o %i
PostUp   = iptables -t nat    -A POSTROUTING -j MASQUERADE -s 192.168.3.0/24 ! -o %i
PostDown = iptables -t nat    -D POSTROUTING -j MASQUERADE -s 192.168.3.0/24 ! -o %i

# Allow forwarding WireGuard traffic (optional)
PostUp   = iptables -t filter -A FORWARD -j ACCEPT -i %i -o %i
PostDown = iptables -t filter -D FORWARD -j ACCEPT -i %i -o %i

# Phone
[peer]
PublicKey  = {{ phone_public_key }}
AllowedIPs = 192.168.3.4/32

# Home VPN
[peer]
PublicKey  = {{ home_vpn_public_key }}
AllowedIPs = 192.168.3.6/32
AllowedIPs = {{ home_network_range }}

This way, I could pass all traffic from my phone through WireGuard but my home range would be routed through the peer inside my network.

In my network, I’m running WireGuard with following configuration:

[interface]
Address    = {{ wireguard_ip }}
ListenPort = 51820
PrivateKey = {{ wireguard_private_key }}

# Route wireguard traffic
PostUp   = iptables -t nat    -A POSTROUTING -j MASQUERADE -s 192.168.3.0/24 ! -o %i
PostDown = iptables -t nat    -D POSTROUTING -j MASQUERADE -s 192.168.3.0/24 ! -o %i

# AWS
[peer]
Endpoint            = {{ aws_ip }}:51820
PublicKey           = {{ aws_public_key }}
AllowedIPs          = 192.168.3.254/24
PersistentKeepalive = 30

The important part with this is the iptables section which will make sure that requests for other IP addresses inside my network are routed to the correct place and the response is routed back to where it belongs.

Now the only this I have to do, is install the WireGuard app on my phone, add myself as a peer, and set the DNS server when using WireGuard to the IP of my AdGuard Home instance.

What I learned

  • How Pi-Hole blocks ads by returning null to DNS requests for known ad domains.
  • How to deploy on Kubernetes using ArgoCD.
  • How to use WireGuard to route traffic through a VPN.