Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

NAT and port forwarding on a Linux gateway with nftables

By updated

Improve this page

A Linux gateway does two things with NAT: it lets an internal network go out to the Internet behind a single address (MASQUERADE), and it exposes an internal service on a public port (DNAT). This guide configures both directions on a Linux router with nftables, the kernel's modern firewall. You enable routing, write the MASQUERADE and DNAT rules, then check each translation with conntrack. All commands and outputs come from a real lab: a Linux gateway and an internal host (10.0.0.2) behind it. Audience: administrators setting up a router, a homelab or a DMZ.

If the terms SNAT, DNAT or PAT are new to you, first lay down the concepts with understanding NAT (in French). This guide is the hands-on part.

What you will learn

  • Enable IP routing on the gateway.
  • Share the outgoing connection of an internal network with MASQUERADE.
  • Forward a public port to an internal service with DNAT.
  • Check the translations with conntrack.
  • Make the rules persistent across reboots.

Prerequisites

  • A Linux gateway with two "sides": an interface towards the Internet (the WAN, here eth0) and access to the internal network (the LAN, here 10.0.0.0/24).
  • An internal host to reach (here 10.0.0.2, with a web service on port 80).
  • nftables installed (sudo apt install nftables or sudo dnf install nftables).
  • sudo rights.

The principle: two directions, two chains

NAT rewrites the addresses of packets that cross the gateway, and nftables does it in two distinct chains:

  • postrouting (outgoing): just before sending, the source address is rewritten. This is SNAT; its automatic form is MASQUERADE, which takes the IP of the outgoing interface.
  • prerouting (incoming): as soon as it arrives, the destination address is rewritten. This is DNAT, used for port forwarding.

Remember the rule: MASQUERADE to go out, DNAT to come in.

Enable IP routing

By default, Linux does not route packets from one interface to another. You must allow it with the ip_forward parameter, first on the fly then in a persistent file:

Terminal window
sudo sysctl -w net.ipv4.ip_forward=1

To make it permanent, write it to a dedicated file:

Terminal window
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-routeur.conf

Without this step, no NAT rule will work: packets would be dropped instead of being routed, whatever the ruleset.

Share the connection: MASQUERADE

Goal: the machines of the internal network (10.0.0.0/24) must reach the Internet behind the gateway's address. You create a NAT table and a MASQUERADE rule in the postrouting chain.

Terminal window
sudo nft add table ip nat
sudo nft add chain ip nat postrouting '{ type nat hook postrouting priority 100 ; }'
sudo nft add rule ip nat postrouting ip saddr 10.0.0.0/24 oifname "eth0" masquerade

The rule reads: "for any packet coming from 10.0.0.0/24 and going out through eth0, replace the source address with that of eth0". Let us check that the internal host does get out:

Terminal window
# Run from the internal host 10.0.0.2
curl -sS -o /dev/null -w "HTTP %{http_code}\n" http://1.1.1.1
HTTP 301

The internal host, which has no direct route to the Internet, did reach 1.1.1.1 through the gateway.

Forward a port: DNAT

The opposite goal: expose the internal web service (10.0.0.2:80) on the gateway's public port 8080. You add a prerouting chain and a DNAT rule.

Terminal window
sudo nft add chain ip nat prerouting '{ type nat hook prerouting priority -100 ; }'
sudo nft add rule ip nat prerouting iifname "eth0" tcp dport 8080 dnat to 10.0.0.2:80

The rule reads: "any packet arriving on eth0 for port 8080 is forwarded to 10.0.0.2:80". Let us test from an external machine:

Terminal window
# From a client on the public network
curl http://<gateway-ip>:8080/
BONJOUR-DEPUIS-HOTE-INTERNE

The external client thinks it is talking to the gateway on 8080; it actually receives the reply of the internal server on its port 80. This is port forwarding.

One prerequisite governs the whole manoeuvre, and it has nothing to do with the rules: for DNAT to work, the internal host must send its replies back through the gateway, which must therefore be its default route. Otherwise the return trip bypasses the translation, and the external client rejects a reply coming from an address it never contacted. It is the most frequent cause of a DNAT that "does not answer" while the internal service did receive the request.

The complete ruleset

Your two rules live in a single ip nat table. Displaying it confirms the whole configuration:

Terminal window
sudo nft list table ip nat
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.0.0.0/24 oifname "eth0" masquerade
}
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iifname "eth0" tcp dport 8080 dnat to 10.0.0.2:80
}
}

Check with conntrack

