What Is NAT?

In the words of a brief Google search: “NAT stands for Network Address Translation, a service that allows private networks to use the internet and cloud. It works by translating private IP addresses to a public IP address before sending packets to an external network.”

To translate that description to the problem I was solving: this essentially means whether you have specific ports open on your network connection with a public IP address.

The Problem We’re Solving

I’ve recently set up a new internet connection and, as a gamer, I was disappointed when I went to play a game with my friend and was met with the words ‘NAT: Closed’. This prevented my friend and I from being able to connect to each other’s game sessions.

The reason for this is that my new internet connection is actually using the 5g mobile network. I’ve recently moved house and now live in an area which doesn’t support running fibre directly to the property. As a web developer, I require faster speeds than the old copper lines can provide, so using 5g seemed like an acceptable alternative.

Celluar providers use something called ‘CGNAT’. This is commonly referred to as a ‘double NAT’ where there’s essentially a second layer of closed ports that’s out of your reach as the user. This means that if you open a port on your 5g router, it will still be closed further up-stream, stripping you of the control you require for certain activities, such as online gaming or hosting publicly accessible services.

What Is WireGuard?

WireGuard is an open-source VPN protocol, similar to the more widely-known OpenVPN, that facilitates secure connections between devices and the internet.

In a nutshell, this means that you can run a WireGuard server from one computer, and then connect to it from another one. Each computer, referred to as a client, which connects to the server, can then leverage the server’s internet connection, including its IP address. This allows the clients to behave as if they were physically connected to the same network as the server.

How WireGuard Can Help Bypass A Restricted NAT

When connected to a VPN like WireGuard, the client’s traffic is proxied via the server. This means that the client’s network is bypassed. By configuring WireGuard correctly, it’s possible to declare rules for network traffic to forward incoming and outgoing packets to and from a connected client. This includes controlling individual ports, meaning you can port forward via the VPN server.

Requirements

To make this setup work, you will require the following:

  • A Linux server with…
    • Root SSH access
    • A static IP address
    • Control of any firewalls for opening ports
    • Docker
  • A basic knowledge of…
    • The Linux terminal
    • Docker & Docker compose
    • Networking ports

WireGuard has very low system requirements, so a low-end VPS is usually fine, so long as it has reliable uptime. I’d recommend at least 2 cores, 1GB RAM, and as fast of an internet connection as possible.

If you don’t have Docker installed, you can follow their documentation. For example, I followed their guidance for installing on Ubuntu for my VPS which runs Ubuntu 24: https://docs.docker.com/engine/install/ubuntu/

Installing wg-easy Via Docker

wg-easy is a Docker image which ships WireGuard and a nice web UI for managing connections. There are other Docker images out there that are slightly different and don’t include the web UI, but I like any tools that make my life easier, so this is my image of choice.

First, SSH into your server and navigate to your directory of choice. I would recommend not running anything else on this VPS, so in my instance, I just used the root user’s directory which you’re placed in upon connecting.

Creating A Docker Compose File For wg-easy

Create a new file docker-compose.yml using your text editor of choice. I’m using nano:

nano docker-compose.yml
services:
  wireguard:
    image: ghcr.io/wg-easy/wg-easy
    container_name: wireguard-easy
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    environment:
      - LANG=en
      - WG_HOST=[SERVER_PUBLIC_IP_ADDRESS]
      - PASSWORD_HASH=[YOUR_HASHED_PASSWORD]
      - PORT=51821
      - WG_PORT=51820
      - WG_MTU=1380
      - WG_PERSISTENT_KEEPALIVE=25
      - WG_POST_UP=iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE;iptables -A INPUT -p udp --dport 51820 -j ACCEPT;iptables -A FORWARD -i wg0 -j ACCEPT;iptables -t nat -A PREROUTING -p tcp -i eth0 -m multiport '!' --dports 22,51821 -j DNAT --to-destination 10.8.0.2;iptables -t nat -A PREROUTING -p udp -i eth0 '!' --dport 51820 -j DNAT --to-destination 10.8.0.2;
      - WG_POST_DOWN=iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE;iptables -D INPUT -p udp --dport 51820 -j ACCEPT;iptables -D FORWARD -i wg0 -j ACCEPT;iptables -t nat -D PREROUTING -p tcp -i eth0 -m multiport '!' --dports 22,51821 -j DNAT --to-destination 10.8.0.2;iptables -t nat -D PREROUTING -p udp -i eth0 '!' --dport 51820 -j DNAT --to-destination 10.8.0.2;
    volumes:
      - ./wg-easy:/etc/wireguard
    ports:
      - "51821:51821"
      - "51820:51820/udp"
    sysctls:
      - net.ipv4.ip_forward=1
      - net.ipv4.conf.all.src_valid_mark=1
    restart: unless-stopped

If you’re also using nano, you can save this document by pressing ctrl+o and then y. Then ctrl+x to exit.

