The Beginning
A little over a year ago, I replaced my Sony Android TV with an LG smart TV. The LG TV is great, but since it’s based on webOS it doesn’t have native VPN support.
Fortunately, I had a Raspberry Pi lying around, which I got a while ago to tinker with. So I decided to purpose the Raspberry Pi to function as my home network gateway, providing both a DNS sinkhole and a VPN exit point to all devices on my home network (plus some other functionalities). After all, one can only survive so many German YouTube ads before “einen Dachschaden haben”.
The Idea
The idea was to have a single point of entry and exit for all my home network traffic. This way, I could ensure that all devices on my network would benefit from the DNS filtering/caching, VPN connection -when enabled-, and other network services I wanted to implement.
From a network topology perspective, the Raspberry Pi would sit between the router and the rest of my home network, and intercept all traffic going in and out of the network.
The Draft
---
config:
theme: neutral
---
graph TD
Internet[Cat Memes] <--> ISP[ISP: 2.213.76.114]
Internet <--> VPN[VPN Provider: 10.2.0.2]
ISP <--> Router[Router: 192.168.0.1]
Router <--direct mode--> RPI[Raspberry Pi: 192.168.0.254]
VPN <--VPN mode--> RPI
RPI <--> D1[TV: 192.168.0.100]
RPI <--> D2[Laptop: 192.168.0.151]
RPI <--> D3[Phone: 192.168.0.200]
For this to work, the idea was to have the following criteria fulfilled:
- The router needs to be configured to use the Raspberry Pi as its default gateway.
- A VPN client needs to be installed and configured on the Raspberry Pi.
- A DNS sinkhole service needs to be set up on the Raspberry Pi.
- The Raspberry Pi needs to be set up to forward traffic between its network/VPN interfaces.
- Additional services like DHCP, firewall rules, and monitoring tools can be configured as needed.
1. The Raspberry Pi Setup
My Raspberry Pi 5 is running Raspberry Pi OS Lite. I decided to use Pi-Hole as a DNS sinkhole, WireGuard as a VPN client, and ProtonVPN as a VPN provider.
The Pi-Hole official setup guide describes how to configure the Raspberry Pi as a DNS resolver for the whole network.
This poses a problem for setting a VPN up, since VPN providers often use their own DNS servers to prevent DNS leaks, but the Pi-Hole would intercept these requests and resolve them using the Pi-Hole’s upstream DNS servers.
This is a DNS leak, albeit not in the traditional sense, since the DNS requests are still going through the VPN tunnel; the leak is of the VPN exit node IP by the DNS upstream servers, which is not ideal, but acceptable* for my use case.
There are solutions to this problem:
- Use WireGuard’s
PostUpandPostDownscripts to change the Pi-Hole DNS servers when the VPN connects/disconnects. - Use
systemdservices to automatically manage and switch the Pi-Hole DNS configuration based on the VPN state.
Finally, I also needed a simpler way to monitor the VPN connection status and to enable/disable the VPN connections without requiring to ssh into the Raspberry Pi and running some shell commands.
2. The Fritz!Box Setup
It’s a commonly known fact that Fritz!Box routers are some of the most user-friendly and easiest to automate routers out there. /s1
That means that many of the commonly used features in other routers, or could help automating this setup didn’t work on the Fritz!Box, so after many hours of trial and error, reading forums, and occasionally German documentation PDFs, I disabled as many features as possible on the Fritz!Box, to avoid conflicts and weird behavior that were extremely hard to debug, and sometimes were exactly the opposite of what was shown on the Fritz!Box UI, which included:
- Disable the DHCP server on the Fritz!Box to avoid IP conflicts (both IPv4 and IPv6).
- Disable “Fallback to public DNS servers”.
- Disable DNS over TLS (DoT).
- Remove all static routes configurations.
- Configure the standard profile filtering to block DNS.
- Configure the unrestricted profile to allow the Raspberry Pi unrestricted internet access.
- Set the DNS server to the Raspberry Pi’s IP address (both IPv4 and IPv6).
This setup is highlighted in the official Pi-Hole tutorial.
The Implementation
Since I have pretty much a less-than-average fish memory, I needed to have everything either documented or automated. Luckily, I had already started using Ansible to [automate my whole home setup a while ago]2, and I had the perfect place to implement this project in a way that I could debug and maintain.
The Pi-Hole installation
The RPI Pi-Hole role depends on systemd and network roles.
systemd is required to manage the Pi-Hole FTL DNS service, and the network role is used to manage the whole
network configuration-as-a-code.
The network role ensures that network-manager, systemd-resolved, and dnsutils are installed, and ensures that
avahi-daemon, avahi-utils, and dhcpcd are removed to avoid conflicts with systemd-resolved.
Additionally, the network role configures the static IP address for the Raspberry Pi, and configures
the network-manager DNS resolver (using systemd-resolved as a backend), and the default upstream DNS servers.
Other than that, the Pi-Hole installation is pretty “standard”, following the official Pi-Hole installation script.
I created the pihole user, and the required setup directories manually for clarity, downloaded and ran the
installation script, after creating the required custom configurations file (/etc/pihole/pihole.toml)
And to avoid DNS conflicts between the Pi-Hole and network services (NetworkManager and systemd-resolved),
the role disables the default NetworkManager DNS configurations and configures systemd-resolved to use the Pi-Hole
as the default DNS resolver for the whole system.
This role is completely independent of the VPN setup, so it can be used standalone if needed.
Here is the full Pi-Hole role implementation.
The Gateway installation
The gateway role was a bit more complex to develop, since I didn’t have much experience with managing networks or devices, but after some research, trial and error, and some snippets of code from different internet posts about different topics, I managed to create a role that does the following:
- Ensures that
iptables,iproute2, andudevare installed. - Configures
sysctlto enable IP forwarding. - Template the script that toggles the direct/VPN gateway mode.
- Configure (event-driven)
udevrules to trigger thesystemdservices when the VPN interface goes up/down. - Adds the
systemdservices that respond to the differentudevevents to switch the gateway mode.
I opted to use this approach instead of using WireGuard’s PostUp and PostDown scripts because I wanted to have
the whole process automated and developed once but deployed everywhere, without the need to edit the WireGuard
configuration files manually, despite the fact that systemd approach is (IMHO) way more complex, but it’s also
possible to extend it to eliminate the DNS leak problem mentioned before.3
But what was the most complicated part of this process was to figure out the exact iptables rules required
to properly forward the traffic between the different interfaces, and being able to debug when something went wrong.
Because I also wanted to preserve the Locality of Behavior principle, I created the bash script that toggles
which mode is active (direct/VPN), and ensured that the setup is almost exclusively managed by systemd services.
Here is the full Gateway role implementation, but in a nutshell, the key parts are:
- Flush all existing
iptablesrules before applying new ones. apply_vpn_mode()- Routes local network through VPNapply_direct_mode()- Routes local network directly to the internet
VPN mode iptables rules explained
iptables -t nat -A POSTROUTING -s "$GATEWAY_SUBNET" -o wg+ -j MASQUERADE
- NAT rule: Rewrites source IP of packets from
192.168.0.0/24going out through WireGuard (wg0,wg-US, etc.) interfaces. - Changes:
192.168.0.151→10.2.0.2(VPN IP) - Why: So remote servers respond back through the VPN tunnel
iptables -A OUTPUT -o wg+ -j ACCEPT
- Allows Raspberry Pi’s own traffic to go out through WireGuard interfaces.
- For: Pi’s own DNS queries, updates, etc.
iptables -A FORWARD -s "$GATEWAY_SUBNET" -o wg+ -j ACCEPT
- Allows forwarding packets from local network (
192.168.0.0/24) to WireGuard. - For: Other devices’ traffic being routed through the Pi.
iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
- Allows return traffic (responses) back through.
- For: When http.cat responds, let the packets come back.
Direct mode iptables rules explained
Same rules, but routes through router (eth0) instead of VPN (wg+) interfaces.
The Web Interface
Because I didn’t find exactly what I wanted, which is a simple web interface to check and toggle the VPN connection status, which can be installed and configured via code, I created the wg-portal, which is a simple web application built with Go 🤩, and a little bit of JavaScript 🤢🤮.
The Results
I started this project out of curiosity and to learn more about networking concepts, Go web development, ansible automation, I could have just set up only the VPN or DNS sinkhole, but I wanted to explore, experiment, and challenge myself a little to make this different tools work together, so it was to my extreme surprise that not only did everything work as expected, but that it also worked reliably and stably for months now.
Here is a GIF of the web interface in action:

