Lets be frank, the Internet is simply unusable with all the ads floating around.
I use the uBlock Origin extension in my browser, as do most of the people reading this genre of articles, but the same is not true for the majority of the population, including other members of my family. So in order to enhance their web browsing experience I decided to block ads at the DNS level.
But why stop there I thought, why not also improve their privacy while I'm at it. So I also decided to setup a VPN. Now let me clarify some things here, I'm not a big fan of VPNs, the way they're advertised by the big companies, here's a great video by Tom Scott explaining what I mean. But they also have their use cases, some of which are:
I took a look at apps like Blokada and DNS66 which grant you device wide ad blocking on mobile devices. On Android the way this works is, by creating an internal VPN on the phone, so that all traffic from the device can be routed via it, and it has a file with all the blacklisted domains, to filter out traffic.
But there's a caveat with this approach. I cannot use another VPN to route my traffic, due to an Android limitation.
There can be only one VPN connection running at the same time. The existing interface is deactivated when a new one is created.
This means my browsing patterns are still accessible to my ISP. So, I started looking for ad blocking DNS servers, so that I could point Android's global DNS to it. I found AdGuard and PiHole to be the top projects. Hosting these at home, on Raspberry Pi seemed like a plausible solution, but then again one can't use it while traveling. The solution is to obviously host it on a publicly accessible server. Amongst the two I found AdGuard more appealing due to the following reasons
You can find more about their differences here. Both are great projects, but AdGuard met my requirements perfectly.
When it comes to VPN, I did not even consider using OpenVPN, WireGuard was the obvious choice, because of it's speed, smaller, easily auditable codebase (not that I was going to audit it, but still), cross platform compatibility and integration into the Linux Kernel.
Being a big fan of Kubernetes and maintaining infrastructure as code, I wanted a way to be able to easily deploy and version control my deployment. After much searching I stumbled upon, kilo, a network overlay built on WireGuard for Kubernetes. It could do exactly what I wanted, while also enhancing the security of my cluster, by encrypting the inter pod communication, and allowing me to build secure clusters, over nodes spanning multiple cloud providers. Also it would give me the added benefit of easily debugging applications deployed on my Kubernetes Cluster, since when connected I would be a peer on the network, thereby getting access to the all the private IPs of the deployments, services etc. You can watch this talk by Lucas Servén Marín to know more.
Without further ado, let's jump right into the setup. I'll be explaining the steps for setting it up on a k3s cluster. You may need to modify them as per your cluster. After deploying k3s, the 1st thing which needs to be done is to setup kilo. First download the manifest for kilo on k3s.
curl -LO https://raw.githubusercontent.com/squat/kilo/master/manifests/kilo-k3s.yaml
Now you need to modify and add - --mesh-granularity=full
to the DaemonSet
section under the args
for the kilo
container.
...
containers:
- name: kilo
image: squat/kilo
args:
- --kubeconfig=/etc/kubernetes/kubeconfig
- --hostname=$(NODE_NAME)
- --mesh-granularity=full
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
...
This is done to ensure all our nodes are meshed together regardless of the datacenter. Then simply apply the manifest.
kubectl apply -f kilo-k3s.yaml
This will later be useful for setting up WireGuard VPN. More on this later. Now we can proceed to setup AdGuard. Here is the spec for AdGuard.
apiVersion: apps/v1
kind: Deployment
metadata:
name: adguardhome
spec:
selector:
matchLabels:
app: adguardhome
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 0
template:
metadata:
labels:
app: adguardhome
spec:
volumes:
- name: tls-cert-secret
secret:
secretName: production-tls-cert
- name: adguard-config
hostPath:
path: "/path/to/store/conf"
type: DirectoryOrCreate
- name: adguard-logs
hostPath:
path: "/path/to/store/work"
type: DirectoryOrCreate
containers:
- name: adguardhome
image: adguard/adguardhome:v0.102.0
ports:
# Regular DNS Port
- containerPort: 53
hostPort: 53
protocol: UDP
- containerPort: 53
hostPort: 53
protocol: TCP
# DNS over TLS
- containerPort: 853
hostPort: 853
protocol: TCP
volumeMounts:
- name: tls-cert-secret
mountPath: /certs
- name: adguard-config
mountPath: /opt/adguardhome/conf
- name: adguard-logs
mountPath: /opt/adguardhome/work
terminationGracePeriodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
name: adguardhome
labels:
app: adguardhome
spec:
type: ClusterIP
selector:
app: adguardhome
ports:
- port: 80
# targetPort: 3000
targetPort: 80
protocol: TCP
The RollingUpdate
is intentionally configured to not wait till a new pod is up, and directly terminate the existing pod during deploys. On the surface it seems like an anti-pattern, but since I'm using the hostPort
directive, a new Pod wouldn't get scheduled unless port 53
was available on the host for it to bind to, so the existing Pod has to terminate before a new Pod can be deployed.
Also I initially intended to use a ConfigMap
for holding the AdGuardHome.yml
, but there was some issue with AdGuard trying to write to it while initally comming up, but since ConfigMap
's are moundted as ReadOnly
, Pod creation used to fail, so I decided to go with a Volume instead, until I could figure out the issue.
For the initial setup, the AdGuard admin UI will be accessible on port 3000, so you'll have to switch the targetPort
to 3000
in the adguardhome
service initially, access the admin UI, setup the password, and then revert the targetPort
to 80
.
You may also enable DNSSEC
under DNS Settings
for guaranteeing authenticity of DNS responses by signing them, and making tampering detectable.
The volume mounts for tls-cert-secret
are only necessary if you want to enable DNS-over-TLS. And you need to configure your ingress resource before mounting it here.
For enabling DNS-over-TLS, on the AdGuard admin UI you can goto Encryption Settings
-> Enable Encryption
, and put in the Certificate path as /certs/tls.crt
and the Key path as /certs/tls.key
. Again let me re-iterate that your Ingress resource needs to be configured properly and you need to have a valid TLS certificate for the domain you're hosting AdGuard on.
On Android phones for Android Pie and later, you may goto Settings
-> WiFi and Internet
-> Private DNS
. Select Private DNS Hostname Provider
and set it to the domain name to the once you've configured on. You may also configure it on your home router to ensure all the devices get DNS level Ad Blocking.
This concludes the AdGuard part of the setup.
Now coming to setting up WireGuard.
I'll be referring to the k3s cluster as the server and the local laptop as the client from here on.
You'll need WireGaurd installed on both your server and client machine. Follow the steps as per your distribution to install the same. If you're using a bleeding edge distro like Archlinux or Gentoo, you don't need to do anything on the server side, since WireGuard is already baked into the kernel at this point. On the client side however you'll need to install it for getting the command line client to enable / disable the interface.
Another useful tool to have on the client side is kgctl
. You can install it using
go get github.com/squat/kilo/cmd/kgctl
Now we need to create a private and a public key pair on the client.
wg genkey | tee privatekey | wg pubkey > publickey
This'll create 2 files with the respective key contents. This key pair needs to be authorized on the server. You can do this simply by creating a peer resource. Create a file named archie.yaml
apiVersion: kilo.squat.ai/v1alpha1
kind: Peer
metadata:
name: archie
spec:
allowedIPs:
- 10.120.120.1/32 # This is just and example, you can use any valid available CIDR here
publicKey: CLIENT_PUBLIC_KEY # Enter the public key here, the one you just generated
persistentKeepalive: 10
Finally apply the manifest
kubectl apply -f archie.yaml
Remember, the allowedIPs
should be a valid CIDR, which is available on both the server and the client. Now we can use the kgctl
tool to generate the peer
section of the client WireGuard config.
kgctl showconf peer archie
This will return something like
[Peer]
AllowedIPs = 10.42.0.0/24, 10.42.0.0/32, 10.4.0.1/32
Endpoint = YOUR_SERVER_IP:51820
PersistentKeepalive = 10
PublicKey = SERVER_PUBLIC_KEY
Create a file on the client named adgaurd.yaml
at the location /etc/wireguard
and add the following contents to it
[Interface]
Address = 10.120.120.1/32 # Use the same CIDR whitelisted in the Peer manifest
PrivateKey = CLIENT_PRIVATE_KEY # The one you generated above on your client
DNS = YOUR_SERVER_IP # Enter the IP of the server to block ads, FQDN don't work on Android for some reason
[Peer]
AllowedIPs = 0.0.0.0/0, ::/0 # This modification is important to route all the traffic from your machine via wireguard interface
Endpoint = YOUR_SERVER_IP:51820
PersistentKeepalive = 10
PublicKey = SERVER_PUBLIC_KEY # as recieved from the above config
Now to enable the VPN on your client machine you may use
wg-quick up adguard
You can verify you're connected to the VPN server by visiting ifconfig.io. It should show your IP as the IP of your server.
To disconnect, you may use
wg-quick down adguard
Using the same process outlined above you can add multiple Peers and create multiple configs. You can also install and use the qrencode
utility to convert the same config to a QR code for easy scanning on mobile phones.
qrencode -t ansiutf8 < /etc/wireguard/adguard.conf
This will print a QR code right in your terminal. You can install the WireGuard Android App on your phone and this scan this QR code to import the config.
And viola, whenever you're connected, your ISP can't snoop in on the websites you're visiting, add to that all your traffic will be filtered out for ads, while also routing your data through the server for protection against malicious actors in your network, and for the cherry on top, you can have all your devices connected and talking to each other regardless of the location / network they are on.
Guess this post made it to the front page on Y Combinator's Hacker News! You can find the HN post here. And here's the web archive link (8th one on this). This invited a good amount of traffic onto my blog :)