Let’s break this down:

  • Lines 5 – 7: We’re giving the Docker container extra capabilities for managing networks and system modules. This is required for the port forwarding to work correctly.
  • Lines 8 – 17: We’re providing some environment variables to the container as settings. This modifies the behaviour and configuration of the container when it’s running. I’ll go through these more granularly in a moment.
  • Lines 18 & 19: We’re mapping the local directory ./wg-easy to a directory within the container /etc/wireguard. This will allow any changes made to that folder within the container to persist, as it’ll be saved on the host.
  • Lines 20 – 22: We’re opening the minimum ports required for the VPN and the web UI to be reachable.
  • Lines 23 – 25: These are modifications to the sysctl of the container. These allow traffic to be forwarded from client ports to the server ports.
  • Line 26: This will keep the container running if it encounters an error. Unless manually stopped, it will reboot. This ensures the VPN is accessible if something causes it to crash.

Creating Your Password Hash For wg-easy

To create your password hash for the PASSWORD_HASH environment variable, you can use the following command:

docker run --rm -it ghcr.io/wg-easy/wg-easy wgpw 'YOUR_PASSWORD'
PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' // literally YOUR_PASSWORD

This runs a Docker image designed to take your password as input and then returns a hashed version.

If your hashed password contains any $ characters, you need to double them up so that the docker-compose.yml can parse them correctly.

Read more about this in wg-easy’s documentation on generating passwords.

Running Docker Compose for wg-easy

To spin up a container using the configurations outlined in this file, you can run:

docker compose up -d

This will build the container with the provided configuration and run it in detached mode, meaning it runs in the background.

If you make any changes to the docker-compose.yml file, you rebuild the container with the latest configuration by first taking down the old container:

docker compose down

You can then rebuild it with the new configurations:

docker compose up --force-recreate --build -d

Checking wg-easy Is Running Correctly

To see your running containers, you can use the following command:

docker ps
CONTAINER ID   IMAGE                     COMMAND                  CREATED          STATUS                    PORTS    NAMES
e82655a72221   ghcr.io/wg-easy/wg-easy   "docker-entrypoint.s…"   48 minutes ago   Up 47 minutes (healthy)   ...      wireguard-easy

I’ve stripped down the output of PORTS as it was quite long, so bare that in mind as your output will show these.

Using the name of the container, wireguard-easy, you can check the logs with:

docker logs wireguard-easy
2024-12-06T10:52:14.673Z Server Listening on http://0.0.0.0:51821
2024-12-06T10:52:14.678Z WireGuard Loading configuration...
2024-12-06T10:52:14.680Z WireGuard Configuration loaded.
2024-12-06T10:52:14.680Z WireGuard Config saving...
2024-12-06T10:52:14.682Z WireGuard Config saved.
$ wg-quick down wg0
$ wg-quick up wg0
2024-12-06T10:52:14.829Z WireGuard Config syncing...
$ wg syncconf wg0 <(wg-quick strip wg0)
2024-12-06T10:52:14.888Z WireGuard Config synced.

Creating Client Credentials For WireGuard

To connect to the server for the first time, you must create credentials for a client connection. If you’ve now got the Docker container running as per the above, you should have the wg-easy web interface at your disposal.

Open The wg-easy Web GUI Port

I can’t advise on this for every VPS, as there can be variations, but to connect to the web interface, you will likely need to open port 51821 on your VPS. With this open, you should be able to connect by going to http://SERVER_IP:51821 in your browser.

My VPS runs Ubuntu and has no other firewalls besides the default one: UFW. This meant that opening the required port was as simple as running:

ufw allow 51821 # for the web GUI
ufw allow 51820/udp #for the VPN connection itself

# Reload UFW so the new rule takes affect
ufw reload

Connect To The wg-easy Web GUI

In your browser, visit http://YOUR_SERVER_IP:51821.

If the Docker container is running and the port is publicly accessible, you should see the following login screen:

This is where we’re going to enter the password that we hashed earlier and set as the PASSWORD_HASH environment variable in docker-compose.yml.

Using The Web GUI To Manage Clients

This is a screenshot of my web GUI as of writing this article. As you can see, I have set up two possible client connections. One is for my desktop computer, and the other is for my phone. I could use these connections on either device, but I’ve given them these names to mark the device they’re intended for. Each client connection can only support one connection at a time, so it’s best to have 1 per device to avoid conflicts.

You can see under their name is an internal IP address. This is the internal IP they’re given within the Docker container upon connecting. We can also see their upload/download statistics, whether the client is enabled, and then options for downloading their credentials and removing them.

To create a new client, click + New, on the top-right of the list:

All you need to do is provide a name and then you’re ready to use the new connection.

Connecting To WireGuard From A Client

Depending on your system, you’ll need to install the appropriate WireGuard client. Fortunately, it’s supported on all major platforms: https://www.wireguard.com/install/

I’m using Windows 11 for this demonstration, but have used it on both Android and IOS as well.

