Reported symptoms
A partner integration went live on Tuesday. The design is unremarkable: an
SFTP endpoint published on 203.0.113.30 at the DC-East edge, destination
NAT to 10.30.0.12 in the DMZ, and a permit in the WAN-DMZ chain that
governs traffic from the WAN zone to the DMZ zone.
The partner cannot connect. Their client sits there and times out; it is never refused. From where they are standing, the host is switched off.
By Thursday the incident had three owners and no cause:
- The change ticket says the router is fine.
show nat destinationshows the rule matching, with a counter that rises in step with the partner’s retry interval. NAT is working. That is not a guess, it is a counter. - The server team has closed their half. The service answers from a jump host inside the DMZ, and it answers from the office LAN through the public address. The daemon is up, listening and reachable — from everywhere except the one place it is meant to be reachable from.
- The firewall has never heard of the address.
show log firewall | grep 203.0.113.30returns nothing at all. The obvious reading is that the partner’s traffic is not arriving, so the network team started with the upstream provider.
Two facts nobody has connected yet. A publication configured fourteen months ago on this same router, through this same chain, works perfectly. And a second engineer published an SMTP relay in the same change window; it is also silently unreachable, and it has its own ticket.
Evidence provided
$ sudo tcpdump -ni eth0 'host 203.0.113.30 and tcp port 22'09:41:02.113455 IP 198.51.100.77.44120 > 203.0.113.30.22: Flags [S], seq 1884213, win 64240, length 0
09:41:03.137902 IP 198.51.100.77.44120 > 203.0.113.30.22: Flags [S], seq 1884213, win 64240, length 0
09:41:05.177881 IP 198.51.100.77.44120 > 203.0.113.30.22: Flags [S], seq 1884213, win 64240, length 0Illustrative output
$ sudo tcpdump -ni eth2 'host 10.30.0.12 and tcp port 22'0 packets capturedIllustrative output
$ sudo conntrack -L -d 203.0.113.30 -p tcptcp 6 108 SYN_SENT src=198.51.100.77 dst=203.0.113.30 sport=44120 dport=22 [UNREPLIED] src=10.30.0.12 dst=198.51.100.77 sport=22 dport=44120 mark=0 use=1
conntrack v1.4.7 (conntrack-tools): 1 flow entries have been shown.Illustrative output
$ show firewallRule Action Packets Bytes Description
---- ------ ------- ----- -----------
10 accept 91422 7.3M Established/related
20 drop 4 320 Invalid
30 accept 208114 14M HTTPS to web server
40 accept 0 0 SFTP to partner endpoint
9999 drop 11908 714K Default denyIllustrative output
$ show configuration commands | match 'WAN-DMZ rule 40'set firewall ipv4 name WAN-DMZ rule 40 action 'accept'
set firewall ipv4 name WAN-DMZ rule 40 description 'SFTP to partner endpoint'
set firewall ipv4 name WAN-DMZ rule 40 destination address '203.0.113.30'
set firewall ipv4 name WAN-DMZ rule 40 destination port '22'
set firewall ipv4 name WAN-DMZ rule 40 protocol 'tcp'
set firewall ipv4 name WAN-DMZ rule 40 state 'new'Illustrative output
$ show configuration commands | match 'WAN-DMZ rule 30 destination'set firewall ipv4 name WAN-DMZ rule 30 destination address '10.30.0.9'
set firewall ipv4 name WAN-DMZ rule 30 destination port '443'Illustrative output
$ show log firewall | grep 10.30.0.12 | tail -2Work the evidence before reading on
Nothing here is missing. Every command answers, every counter is consistent with every other counter, and the contradiction is in what they mean rather than in what they say.
- The SYN arrives on the WAN interface and nothing leaves on the DMZ interface. Between those two captures the packet was discarded. Which parts of the router had a chance to discard it, and in what order?
- The conntrack entry’s reply tuple names
10.30.0.12. What does the presence of that address, in that half of the entry, tell you about what has already happened to the packet before any filter rule sees it? - Rule 40 reads correctly and has fired zero times. Rule 30, which works, is written differently in exactly one respect. Put the two side by side.
grep 203.0.113.30on the firewall log is empty andgrep 10.30.0.12is not. What is the log recording — the packet as it arrived, or the packet as the firewall saw it?
Before continuing: at the moment the WAN-DMZ chain evaluates this packet, what is in its destination address field?
Root cause
1. NAT happens first, and it changes the thing the rule matches on
The packet path is fixed. Destination NAT is evaluated in the prerouting hook; the routing decision comes next; the forward filter chain comes after that. So the sequence for this flow is:
- The SYN arrives with destination
203.0.113.30:22. - Prerouting DNAT rewrites the destination to
10.30.0.12:22. This is the counter everybody has been citing as proof the router is healthy. - The routing decision selects the DMZ interface.
- The
WAN-DMZchain evaluates a packet whose destination is10.30.0.12.
Rule 40 says destination address 203.0.113.30. At step 4 there is no
packet anywhere in the router with that destination address, and there has
not been since step 2. The rule is unmatchable. Rules 10, 20 and 30 do not
describe an inbound SFTP connection either, so the packet reaches the end
of the chain and the default action drops it.
The rule was reviewed and approved because it is the rule a reasonable person writes. It names the address in the ticket, in the diagram and in the partner’s connection string. It is simply written against the wrong stage of the packet path.
2. The NAT counter is the trap, not the reassurance
The DNAT counter rising is true, and its meaning is the opposite of the one it was given. NAT firing is not evidence that the router is passing the traffic — it is the event that made the filter rule stop matching.
This is worth sitting with, because the same shape recurs: a working component whose correct operation is a precondition for the failure. As long as “NAT works” is treated as evidence for “the firewall must be fine”, the investigation cannot reach the answer.
3. The log recorded the truth under a name nobody searched for
Firewall logging happens in the filter chain, so the entry describes the
packet as the filter saw it — post-translation. Every log line for this
flow carries DST=10.30.0.12. Every search anybody ran was for
203.0.113.30, because that is the address in the ticket.
An empty grep was then read as “the traffic never arrived”, which sent the network team to the upstream provider for most of a day. The evidence was present the entire time, indexed under the address that the incident had no reason to know about yet.
Resolution
- Say out loud in the incident channel that the NAT counter is not evidence the router is passing traffic, and show the conntrack reply tuple. Until that is agreed, the investigation keeps returning to the provider and the server team.
- Correct rule 40 to name the post-translation destination — the DMZ host address and the port the host is listening on — and leave the NAT rule untouched. It was never wrong.
- Scope the corrected rule to the ingress it is meant to serve. An internal destination address is reachable from every zone that can route to it, whereas the public address it replaced was implicitly external-only, so pin the rule to the inbound interface or the source zone. Restoring the service must not silently widen it.
- Apply the change under commit-confirm. This is a WAN-facing chain on a router you are reaching across the network you are editing, and a mistake here is a console visit.
- Watch rule 40 counter before confirming the commit. A rule that reads correctly and counts zero is precisely the state you started in, so the counter moving is the first real evidence you have had.
- Fix the SMTP publication in the same change. It is the same defect, it was made in the same window, and leaving it to its own ticket means running this incident again next week.
- Refuse the broad any-to-DMZ permit, including as a temporary measure. If there is no window to make the change properly, the honest position is that the service is not published yet — with a named owner and a date — rather than published through a hole.
Verification
- The permit rule fires. The rule 40 packet counter advances when the partner reconnects. Configuration review passed on this rule for two days while the service was dead, so the counter is the check that means something.
- The state is complete.
sudo conntrack -Lfor the four-tuple shows ESTABLISHED with the [ASSURED] flag and non-zero counters in both directions, replacing the SYN_SENT [UNREPLIED] entry. - The drops stop. The chain default-deny counter no longer climbs in step with the partner retry interval, and
show log firewallno longer shows drops addressed to the internal service address. - A real transfer completes, not just a handshake. A publication that accepts the connection and then stalls has a second fault, and finding it now is cheaper than finding it during the partner batch run.
- The negative test still fails as intended. From a source that has no business reaching the DMZ, the connection is still dropped — this is what distinguishes a corrected rule from a widened one.
- The SMTP publication passes every check above, on its own evidence rather than by assumption.
Prevention
- Treat a publication as one change with two halves that must agree about which address they name. The NAT rule names the public address; the filter rule names the private one. Write them together and review them together.
- Make it a checklist line rather than folklore: a filter rule carrying a public destination address, in a chain evaluated after prerouting, is a defect on sight. That single check would have caught this in review.
- Verify changes from state, not from configuration. Rule counters, conntrack entries and a real connection from outside are checks that can fail; reading the rule back and agreeing that it looks right is not.
- Alert on default-deny drops whose destination is an internal service address. Very little else produces that pattern, and it is this fault announcing itself.
- Keep
dropon the WAN chain, but write down what it costs you: the partner sees a dead host rather than a refusal, and the first hour of every such incident goes to the server team. Give the service desk a documented way to tell the two apart. - When one change window produces two unreachable services, work them as one fault until the evidence separates them.