Building an encrypted travel wifi router
This article is about building a secure travel wifi router using a RaspberryPi and the Wireguard VPN protocol. It is a long and technical article describes how I stopped worrying about untrusted and insecure wifis in hotel rooms and conference venues.
The Problem
I travel a lot and therefore often rely on wifi provided in aircrafts, hotels or conference venues. Unfortunately, the state of security of those uplinks is worrying, connections are often buggy and rarely encrypted. A WPA2-protected wifi with pre-shared key (PSK) does not provide individual security. Everyone knowing the password can easily eavesdrop on all the traffic, not just their own. Only few sites offer more secure wifi, e.g. facilitating WPA enterprise and individual accounts.
Why don’t I just use a VPN on my devices then? Well, first I carry quite a few devices, and not all of them are capable of running a modern VPN. Secondly, some of them can not handle IPv6-only VPN connections. That’s a show stopper for me. Furthermore, many hotspots are protected by a captive portal that requires me to login to the portal on every device before I can establish a VPN tunnel. Given that I am allowed to connect more than one device at all. Even worse, some captive portals require re-authentication every 12 or 24 hours or whenever a devices re-enters the area of wifi coverage. The most important reason why I avoid using on-device VPN termination whenever possible is that devices can easily be tricked to circumvent the VPN connection for some traffic. The most harmless threat being DNS leakage, but more sophisticated attacks include fake proxy configuration, rogue routers and all sorts of MITM attacks on HTTPS and other protocols.
The Solution
I tried many different approaches to face the problem over the last couple of years.
Here are my findings on what a sufficient solution should be capable of:
- Provide a private, secure wifi for my devices.
- Private data passing the untrusted wifi must be encrypted.
- Do not leak any data from inside the secure wifi.
- Mitigate most common attacks by not trusting the untrusted wifi’s link layer at all.
- Provide a way to quickly (re-)authenticate on captive portals.
- On the untrusted wifi, act like any other of-the-shelf device. Appear to be normal :)
May I proudly present the current iteration of my solution:
Let’s go through the above network diagram from the bottom up. At first, we have the devices we want to securely connect to the Internet. That is, for example, a notebook, a phone and another random gadget. They join the private wifi provided by private router. The private router encrypts all traffic that is headed towards the Internet using a VPN. The encrypted traffic is then routed through the untrusted wifi (e.g. an open hotel wifi) via the access device. This can be a cheap smartphone or a pocket router. I strongly suggest using something with a screen and a browser, because the access device not only has to provide an attack-free link to the private router, but also needs to authenticate to all kinds of weird captive portals. Android 6.0 with automatic security patches is a good idea and has successfully been tested with this setup. For providing the uplink for the private router I recommend USB tethering. Not only does the USB cable charge the access device, it also provides enough freedom to place it somewhere where the untrusted wifi signal is strong.
We gain a security benefit from using a dedicated access device for shielding the untrusted wifi’s link layer from the private router. Sadly, many untrusted wifis are legacy-IP only, in such environments we pay for the benefit with an additional layer of NAT. However, more firewalls are better they said, right?
Back to topic: Once the encrypted traffic worked its way from the private router via the access device, through the untrusted wifi it finally reaches the Internet. Which, of course, we don’t trust either, although most of our packets from the private wifi will end up there eventually. Encrypted traffic finally hits the VPN server where it will be decrypted and routed properly (read: released into the wild, wild Internet).
Too abstract? Here are two possible setups for clarification.
The photo above shows a mobile phones being used as the access device for the private router.
Here I used a small OpenWRT router as access device for a wired, but untrusted network. I could have connected the private router directly to the wired network if it was a bit more trustworthy.
Let’s start tinkering! The remainder of this article describes a setup that
- protects agains eavesdropping on the untrusted wifi,
- circumvents device limits in the untrusted wifi,
- shields your devices from typical attacks against VPNs on the link-layer of the the untrusted wifi,
- and gives you access to the whole Internet in locations where they only have legacy IP and/or censorship.
Requirements
- A small linux-capable computer with integrated or attached wifi hardware, preferably a RaspberryPi. This will become the private router.
- A server, preferably a dual-stacked virtual instance running Debian Linux Jessie. This will become the VPN server.
- A spare Global Unicast /64 that is routed to the VPN server. We will use this prefix on the private wifi.
- A smartphone, preferably running a recent version of hardened Android. Beware of super-cheap devices, some of them perform terribly when running in tethering mode. You have been warned!
- Basic understanding of IP routing, policy routing, packet filtering and Linux CLI
- No fear to compile a Linux kernel module. Scared? Don’t be, it’s not that hard, really!
- Endurance, as this is not a 10 minute project, but it’s worth it!
tl;dr
Brief overview of what we are going to do:
- Addressing
- VPN server
- Wireguard
- VPN
- Routing
- Recursive, validating DNS
- Filtering
- Private router
- Wireguard
- VPN
- Private wifi
- Caching DNS Forwarder
- Routing
- Filtering
- Legacy IP (optional)
- Connect access device
- Celebrate!
Addressing
We want our addressing to be close to the one shown in the following graphic, just with different numbers of course:
For the in-tunnel addressing, basically a point-to-point connection, we use Unique Local Addresses (ULA). I
strongly suggest generating an individual pseudo-random RFC4193 prefix out of
fc00::/7
. Use this fancy online
tool from our friends at SixXS to generate yourself your very own prefix! I’ll be using
fd12:3456:7890::/48
for the remainder of this article. Please replace those
addresses accordingly.
The private wifi uses a slice of your Global Unicast prefix, whatever this may be. I happen
to have a /48
prefix, but heard from others that they got even bigger chunks
from their registry. No worries, a single, routable /64
is sufficient!
VPN Server
The VPN server
- takes care of routing the private wifi prefix through the tunnel to the private router,
- encrypts all packets entering the tunnel (from the Internet to the private router),
- decrypts all packets leaving the tunnel (from the private router to the Internet), and
- acts as a first line of defense for unwanted packets from the Internet.
We start with a fresh install of Debian Linux Jessie, for example on a small VM in a datacenter. Then we configure network connectivity, backup service and basic filter ruleset to our personal preferences. You probably have your own deployment and configuration method and tools, so I refrain from bugging you with basic system administrator tasks and just trust your workflow. At this point you should have the machine ready to be accessed via SSH and know how to gain superuser privileges.
Wireguard
Wireguard is a new, promising VPN protocol. After many years of working with OpenVPN, L2TP, IPsec and even SSH as VPN, working with Wireguard feels just awesome. It is simple, extremely reliable and it just works.
Here’s how the creators define their protocol:
WireGuard is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPSec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN.
As Wireguard is an in-kernel VPN implementation, it is either already part of your favorite distribution or you have to build it from source. We are using Debian Jessie on the VPN server, which means we have to install a backported kernel and build the Wireguard kernel module and userspace tools.
Add this line to /etc/apt/sources.list
:
deb http://ftp.de.debian.org/debian/ jessie-backports main
Then update the package list, install the latest kernel and reboot:
# apt-get update && apt-get dist-upgrade
# apt-get install -t jessie-backports linux-image-amd64
# reboot
Now we need some headers and tools:
# apt-get install libmnl-dev linux-headers-amd64 build-essential git
Now it’s time to grab a copy of the source and compile it.
$ git clone https://git.zx2c4.com/WireGuard
$ cd WireGuard/src
$ make
If everything went well, we can install the module and tools:
# make install
With modprobe wireguard
we load the module into the running kernel. By adding
a line reading wireguard
to /etc/modules
the system does this
automatically after the next reboot.
VPN
Head over to the Wireguard website and browse through the documentation to make yourself comfortable with the concept. Wireguard is a crypto-routing, in-kernel, device-based VPN technology. If you have a hard time understanding what this means, consider giving the documentation another shot. It took me a while to grasp how nice and fancy the approach is compared to other VPN technologies. Especially, if one plans to establish mostly static routes, like we will in the upcoming sections.
I assume we are all set and ready for our first Wireguard tunnel? Let’s do it!
First step in asymmetric cryptography is always to generate a key pair. We do this by
creating a private key and then deriving a public key from it. Wireguard’s own userspace
tool wg
takes care of this:
$ wg genkey > server_private.key
$ wg pubkey > server_public.key < server_private.key
The first key pair is meant to be used for the server. To proceed with this article, we need the router’s public key, so we will generate the key pair right now:
$ wg genkey > router_private.key
$ wg pubkey > router_public.key < router_private.key
I usually suggest storing the key pair only on the same system on which it is used. Please
transfer the private key securely to the private router later and remove it from the VPN
server (e.g. use shred
).
This should leave us with two files for the server and another two files for the private router. They contain the VPN server’s private and public key. Make sure you don’t confuse these two! The tunnel won’t work if the private and public keys of the endpoints are not correctly distributed!
The tunnel endpoint will be the wg0
interface, which we need to configure. I
prefer using the config files instead of the very long CLI commands of wg
. So,
here is the first part of the /etc/wg0.conf
file:
[Interface]
PrivateKey = VPN_SERVER_PRIVATE_KEY
ListenPort = 500
We usually don’t run services on privileged ports unless necessary, and yet here I am using port 500. Why is that? Well, my argument goes like this:
- This is an in-kernel VPN protocol, so opening port 500 does not require any additional capabilities to the ones the kernel already has. Which is ALL THE CAPABILITIES :)
- UDP port 500 is commonly used for IPSec, which increases the chances that this port is not blocked in an maybe restricted wifi.
The second part of the /etc/wg0.conf
file looks like this:
[Peer]
PublicKey = PRIVATE_ROUTER_PUBLIC_KEY
AllowedIPs = fd12:3456:7890::2/128, 2001:db8:aaa:bbbb::/64
What may be confusing when done the first time is the AllowedIPs
directive.
Let me go into detail here, as it is essential for secure crypto-routing that we filter for
source addresses. When a packet enters the tunnel, it gets encrypted and becomes the
payload of a Wireguard packet, which itself is the payload of a UDP datagram which
in turn is the payload of an IP packet. For the sake of simplicity, let’s ignore the UDP
header for a moment, as it does not add any value to the discussion.
So, we have our Wireguard packet coming in from another endpoint, and the payload is the
original IP packet. Without AllowedIPs
, we would decrypt the payload, thus get
the original packet, and route it according to our routing table. How could we know the
source address of the original packet wasn’t spoofed? Do we trust our endpoint that much?
Probably not! This is why we put some restrictions on the original packet’s source address
using AllowedIPs
. Even if the encrypted packet authenticates and decrypts
properly, we would not route it unless its payload (read: the original packet) came from
within an allowed prefix.
Now it’s time to tell the system to bring up the wg0
interface on boot. A
quite convenient way is adding a corresponding section to
/etc/network/interfaces
:
auto wg0
iface wg0 inet6 manual
pre-up ip link add dev wg0 type wireguard
This creates the interface using ip
(a userspace tool for the kernel’s
RTNETLINK
API). The interface, however, will still lack some essential
information, e.g. IP address and Wireguard-specific configuration data. The IP address can
be set using ip
even before the interface comes up:
pre-up ip address add fd12:3456:7890::1 peer fd12:3456:7890::2 dev wg0
And we can also apply the /etc/wg0.conf
configuration file while the interface
is still down:
pre-up wg setconf wg0 /etc/wg0.conf
The next directive actually brings up the interface we just configured:
up ip link set up dev wg0
We can also explicitly allow IP forwarding on the new interface. This step may or may not
be required, depending on your sysctl.conf
settings.
post-up echo 1 > /proc/sys/net/ipv6/conf/wg0/forwarding
It is good style to remove the wg0
interface on shutdown. That may also
prevent hard to debug errors in some cases.
down ip link del dev wg0
The complete section looks like this:
auto wg0
iface wg0 inet6 manual
pre-up ip link add dev wg0 type wireguard
pre-up ip address add fd12:3456:7890::1 peer fd12:3456:7890::2 dev wg0
pre-up wg setconf wg0 /etc/wg0.conf
up ip link set up dev wg0
post-up echo 1 > /proc/sys/net/ipv6/conf/wg0/forwarding
down ip link del dev wg0
Remember to set appropriate file permission for all files containing private key data! From
now on, the wg0
interface should come up right after the system boot. Why
don’t you try it out now?
Routing
At first, we have to globally enable routing by setting the corresponding variable in
/etc/sysctl.conf
:
net.ipv6.conf.all.forwarding=1
After that we apply the change:
# sysctl -p
The next step is to add routes for the private wifi, because devices in that network will
want to receive packets via the VPN tunnel. There are plenty of places where routes may be
set up. On Debian-based distributions I prefer to add the routes when the related interface
comes up. That’s best done using the post-up
directive in
/etc/network/interfaces
:
auto wg0
iface wg0 inet6 manual
✂️
post-up ip route add 2001:db8:aaaa:bbbb::/64 via fd12:3456:7890::2 dev wg0
That’s it for routing on the VPN server. The rest will be taken care of by the default and interface routes. Easy, wasn’t it?
Recursive, validating DNS
The Domain Name System (DNS) is an essential part of connectivity. Inside the private wifi we want a DNS server that
- responds fast,
- validates resource records,
- does not make excessive use of the maybe limited hotel wifi bandwidth, and
- prevents DNS leaks to protect our privacy.
To have a fast response time, the DNS server should cache results from previous queries, serving them directly from the private router to the connected clients. The DNS server on the private router must not resolve recursively, as it can produce a lot of back and forth traffic. Bandwidth and latency may be suboptimal in the typical hotel wifi situation. Validating resource records can be done by using DNSSEC, but adds some extra data that needs to be fetched.
I came up with this diagram to solve the problem:
This DNS setup uses two DNS servers, one on the private router and another one on the VPN server. It has the nice advantage that we can run the bandwidth-heavy, latency-critical and computing operations on the server, which is expected to be better connected and also more powerful than the private router. The on-server DNS instance takes care of resolving recursively, validating and some caching, the local DNS server on the private router just forwards queries and caches the responses. Since the connecting between these two DNS servers happens to be inside the tunnel, we consider the responses from the recursive server trusted (as in: not modified during transit, no need to run DNSSEC again). The tunnel also prevents DNS leakage.
To install the DNS daemon on the VPN server just run:
# apt-get install unbound
The configuration file /etc/unbound/unbound.conf
I used looks like this:
server:
num-threads: 4
interface: ::0
interface-automatic: no
max-udp-size: 3072
access-control: ::0/0 refuse
access-control: ::1 allow
access-control: fd12:3456:7890::2/128 allow
harden-glue: yes
harden-dnssec-stripped: yes
harden-referral-path: yes
unwanted-reply-threshold: 10000000
val-clean-additional: yes
val-permissive-mode: yes
val-log-level: 1
cache-min-ttl: 1800
cache-max-ttl: 14400
prefetch: yes
prefetch-key: yes
This configuration tells unbound
to listen on any interface, but to only allow
queries from localhost (::1
) and the private router via VPN
(fd12:3456:7890::2/128
). Modify the file according to your addressing scheme.
Then start the daemon by running:
# systemctl start unbound
To test the setup just run a query against the server:
$ dig www.danrl.com. SOA +dnssec @::1
The answer should contain ad
flags, look for something like this:
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57665
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1
If everything is fine, we enable the DNS daemon permanently:
# systemctl enable unbound
Filtering
Let me say a few words regarding filtering first: Filter rules are constantly evolving as new attacks and threats appear or protocols develop. Proper filter rule management is therefore a must-have for all systems we are responsible for. Furthermore, filter rules are not pure science but are also highly influenced by what one considers best practice. I have seen many well-thought-through filter rule sets, but I rarely see two that are the same. This leads me to the conclusion that filtering is sometimes more art than science and everyone has personal preferences on how rules should be ordered or look like. I will assume that you set up your own basic filtering right after you installed the operating system and that you know best how you want to manage your rules. That said, we will only discuss rules here that are specific for the problem we are solving. You are expected to add the discussed rules to you existing rule set where you think they are placed best.
If you already have connection tracking in place, please skip the next rule. Otherwise just
add the following rules to the INPUT
and FORWARD
chains at a very
early stage.
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
We have to allow incoming Wireguard packets, remember port 500?
-A INPUT -p udp -m udp --dport 500 -m conntrack --ctstate NEW -j ACCEPT
I suggest putting this rule in the legacy IP filter as well. It will allow the tunnel to operate on legacy IP, which is often the only protocol that is available in some places. As of 2016, the market penetration of state-of-the-art IP in hotel and venue wifis is still shamefully low. We have to do better, folks! But that’s another (long) story…
We run a recursive DNS service on the VPN server to provide validated resource records (RR) for the private router. We should allow the private router to talk to the DNS service to make them work:
-A INPUT -s fd12:3456:7890::2/128 -p tcp -m tcp --dport 53 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -s fd12:3456:7890::2/128 -p udp -m udp --dport 53 -m conntrack --ctstate NEW -j ACCEPT
On a side note, if TCP port 53 sounds odd to you, it is well inside the bounds of specification. It wasn’t used widely during the legacy IP era and before DNSSEC became (somewhat) popular.
To make life a bit easier, especially when debugging, we allow forwarding of packets that stay in the tunnel (if they hit the VPN server at all).
-A FORWARD -i wg0 -o wg0 -m conntrack --ctstate NEW -j ACCEPT
Finally we want to allow forwarding of packets from the private wifi to the Internet.
-A FORWARD -s 2001:db8:aaa:bbbb::/64 -i wg0 -o eth0 -m conntrack --ctstate NEW -j ACCEPT
That’s it from filtering for now.
Private Router
The private router provides the private wifi and acts as the client side of the tunnel.
I chose a RaspberryPi 3 as hardware platform for the private router, because it has a built-in wifi chip. Other platforms work well, too. I had this setup working on much smaller devices, too, e.g. an OpenWRT-capable router of the size of an USB thumb drive.
For the operating system I used Raspbian Lite as provided by the RaspberryPi Foundation. To
operate, the private router needs an uplink, which can be provided either via Ethernet or
USB tethering. Therefore we configure the corresponding interfaces to automatically gain
connectivity. We add the following lines to /etc/network/interfaces
:
allow-hotplug eth0
iface eth0 inet dhcp
allow-hotplug usb0
iface usb0 inet dhcp
Wireguard
The Wireguard installation is quite similar to the one we performed on the VPN server:
# apt-get install libmnl-dev linux-headers-rpi build-essential git
$ git clone https://git.zx2c4.com/WireGuard
$ cd WireGuard/src
$ make
# make install
# modprobe wireguard
Again, consider adding a line reading wireguard
to /etc/modules
.
VPN
We will configure the private router’s end of the tunnel, the wg0
interface,
using a configuration file (/etc/wg0.conf
):
[Interface]
PrivateKey = PRIVATE_ROUTER_PRIVATE_KEY
ListenPort = 4000
[Peer]
PublicKey = VPN_SERVER_PUBLIC_KEY
Endpoint = vpn.example.com:500
AllowedIPs = ::/0
PersistentKeepalive = 21
The ListenPort
directive has a sometimes misleading name. Wireguard allows
configurations that mock the more common client server model. In that case,
ListenPort
on the client becomes the source port of outgoing packets.
Technically, the Wireguard module is also listening on this port, but let’s ignore this
fact for the moment. In our case ListenPort
will become the outgoing port and
our filter rules will prevent any incoming packets that are not covered by connection
tracking. The directive Endpoint
expects the hostname of the VPN server
followed by a colon and the port number. Since we want to receive packets from the Internet
through the tunnel, we set AllowedIPs
to ::/0
.
The wg0
interface on the private router is similar to the one on the VPN
server, just with opposite adressing. We add the interface configuration to
/etc/network/interfaces
:
auto wg0
iface wg0 inet6 manual
pre-up ip link add dev wg0 type wireguard
pre-up ip address add fd12:3456:7890::2 peer fd12:3456:7890::1 dev wg0
pre-up wg setconf wg0 /etc/wg0.conf
up ip link set up dev wg0
post-up echo 1 > /proc/sys/net/ipv6/conf/wg0/forwarding
down ip link del dev wg0
After that we can fire up the interface using Debian’s ifup
scripts:
# ifup wg0
Private Wifi
The RapsberryPi has built-in wifi that is compatible with hostapd
, we can run
a software access point on it. Hooray!
Here is how my /etc/hostapd/hostapd.conf
looks like (except
wpa_passphrase
of course):
interface=wlan0
hw_mode=g
channel=10
ieee80211d=1
country_code=US
ieee80211n=1
wmm_enabled=1
ssid=privatewifi
auth_algs=1
wpa=2
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
wpa_passphrase=bratwurstundsauerkraut
There is no need to start hostapd
on system boot. The ifup
scripts can take care of that when the wlan0
interface comes up. In my
experience, the daemon comes up more smoothly this way. Now is also a good time to
configure addressing on wlan0
in /etc/network/interfaces
:
auto wlan0
iface wlan0 inet6 static
hostapd /etc/hostapd/hostapd.conf
address 2001:db8:aaaa:bbbb::1
netmask 64
Let’s fire up the interface and test our configuration:
# ifup wlan0
You should now be able to see and join the SSID privatewifi with your favorite
device. However, joining may fail due to a lack of addressing. We need to distribute router
advertisements to give joining devices a chance to learn about the on-link prefix. I may be
biased towards the awesome
ratools
regarding this task 😉. However, as of August 2016,
ratools
is not available in Debian’s repositories and would require
installation from source. To not make things more complicated as they already are, let’s
stick with radvd
which is old but mature:
# apt-get install radvd
Configuration for radvd
takes place in /etc/radvd.conf
:
interface wlan0 {
IgnoreIfMissing on;
AdvSendAdvert on;
MaxRtrAdvInterval 300;
AdvLinkMTU 1423;
prefix 2001:db8:aaaa:bbbb::/64 {
AdvOnLink on;
AdvAutonomous on;
AdvValidLifetime 3600;
AdvPreferredLifetime 1800;
};
RDNSS 2001:db8:aaaa:bbbb::1 {
AdvRDNSSLifetime 1800;
};
};
This configuration just works and advertises reasonable values, although there is some room
for improvements. You can play around with MaxRtrAdvInterval
to directly save
airtime or AdvPreferredLifetime
and AdvRDNSSLifetime
to
indirectly save airtime by influencing client behavior.
Our Wireguard tunnel has a MTU of 1423 octets, and since we are going to push almost
everything from the private wifi through the tunnel, we should advertise this limitation.
This is why I put in the AdvLinkMTU
option.
Please note that we already advertise the resolving DNS server here, which we will install and configure in the next step.
Caching DNS Forwarder
Let’s set up the forwarding and caching DNS server we just talked about. Again,
unbound
is our friend:
# apt-get install unbound
The /etc/unbound/unbound.conf
configuration file looks a bit different this
time:
server:
num-threads: 4
interface: 2001:db8:aaaa:bbbb::1
interface-automatic: no
access-control: ::0/0 refuse
access-control: 2001:db8:aaaa:bbbb::/64 allow
cache-min-ttl: 1800
cache-max-ttl: 14400
prefetch: yes
forward-zone:
name: "."
forward-addr: fd12:3456:7890::1
The most important part is everything below forward-zone
. The dot means
all zones and forward-addr
is the upstream DNS server to which we
forward requests to. Make sure it is one of the listening addresses from the VPN server’s
unbound.conf
file.
Time to start the daemon:
# systemctl start unbound
And now, testing! Resolving a domain using our new DNS server should look something like this:
$ host www.danrl.com 2001:db8:aaaa:bbbb::1
✂️
Address: 2001:db8:aaaa:bbbb::1#53
✂️
www.danrl.com has IPv6 address 2400:cb00:2048:1::681c:25
If everything works fine, enable the daemon:
# systemctl enable unbound
Routing
Routing on the private router is slightly more complicated than on the VPN server. We have to use policy routing to make sure a packet never leaves our trusted networks, which are the tunnel and the private wifi. Even if a better route exists, the kernel must not forward any packet from a trusted network to an untrusted one.
First we have to enable forwarding via /etc/sysctl.conf
:
net.ipv6.conf.all.forwarding=1
And apply the change:
# sysctl -p
We will be using a custom routing table for the private wifi, as we don’t want packets from
there to use the system’s main routing table. This allows us to route everything coming
from the private wifi through the VPN tunnel, even though the system uses other routes for
its own packets. Create a custom routing table by adding the line 200
privatewifi
to /etc/iproute2/rt_tables
. The file should look something
like this afterwards:
# reserved values
255 local
254 main
253 default
0 unspec
# custom
200 privatewifi
This ensures the custom routing table will be created after the next reboot.
Now we have a custom route table, but it lacks content. It is just empty:
# ip route show table privatewifi
[nothing to see here]
Here are the requirements for our custom routing:
- We want the custom table to be flushed before the VPN tunnel comes up, so that we start with a clean table every time the VPN flaps (if it flaps at all).
- We want the default route to point to the VPN server’s
wg0
interface. This is the server’s in-tunnel address if you will. - We want the interface route (on-link prefix) of interface
wlan0
to be present in the table, so that local packets do not get routed away. Yes, sounds strange, but that does happen if the interface route is missing. Policy routing and custom routing tables are tricky sometimes. - We want to force every packet that wants to leave the private wifi to use our custom routing table. Here is where policy routing jumps in.
Phew! If that’s a bit too much to comprehend, just go through the bullet points one more time and draw the situation with pen and paper. It’s OK to get confused when dealing with policy routing and multiple routing tables. Even the experts make terrible mistakes applying this magic sometimes 🤕
Here is how I implemented policy routing on the private router. I like to keep the rules
separated by interface, and I also like to place them in
/etc/network/interfaces
:
iface wg0 inet6 manual
✂️
post-up ip route flush table privatewifi
post-up ip route add default via fd12:3456:7890::1 dev wg0 table privatewifi
iface wlan0 inet6 static
✂️
post-up ip -6 route add 2001:db8:aaaa:bbbb::/64 dev wlan0 table privatewifi
post-up ip -6 rule add from 2001:db8:aaaa:bbbb::/64 lookup privatewifi
Filtering
Again, I assume we have a decent basic filtering set up. The following rules allow clients on the wifi to access the caching DNS forwarder. Since DNS queries and responses can be quite large these days, we also have to consider that some clients may ask using TCP.
-A INPUT -s 2001:db8:aaa:bbbb::/64 -p udp -m udp --dport 53 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -s 2001:db8:aaa:bbbb::/64 -p tcp -m tcp --dport 53 -m conntrack --ctstate NEW -j ACCEPT
Finally, clients may want to access the Internet. We therefore allow packets coming from inside the private wifi to be forwarded to the VPN server using the Wireguard interface wg0. It is very important that we do not allow any other outgoing interface! Having a strict forwarding rule prevents leaks caused by wrong routing. Wrong routing can happen if we made a mistake at the policy routing stage or if someone successfully injects wrong routes, e.g. via a compromised access device. Also, if the private router used without the additional protection of an access device, route injection becomes a more likely attack. So, here is the corresponding rule:
-A FORWARD -i wlan0 -o wg0 -s 2001:db8:aaa:bbbb::/64 -m conntrack --ctstate NEW -j ACCEPT
Connect Access Device
It’s simple, just connect your access device, e.g. the suggested Android smartphone, to the private router and start enjoying your encrypted wifi with secured Internet access.
A longer version is available, too.
Legacy IP (optional)
To access the legacy Internet one could set up NAT64 (preferred) or run the whole setup dual-stacked. If you like to run a dual stack network, you just have to repeat the above steps involving IP addresses using legacy IP addresses and legacy networks instead. It is pretty straightforward, except one caveat: ICMPv4 path MTU discovery in this VPN setup is not working well with some legacy-only servers and websites (they still exist!). Some packets may be dropped just because of their size, with no way for a device connected to the private wifi to determine the right packet size. A quick and dirty fix is to mangle legacy TCP connections and force a lower maximum segment size (MSS) on them.
On the VPN server add this line to your filter rules:
-t mangle -A FORWARD -o wg0 -p tcp -m tcp --tcp-flags SYN,RST SYN -s 100.120.0.0/24 -j TCPMSS --set-mss 1360
Celebrate!
Here is my private router in action at the Detroit Metropolitan Airport, providing secure wifi for my gadgets while waiting for my flight to DEFCON.
Update (July 2018)
I receive quite a few emails on the topics of OpenWrt and WireGuard every week. Unfortunately, I do not have the time to answer all of them individually. So I kindly ask you to direct questions regarding WireGuard and OpenWrt/LEDE to the OpenWrt Forums or to the WireGuard Mailing List. There the questions will be exposed to a wider audience and may additionally help other people facing the same challenges. Thank you!