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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
ct status dnatnftables 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.
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.
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.
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.
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.
| 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 Configuration:
DNAT ✓
FORWARD ✗ Result:
Timeout Fix the forwarding policy—not the DNAT translation.
Configuration:
DNAT ✓
Firewall rule ✓
net.ipv4.ip_forward = 0 Linux still is not forwarding IPv4 packets as required.
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.
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.
External DNAT works.
Internal access through the public address fails.
The actual problem may be:
Hairpin NAT not Internet port forwarding.
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.
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.
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.
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.
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.
No.
DNAT performs destination translation.
Filtering still determines whether the translated packet may be forwarded.
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.
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.
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.
That points toward a possible hairpin-NAT/NAT-loopback issue.
External DNAT and internal access to the public address are different packet paths.
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.
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.
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.
AI Agents as Code: Managing AI Agent Configurations, Prompts and Tools with Git AI agents…
Data Center Power Distribution Architecture: UPS, PDU, Redundancy and Rack Power Explained Data center power…
DNSSEC Troubleshooting: Fix SERVFAIL & DNSKEY Errors DNSSEC Troubleshooting: How to Fix SERVFAIL, DS, DNSKEY…
Terraform State Management Explained: Remote State, Locking, Backends and Best Practices Terraform makes infrastructure reproducible…
RTO vs RPO Explained: Disaster Recovery Objectives, Examples and Planning A disaster recovery plan is…
Network Intrusion Detection Explained: NIDS, Sensors, Signatures and Traffic Analysis A firewall can decide whether…
View Comments