nftables DNAT Port Forwarding Not Working: Complete Linux Troubleshooting Guide
You created the DNAT rule.
The public port is correct.
The internal server is running.
Yet the connection still times out.
This is one of the most frustrating Linux firewall problems because DNAT is only one part of the packet path.
A working port-forwarding configuration requires several independent components to be correct:
Internet
↓
WAN Interface
↓
PREROUTING
↓
DNAT
↓
Routing Decision
↓
FORWARD Chain
↓
Internal Server
↓
Return Route
↓
Conntrack / Reverse NAT
↓
Internet Client
If any stage fails, the user often sees exactly the same symptom:
Connection timed out
This guide presents a systematic method for troubleshooting nftables DNAT and Linux port forwarding without randomly changing firewall rules.
It builds directly on CorgiNetwork’s Linux DMZ with nftables architecture and the stateful filtering concepts introduced in Linux nftables Inter-VLAN Firewall.
How nftables DNAT Actually Works
A common misconception is:
If the DNAT rule exists, port forwarding should work.
That is incorrect.
Consider:
Public IP
203.0.113.10
Public Port
443
Internal Server
192.168.20.10
Internal Port
443
We want:
Internet Client
|
| 203.0.113.10:443
↓
Linux Firewall
|
| DNAT
↓
192.168.20.10:443
A typical nftables rule could be:
nft add rule ip nat prerouting \
iifname "eth0" \
tcp dport 443 \
dnat to 192.168.20.10:443
The official nftables NAT documentation explains that NAT chains have special semantics: the first packet of a flow is used to establish the NAT binding, while subsequent packets use the binding maintained for that connection.
Therefore the actual architecture is closer to:
Packet
↓
DNAT rule
↓
Conntrack/NAT binding
↓
Routing
↓
Filtering
↓
Destination
DNAT is not an alternative to routing or firewalling.
Our Troubleshooting Topology
Assume:
INTERNET
|
|
203.0.113.10
|
eth0
|
+---------------+
| Linux Router |
| nftables |
+---------------+
|
dmz0
|
192.168.20.0/24
|
|
192.168.20.10
Web Server
Linux firewall:
WAN:
203.0.113.10
DMZ:
192.168.20.1
Server:
192.168.20.10
Desired translation:
203.0.113.10:443
↓
192.168.20.10:443
Step 1: Verify the Application First
Before troubleshooting nftables, prove that the destination application actually works.
From another system inside the DMZ, test:
curl -vk https://192.168.20.10/
Or:
nc -vz 192.168.20.10 443
On the server, inspect listening sockets:
ss -lntp
You want something similar to:
LISTEN 0 511 0.0.0.0:443
or:
LISTEN 0 511 192.168.20.10:443
If the application listens only on:
127.0.0.1:443
remote packets arriving at:
192.168.20.10:443
will not reach that listener.
No nftables rule can fix an application bound only to the wrong address.
Step 2: Check the Server’s Local Firewall
Even if the Linux router permits the packet, the destination server may have its own firewall.
The packet path can therefore be:
Internet
↓
Linux Firewall
↓
DNAT
↓
FORWARD ACCEPT
↓
DMZ Server
↓
LOCAL FIREWALL
↓
DROP
If the destination server uses nftables:
sudo nft list ruleset
Check whether TCP/443 is accepted.
This distinction is important:
Perimeter firewall
≠
Server firewall
Both can independently block the connection.
Step 3: Verify IPv4 Forwarding
A Linux system does not become a router merely because it has multiple interfaces.
Check:
sysctl net.ipv4.ip_forward
Expected:
net.ipv4.ip_forward = 1
If it is:
net.ipv4.ip_forward = 0
enable it temporarily:
sudo sysctl -w net.ipv4.ip_forward=1
Then configure persistent forwarding according to your Linux distribution.
Without forwarding:
WAN packet
↓
Linux
X
↓
DMZ
The DNAT rule may exist, but Linux still will not function as the required IPv4 router.
Step 4: Verify the Routing Table
Run:
ip route
The firewall must know how to reach:
192.168.20.10
Test the actual lookup:
ip route get 192.168.20.10
A reasonable result might identify:
192.168.20.10 dev dmz0
If Linux chooses the wrong interface, DNAT is not the fundamental problem.
CorgiNetwork’s Linux Traffic Uses the Wrong Interface explains how route metrics, prefixes, policy rules, and source selection affect this decision.
Step 5: Inspect the NAT Table
Do not assume your rule loaded correctly.
Run:
sudo nft list ruleset
Or:
sudo nft list table ip nat
A basic NAT configuration might contain:
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat;
iifname "eth0"
tcp dport 443
dnat to 192.168.20.10:443
}
}
The official nftables chain documentation distinguishes nat chains from ordinary filter chains and notes that NAT chains are consulted with special first-packet semantics.
Do not put DNAT into an ordinary filtering chain and expect it to behave as a NAT chain.
Step 6: Add a Counter to the DNAT Rule
Counters are one of the fastest ways to determine whether traffic reaches the rule.
For example:
sudo nft add rule ip nat prerouting \
iifname "eth0" \
tcp dport 443 \
counter \
dnat to 192.168.20.10:443
Then:
sudo nft list chain ip nat prerouting
Generate an external connection.
If you see:
packets 0 bytes 0
the packet is not matching that rule.
Investigate:
wrong WAN interface
wrong destination port
wrong protocol
traffic never reaches the firewall
upstream NAT/router
cloud firewall
ISP filtering
wrong public IP
If the counter increases, you have proved:
Internet traffic
↓
Linux WAN
↓
DNAT rule
✓
Move to the next stage.
Step 7: DNAT Does Not Mean ACCEPT
This is perhaps the most important lesson in the article.
You may have:
DNAT = working
while:
FORWARD = blocked
The packet changes from:
Destination:
203.0.113.10
to:
Destination:
192.168.20.10
but it still needs permission to cross the router.
Suppose your forwarding chain is:
chain forward {
type filter hook forward priority filter;
policy drop;
}
Then the translated packet will be dropped unless an appropriate rule accepts it.
For example:
sudo nft add rule inet firewall forward \
iifname "eth0" \
ip daddr 192.168.20.10 \
tcp dport 443 \
ct state new \
counter \
accept
Return traffic can then use:
ct state established,related accept
This is the stateful design explained in Linux nftables Inter-VLAN Firewall.
The Critical Packet Transformation
Understanding where translation occurs prevents many firewall mistakes.
The external packet starts as:
SOURCE
198.51.100.50
DESTINATION
203.0.113.10:443
After DNAT:
SOURCE
198.51.100.50
DESTINATION
192.168.20.10:443
Therefore, depending on the hook and rule design, your forwarding policy may need to reason about the translated destination.
Conceptually:
WAN
↓
PREROUTING
↓
DNAT
↓
192.168.20.10
↓
Routing
↓
FORWARD
This packet-path knowledge is much more reliable than copying firewall commands from another server.
Step 8: Check the Forward Rule Counter
Add or inspect:
counter
on the relevant forwarding rule.
Then:
sudo nft list chain inet firewall forward
Possible situation:
DNAT counter:
10 packets
FORWARD counter:
0 packets
This is valuable evidence.
It tells you:
Packet reaches NAT
↓
Translation happens
↓
Expected filter rule does not match
Now inspect:
input interface
destination IP
destination port
protocol
connection state
rule order
instead of rewriting the entire firewall.
Step 9: Understand Conntrack
nftables NAT is normally stateful.
The official nftables NAT guide explains that the first packet establishes a NAT binding and later packets use the connection’s stored translation information.
If conntrack is installed:
sudo conntrack -L
Filter for the service:
sudo conntrack -L -p tcp | grep 443
A DNAT connection should give you evidence linking the original and translated flow.
nftables also exposes connection-tracking metadata; the official conntrack matching documentation documents status information including DNAT and SNAT state.
This gives us another troubleshooting layer:
Packet Capture
+
Rule Counters
+
Conntrack
=
Much stronger evidence
Step 10: Check the Return Route
This is one of the most common reasons port forwarding appears to work only halfway.
The request reaches:
192.168.20.10
The server receives it.
But how does the server return traffic to the Internet client?
Check:
ip route
on the DMZ server.
A typical configuration might use:
default via 192.168.20.1
where:
192.168.20.1
is the nftables router.
Then the path is symmetrical:
Internet
↓
Firewall
↓
Server
Internet
↑
Firewall
↑
Server
Conntrack can correctly associate both directions.
The Asymmetric Return-Path Problem
Suppose the server instead uses another gateway:
Internet
↓
Firewall A
↓
Server
↓
Firewall B
↓
Internet
The client initiated a connection toward Firewall A, but the reply bypasses it.
This can break the expected NAT and stateful-firewall behavior.
It may look like:
SYN arrives
Server receives SYN
Server sends SYN-ACK
Client never establishes connection
Before changing DNAT, inspect the return path.
For Linux-specific asymmetric-routing behavior, see Linux rp_filter and Asymmetric Routing.
Step 11: Use tcpdump on Both Interfaces
Packet capture turns assumptions into evidence.
On WAN:
sudo tcpdump -ni eth0 tcp port 443
On DMZ:
sudo tcpdump -ni dmz0 host 192.168.20.10 and tcp port 443
Run them while generating an external connection.
You may observe several scenarios.
Scenario A: Nothing Arrives on WAN
WAN:
nothing
The problem is upstream of nftables.
Investigate:
public DNS
public IP
upstream router
cloud firewall
ISP
carrier-grade NAT
wrong interface
client network
Do not edit the nftables rules yet.
Scenario B: Packet Arrives on WAN but Not DMZ
WAN:
SYN ✓
DMZ:
nothing
Possible causes:
DNAT not matching
IP forwarding disabled
forward-chain drop
wrong route
wrong interface
Now inspect counters and the routing table.
Scenario C: SYN Reaches Server but No SYN-ACK
WAN:
SYN
DMZ:
SYN
DMZ:
no SYN-ACK
The router probably delivered the packet.
Investigate the server:
service not listening
server firewall
wrong bind address
application failure
Check:
ss -lntp
and:
sudo nft list ruleset
on the destination host.
Scenario D: Server Replies but Client Never Receives It
WAN:
incoming SYN
DMZ:
SYN → server
SYN-ACK ← server
WAN:
no SYN-ACK
Now investigate:
return routing
conntrack
postrouting
SNAT requirements
firewall state
policy routing
This is very different from Scenario C.
Step 12: SNAT vs Masquerading
DNAT changes the destination.
SNAT changes the source.
For ordinary Internet port forwarding where the internal server uses the nftables router as its default gateway, additional SNAT on the inbound forwarded connection may not be necessary.
But SNAT becomes important in certain topologies.
Suppose:
Client
↓
Firewall
↓
Server
but the server would otherwise return through another router.
One possible architectural solution is to fix the routing.
Another is to intentionally use SNAT for the relevant flow when the design requires it.
Do not automatically masquerade everything merely because port forwarding fails.
Doing so can hide the original client address and mask an underlying routing problem.
What Is Masquerading?
The official nftables NAT documentation describes masquerading as a special form of SNAT where the source is derived from the output interface address.
Typical outbound example:
sudo nft add rule ip nat postrouting \
oifname "eth0" \
ip saddr 192.168.20.0/24 \
masquerade
This is useful for private networks sharing an external address.
It should not be treated as a universal “fix NAT” command.
Step 13: Check for Multiple Default Routes
A multi-homed Linux firewall may contain:
WAN1
WAN2
DMZ
LAN
VPN
Management
Run:
ip route
and:
ip rule
Then test:
ip route get CLIENT_IP
For source-aware troubleshooting:
ip route get CLIENT_IP from SOURCE_IP
If replies choose the wrong WAN, you may need policy routing.
CorgiNetwork’s Linux Policy Routing with ip rule and Multiple Routing Tables explains this architecture.
For Ubuntu/Netplan environments, Netplan Multiple Default Routes and Policy Routing covers persistent configuration.
Step 14: Watch for ARP Problems
On multi-interface Linux systems, an apparent NAT problem may actually involve neighbor resolution.
Check:
ip neigh
Capture ARP:
sudo tcpdump -ni dmz0 arp
If multiple interfaces exist on the same subnet, Linux ARP behavior can become especially confusing.
The dedicated Linux ARP Flux with Multiple NICs on the Same Subnet guide explains this scenario.
Step 15: Check Rule Order
nftables evaluates your rules according to the configured chains and ruleset.
Consider:
1. ct state established,related accept
2. unwanted traffic drop
3. DNAT traffic accept
If an earlier rule explicitly drops the connection, your later rule may never be reached.
Inspect handles:
sudo nft -a list chain inet firewall forward
Look at counters.
A rule with:
0 packets
while an earlier drop counter continuously increases is strong evidence of rule-order behavior.
Step 16: Use nftables Tracing
For difficult cases, nftables tracing can reveal which rules the packet encounters.
The nftables documentation includes ruleset tracing among its troubleshooting facilities.
After enabling tracing for carefully selected traffic, inspect with:
sudo nft monitor trace
You can observe information conceptually like:
packet
↓
prerouting
↓
DNAT
↓
forward
↓
rule
↓
verdict
This is particularly useful when a large ruleset contains:
jump chains
sets
maps
multiple tables
policy routing
complex filtering
Avoid indiscriminate tracing on a busy production router.
Step 17: Test from a Truly External Network
A surprisingly common mistake is testing the public address from the internal LAN.
You try:
LAN Client
↓
203.0.113.10:443
and conclude:
Port forwarding is broken.
But this introduces a different problem:
hairpin NAT, also called NAT loopback or NAT reflection.
External forwarding may work perfectly while internal clients cannot access the service through its public address.
Test first from a genuinely external connection, such as an appropriate remote system.
Separate:
External DNAT test
from:
Internal hairpin NAT test
because they are different packet paths.
Understanding Hairpin NAT
Consider:
LAN Client
192.168.10.50
|
| connects to
v
203.0.113.10:443
|
v
Linux Router
|
DNAT
|
v
192.168.20.10:443
The return path must still make sense.
Depending on the topology, additional NAT/routing design may be necessary to ensure the server’s reply traverses the firewall in the expected way.
Do not diagnose hairpin NAT until normal external DNAT has been proven.
Step 18: Verify DNS Separately
Suppose users report:
https://example.com
does not work.
But:
203.0.113.10:443
does.
That is not necessarily a port-forwarding failure.
Check DNS separately:
dig example.com
or:
getent ahosts example.com
Troubleshooting should separate:
DNS
↓
Routing
↓
NAT
↓
Firewall
↓
Application
instead of treating them as one system.
Step 19: Be Careful with Docker and Other Firewall Managers
Container platforms, virtualization tools, VPN software, and firewall-management applications may install or manage packet-filtering and NAT rules.
If your ruleset behaves unexpectedly:
sudo nft list ruleset
Inspect the entire active configuration, not only the table you created.
Look for:
Docker
libvirt
firewalld
ufw
VPN software
container networking
orchestration systems
The packet may encounter policy outside your custom chain.
Never assume your manually created table is the only Netfilter configuration on the system.
Step 20: Check nftables vs Legacy iptables
Modern Linux distributions frequently expose nftables directly or through compatibility tooling.
Inspect:
sudo nft list ruleset
and understand which firewall-management stack your distribution is using.
Do not simultaneously modify several firewall front ends without understanding how they interact.
The official nftables overview describes nftables as the modern Linux packet-classification framework and also documents the compatibility relationship with the older xtables tooling.
Advanced: Use ct status dnat
nftables connection tracking can expose whether a flow has undergone destination NAT.
The official conntrack metadata documentation includes a dnat conntrack status.
This makes it possible to design policies that reason about DNAT state rather than only repeating destination-port logic.
For advanced firewall architectures, this can help distinguish forwarded connections created through NAT from ordinary routed traffic.
However, start with a simple, explicit policy before adding clever abstractions.
Readable firewall rules are easier to audit and troubleshoot.
Advanced: Multiple Port Forwards with Maps
Imagine:
WAN TCP/443
↓
192.168.20.10
WAN TCP/8443
↓
192.168.20.20
WAN TCP/9443
↓
192.168.20.30
You could write multiple DNAT rules.
But nftables maps can represent key/value relationships efficiently.
The official nftables Maps documentation shows that destination ports can be mapped to different backend addresses, while the official Multiple NATs using nftables maps documentation demonstrates how multiple NAT mappings can be consolidated.
This connects directly with our previous CorgiNetwork article on nftables Sets and Maps.
Conceptually:
443
↓
Server A
8443
↓
Server B
9443
↓
Server C
becomes:
Destination Port
↓
MAP
↓
Backend Address
For a small firewall, individual rules may be clearer.
For dozens of mappings, maps become much more attractive.
Advanced: DNAT Load Balancing
nftables can go further than static one-to-one forwarding.
The official nftables load-balancing documentation documents NAT-based distribution using mechanisms such as number generation and hashing.
Conceptually:
Internet
|
TCP/443
|
v
Linux Firewall
|
+------ Server A
|
+------ Server B
|
+------ Server C
This is an advanced topic and should not be introduced merely to solve ordinary port-forwarding failures.
First make one deterministic DNAT path work.
Then scale it.
The Fastest Troubleshooting Workflow
When DNAT fails, use this sequence.
1. Is the destination service listening?
↓
2. Can another internal host reach it?
↓
3. Is server firewall permitting it?
↓
4. Is net.ipv4.ip_forward = 1?
↓
5. Does Linux have the correct route?
↓
6. Does traffic reach WAN?
↓
7. Does DNAT counter increase?
↓
8. Does FORWARD counter increase?
↓
9. Does packet reach internal server?
↓
10. Does server reply?
↓
11. Does reply return through firewall?
↓
12. Does conntrack contain the flow?
↓
13. Does translated reply leave WAN?
↓
14. Does client receive it?
This workflow is far more efficient than:
Change rule
Test
Change NAT
Test
Add masquerade
Test
Flush firewall
Test
Random firewall modification destroys evidence.
Diagnostic Command Set
Keep these commands available:
ip -br addr
ip route
ip rule
ip route get DESTINATION
ip neigh
sysctl net.ipv4.ip_forward
sudo nft list ruleset
sudo nft -a list ruleset
sudo conntrack -L
sudo ss -lntp
sudo tcpdump -ni eth0
sudo tcpdump -ni dmz0
sudo tcpdump -ni any host SERVER_IP
sudo nft monitor trace
Each answers a different question.
A Packet-Path Troubleshooting Matrix
| Observation | Likely Investigation |
|---|---|
| No packet on WAN | Upstream/public routing |
| WAN packet but DNAT counter 0 | NAT match |
| DNAT counter increases but forward counter 0 | Filter rule |
| Packet reaches server but no reply | Application/server firewall |
| Reply leaves server but not firewall | Return routing |
| Firewall sees reply but wrong WAN used | Routing/policy routing |
| External access works, internal public-IP access fails | Hairpin NAT |
| IP works but hostname fails | DNS |
This table is much more useful than assuming every timeout means:
nftables is blocking it
Common Mistake 1: DNAT Without Forwarding
Configuration:
DNAT ✓
FORWARD ✗
Result:
Timeout
Fix the forwarding policy—not the DNAT translation.
Common Mistake 2: Forwarding Disabled
Configuration:
DNAT ✓
Firewall rule ✓
net.ipv4.ip_forward = 0
Linux still is not forwarding IPv4 packets as required.
Common Mistake 3: Wrong Return Gateway
Configuration:
Request:
Internet → Firewall A → Server
Reply:
Server → Firewall B → Internet
This breaks the expected stateful/NAT path.
Fix routing architecture or deliberately design the required source translation.
Common Mistake 4: Service Bound to Localhost
Server:
127.0.0.1:443
but DNAT targets:
192.168.20.10:443
The firewall cannot make the application listen on a different interface.
Common Mistake 5: Testing Only from Inside
External DNAT works.
Internal access through the public address fails.
The actual problem may be:
Hairpin NAT
not Internet port forwarding.
Common Mistake 6: Masquerading Everything
A user sees a timeout and immediately adds:
masquerade
everywhere.
The connection begins working.
But now the backend sees every client as the firewall.
This may hide:
incorrect return routing
rather than fixing it.
Use NAT intentionally.
Common Mistake 7: Flushing the Production Firewall
Avoid:
nft flush ruleset
as a troubleshooting shortcut on a remote production server.
You may remove:
SSH rules
VPN rules
NAT rules
container rules
security controls
and lock yourself out.
Inspect before modifying.
Common Mistake 8: Ignoring IPv6
You troubleshoot IPv4 DNAT while clients resolve an AAAA record and attempt IPv6.
Check:
dig A example.com
and:
dig AAAA example.com
If the application is dual-stack, troubleshoot the actual address family being used.
Do not assume an IPv4 NAT rule controls IPv6 traffic.
Production Hardening After DNAT Works
Once connectivity is proven, improve the policy.
Instead of:
Internet
↓
ANY
↓
DMZ Server
permit only:
Internet
↓
TCP/443
↓
Web Server
For administration:
Internet
X
|
SSH
and preferably:
Management/VPN
|
SSH
|
v
DMZ Server
For DMZ-to-LAN:
DMZ
|
X
|
LAN
unless a documented application dependency requires an explicit exception.
The complete segmentation model is covered in Linux DMZ with nftables.
Frequently Asked Questions
Why is my nftables DNAT rule not working?
The DNAT rule may not be the failing component.
Check:
service
server firewall
IP forwarding
routing
DNAT match
forwarding policy
return route
conntrack
SNAT requirements
in that order.
Does DNAT automatically allow traffic through nftables?
No.
DNAT performs destination translation.
Filtering still determines whether the translated packet may be forwarded.
Why does the DNAT counter increase but the server receives nothing?
Likely areas include:
FORWARD chain
routing
wrong internal interface
VLAN configuration
Capture the packet on the internal interface to determine whether it leaves the router.
Why does the server receive the connection but the client times out?
Check whether the server replies.
If it does, verify that the reply returns through the same nftables router and that conntrack sees the connection correctly.
An asymmetric return path is a common cause.
Do I need masquerade for port forwarding?
Not automatically.
If the internal server correctly routes replies through the nftables firewall, DNAT can operate without blindly masquerading the incoming connection.
Use SNAT or masquerading when the architecture actually requires source translation.
Why does port forwarding work externally but not from my LAN?
That points toward a possible hairpin-NAT/NAT-loopback issue.
External DNAT and internal access to the public address are different packet paths.
How do I see whether nftables performed DNAT?
Use several forms of evidence:
sudo nft list ruleset
sudo conntrack -L
sudo tcpdump -ni any
Counters on the NAT rule are particularly useful for proving whether the initial flow matched it.
Can nftables forward different ports to different servers?
Yes.
You can use individual DNAT rules or nftables maps.
The official nftables Maps documentation describes key/value mappings, and the Multiple NATs using nftables maps example demonstrates multiple NAT mappings.
Conclusion
When nftables port forwarding fails, do not begin by changing the DNAT rule.
Follow the packet.
Internet Client
|
v
WAN Interface
|
v
PREROUTING
|
DNAT
|
v
Routing Decision
|
v
FORWARD Chain
|
v
DMZ Interface
|
v
Server Firewall
|
v
Application
|
v
Return Route
|
v
Conntrack / Reverse NAT
|
v
WAN
|
v
Client
At every stage, collect evidence.
Use:
ip route
ip rule
nft counters
conntrack
tcpdump
nft monitor trace
to determine exactly where the flow stops.
The official nftables NAT documentation explains the stateful NAT behavior behind this process, while the nftables conntrack documentation provides the connection-state metadata needed to understand translated flows.
Within CorgiNetwork, this troubleshooting workflow builds on the Linux DMZ with nftables architecture and the Linux nftables Inter-VLAN Firewall stateful-firewall foundation.
If routing chooses the wrong NIC, move to Linux Traffic Uses the Wrong Interface.
If the system needs source-dependent paths, continue with Linux Policy Routing with ip rule and Multiple Routing Tables.
If multiple default gateways must persist under Ubuntu, use Netplan Multiple Default Routes and Policy Routing.
If the return path is asymmetric, Linux rp_filter and Asymmetric Routing explains the Linux-specific behavior that can complicate troubleshooting.
And if Layer 2 segmentation is involved, Linux Bridge VLAN Filtering Explained provides the VLAN layer underneath the routing and firewall configuration.
The complete CorgiNetwork path is now becoming:
Linux Namespaces
↓
Linux Bridge
↓
VLAN Filtering
↓
Inter-VLAN Routing
↓
Stateful nftables
↓
Sets & Maps
↓
DMZ Architecture
↓
DNAT / Port Forwarding
↓
Packet-Path Troubleshooting
↓
Production Linux Firewall Engineering
The most important troubleshooting rule is simple:
Do not guess which nftables rule is broken. Prove where the packet stops.