VPN-Gateway with NAT


Initial Situation

A friend of mine has an Office in the Basement of his home. He quite successfully manages his IT himself but every now and then he needs a helping hand, just as we all do. For this Situations I had a VPN into his Office for quite some time.

After he switched Internet Providers we found that his new connection does not come with a Public IPv4 Address but instead his router receives a private IP-Address and his provider then does Carrier-Grade NAT. He also receives a public IPv6 Subnet which is publicly routed.

My friend has a AVM Fritz!Box, which is quite a versatile Modem/Router/Phone System/Wifi-AP/Dect-AP in itself, but it has one really huge drawback: It only support IPSec VPN.

AVMs VPN Implementation does not allow acting as a VPN-Server on an IPv6-Only Connection (according to this Info-Page):

IPv6 VPN connections are not supported by the FRITZ! Box, since no IPv4 data can be transmitted over them. In addition, such IPv6 connections could only be made if both subscribers have IPv6 Internet connectivity, which in most mobile networks and Wi-Fi hotspots is not the case.

Running an OpenVPN-Server On-Site

My first reaction was keeping the VPN-Server On-Site but switching to OpenVPN, because I knew that OpenVPN could tunnel IPv4 over IPv6. One of the devices in his Office, a Synology Disk-Station is able to act as an OpenVPN server and so –in a pinch– we went with that.

Although that simple Setup worked okay for some time, it had some annoying problems:

  • Now his Storage is connected to the Internet and acts as a VPN-Gateway
  • The Disk-Station can't NAT VPN connections, so that one has to do SSH-Hopping and SSH Port-Forwarding tricks to reach all devices in the Network
  • The Public IPv6 Net changes every now and then and so does the Disk-Station's IPv6 Address, so some DynDNS-Tricks are required to always know the Server's address
  • Connections are only possible from networks with working IPv6 connectivity, which unfortunately rules out my work Network. Meh…

A more robust solution

With this solution only working half of the time I decided I wanted a more robust solution, that would possibly also scale to other friends' networks.

I wanted a solution that

  • Is reachable via IPv4 and IPv6
  • Has a static Connection-Address
  • Does not require any configuration on the target Network
  • Thus allows the target Networks' owner to switch Uplink-Providers or physically move without me knowing.

What I came up with was an OpenVPN-Server running on my VPS hosted by NetCup which provides stable IPv4 and IPv6 connectivity with static IPs. On-Site I used an TP-Link TL-WR841N v13 Router because it resells at only about 16€. (!) The v13 is only supported in recent OpenWRT Snapshots any may or may not work as expected for you. If you want to be sure, you might want to get yourself a TL-WR1043ND which resells at around 32€ but is completely and officially supported.

The On-Site OpenWRT Router gets its Uplink via DHCP from whatever Router is present on the Network and acts as an OpenVPN Client through that Internet Connection. As long as it can connect to the OpenVPN-Server I can SSH into it and examine what's going on in the target network.

Setting up the OpenVPN Server

Setting up the OpenVPN-Server on Debian is quite well documented on the Debian-Wiki and I basically followed it to the word.

One thing I changed was using the name of my friends business (HTDuS) in all configuration- and folder-names, so that I can easily add another network in the future. Also I chose a non-default port and a usefuk tun-Device-Name.

; general server stuff
mode server
local ::0
port 1200
proto udp
dev tun_htdus

; directories
chroot /etc/openvpn

; mtu
link-mtu 1400
mssfix 0 ; disabled

; certs
ca ./easy-rsa-htdus/keys/ca.crt
cert ./easy-rsa-htdus/keys/mazdermind.de.crt
key ./easy-rsa-htdus/keys/mazdermind.de.key  # This file should be kept secret
dh ./easy-rsa-htdus/keys/dh2048.pem

; verify
crl-verify ./easy-rsa-htdus/keys/crl.pem
tls-auth ./easy-rsa-htdus/keys/ta.key 0
remote-cert-tls client
persist-key
persist-tun

; crypto
cipher AES-128-CBC
auth SHA512
tls-cipher "DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA"
tls-server

; server options
server 10.97.0.0 255.255.255.0
client-config-dir clients-htdus
keepalive 10 30
client-to-client
;mode server
topology subnet

user nobody
group nogroup

; logging
ifconfig-pool-persist ipp-htdus.txt
status /etc/openvpn/status-htdus.txt
#log /var/log/openvpn-htdus.log
verb 3

/etc/openvpn/htdus.conf

As a VPN-Net I chose 10.97.0.0/16. This is the IP-Pool the VPN Clients will be assigned an IP from. I basically wanted it to be visually far away from the IP-Range used in the target Network, which is 192.168.124.0/16, so I can easily identify them if I have to.

The client-config denotes which routes should be announced to the default_clients who's responsible for them (in this case: the OpenWRT):

