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
wg0with clients in10.8.0.0/24, and a backend at10.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:
ip -br addrExamples 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:
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-ip-forward.conf
sudo sysctl --system
sysctl net.ipv4.ip_forwardThe 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
postroutingchain:
oifname $VPN ip daddr $BACKEND tcp dport 443 masqueradeThe 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:
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.confOpen a new SSH session. If it works, cancel the rollback and enable the service at boot:
sudo systemctl stop nft-rollback.timer
sudo systemctl enable --now nftablesOn 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:
curl -4 https://ifconfig.meIt should show the VPS address. From outside, test the port forward:
nc -vz 203.0.113.10 8443On the VPS, watch NAT translations as they happen:
sudo conntrack -L -p tcp --dport 8443conntrack 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_forwardis0, the masquerade rule names the wrong interface, or the forward chain does not allowwg0toeth0. - 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 rulesetremoved 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
- Prefer firewalld for the same job? Port forwarding and masquerading with firewalld.
- Add client devices to the tunnel: Add WireGuard clients.
