Home | Send Feedback

Expose server behind NAT with WireGuard and a VPS

Published: January 10, 2019  •  linux

In this blog post we are going to look at a way to expose services, running on a computer that sits behind a NAT or firewall, to the Internet. For example, you have a small server at home and you would like to access this server or maybe the whole network from anywhere in the world.

If the ISP assigns a static public IP address to your router you just have to forward ports in your firewall to the services you want to expose.

But, here where I live, it's not very common that home users get static IP address. I only get dynamic IP addresses from my ISP. With dynamic IP addresses you can try to use a dynamic DNS service. This is a service that maps your current external IP address to a domain name and each time your ISP assigns a new IP address to your router, it sends an update to the dynamic DNS service. This functionality is often implemented in firewall and router operating systems. For example pfSense, the firewall I use, includes a whole configuration page for setting up a dynamic DNS.

One problem you might face with either a static IP address or a dynamic DNS solution is that sometimes ISP block ports. It is very common that they block the outgoing port 25, to prevent spam email messages. This is especially a problem when you are planning to run your own email server at home.

Other solutions, to expose services, include tools like ngrok and localtunnel. They start a tunnel from your machine to an external server and assign a public URL to this tunnel. Traffic from this URL is forwarded through the tunnel to your service. Very convenient and easy to use. ngrok, for example, consists of just one binary, you download and run it without any installation.

The architecture of the solution we are going to build in this blog post looks very similar to the architecture that these tools are built on.

Overview

Here an overview of the solution:

overview

On the right side you have a computer that sits behind a NAT router or firewall and can't directly be accessed from the Internet. This could be a small server in your home, for this demo I utilize a Raspberry Pi.

On the left side we set up a server with a static public IP address. For this demo I rented a VPS from Amazon Lightsail. I choose the smallest VPS with a price of 3.50 USD per month. These small VPS are more than sufficient for running a VPN. This setup is not restricted to a Lightsail VPS, it works with any VPS from any provider and also works with your own servers that have static public IP addresses.

Between the two machines we set up a VPN with WireGuard, so both computers can talk to each other, as if they are sitting in the same local network.

When we want to access our private server, we connect to the public IP address of the VPS and the connection gets forwarded over the VPN to our server at home.

VPN

Now we have seen the architecture of this solution, let's start by configuring the WireGuard VPN. When you want to follow this tutorial and also use a Lightsail VPS, go to my previous blog post and follow the instructions until and including the section Install required packages.

I installed Debian 9.5 on my Lightsail VPS.

If you use another server, install WireGuard and enable IP forwarding.

Next, we install WireGuard on our home server as well. As mentioned before, I use a Raspberry Pi for this demo. I already have Raspbian Stretch Lite installed on the Pi and follow the instructions from this tutorial for the WireGuard installation: https://github.com/adrianmihalko/raspberrypiwireguard

sudo -i
apt update
apt dist-upgrade 
apt install raspberrypi-kernel-headers
echo "deb http://deb.debian.org/debian/ unstable main" | tee --append /etc/apt/sources.list.d/unstable.list
apt install dirmngr
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 8B48AD6246925553 
printf 'Package: *\nPin: release a=unstable\nPin-Priority: 150\n' | tee --append /etc/apt/preferences.d/limit-unstable
apt update
apt install wireguard 
reboot

Open a SSH connection to both machines. WireGuard utilizes public/private cryptography and we need to create a key pair on each machine and then exchange the public keys.

Run the following two commands on both computers. The first command creates the private key and writes it directly into the WireGuard configuration file. The second command creates the public key, writes it into the file publickey and prints it into the console.

(umask 077 && printf "[Interface]\nPrivateKey = " | sudo tee /etc/wireguard/wg0.conf > /dev/null)
wg genkey | sudo tee -a /etc/wireguard/wg0.conf | wg pubkey | sudo tee /etc/wireguard/publickey

Make a note of both public keys and open the WireGuard configuration file on both machines

sudo nano /etc/wireguard/wg0.conf

Enter the following configuration settings. For this example I assign 192.168.4.1 to the VPS and 192.168.4.2 to the server at home. Choose a network that is not already assigned in your home network. The external static IP address of my VPS server is 18.184.64.177 and the port I want WireGuard to connect to is UDP 55107. Make sure that you open an UDP port in the firewall of your VPS for WireGuard. Choose a random port.

VPS

[Interface]
PrivateKey = qHOQs4...
ListenPort = 55107
Address = 192.168.4.1