root@mazdermind /etc/openvpn/clients-htdus $ ls -la
total 16
-rw-r--r-- 1 root root  102 Jun 13 10:56 default_clients
lrwxrwxrwx 1 root root   15 Jun 10 23:14 ray -> default_clients
lrwxrwxrwx 1 root root   15 Jun 10 23:14 spock -> default_clients
-rw-r--r-- 1 root root   70 Jun 13 10:55 wrt

root@mazdermind /etc/openvpn/clients-htdus $ cat wrt
iroute 192.168.124.0 255.255.255.0

root@mazdermind /etc/openvpn/clients-htdus $ cat default_clients 
push "route 192.168.124.0 255.255.255.0 10.97.0.1"

As you can see you can freely route some Subnets to one Client and other Nets to other Clients to support, for example, multiple Locations.

For the Clients I generated Keys with Passphrases, while I generated one without for the WRT, because I wanted it to connect on Power-Up without anyone being there to enter a Passphrase.

If you have an iptables-Firewall on your Server (you should!) you will need to allow the incoming VPN Connections:

sudo iptables  -A INPUT -i eth0 -p udp -m udp --sport 1024:65535 --dport 1200 -m comment --comment "openvpn htdus" -j ACCEPT
sudo ip6tables -A INPUT -i eth0 -p udp -m udp --sport 1024:65535 --dport 1200 -m comment --comment "openvpn htdus" -j ACCEPT

You can now enable and start the OpenVPN-Server as follows:

sudo systemctl enable openvpn@htdus.service
sudo systemctl start openvpn@htdus.service
sudo systemctl status openvpn@htdus.service

Generating Client-Configs

Testing the Connection requires generating client-Config-Files. I prefer the Config-Style with embedded keys, so I wrote me a small Shell-Script which generates the Client-Configs for me:

#!/bin/sh
set -e

CLIENT="$1"

if [ ! -e "keys/$CLIENT.key" ]; then
  echo "empty or invalid client: '$CLIENT'"
  exit 1
fi

cat <<EOT
#viscosity name HTDuS

; general
client
dev tun_htdus
topology subnet

; connection
remote mazdermind.de
port 1200
proto udp
keepalive 10 30
resolv-retry infinite
link-mtu 1400
mssfix 0
nobind
max-routes 1000

; crypto
cipher AES-128-CBC

; certificates
key-direction 1

; auth
tls-cipher "DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA"
tls-client
auth SHA512
remote-cert-tls server

; logging
;verb 0
EOT

echo '<ca>'
cat "./keys/ca.crt"
echo '</ca>'

echo '<cert>'
cat "./keys/$CLIENT.crt"
echo '</cert>'

echo '<key>'
cat "./keys/$CLIENT.key"
echo '</key>'

echo '<tls-auth>'
cat "./keys/ta.key"
echo '</tls-auth>'

/etc/openvpn/easy-rsa-htdus/generate-config-for-client.sh

Isolating the Host from the VPN

With suche a Client-Config you can now connect to the VPN from any Linux-System by simply calling sudo openvpn --config my-conf.ovpn. You'll get an Interface named tun_htdus and an IP from the 10.97.0.0/16 range and routes pointing 10.97.0.0/16 and 192.168.124.0/16 via the VPN-Server 10.97.0.1 into the tun_htdus-Interface.

You can now ping 10.97.0.1 which is you VPN-Server. Yay!

But depending on what your Server hosts, you can now also reach a lot of services on your Host-System by just adding appropriate routes on your client into the tun-device.

In my case I have some Docker-Container running on the host and Docker uses 172.17.0.0/16 for its internal containers. Simply adding this to the routes on the client would expose the internal Docker-Containers network' to all VPN Clients.

So some further iptables-Rules are required:

sudo iptables  -I INPUT 0  -d 10.97.0.0/16 -i tun_htdus -p icmp -m icmp --icmp-type 8 -m comment --comment "allow ipv4 pings from vpn to host on vpn-net" -j ACCEPT
sudo iptables  -I INPUT 1 -i tun_htdus -p tcp -m comment --comment "reject remaining tcp vpn-to-host" -j REJECT --reject-with tcp-reset
sudo iptables  -I INPUT 2 -i tun_htdus -p udp -m comment --comment "reject remaining udp vpn-to-host" -j REJECT --reject-with icmp-port-unreachable

sudo ip6tables -I INPUT 0 -i tun_htdus -p tcp -m comment --comment "reject remaining tcp vpn-to-host" -j REJECT --reject-with tcp-reset
sudo ip6tables -I INPUT 1 -i tun_htdus -p udp -m comment --comment "reject remaining udp vpn-to-host" -j REJECT --reject-with icmp-port-unreachable

They need to be on top of the INPUT-Chain, or some ACCEPT rules which do not explicitly specify interfaces or ip-ranges further down might allow the traffic anyway. I had, for example, some rules like those, which, unexpectedly allowed HTTP-Traffic from the VPN to Docker-Containers running on the Host.

sudo iptables -A INPUT -p tcp -m tcp --sport 1024:65535 --dport 80 -m comment --comment "allow http" -j ACCEPT

