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:

May I proudly present the current iteration of my solution:

Encrypted Travel Wifi Setup Diagram

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.

Encrypted Travel Wifi Setup Mobile Phone

The photo above shows a mobile phones being used as the access device for the private router.

Encrypted Travel Wifi Setup Wired WAN

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

Requirements

tl;dr

Brief overview of what we are going to do:

Addressing

We want our addressing to be close to the one shown in the following graphic, just with different numbers of course:

Interfaces, Addresses and Prefixes

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

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:

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.

VPN Packet

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

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:

Encrypted Travel Wifi DNS Diagram

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:

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.

The private router in action!

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!