[Peer]
PublicKey =  ums9y... <--- public key from the machine at home
AllowedIPs = 192.168.4.2/32

Home Server (Pi)

[Interface]
PrivateKey = OKNAiUi/u...
Address = 192.168.4.2

[Peer]
PublicKey = GJtb+O7nnT... <---- public key from VPS
AllowedIPs = 192.168.4.1/32
Endpoint = 18.184.64.177:55107
PersistentKeepalive = 25

See the Quickstart Guide under the section NAT and Firewall Traversal Persistence for a description, why sometimes you need PersistentKeepalive. In my environment the tunnel closes after a few minutes without any traffic. The PersistentKeepalive solves that problem by periodically (25 seconds) sending packets over the VPN.

Start WireGuard on both machines and enable it, so it automatically starts up the next time you reboot the computer.

sudo systemctl start wg-quick@wg0
sudo systemctl enable wg-quick@wg0

When everything is configured correctly, you should now be able to ping each computer from the other end. ping

Forward traffic

For this demo I install ngIRCd on the Raspberry Pi.

sudo apt install ngircd

This is a lightweight IRC server that listens, by default, on port TCP 6667.

I can connect to this service on my home network, but I want to expose this service, so that my friends can connect to it from everywhere.

For this purpose I need to add some iptables rules to the VPS. Connect to the VPS with SSH and display the current network configuration

sudo ip -4 addr show scope global
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc pfifo_fast state UP group default qlen 1000
    inet 172.26.10.182/20 brd 172.26.15.255 scope global eth0
       valid_lft forever preferred_lft forever
3: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 8921 qdisc noqueue state UNKNOWN group default qlen 1
    inet 192.168.4.1/32 scope global wg0
       valid_lft forever preferred_lft forever

We need some of this information for setting up the iptables rules. Notice that on Amazon Lightsail the servers have an internal IP address (in my case 172.26.10.182). Packet forwarding from the external address (18.184.64.177) to this internal address is outside of our control. Amazon handles this automatically.

Here we set the default rule for the FORWARD chain to DROP, so nobody can forward packets. Then we enable forwarding for packets coming from eth0 with a destination port 6667

sudo iptables -P FORWARD DROP
sudo iptables -A FORWARD -i eth0 -o wg0 -p tcp --syn --dport 6667 -m conntrack --ctstate NEW -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A FORWARD -i wg0 -o eth0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

Next, we need to add a rule that changes the destination address in the TCP packet to 192.168.4.2, the address of the Raspberry Pi on the other side of the VPN. This rule does it only for packets coming from eth0 and with a destination port 6667. And the second rule changes the source address, so the Raspberry Pi is able to send back the response to our server.

sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 6667 -j DNAT --to-destination 192.168.4.2
sudo iptables -t nat -A POSTROUTING -o wg0 -p tcp --dport 6667 -d 192.168.4.2 -j SNAT --to-source 192.168.4.1

I followed the description of this article to set up the rules:
https://www.digitalocean.com/community/tutorials/how-to-forward-ports-through-a-linux-gateway-with-iptables

You find more information there, if something is not working properly. Also, double check if you enabled IP forwarding on this server (/etc/sysctl.conf). And make sure that you open the port of the service (6667) in the firewall of the VPS.
firewall

Forward SSH traffic

You can forward any traffic from the VPS to your private server. In this section I show you how to forward SSH traffic. The Lightsail VPS already utilizes port 22 for its own SSH server so we choose another port (22222) and forward packets to this port to the other side of the VPN.

Make sure that you open the port in the firewall. Then add the following rules. The PREROUTING rule in this example not not only changes the destination address but also the port from 22222 to 22, because SSH on the Raspberry Pi is listening on port 22.

sudo iptables -A FORWARD -i eth0 -o wg0 -p tcp --syn --dport 22 -m conntrack --ctstate NEW -j ACCEPT
sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 22222 -j DNAT --to-destination 192.168.4.2:22
sudo iptables -t nat -A POSTROUTING -o wg0 -p tcp --dport 22 -d 192.168.4.2 -j SNAT --to-source 192.168.4.1

Don't forget to harden your SSH server when you expose it like this. Choose a secure password or even better disable password authentication and use public/private key authentication. Also disable root login over SSH.

Persistent iptables rules

iptables rules are not persistent by default. If we want the rules to survive a reboot we have to save them and load them during the boot process. The netfilter-persistent package takes care of all these tasks. Install it, then save the rules and enable the service so it automatically starts up.

sudo apt install netfilter-persistent
sudo netfilter-persistent save
sudo systemctl enable netfilter-persistent