NAT is not magic: the kernel tracks each connection in a table (conntrack) to rewrite the return traffic on the fly. It is the number one diagnostic tool for NAT. For the outgoing connection (MASQUERADE):

Terminal window
sudo conntrack -L | grep 10.0.0.2
tcp 6 ... src=10.0.0.2 dst=1.1.1.1 sport=60184 dport=80 src=1.1.1.1 dst=192.168.10.79 sport=80 dport=60184 [ASSURED]

Read the two tuples: the original connection (src=10.0.0.2) and its rewriting for the return trip (dst=192.168.10.79, the gateway's IP). The source was indeed masqueraded.

For the incoming forwarding (DNAT), the same command filtered on the public port shows the rewritten destination:

Terminal window
sudo conntrack -L | grep dport=8080
tcp 6 ... src=192.168.10.11 dst=192.168.10.79 sport=49334 dport=8080 src=10.0.0.2 dst=192.168.10.11 sport=80 dport=49334 [ASSURED]

Here, the destination is rewritten: the client targeted 192.168.10.79:8080, the reply comes from 10.0.0.2:80. The translation is proven in both directions.

Make the rules persistent

Rules added on the fly disappear at reboot. To keep them, write the ruleset into the file loaded by the nftables service, preceded by a flush ruleset, then enable the service. Without that first line, reloading the file duplicates the rules: measured on Debian 13, a second nft -f of a raw export takes the masquerade rule from 1 to 2 copies.

Terminal window
# Debian/Ubuntu: the service reads /etc/nftables.conf
{ echo 'flush ruleset'; sudo nft list ruleset; } | sudo tee /etc/nftables.conf
# RHEL/AlmaLinux: the service reads /etc/sysconfig/nftables.conf
{ echo 'flush ruleset'; sudo nft list ruleset; } | sudo tee /etc/sysconfig/nftables.conf
sudo systemctl enable --now nftables

nft list ruleset exports all loaded tables, including those of firewalld or Docker if they are running: review the file before enabling it, or only export your table with nft list table ip nat. At the next boot, nftables.service replays this file, and the sysctl.d file re-enables routing. The gateway is rebuilt identically.

Security and points of attention

A gateway that routes and forwards is a target: a few reflexes are a must, because NAT decides the translation, never the authorisation.

  • Only route what must be routed. Add a forward chain of type filter that only accepts legitimate flows (from the LAN to the WAN, and the established return), rather than a global accept policy.
  • Restrict DNAT to a source when possible: iifname "eth0" ip saddr <trusted-network> tcp dport 8080 dnat ... limits who can reach the internal service.
  • The exposed service remains responsible for its own security: DNAT only routes, it hardens nothing. Put exposed services in a DMZ (in French).
  • Log dropped packets to spot attempts: a log prefix "DROP " rule before the default policy.

Troubleshooting

These six symptoms are read from the left column, and it is worth telling them apart before touching the rules: a packet that does not cross the gateway and a packet that crosses but does not come back have opposite causes. The last row is not a defect but a reflex: conntrack -L settles it in one command.

SymptomProbable causeSolution
The internal host cannot reach the InternetRouting disabledsysctl net.ipv4.ip_forward must be 1
MASQUERADE has no effectWrong oifname interfaceCheck with ip route show default
DNAT does not answerThe internal host does not route through the gatewayMake the gateway the host's default route
It works then stopsA forward rule blocks the returnAllow the established,related state
Rules disappear at rebootRuleset not persisted, or written to a file the service does not readWrite to the unit's file (systemctl cat nftables), preceded by flush ruleset, then systemctl enable nftables
Doubt about the translationDiagnosisconntrack -L shows both tuples

Put it into practice

Building the gateway yourself remains the best way to anchor both directions of NAT: this lab has you enable IP routing, write the MASQUERADE and DNAT rules with nftables, then make the ruleset persistent across reboots.

Key points

  • MASQUERADE in postrouting shares an outgoing connection behind the gateway's IP.
  • DNAT in prerouting forwards an incoming port to an internal host.
  • net.ipv4.ip_forward=1 is the absolute prerequisite: without routing, no rule applies.
  • conntrack -L is the diagnostic tool: its two tuples show the translation in both directions.
  • The internal host must have the gateway as its default route, otherwise the return trip breaks NAT.
  • Persist in the file read by the nftables service (/etc/nftables.conf on Debian, /etc/sysconfig/nftables.conf on RHEL), preceded by flush ruleset, otherwise everything goes at reboot.

Knowledge check

Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn