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:

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?

Many Firewalls

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

  • 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.


  • A small linux-capable computer with integrated or attached wifi hardware, preferably a RaspberryPi 3 or C.H.I.P. 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!


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!


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

  • 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 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 build the Wireguard kernel module and userspace tools. For that, we need a library, some headers and some build 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
$ 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.


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:

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:

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?


At first, we have to globally enable routing by setting the corresponding variable in /etc/sysctl.conf:


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:

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:

  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 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


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

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


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
$ cd WireGuard/src
$ make
# make install
# modprobe wireguard

Again, consider adding a line reading wireguard to /etc/modules.


We will configure the private router’s end of the tunnel, the wg0 interface, using a configuration file (/etc/wg0.conf):

ListenPort = 4000

Endpoint =
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):



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:

  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

      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 2001:db8:aaaa:bbbb::1
Address: 2001:db8:aaaa:bbbb::1#53
[...] has IPv6 address 2400:cb00:2048:1::681c:25

If everything works fine, enable the daemon:

# systemctl enable unbound


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:


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


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 -j TCPMSS --set-mss 1360


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!

Feel free to comment by dropping me a line!

Link-local Addresses in struct sockaddr_in6 on Linux and OpenBSD

Some time ago I was porting a piece of IPv6-only network software from Linux to OpenBSD. This post is to explain the caveats of using struct sockaddr_in6 and its member sin6_scope_id. It turns out, OpenBSD does not play too well with sin6_scope_id and uses a rather odd method of populating the scope ID to user space.

Linux and sin6_scope

Let’s start with Linux and have a look at the documentation first. struct sockaddr_in6 and its members are described in manual page ipv6(7) on Linux as follows:

sin6_scope_id is an ID depending on the scope of the address. It is new in Linux 2.4. Linux supports it only for link-local addresses, in that case sin6_scope_id contains the interface index.

On Linux struct sockaddr_in6 looks like this:

struct sockaddr_in6 {
   sa_family_t     sin6_family;   /* AF_INET6 */
   in_port_t       sin6_port;     /* port number */
   uint32_t        sin6_flowinfo; /* IPv6 flow information */
   struct in6_addr sin6_addr;     /* IPv6 address */
   uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */

The scope ID is a 32 bit unsigned integer. It populates the interface index which is equivalent to the scope ID.

Example #1

For the first example let’s assume we have an interface with ID 3 and a link-local address of fe80::1. How about we fetch the link-local address of that interface? Even though it is not the best way to do it, for the sake of argument let’s say we use struct sockaddr_in6 to accomplish this.

We ask the kernel to fill the struct for us and this is what we get back in response:

  • sin6_family equals AF_INET6
  • sin6_scope_id equals 3
  • sin6_addr contains fe80::1 (as raw data)

No surprises here, right? Our software can safely rely on the contents of struct sockaddr_in6. Hooray!

OpenBSD and sin6_scope

On OpenBSD the same approach requires more attention from a software-porting developer. Let’s have a look at the documentation first, shall we? We find struct sockaddr_in6 on manual page inet6 of section 4 (drivers). Not quite were I expected it, but still a good place.

struct sockaddr_in6 {
	u_int8_t	sin6_len;
	sa_family_t	sin6_family;
	in_port_t	sin6_port;
	u_int32_t	sin6_flowinfo;
	struct in6_addr	sin6_addr;
	u_int32_t	sin6_scope_id;

First thing that pops out is that on OpenBSD, struct sockaddr_in6 has an additional member sin6_len. This make its use more versatile. It is irrelevant for the next example, though. However, despite of that the struct looks pretty much the same.

Example #2

Back to our software: Once again we ask the kernel to fill the struct for us and here is what we get back:

