IP Masquerade HOWTO: share an Internet connection with Linux NAT
IP masquerading lets many computers on a private network share one public IP address through a Linux machine. You need three things: IP forwarding switched on, a NAT rule that rewrites outgoing traffic to the router's address, and firewall rules that let the LAN out but nothing unwanted in. Add dnsmasq and the LAN machines configure themselves.
How masquerading works
The router keeps a table of active connections. When a reply comes back to 203.0.113.5, it looks up which inside machine started that connection and sends the reply there. Machines on the Internet cannot start a connection to an inside machine unless you add a port forward.
The examples use these names; change them to yours (see ip -br addr):
- WAN (Internet side):
eth0, address from the ISP or modem. It can also beppp0for PPPoE orwwan0for a mobile modem. - LAN:
eth1, static address192.168.50.1/24. Private ranges are 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16; pick a /24 that your ISP's modem does not already use.
Step 1: address the LAN side
Configure eth1 permanently with your network tool. With NetworkManager:
sudo nmcli connection add type ethernet con-name lan ifname eth1 \
ipv4.method manual ipv4.addresses 192.168.50.1/24
sudo nmcli connection up lan
Do not set a gateway on the LAN side; the default route goes out through the WAN. For systemd-networkd and netplan versions, see the Ethernet HOWTO.
Step 2: switch on IP forwarding
Linux does not forward packets between interfaces by default (net.ipv4.ip_forward is 0). Turn it on and make it permanent:
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/90-router.conf
sudo sysctl --system
sysctl net.ipv4.ip_forward
The last command should print net.ipv4.ip_forward = 1. Files in /etc/sysctl.d/ are applied at every boot.
IPv6. IPv6 is normally routed, not masqueraded: the ISP delegates a prefix and the router hands out addresses from it. If you set net.ipv6.conf.all.forwarding = 1, the kernel stops accepting router advertisements on interfaces whose accept_ra is 1. If the WAN gets its IPv6 address that way, also set net.ipv6.conf.eth0.accept_ra = 2.
Step 3: NAT and firewall with nftables
This file is a complete router rule set. Save it as /etc/nftables.conf (Debian) or include it from /etc/sysconfig/nftables.conf (Fedora, RHEL; there, stop and disable firewalld first, or use the firewalld version below instead). It replaces any other rules; on a machine running Docker or libvirt, see the note in the Firewall HOWTO about using your own table instead of flush ruleset.
#!/usr/sbin/nft -f
flush ruleset
define WAN = "eth0"
define LAN = "eth1"
define LAN_NET = 192.168.50.0/24
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iifname "lo" accept
meta l4proto { icmp, ipv6-icmp } accept
# SSH from anywhere (narrow this later)
tcp dport 22 accept
# DNS and DHCP for the LAN only (dnsmasq)
iifname $LAN udp dport { 53, 67 } accept
iifname $LAN tcp dport 53 accept
}
chain forward {
type filter hook forward priority filter; policy drop;
# Fix MTU problems on PPPoE and tunnel uplinks
tcp flags syn tcp option maxseg size set rt mtu
ct state established,related accept
ct state invalid drop
# LAN may go out to the Internet
iifname $LAN oifname $WAN accept
# Allow connections that a DNAT rule below forwarded
ct status dnat accept
}
}
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
# Port forward: Internet port 8080 -> web server 192.168.50.10:80
iifname $WAN tcp dport 8080 dnat to 192.168.50.10:80
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname $WAN ip saddr $LAN_NET masquerade
}
}
What each part does:
masquerade- Rewrites the source address of LAN packets leaving through the WAN to whatever address the WAN has right now. Use it when the WAN address comes from DHCP or PPP. With a fixed public address you can use
snat to 203.0.113.5instead. forwardchain- Decides what may cross the router. Here: anything from LAN to WAN, replies coming back, and connections that a port forward sent in. Everything else is dropped.
dnat to- A port forward. Delete the line if you do not need one; the
ct status dnat acceptrule then has nothing to match. maxseg size set rt mtu- Lowers the TCP segment size on new connections to fit the route's MTU. Without it, some web sites hang on PPPoE links whose MTU is smaller than 1500.
Check and load it with a rollback timer if you work over SSH (see avoiding lock-out):
sudo nft -c -f /etc/nftables.conf
sudo nft -f /etc/nftables.conf
sudo systemctl enable nftables
Or: the same with firewalld
On Fedora and RHEL, keep firewalld and use its zones. The built-in external zone already has masquerading switched on; a policy allows traffic from the LAN zone to the Internet zone:
sudo firewall-cmd --permanent --zone=external --change-interface=eth0
sudo firewall-cmd --permanent --zone=internal --change-interface=eth1
sudo firewall-cmd --permanent --zone=internal --add-service=dhcp --add-service=dns
sudo firewall-cmd --permanent --new-policy lan-out
sudo firewall-cmd --permanent --policy lan-out --add-ingress-zone internal
sudo firewall-cmd --permanent --policy lan-out --add-egress-zone external
sudo firewall-cmd --permanent --policy lan-out --set-target ACCEPT
sudo firewall-cmd --reload
sudo firewall-cmd --zone=external --query-masquerade
If NetworkManager manages the interfaces, set the zone in the connection profile too, so it sticks: sudo nmcli connection modify <wan-profile> connection.zone external. A port forward in firewalld:
sudo firewall-cmd --permanent --zone=external \
--add-forward-port=port=8080:proto=tcp:toport=80:toaddr=192.168.50.10
sudo firewall-cmd --reload
firewalld's masquerade and forward-port options are IPv4 only. Check sysctl net.ipv4.ip_forward as in step 2.
Step 4: DHCP and DNS with dnsmasq
dnsmasq is a small DHCP server and DNS forwarder. Install it (sudo apt install dnsmasq or sudo dnf install dnsmasq) and put these lines in /etc/dnsmasq.conf. In the file, options are written like the long command-line options without the leading --.
# Serve only the LAN
interface=eth1
bind-interfaces
# Hand out 192.168.50.100-200, leases last 12 hours
dhcp-range=192.168.50.100,192.168.50.200,12h
# Always give this machine the same address
dhcp-host=52:54:00:12:34:56,192.168.50.10
# Local domain name for LAN hosts
domain=lan
sudo systemctl enable --now dnsmasq
sudo systemctl status dnsmasq
dnsmasq tells clients to use the router itself as their default gateway and DNS server unless you set other values with dhcp-option. It forwards DNS queries to the servers in the router's own /etc/resolv.conf.
bind-interfaces makes dnsmasq listen only on the LAN address and loopback, instead of on all addresses. That matters on Ubuntu and other systems with systemd-resolved, whose local stub resolver already uses port 53 on 127.0.0.53.
The quick way: NetworkManager “shared”
For a laptop sharing Wi-Fi to a wired device, or a one-off setup, NetworkManager can do all of the above itself. Set the LAN profile's IPv4 method to shared:
sudo nmcli connection modify lan ipv4.method shared
sudo nmcli connection up lan
NetworkManager then turns on forwarding, adds NAT rules, and starts its own DHCP and DNS service on that interface. If the profile has no address, it uses 10.42.x.1/24. The DHCP and DNS part is a dnsmasq process that NetworkManager starts, so the dnsmasq program must be installed (on Debian and Ubuntu it is the recommended dnsmasq-base package). Do not combine this with your own dnsmasq service on the same interface.
Test it
- On a LAN client, renew the lease and check:
ip -br addrshows an address in 192.168.50.100–200, andip routeshowsdefault via 192.168.50.1. - Ping the router:
ping -c 3 192.168.50.1. - Test DNS and the way out:
curl -sI https://www.kernel.org | head -1. - On the router, watch translated traffic leave:
sudo tcpdump -ni eth0 icmpwhile the client pings an outside host. The source address is the router's WAN address. - Look at the rule counters:
sudo nft list ruleset(addcounterto a rule to count its packets).
If clients get an address but cannot reach the Internet, check, in this order: ip_forward is 1, the WAN interface name in the rules is right (ip -br link), the forward chain allows LAN to WAN, and the router itself has Internet access.
Common mistakes
- Two DHCP servers on one LAN. If the ISP's modem also serves DHCP on the same segment, clients get random gateways. Use separate segments or turn one server off.
- Same subnet on both sides. If the modem's network is also 192.168.50.0/24, routing breaks. Pick a different range for the LAN.
- Masquerading on the wrong interface.
oifnamemust be the WAN. If it names the LAN, packets to the Internet leave with private source addresses, and the ISP drops them. - Forgetting the forward chain. With
policy dropon forward and no LAN-to-WAN rule, NAT has nothing to translate.
The classic 2002 IP Masquerade HOWTO, written for Linux 2.0 to 2.4, is kept by the LDP at tldp.org.
Sources
- Performing Network Address Translation (NAT), nftables wiki
- Mangling packet headers (TCP MSS clamping), nftables wiki
- IP sysctl (ip_forward, IPv6 forwarding and accept_ra), kernel.org
- sysctl.d(5), freedesktop.org
- firewall-cmd(1), firewalld.org
- firewalld.policies(5), firewalld.org
- external zone definition (masquerade enabled), firewalld source
- dnsmasq manual page, thekelleys.org.uk
- systemd-resolved.service(8), freedesktop.org
- nm-settings-nmcli(5): ipv4.method shared, NetworkManager
- RFC 1918: Address allocation for private internets
The nftables rule set on this page was test-loaded with nftables 1.1.3.