Skip to main content
RunBook Academy

← All break/fix scenarios in VyOS

advancedvyos-firewall~40 min

Firewall Blocks Routed Traffic

Reported symptoms

  • A partner cannot reach a newly published SFTP endpoint on 203.0.113.30: the connection times out rather than being refused, so it looks from their side like the host is switched off
  • The destination NAT rule for that publication has a rising packet counter, which the change ticket cites as proof that the router is doing its part
  • The service answers normally from a jump host inside the DMZ, and from the office LAN through the same public address, so the server team has closed their half of the ticket
  • show log firewall | grep 203.0.113.30 returns nothing at all, which the incident channel reads as the packets never reaching the router
  • tcpdump on the WAN interface shows the partner SYN arriving and retransmitting on schedule; tcpdump on the DMZ interface shows nothing leaving towards the server
  • A different publication configured fourteen months ago on the same router works perfectly
  • A second engineer published an SMTP relay in the same change window; it is also silently unreachable, and is being tracked as an unrelated ticket
  • The firewall configuration review passed: the permit rule exists, it is in the right chain, it says action accept, and it names the right port

Evidence

  • · show nat destination shows the rule matching, with a packet counter rising in step with the partner retry interval
  • · sudo conntrack -L -d 203.0.113.30 -p tcp shows the flow in SYN_SENT and [UNREPLIED], and its reply tuple names 10.30.0.12 — so the translation has already been applied
  • · show firewall for the WAN-DMZ chain shows the new permit rule with a packet counter of zero and the default-deny counter climbing at the retry rate
  • · show log firewall | grep 10.30.0.12 returns the drops that the search for 203.0.113.30 could not find
  • · show configuration commands | match WAN-DMZ shows the permit rule written with destination address 203.0.113.30
  • · The publication that works names 10.30.0.9 in its permit rule, not the public address it is published on
  • · tcpdump on the WAN interface shows the SYN arriving; tcpdump on the DMZ interface shows nothing leaving, so the packet is being discarded inside the router
  • · No packet loss, no interface errors, and the routing table has a connected route to the DMZ subnet
Diagnosis and resolutionclick to reveal

Root cause

Destination NAT runs in the prerouting hook, before the routing decision and before the forward filter chain. By the time the packet reaches the WAN-DMZ chain its destination has already been rewritten from the public address 203.0.113.30 to the DMZ host 10.30.0.12, so a permit rule written as "destination address 203.0.113.30" describes a packet that no longer exists at the point the rule is evaluated. The rule cannot match, none of the other rules describe inbound SFTP either, and the chain's default action drops the packet. The drop is silent from the partner's point of view because the action is drop rather than reject, which is why it presents as a dead host instead of a refused connection. What makes this misdiagnosis so durable is the NAT counter: it really is rising, NAT really is working, and everyone reads that as evidence the router is behaving — when NAT working is precisely the thing that stopped the filter rule matching. The firewall log compounds it. It records the post-translation packet, so it holds no entry containing the public address that every ticket, every diagram and every search is written around, and the absence of log lines is read as "the traffic never got here" rather than "we are grepping for an address the firewall never sees". The fourteen-month-old publication works because whoever wrote its rule named the internal address; the SMTP publication from the same change window fails for the identical reason and is being tracked as a separate incident.

Remediation

Rewrite the permit rule against the post-translation destination — the DMZ host address and the port the host actually listens on — and leave the NAT rule alone, because it was never wrong. Scope the rule while you are in there: a rule that names an internal address is reachable from every zone that can route to that address, whereas the public address it replaced was implicitly reachable only from outside, so pin the rule to the inbound interface or the source zone that the publication is meant to serve. Otherwise the fix quietly widens the exposure at the same time as it restores the service. Make the change with commit-confirm, so a mistake on a WAN-facing router reverts itself instead of requiring a console session, and confirm only after the rule counter has been seen to move. Fix the SMTP publication in the same change: it is the same defect and leaving it for its own ticket means running this incident twice. The remediation to refuse is the one that will be suggested when the partner escalates on a Friday afternoon — a broad any-to-DMZ permit to get traffic flowing now and tighten later. It works immediately, it removes the control the chain exists to provide, and nothing about it ever gets tightened. If there is no window to make the change properly, the honest hold is to leave the service unpublished and tell the partner it is not live, with a named owner and a date, rather than to publish it through a hole.

Verification

Verify on the counter and on the wire, never on the configuration, because the configuration looked correct through an entire review while the service was dead. The permit rule's packet counter must advance when the partner reconnects — a rule that reads correctly and counts zero is exactly the state you started in. sudo conntrack -L for that four-tuple must show the flow ESTABLISHED with the [ASSURED] flag and non-zero counters in both directions, where before it sat in SYN_SENT marked [UNREPLIED]. The default-deny counter on the chain must stop climbing in step with the partner retry interval, and the firewall log must stop showing drops addressed to the internal service address. Then require an actual transfer to complete rather than a completed handshake: a publication that accepts a connection and stalls has a second problem, and you want to find it now rather than at the partner's next batch run. Run the negative test from a source that should not have access and confirm it is still dropped, which is what tells you the fix was a corrected rule and not a widened one. Repeat every one of these checks against the SMTP publication.

Prevention

Publishing a service is two changes that have to agree with each other about which address they are talking about, and they are evaluated at different points in the packet path. Write them together, review them together, and make the reviewer's job explicit: the NAT rule names the public address, the filter rule names the private one, and a filter rule carrying a public destination address in a chain that runs after prerouting is a defect on sight. Put that line in the firewall and NAT review checklist so it is a gate rather than folklore. Change verification has to read state rather than configuration — the rule counter, the conntrack entry, a real connection from outside — because every configuration-shaped check passed here. Monitor the default-deny log for drops whose destination is an internal service address: that pattern is this bug's signature and almost nothing else produces it. Keep drop rather than reject on the WAN chain, but know what it costs you diagnostically, and give the service desk a documented way to distinguish a firewall drop from a dead host so the first hour does not go to the server team. Finally, when the same change window produces two unreachable publications, treat that as one fault with two tickets until proven otherwise.

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 destination shows 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.30 returns 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

Read-only / SafeWAN side: the partner SYN is arriving, and retrying
$ 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 0

Illustrative output

Read-only / SafeDMZ side: nothing leaves the router towards the server
$ sudo tcpdump -ni eth2 'host 10.30.0.12 and tcp port 22'
0 packets captured

Illustrative output

Read-only / Safethe reply tuple already names the internal address
$ sudo conntrack -L -d 203.0.113.30 -p tcp
tcp 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

Read-only / Safechain WAN-DMZ: rule 40 exists, reads correctly, and has never fired
$ show firewall
Rule     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 deny

Illustrative output

Read-only / Safethe rule that passed review
$ 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

Read-only / Safethe publication that works, for comparison
$ 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

Read-only / Safethe log entries that were said not to exist
$ show log firewall | grep 10.30.0.12 | tail -2

Work 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.

  1. 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?
  2. 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?
  3. 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.
  4. grep 203.0.113.30 on the firewall log is empty and grep 10.30.0.12 is 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:

  1. The SYN arrives with destination 203.0.113.30:22.
  2. 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.
  3. The routing decision selects the DMZ interface.
  4. The WAN-DMZ chain evaluates a packet whose destination is 10.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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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

  1. 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.
  2. The state is complete. sudo conntrack -L for the four-tuple shows ESTABLISHED with the [ASSURED] flag and non-zero counters in both directions, replacing the SYN_SENT [UNREPLIED] entry.
  3. The drops stop. The chain default-deny counter no longer climbs in step with the partner retry interval, and show log firewall no longer shows drops addressed to the internal service address.
  4. 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.
  5. 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.
  6. 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 drop on 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.