1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
|
---
title: My Home Network Setup
date: 2025-11-12
draft: true
---
# The Beginning
A little over a year ago, I replaced my Sony Android TV with an LG smart TV.
The LG TV is great in so many different ways, but since it's based on webOS it doesn't have VPN support.
Fortunately, I had a Raspberry Pi lying around, which I got years ago in a sale to tinker with.
After some time, I decided to repurpose the Raspberry Pi to function as my home network gateway,
providing both a [DNS sinkhole][2] and a VPN exit point to all devices on my home network (plus some other
functionalities).<br>
_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 VPN connection,
DNS filtering/caching, and other network services I wanted to implement.
From a network topology perspective, the Raspberry Pi would sit between the modem and the rest of my home network,
and be the only, default, and automatically configured gateway for all home network devices.
# The Draft
```mermaid
---
config:
theme: neutral
---
graph TD
Internet[Cat Memes] <--> ISP[ISP: 2.213.76.114]
Internet <--VPN mode--> VPN[VPN Provider: 10.2.0.2]
ISP <--> Modem[Modem: 192.168.0.1]
Modem <--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 following steps needed to be implemented:
1. The modem needs to be configured to use the Raspberry Pi as its default gateway.
1. A VPN client needs to be installed and configured on the Raspberry Pi.
1. A DNS sinkhole service needs to be set up on the Raspberry Pi.
1. The Raspberry Pi needs to be set up to forward traffic between its network/VPN interfaces.
1. Additional services like DHCP, firewall rules, and monitoring tools can be configured as needed.
## 1. The Raspberry Pi Setup
My Raspberry Pi is running Raspberry Pi OS Lite. I decided to use [Pi-Hole][3] as a DNS sinkhole,
[WireGuard][5] as a VPN client, and ProtonVPN as a VPN provider.
The Pi-Hole official setup automatically configures the Raspberry Pi as a DNS resolver for the whole network.
This poses a problem for the VPN, 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, which is not ideal, but _acceptable_ for my use case.
There are solutions to this problem:
- Use WireGuard's `PostUp` and `PostDown` scripts to change the Pi-Hole DNS servers when the VPN connects/disconnects.
- Use `systemd` services to automatically manage and switch the Pi-Hole DNS configuration based on the VPN state.
Finally, I also needed a simpler[^1] way to monitor the VPN connection status and to easily enable/disable
the VPN connections.
## 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. /s_[^2]
That means that many of the commonly used features in other routers that would help secure this setup won't work
on the Fritz!Box, so after many hours of trial and error, reading forums and sometimes 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][4].
# 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][^3], and
I had the perfect place to implement this project in a way that I could work with.
## The Pi-Hole installation
The bootstrapped RPI Pi-Hole role depends on [`systemd`][10] and [`network`][11] roles.
The `systemd` is required to manage the Pi-Hole FTL DNS service, and the `network` role is used to manage the whole
network configurations 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 configuration file ([`/etc/pihole/pihole.toml`][12])
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 from the VPN setup, so it can be used standalone if needed.<br>
Here is the full [Pi-Hole role implementation][13].
## The Gateway installation
The [gateway][14] 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.
1. Configures `sysctl` to enable IP forwarding.
1. Template the script that toggles the direct/VPN gateway mode.
1. Configure `udev` roles to trigger the `systemd` services when the VPN interface goes up/down.
1. 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 manually edit the WireGuard
configuration files, despite that this approach is (IMHO) way more complex.
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][15], 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][14], 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`
- NAT rule: Rewrites source IP of packets from `192.168.0.0/24` going 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][16] responds, let the packets come back.
### Direct mode `iptables` rules explained
Same rules, but routes through router (`eth0`) instead of VPN (`wg+`) interfaces.
# The Extras
- [wg-portal][17]: A simple web app to monitor and control the VPN connection status.
- [Prometheus][18] + [Grafana][19]: For monitoring the Raspberry Pi's performance and network statistics.
- [Nginx][20]: To serve all the web applications and dashboards.
# The Conclusion
I started this project out of curiosity and to learn more about networking, administration, and automation.
I could have just set up only the VPN or DNS sinkhole, but wanted to explore, experiment and challenge myself a little,
so it was to my extreme surprise that not only everything worked as expected, but that it also worked reliably and
stably for more than a few months now.
A part from rebooting the Raspberry Pi every once in a while to apply updates, 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 the VPN off while checking for flights and realizing that the prices were in GBP instead of EUR!
---
[^1]: Simpler in this case meaning not needing to go to my computer, open a terminal, ssh into the Raspberry Pi,
run some commands, check the status, and then run more commands to enable/disable the VPN, etc.
[^2]:_Fun fact: The Fritz!Box has already implemented [VPN support][8] in the FRITZ!OS 7.50 Update -I think-, but
I couldn't get it to work with more than one configuration, and it wasn't stable, and leaked all the time, which made
it pretty much useless for my use case._
[^3]: Feel free to check the full [install project repository][21] for all of the code about automating the installation
of all of my home devices, and the [configure project repository][22] for all of the code about configuring the devices.
[2]: https://en.wikipedia.org/wiki/DNS_sinkhole
[3]: https://pi-hole.net/
[4]: https://docs.pi-hole.net/routers/fritzbox/
[5]: https://www.wireguard.com/
[8]: https://fritz.com/en/apps/knowledge-base/FRITZ-Box-7583/3448_VPN-with-FRITZ-Box
[10]: https://git.sr.ht/~a14m/ansible-distro-configure/tree/358b8577af5feae702cb80bdc9b61ded0ae2c458/item/roles/systemd/tasks/main.yml
[11]: https://git.sr.ht/~a14m/ansible-distro-configure/tree/358b8577af5feae702cb80bdc9b61ded0ae2c458/item/roles/network/tasks/main.yml
[12]: https://git.sr.ht/~a14m/ansible-distro-configure/tree/358b8577af5feae702cb80bdc9b61ded0ae2c458/item/roles/pihole/templates/pihole.toml.j2
[13]: https://git.sr.ht/~a14m/ansible-distro-configure/tree/358b8577af5feae702cb80bdc9b61ded0ae2c458/item/roles/pihole
[14]: https://git.sr.ht/~a14m/ansible-distro-configure/tree/358b8577af5feae702cb80bdc9b61ded0ae2c458/item/roles/gateway
[15]: https://htmx.org/essays/locality-of-behaviour/
[16]: https://http.cat/
[17]: https://git.sr.ht/~a14m/wg-portal
[18]: https://git.sr.ht/~a14m/ansible-distro-configure/tree/358b8577af5feae702cb80bdc9b61ded0ae2c458/item/roles/prometheus
[19]: https://git.sr.ht/~a14m/ansible-distro-configure/tree/358b8577af5feae702cb80bdc9b61ded0ae2c458/item/roles/grafana
[20]: https://git.sr.ht/~a14m/ansible-distro-configure/tree/358b8577af5feae702cb80bdc9b61ded0ae2c458/item/roles/nginx
[21]: https://git.sr.ht/~a14m/ansible-distro-install
[22]: https://git.sr.ht/~a14m/ansible-distro-configure
|