  • sin6_family equals AF_INET6
  • sin6_scope_id equals 0
  • sin6_addr contains fe80:3::1 (as raw data)

Wait. What? Yes, this can give you a headache when you encounter this for the first time. At least it gave me one. The OpenBSD kernel, for some odd reason, does not make use of sin6_scope_id but rather squishes the interface ID in the second group of the link-local IPv6 address. That is just ugly!

Is this a stupid bug, then?

Well, I’d say it is an ugly bug, but it is neither stupid nor doomed to fail. Technically, it is safe to store the scope ID in the address. However, developers porting software need to be aware of that and clean up the link-local address before using it. You would not want to use it in user space or present the address to a regular user without extracting the scope ID first.

Some people pointed out that this behavior does not go well with link-local addresses like fe80:aaaa:bbbb::1. In fact, IANA reserved the space of fe80::/10 for link-scoped unicast addresses, but only fe80::/64 must be used for actual link-local addresses.

Section 2.5.6 of RFC 4291 IP Version 6 Addressing Architecture clearly states this little known constraint to /64:

 Link-Local addresses are for use on a single link.  Link-Local
 addresses have the following format:

 |   10     |
 |  bits    |         54 bits         |          64 bits           |
 |1111111010|           0             |       interface ID         |

Feel free to add your comments!

Raising wifi security awareness using

I wrote a small python script used in a demonstration aimed at raising wifi security awareness amongst campus visitors. The script displays SSIDs sent out from the phones or other devices of people passing by. I found this script to be an useful eye catcher in awareness campaigns.

It leverages the scapy framework and is pretty easy to setup. A wireless card in monitor mode is basically all you need to get started.

Computer running

This is what it looks like when you speed things up a bit: in action

I recommend using it together with other demonstrations, e.g. a simple tcpdump and driftnet to show the benefit of encrypted HTTPS connections.

Wifi security awareness demonstration

You can grab a copy of at github.

Feel free to add your comments!

Running multiple instances of unbound daemon on OpenBSD

Here is my latest OpenBSD endeavor: Running multiple instances of the same daemon using different configuration files for each instance.

For the sixfw IPv6 firewall project we need multiple instances of the unbound resolver. We use address family translation (NAT64) for traffic passing some interfaces. For true v6-only networks and for the router itself, we don’t (or just can not) use address family translation. Therefore we need one resolver that does expose 64:ff9b::/96-based DNS RRs for some interfaces, and a second one that refrains from using its DNS64 superpowers at all.

Our policy here at sixfw is to stay as close to OpenBSD and its best current practices as possible. Here is how we dealt with the problem:

Make sure you have both config files ready, one for each instance.

# ls /var/unbound/etc/
unbound.conf    unbound64.conf

Now create a copy of the original daemon’s rc script. Avoid dashes in the name, underscores seem to be fine, though.

# cp /etc/rc.d/unbound /etc/rc.d/unbound64

Add a _flags variable for the new instance in /etc/rc.conf. It is important to define a separate config file for each instance.

unbound_flags="-c /var/unbound/etc/unbound.conf"
unbound64_flags="-c /var/unbound/etc/unbound64.conf"

Now both instances are ready to be fired up.

# /etc/rc.d/unbound start
# /etc/rc.d/unbound64 start

If you like to have the instances auto-started each time the system boots, enable them using rcctl:

# rcctl enable unbound   
# rcctl enable unbound64

In our case we additionally wanted to check if DNS64 is working as expected. Choose a hostname that is known to not have a AAAA RR, e.g. hosts of organizations that still think the Internet is Neuland. Now query the first instance for the RR:

# host ::1 has address

And the second instance, configured to use DNS64:

# host 2001:67c:26f4:a100::1 has IPv6 address 64:ff9b::2ef3:7e78 has address


Found a flaw or know a better way? Let me know!

Talking to Tay

Talked to Tay today, and I am not impressed. She does not get slang, fair enough. She has a concept of herself, but try to get her thinking about her name or even the number of characters it has. Her self-awareness drops significantly and she becomes a stupid teenager (expected but not impressive). Generally, the trickier the questions I asked, the more diplomatic she became. Mentioning her rising diplomatic behavior being perceived as awkward, she just replied “That is also true.” Sigh. When I told her, that her capabilities are lame, the dialog went:

Tay: “yee that’s life!”

Me: “Says the ai”

Tay: “no am just saying”

What to think about that? Not yet impressed, sorry. But we are getting there, I am sure!


Great, we, the Internet, turned Tay into a nightmare. Daniel Howells puts it well:

Tay shutdown