The desktop client should look something like this:

Add a new connection, or ‘tunnel’, is as simple as clicking the button on the bottom right. As you can see, I’ve already added the ‘Desktop’ configuration you saw earlier. I did this by downloading the config from the web GUI, and adding it accordingly.

On Android and IOS devices you can add them by simply scanning the QR code from the web GUI, making it super simple and intuitive.

Once connected, you should be able to browse the internet as you normally would, with the only difference being that now your IP address should reflect that of the WireGuard server host’s.

Making Your NAT Open For Gaming With WireGuard

Up to this point, we have learned…

  • What WireGuard is.
  • How to set up WireGuard with wg-easy and Docker.
  • How to open ports and connect to wg-easy.

To finish everything off, it’s time to learn how to open specific ports and proxy traffic to specific clients within your wg-easy network.

Passing Traffic From All Ports To One Client

Within our docker-compose.yml file, there were two environment variables; WG_POST_UP and WG_POST_DOWN. Using these, we can create rules that tell our wg-easy container how to route traffic to specific clients.

To help demonstrate this, here are the rules for the WG_POST_UP used in my example above, broken into individual lines, with comments added above each line explaining the function of the line below:

# Enables devices on the WireGuard VPN (10.8.0.0/24) to access external networks by masquerading their traffic with the host's public IP on eth0.
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
# Allows incoming WireGuard VPN connections on UDP port 51820, the default port for WireGuard.
iptables -A INPUT -p udp --dport 51820 -j ACCEPT
# Permits traffic from the WireGuard interface (wg0) to be forwarded to other networks, enabling communication between VPN clients and external resources.
iptables -A FORWARD -i wg0 -j ACCEPT
# Redirects all incoming TCP traffic (except for SSH on port 22 and the excluded port 51821) on eth0 to the WireGuard VPN client at 10.8.0.2.
iptables -t nat -A PREROUTING -p tcp -i eth0 -m multiport '!' --dports 22,51821 -j DNAT --to-destination 10.8.0.2
# Redirects all incoming UDP traffic (excluding WireGuard's default port 51820) on eth0 to the WireGuard VPN client at 10.8.0.2.
iptables -t nat -A PREROUTING -p udp -i eth0 '!' --dport 51820 -j DNAT --to-destination 10.8.0.2;

The first 3 rules are required in order for WireGuard to run. If you leave WG_POST_UP blank, then these rules are generated automatically. However, to add additional rules, you must specify these first.

The last 2 rules are where the magic happens: These forward all incoming traffic from all ports except 22/TCP, 51821/TCP and 51820/UDP to one specific client. The client is specified by using their internal IP address, in this example, it’s 10.8.0.2, which is the client I created above called ‘Desktop’. The ports are excluded because they’re required for wg-easy and for SSH to run. If you want to add any further exclusions, you can do so by using the existing ports as an example.

When setting these rules as WG_POST_UP OR WG_POST_DOWN, you must put them onto 1 line and split them up with a semicolon (;).

If you use the above example for WG_POST_UP, you must then set your WG_POST_DOWN to do the opposite actions. This ensures that the container is stopped nicely. You do this by adding the same commands, but swapping any -A flags for a -D flag. You can use the docker-compose.yml file above to see an example.

Opening New Ports For The Client

Now that we have all traffic proxied to a single client, whenever you want to open a port you can simply do the following:

  1. Add the port to the open ports in docker-compose.yml.
  2. Exclude the port from your VPS firewall.
  3. Restart the wg-easy container.

For example, if I wanted to open port 25565, the port used by Minecraft, I would…

#docker-compose.yml
services:
  wireguard:
    ...
    ports:
      ...
      # Add the port here
      - "25565:25565"
    ...
# Exclude the port from UFW
ufw allow 25565
# Reload UFW to use the new rule
ufw reload
# Stop the container
docker compose down
# Start the container
docker compose up --force-recreate --build -d

And it’ll all just work!

Closing Thoughts

I’ve used this setup to bypass NAT restrictions and play online games such as Call Of Duty Black Ops 2 on an open NAT type. When playing the game without the VPN said ‘NAT: Restricted’. I’ve done so with zero drops in connectivity and no lag or noticeable delay.

Secondarily to gaming, this means I now have a static IP address I can use in my daily life as a developer. This allows me to host some temporary sites from local devices or access development sites from mobile devices for testing and has also allowed me to add security to other projects by restricting access based on user IP addresses, meaning only those connected to the VPN may access them.

This guide might look long and feel somewhat daunting at first glance, but the length is due to the depth in which I’ve explained how each part works. If you want to set up a VPN like this for yourself, it’s important to understand what it is you’re doing, as an improper configuration might leave you vulnerable to malicious users. With that in mind, if anything isn’t clear, please feel free to leave a comment below and I’ll my best to help out and answer to the best of my ability.

Leave a Reply

Your email address will not be published. Required fields are marked *