/~/blog/ My Home (VPN) Network Setup
2025.11.13

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:

  1. The router needs to be configured to use the Raspberry Pi as its default gateway.
  2. A VPN client needs to be installed and configured on the Raspberry Pi.
  3. A DNS sinkhole service needs to be set up on the Raspberry Pi.
  4. The Raspberry Pi needs to be set up to forward traffic between its network/VPN interfaces.
  5. 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:

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:

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:

  1. Ensures that iptables, iproute2, and udev are installed.
  2. Configures sysctl to enable IP forwarding.
  3. Template the script that toggles the direct/VPN gateway mode.
  4. Configure (event-driven) udev rules to trigger the systemd services when the VPN interface goes up/down.
  5. Adds the systemd services that respond to the different udev events 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:

  1. Flush all existing iptables rules before applying new ones.
  2. apply_vpn_mode() - Routes local network through VPN
  3. apply_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

iptables -A OUTPUT -o wg+ -j ACCEPT

iptables -A FORWARD -s "$GATEWAY_SUBNET" -o wg+ -j ACCEPT

iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

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:
wg-portal 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.



  1. 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. ↩︎

  2. 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. ↩︎

  3. 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). ↩︎