The Problems
A part from rebooting the Raspberry Pi every once in a while, I haven’t had to do much of any manual work to keep the setup running, and the only problems I’ve had -a part from the occasional ISP issues- was forgetting to turn off the VPN before checking for flights and realizing that the prices were in GBP instead of EUR 😅!
The Future
I think this has been functioning pretty well, and I don’t see any immediate need to change anything, but maybe in the future I would work on the wg-portal project to try to remove the JavaScript dependency, and use HTMX (since I don’t have much experience with it yet), and maybe add more documentation about the whole Linux network setup and the assumptions/decisions made during the development of the Ansible roles.
-
Fun fact: The Fritz!Box has already implemented VPN support in the FRITZ!OS 7.50 Update -I think-, but it wasn’t stable, therefore leaked all the time, which disrupted the internet connection for all devices on the network, and I couldn’t get it to work with more than one WireGuard configuration at a time. ↩︎
-
Feel free to check the full install project repository for all of the code about automating the installation of all of my home devices, and the configure project repository for all of the code about configuring the devices. ↩︎
-
By modifying the script to also change the Pi-Hole configuration file when switching modes, and restarting the Pi-Hole FTL service, it would be possible to have the Pi-Hole use the VPN DNS servers when in VPN mode.
It would be a bit slow (due to Pi-Hole FTL restarts), but it also would mix the responsibilities of the gateway and Pi-Hole roles (which I didn’t want to implement to preserve the modularity, and locality of the roles). ↩︎