$linuxjunkies
>

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.

IntermediateUbuntuDebianFedoraArch8 min readUpdated June 7, 2026

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_forward equals 1. A value of 0 means forwarding is off regardless of firewall rules.
  • nft: Error: Could not process rule — table already exists: The flush ruleset at the top of the config should prevent this. If loading manually, run nft flush ruleset first.
  • LAN clients can ping the gateway but not beyond: The masquerade rule is in the postrouting chain of the ip nat table. Run nft list table ip nat and confirm the rule is there and oifname matches your actual WAN interface name exactly.
  • Interface names differ from eth0/eth1: Modern kernels use predictable names like enp3s0 or ens18. Use ip -brief link show and substitute throughout.
  • Firewalld conflict (Fedora/RHEL): firewalld manages nftables rules underneath. Either use firewall-cmd --add-masquerade --zone=public --permanent to 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 nftables shows enabled and that your ruleset file is the one the service loads (check ExecStart in systemctl cat nftables).
tested on:Ubuntu 24.04Debian 12Fedora 40Rocky 9

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