I added an explicit -i eth0 to them, to further harden the Firewall-Setup.

One thing to be aware of while configuring you Firewall: client-to-client in the OpenVPN Server-Config allows VPN-Clients to communicate with each other without the Packagets being processed by the OpenVPN Servers' Kernel, so they are not affected by iptables and do not require FORWARD rules.

This is great I you don't want to have the Host interfere with the OpenVPN but removes some control from your fingertips. Just be aware of this option when you need more control about which Client should be able to communicate with which net.

Installing OpenWRT

Setting up OpenWRT depends on which router you got. For the TL-WR841N v13 look up the TFTP Firmware-Link from the OpenWRT-Wiki, connect the TP-Link and your Computer to an Ethernet-Switch, add 192.168.0.66/24 to your Interface, start an TFTP-Server and make it serve the Image as wr841nv13_tp_recovery.bin. Hold the Reset-Key on the WRT and power it on. It fetches the image and Flashes itself.

This procedure is not trivial and it might need multiple tries and debugging to get it going. Most WRTs can usually be also flashed via the vendors Web-UI but this specific firmware-Version rejects non-vendor updates.

Give your self some time and search the Internet for hints, if it doesn't work. You can not really brick these devices, because the TFTP Recovery Mode is burned into their ROM and can not be over-flashed.

After Flashing the Device it will fetch an IP-Adresse via DHCP and open SSH with Username root and no Password. You can then install LUCI, the WebUI by calling opkk update; opkg install luci.

Configuring the OpenWRT

At this point you should be able to connect multiple Computers to the VPN and communicate between them using their 10.97.0.0/16 IPs. They should all have a route to 192.168.124.0/16 through the VPN but obviously nothing is responding to those Packets.

To get OpenVPN running on the WRT you need to install the required Packages: ovpn install openvpn-openssl luci-app-openvpn should give you what you need.

I chose to not migrate my working OpenVPN-Config to the uci-Syntax, so I uploaded my working OpenVPN Client-Config to the WRT as /etc/openvpn/htdus.ovpn and configured UCI via the SSH-Terminal as follows:

uci set openvpn.htdus='HTDuS'
uci set openvpn.htdus.enabled='1'
uci set openvpn.htdus.config='/etc/openvpn/htdus.ovpn'
uci commit

The LUCI-UI shows the VPN and can control it, although the Settings will not be editable via the UI.

The OpenWRT should now have an Interface tun_htdus with one of the 10.97.0.0/16 and if one of the other clients sends Packages to the 192.168.124.0/16 net, the OpenVPN Server will forward them to the OpenWRT, but it does not yet know what to do with them.

Your Interfaces-Section should look somehow like this: ![Interfaces-Section of the LUCI Config-UI] ({static}interfaces.png)

You should now add a Firewall-Zone that allows Forwarding into the LAN-Zone. The OpenWRT's Firewall-Concept basically only applies to who's allowed to initiate a connection. All Packets relating to an already established Connection are always allowed.

The Firwall-Zone should look something like this: ![Interfaces-Zone in the LUCI Config-UI] ({static}zone.png)

Now enable Masquerading on your LAN-Zone, like this:

The Firewall should now look something like this: ![Interfaces-Section of the LUCI Config-UI] ({static}firewall.png)

  • VPN INPUT allows SSH & LUCI from the VPN
  • VPN OUTPUT allows connections from the Device into the VPN (ie. Pings)
  • LAN INPUT allows SSH & LUCI from the LAN
  • LAN OUTPUT allows connections from the Device into the LAN (ie. DNS, OpenVPN)

Why Masquerading on the LAN-Interface?

Imagine your Home-Router which has Computers on a LAN behind it, trying to communicate with Hosts on the Internet. The Hosts on the Internet always ever see the Routers IP-Address, so it is NATing all Packets from IPs not belonging to its external Net.

Our scenario here is quite similar, only that our VPN-Clients are behind the Router with their VPN-IPs being "internal" and the target-Network representing being "the internet": It's where the Packets need to be forwarded to it should never see the "internal" IP-Addresses but only the VPN-Gateways' Address.

Usually one would enable Masquerading on the WAN-Interface, so that packets going from LAN to WAN would be NATed, but we want the Packets going from VPN to LAN go be NATed, so we enable Masquerading on the LAN-Zone.

Testing the Solution

Testing requires us to have a Network with the targeted IP-Range present. I tried to simulate that by adding another IP to the lan-Interface with ip a a 192.168.124.42/24 dev br-lan but the MASEQERADE-Filter will only use the first IP it finds on an interface and not consider other IPS. Meh.

In the end I mis-used the Guest-Network provided by my Fritz!Box to simulate being in a different Network (and add it's IP-Range to the VPN for testing purposes).

Conclusion

I learned a ton about OpenVPN, Routing, OpenWRT and iptables. The whole Setup took me about 3 days and another on to write this Blog-Post.

I hope it saves you some days or gives you an idea how to setup your VPN-Gateway.