YS Learns..

daily tinkers with software, infrastructure, coffee, running and photography

Blocking Ads with Pi-hole in Kubernetes

#pihole, #kubernetes

One of the common things people do with a vanilla Raspberry Pi is to use it exclusively to host Pi-Hole. If you haven’t heard of Pi-Hole, it’s essentially a service that blocks advertisements via DNS. Imagine a /etc/hosts file with a gigantic list of domains belonging to ad networks and forcing them to point to 0.0.0.0.

The simpler alternative to this approach is using browser extensions like Adblock Plus. To cover your entire network however, you would have to install an extension on every browser on every device, consuming memory and having to manage individual block lists separately. Mobile devices and apps also add to the decentralisation.

Since I already have an operational Kubernetes cluster, I wanted to see how different it would be managing Pi-Hole as a microservice rather than installed directly on a Pi.

alt text
Pi-Hole + Kubernetes

Deploying to Kubernetes

Pi-hole already has images published on their Docker Hub so the deployment manifest is simple, requiring two areas of persistent storage for configuration data and the admin password to be configured in the environment variable WEBPASSWORD.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pihole-data
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pihole-dnsmasq
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pihole
  labels:
    app: pihole
spec:
  selector:
    matchLabels:
      app: pihole
  template:
    metadata:
      labels:
        app: pihole
    spec:
      containers:
      - name: pihole
        env:
        - name: WEBPASSWORD
          value: a-strong-password-here
        image: pihole/pihole:latest
        volumeMounts:
        - mountPath: /etc/pihole
          name: pihole-data
        - mountPath: /etc/dnsmasq.d
          name: pihole-dnsmasq
      volumes:
      - name: pihole-data
        persistentVolumeClaim:
          claimName: pihole-data
      - name: pihole-dnsmasq
        persistentVolumeClaim:
          claimName: pihole-dnsmasq

We’ll then need to expose the admin console on port 80 and the DNS service on port 53, but this needs to be done on both TCP and UDP. Kubernetes does not allow you to create a single service that handles both TCP and UDP, so we will have to create 2 services, then force our load balancer to host them on the same external IP. I’m using Metal LB, which supports this via the addition of an allow-shared-ip annotation to both services and supplying a common identifier, which I will list as pihole-svc.

apiVersion: v1
kind: Service
metadata:
  name: pihole-tcp
  annotations:
    metallb.universe.tf/allow-shared-ip: pihole-svc
spec:
  type: LoadBalancer
  selector:
    app: pihole
  externalTrafficPolicy: Local
  ports:
  - name: pihole-admin
    port: 80
    targetPort: 80
    protocol: TCP
  - name: dns-tcp
    port: 53
    targetPort: 53
    protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: pihole-udp
  annotations:
    metallb.universe.tf/allow-shared-ip: pihole-svc
spec:
  type: LoadBalancer
  selector:
    app: pihole
  externalTrafficPolicy: Local
  ports:
  - name: dns-udp
    port: 53
    targetPort: 53
    protocol: UDP

Once you kubectl create -f the manifests, you should see the single pod start up and the load balancer assign a shared IP for the services.

NAME        TYPE          CLUSTER-IP    EXTERNAL-IP  PORT(S)                   AGE
pihole-tcp  LoadBalancer  10.121.27.29  10.0.0.77   80:30600/TCP,53:31765/TCP  1m
pihole-udp  LoadBalancer  10.103.12.15  10.0.0.77   53:32637/UDP               1m

Administration

You can now visit the admin console at http://10.0.0.77 (the IP assigned above) and login using the password defined in the deployment. Pi-hole is actually usable right away but you can choose to override some settings like upstream DNS in Settings > DNS or add more block lists to the default in Group Management > Adlists. You can Google for Pi-Hole lists where you will find many, many different lists you can add directly to your Pi-Hole. Once you add new lists, you need to go to Tools > Update Gravity to download the contents from those lists into your local database. Pi-hole will periodically do this for you to refresh any changes in those lists.

Testing and Go-Live

To test if Pi-Hole is working, just perform a nslookup google.com 10.0.0.77 and check that it resolves and appears in your admin console’s Query Log page. Once you’re satisfied, go to your DHCP server (usually in your home router) and override the DNS server to 10.0.0.77 (whatever your LB assigned). All new devices obtaining new DHCP leases will henceforth use Pi-Hole to resolve hostnames and block matching domains (and associated ads). You can then get some interesting metrics back in the admin console after using it for a day or so.

alt text
Shocking metrics on my network: 45% of all queries were to ad networks!?

Adding DHCP

Now that Pi-Hole has relieved your home router’s duties in resolving DNS, what if you also wanted to relieve it of IP assignment duties? You’ll be happy to know you can centralise this as well in Pi-Hole and it comes out-of-the-box. Unfortunately, since we’re in the Kubernetes world, some extra work is involved. This “breaks” the container model a little and adds some security considerations, so exercise your discretion. The core problem here is that if Pi-Hole sits in a container, it cannot listen to DHCP requests on Layer 2 happening outside the cluster’s internal network. Hence, it will need access to the host network as well as root access on the host to listen on ports 53 and 67. The actual work involved is not too difficult: just add hostNetwork and securityContext to your deployment.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pihole
  labels:
    app: pihole
spec:
  selector:
    matchLabels:
      app: pihole
  template:
    metadata:
      labels:
        app: pihole
    spec:
      containers:
      - name: pihole
        securityContext:            privileged: true        env:
        - name: WEBPASSWORD
          value: a-strong-password-here
        image: pihole/pihole:latest
        volumeMounts:
        - mountPath: /etc/pihole
          name: pihole-data
        - mountPath: /etc/dnsmasq.d
          name: pihole-dnsmasq
      hostNetwork: true      volumes:
      - name: pihole-data
        persistentVolumeClaim:
          claimName: pihole-data
      - name: pihole-dnsmasq
        persistentVolumeClaim:
          claimName: pihole-dnsmasq

You’ll then need to edit your UDP service to expose the DHCP port.

apiVersion: v1
kind: Service
metadata:
  name: pihole-udp
  annotations:
    metallb.universe.tf/allow-shared-ip: pihole-svc
spec:
  type: LoadBalancer
  selector:
    app: pihole
  externalTrafficPolicy: Local
  ports:
  - name: dhcp-udp    port: 67    targetPort: 67    protocol: UDP  - name: dns-udp
    port: 53
    targetPort: 53
    protocol: UDP

You can then head into the admin console under Settings > DHCP and enable the DHCP server, setting up your desired range and any static leases. Once this is enabled, remember to disable your router’s DHCP server. Now you have a single control plane to manage all local and remote hostname resolution. Happy ad-free life!


Yong Sheng Tan

Written by Yong Sheng Tan from sunny Singapore

Twitter  ·  GitHub  ·  LinkedIn

All thoughts, opinions, code and other media are expressed here in a personal capacity and do not represent any other entities or persons