IP Masquerading and NAT on Linux
Set up IP masquerading and NAT on Linux using nftables to turn any machine into a router. Covers ip_forward, SNAT, DNAT, and iptables equivalents.
Before you start
- ▸Root or sudo access on the Linux machine acting as the gateway
- ▸Two network interfaces: one facing the upstream/WAN and one facing the LAN
- ▸LAN clients configured to use the gateway machine as their default route
- ▸nftables package installed (version 0.9.3 or later recommended)
IP masquerading lets a Linux machine act as a gateway, sharing one public IP address among many private hosts. It is a form of source NAT (SNAT) where the kernel rewrites outgoing packets so they appear to originate from the gateway's external interface, then maps replies back to the correct internal host. This is how home routers, cloud NAT gateways, and container networking all work at the kernel level. This guide sets it up properly using nftables (the modern approach), shows the equivalent iptables one-liner for reference, and covers the kernel forwarding knob that makes it all function.
Prerequisites and Assumptions
The examples use a machine with two network interfaces:
- eth0 — the WAN/uplink interface with the public or upstream IP.
- eth1 — the LAN interface, e.g. 192.168.10.1/24.
Adjust interface names to match your system (ip link show will list them). The internal subnet used throughout is 192.168.10.0/24. Commands are run as root or via sudo.
Step 1: Enable IP Forwarding
By default the Linux kernel drops packets not destined for its own addresses. Forwarding must be turned on before any NAT rule has any effect.
Temporary (survives until reboot)
sysctl -w net.ipv4.ip_forward=1
Permanent via sysctl.d
echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-ip-forward.conf
sysctl --system
For dual-stack environments also add net.ipv6.conf.all.forwarding = 1 to the same file. Verify the value stuck:
sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 1
Step 2: Configure NAT with nftables
nftables is the default packet-filtering framework on Debian 10+, Ubuntu 20.10+, Fedora 32+, RHEL 8+, and Arch Linux. It replaces iptables, ip6tables, and arptables with a single unified tool.
Install nftables if needed
Debian/Ubuntu:
apt install nftables
Fedora/RHEL/Rocky:
dnf install nftables
Arch:
pacman -S nftables
Write the ruleset
The cleanest approach is a dedicated configuration file. Create /etc/nftables-nat.conf:
cat > /etc/nftables-nat.conf << 'EOF'
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain forward {
type filter hook forward priority 0; policy drop;
# Allow established and related traffic back through
ct state established,related accept
# Allow forwarding from LAN to WAN
iifname "eth1" oifname "eth0" accept
# Allow forwarding from WAN back to LAN for established sessions only
iifname "eth0" oifname "eth1" ct state established,related accept
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
# Masquerade all traffic leaving through the WAN interface
oifname "eth0" masquerade
}
}
EOF
A few things to note: flush ruleset clears existing rules so this file is idempotent. The inet table covers both IPv4 and IPv6 for filtering, while the ip nat table is IPv4-only (IPv6 NAT is rarely appropriate). The masquerade target is a special form of SNAT that automatically uses the outbound interface's current address — useful when the WAN IP is assigned by DHCP.
Load the ruleset
nft -f /etc/nftables-nat.conf
Verify it loaded without errors:
nft list ruleset
Make it persistent
Tell the nftables systemd service to load your file on boot. Edit /etc/nftables.conf (the default path the service reads) to include your file, or simply replace its contents:
cp /etc/nftables-nat.conf /etc/nftables.conf
systemctl enable --now nftables
On Fedora/RHEL the service name is the same; on those systems /etc/sysconfig/nftables.conf is the default — confirm with systemctl cat nftables.
Step 3: The iptables Equivalent (for Reference)
If you are on an older system, a container environment that only exposes the iptables API, or you simply need to understand legacy configurations, the equivalent masquerade rule is:
# Enable forwarding (same as above)
sysctl -w net.ipv4.ip_forward=1
# Masquerade outbound traffic on eth0
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# Allow forwarded traffic for the LAN subnet
iptables -A FORWARD -i eth1 -o eth0 -s 192.168.10.0/24 -j ACCEPT
iptables -A FORWARD -i eth0 -o eth1 -m state --state ESTABLISHED,RELATED -j ACCEPT
Persist these with iptables-save and the distribution's mechanism (iptables-persistent on Debian/Ubuntu, or include them in a startup script). On modern systems, iptables is usually a shim over nftables anyway — iptables --version will say "nf_tables" if that is the case.
Step 4: Static SNAT Instead of Masquerade
If your WAN IP is static, use snat instead of masquerade. SNAT is slightly faster because the kernel does not need to look up the outbound interface address for every packet.
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "eth0" snat to 203.0.113.5
}
}
Replace 203.0.113.5 with your actual public IP.
Step 5: Port Forwarding (DNAT) — Optional
Masquerading handles outbound traffic. To allow external hosts to reach a service on an internal machine, add a DNAT rule in the prerouting chain:
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
# Forward external TCP port 8080 to internal host 192.168.10.50:80
iifname "eth0" tcp dport 8080 dnat to 192.168.10.50:80
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "eth0" masquerade
}
}
Also add a matching forward rule in the filter table to allow this traffic through.
Verification
From the gateway machine, confirm the ruleset is active:
nft list table ip nat
From an internal host (with its default gateway set to 192.168.10.1), test outbound connectivity:
ping -c 3 8.8.8.8
curl -s https://ifconfig.me
The IP returned by ifconfig.me should be the gateway's WAN address, confirming masquerading is working. Watch live NAT connection tracking on the gateway:
conntrack -L -n
# or, if conntrack is not installed:
cat /proc/net/nf_conntrack
Troubleshooting
- Packets forwarded but no internet on LAN clients: Check
sysctl net.ipv4.ip_forwardequals 1. A value of 0 means forwarding is off regardless of firewall rules. - nft: Error: Could not process rule — table already exists: The
flush rulesetat the top of the config should prevent this. If loading manually, runnft flush rulesetfirst. - LAN clients can ping the gateway but not beyond: The masquerade rule is in the
postroutingchain of theip nattable. Runnft list table ip natand confirm the rule is there andoifnamematches your actual WAN interface name exactly. - Interface names differ from eth0/eth1: Modern kernels use predictable names like
enp3s0orens18. Useip -brief link showand substitute throughout. - Firewalld conflict (Fedora/RHEL): firewalld manages nftables rules underneath. Either use
firewall-cmd --add-masquerade --zone=public --permanentto delegate to firewalld, or disable firewalld (systemctl disable --now firewalld) before managing nftables directly. Do not run both. - Changes lost after reboot: Ensure
systemctl is-enabled nftablesshowsenabledand that your ruleset file is the one the service loads (checkExecStartinsystemctl cat nftables).
Frequently asked questions
- What is the difference between masquerade and SNAT in nftables?
- Both perform source NAT. masquerade dynamically looks up the outbound interface's IP for each connection, which is necessary when the WAN address changes (DHCP). snat to <address> hard-codes the source IP and is marginally faster for static WAN addresses because it skips that lookup.
- Do I need to do anything special for IPv6 NAT?
- IPv6 NAT is technically possible with nftables but strongly discouraged; the whole point of IPv6 is that every host gets a globally routable address. For IPv6, enable forwarding (net.ipv6.conf.all.forwarding = 1) and use proper prefix delegation or a routed /64 instead of masquerading.
- Will this work on a cloud VM where the WAN IP is on a virtual NIC?
- Yes, but most cloud providers perform NAT at the hypervisor level, so the VM's NIC may already have a private address. In that case masquerading is typically not needed for outbound; you would only configure DNAT rules for inbound port forwarding, and the cloud provider's security groups must also allow the traffic.
- Can I use firewalld instead of managing nftables directly on Fedora or RHEL?
- Yes. Run firewall-cmd --add-masquerade --zone=public --permanent followed by firewall-cmd --reload. firewalld translates this into the correct nftables rules automatically. Do not mix direct nftables management with firewalld on the same system.
- Why does my nftables ruleset disappear after a reboot?
- Rules loaded with nft -f at the command line are not persistent. You need the nftables systemd service to be enabled and pointing at your config file. Verify with systemctl is-enabled nftables and systemctl cat nftables to check the ExecStart path.
Related guides
Build a Mesh VPN with Nebula
Build a fully self-hosted mesh VPN with Nebula: create a CA, sign node certs, configure lighthouses, enforce group-based firewall rules, and run as a systemd service.
Common Linux Network Ports Reference
Learn Linux port ranges, read /etc/services, find what's listening with ss and nmap, and apply solid firewall rules to expose or block the right ports.
How to Configure a Static IP on Linux
Configure a static IP on Linux using Netplan, NetworkManager (nmcli), or systemd-networkd across Ubuntu, Fedora, Debian, and Arch with verified steps.
Expose a Service with Cloudflare Tunnel
Expose local services to the internet without port-forwarding using Cloudflare Tunnel. Install cloudflared, create a named tunnel, configure ingress rules, and run as a systemd service.