Networking & DNS

NAT, masquerade and port forwarding with nftables

Use nftables to masquerade VPN clients out through your VPS and forward ports to a backend with DNAT. Includes IP forwarding, forward chain rules and a complete VPN gateway ruleset.

AdvancedLinux server5 min read

NAT lets your VPS act as a gateway. Masquerade rewrites the source address of traffic leaving the server, so clients behind it, such as WireGuard peers, can reach the internet through your public IP. DNAT rewrites the destination of incoming traffic, which is how port forwarding works. This guide sets up both with nftables and ends with a complete ruleset for a VPN gateway.

Before you start

  • A Linux VPS running Ubuntu 22.04 or 24.04, Debian 12, or AlmaLinux or Rocky Linux 8 or 9.
  • Root or a sudo user, and plain nftables as your only firewall manager. If you are new to it, read Get started with nftables first.
  • A private network behind the VPS. Examples use a WireGuard interface wg0 with clients in 10.8.0.0/24, and a backend at 10.8.0.5. See Set up a WireGuard VPN server.
  • Access to the browser console in your VPS provider's control panel.

Find your public interface name. It is often eth0 or ens3:

Bash
ip -br addr

Examples below use eth0. Replace it everywhere if yours differs.

Warning: The ruleset below starts with flush ruleset and a default-drop input policy. It keeps SSH open on port 22. If your SSH port is different, change it before loading, or you will be locked out. The browser console in your VPS provider's control panel is your way back in, where sudo nft flush ruleset removes all rules.

Enable IP forwarding

The kernel does not route packets between interfaces until forwarding is on:

Bash
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-ip-forward.conf
sudo sysctl --system
sysctl net.ipv4.ip_forward

The last command should print net.ipv4.ip_forward = 1.

How NAT hooks work

NAT in nftables uses chains of type nat on two hooks:

  • prerouting (priority dstnat): runs as packets arrive, before routing. Use it for DNAT to change where a packet goes.
  • postrouting (priority srcnat): runs just before packets leave. Use it for masquerade or SNAT to change where a packet appears to come from.

NAT only rewrites addresses. The filter forward chain still decides whether routed traffic is allowed at all. You need both.

The VPN gateway ruleset

Put this in /etc/nftables.conf on Ubuntu and Debian, or /etc/sysconfig/nftables.conf on AlmaLinux and Rocky Linux:

#!/usr/sbin/nft -f

flush ruleset

define WAN = "eth0"
define VPN = "wg0"
define VPN_NET = 10.8.0.0/24
define BACKEND = 10.8.0.5

table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;
        ct state established,related accept
        ct state invalid drop
        iif "lo" accept
        meta l4proto { icmp, ipv6-icmp } accept
        tcp dport 22 accept comment "SSH"
        udp dport 51820 accept comment "WireGuard"
        iifname $VPN accept comment "trust VPN clients"
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
        ct state established,related accept
        ct state invalid drop
        iifname $VPN oifname $WAN accept comment "VPN to internet"
        ct status dnat accept comment "allow port forwards"
    }
}

table ip nat {
    chain prerouting {
        type nat hook prerouting priority dstnat;
        iifname $WAN tcp dport 8443 dnat to $BACKEND:443
    }

    chain postrouting {
        type nat hook postrouting priority srcnat;
        oifname $WAN ip saddr $VPN_NET masquerade
    }
}

What it does:

  • Input allows SSH, the WireGuard port and anything from VPN clients. Everything else is dropped.
  • Forward lets VPN clients reach the internet, lets replies come back, and allows any connection that a DNAT rule redirected.
  • Prerouting forwards TCP 8443 on the public IP to port 443 on the backend.
  • Postrouting masquerades VPN client traffic as it leaves through eth0.

A NAT table only sees the first packet of each connection. Conntrack applies the same translation to the rest of it automatically.

Port forwards and the return path

For a DNAT forward to work, the backend's replies must come back through the VPS. On a WireGuard backend whose default route goes through the tunnel, that happens naturally. If the backend has its own internet connection, replies go out that way and the connection breaks. Two fixes:

  • Route the backend's traffic through the VPS.
  • Add SNAT for forwarded traffic so the backend replies to the VPS. Add this line to the postrouting chain:
  oifname $VPN ip daddr $BACKEND tcp dport 443 masquerade

The backend then sees the VPS address as the client, not the visitor's real IP.

Load and test

Check syntax, set a rollback, then load:

Bash
sudo nft -c -f /etc/nftables.conf
sudo systemd-run --on-active=5m --unit=nft-rollback /usr/sbin/nft flush ruleset
sudo nft -f /etc/nftables.conf

Open a new SSH session. If it works, cancel the rollback and enable the service at boot:

Bash
sudo systemctl stop nft-rollback.timer
sudo systemctl enable --now nftables

On AlmaLinux and Rocky Linux, disable firewalld first so it does not replace these rules.

Check it works

From a VPN client, check the public IP it now uses:

Bash
curl -4 https://ifconfig.me

It should show the VPS address. From outside, test the port forward:

Bash
nc -vz 203.0.113.10 8443

On the VPS, watch NAT translations as they happen:

Bash
sudo conntrack -L -p tcp --dport 8443

conntrack comes from the conntrack package on Debian and Ubuntu, or conntrack-tools on AlmaLinux and Rocky Linux.

Troubleshooting

  • VPN clients connect but have no internet: ip_forward is 0, the masquerade rule names the wrong interface, or the forward chain does not allow wg0 to eth0.
  • Port forward times out: the backend replies via another route. Add the SNAT line above, or route its traffic through the VPS.
  • Docker networks broke: flush ruleset removed Docker's rules. Restart Docker after loading, or avoid running Docker on a hand-built NAT gateway.
  • Rules lost after reboot: the nftables service is not enabled, or firewalld overwrote the ruleset.
  • Locked out: use the browser console and run sudo nft flush ruleset, then fix the SSH port in the file.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.