Skip to main content
RunBook Academy

← All break/fix scenarios in VyOS

advancedvyos-firewall~40 min

Stateful Firewall Breaks Asymmetric Routing

Reported symptoms

  • Some external services are unreachable from the DC-East office network and others are instant, and the split follows no pattern the service desk can see — one SaaS vendor times out while another linked from the same page loads normally
  • The nightly off-site backup to a partner endpoint has failed with a connection timeout three nights running, after two years of clean runs
  • ping and traceroute from the affected clients to the affected destinations succeed every time, which is why the first two escalations were closed as "network is fine, application problem"
  • The monitoring dashboard shows every affected endpoint green, because the probes that reach them are ICMP
  • Long-lived sessions to the affected destinations all died within the same minute; sessions to unaffected destinations were untouched
  • R1-B, the ISP-B edge router, has gone from a handful of default-deny log lines a minute to several thousand, every one of them sourced from a well-known cloud provider, which the on-call engineer opened as a suspected scan
  • No firewall or NAT change has been committed on either edge router for six weeks
  • The only change in the window is the ISP-B eBGP session on R1-B, turned up two nights ago and signed off as successful

Evidence

  • · sudo conntrack -L -d 198.51.100.25 on R1-A shows the flow present, in SYN_SENT, marked [UNREPLIED], with packets counted in one direction only
  • · The same command on R1-B returns nothing at all for that four-tuple
  • · tcpdump on R1-A eth0 (ISP-A) shows the client SYN leaving and no SYN/ACK ever coming back
  • · tcpdump on R1-B eth0 (ISP-B) shows the SYN/ACK for that exact four-tuple arriving, on time, intact
  • · show log firewall on R1-B shows the drop, with the remote server as SRC, the office client as DST, and the SYN and ACK flags set
  • · show firewall on R1-B: the established/related rule counter is flat while the catch-all deny counter climbs at roughly the connection-attempt rate
  • · The same chain accepts the ICMP echo replies for those destinations, because its ICMP accept rule sits above the invalid drop and carries no state clause
  • · show ip bgp summary on R1-B shows an eBGP session to ISP-B established two nights ago, advertising the same 203.0.113.0/24 that R1-A advertises to ISP-A
  • · The office network is publicly addressed out of that /24 and is not NATed on either edge router, so the return packets carry the client address and are routed to whichever edge the remote network prefers
Diagnosis and resolutionclick to reveal

Root cause

Connection state is per-router, and this estate now has two routers in the return path. R1-A and R1-B both advertise 203.0.113.0/24, so when the ISP-B session came up, part of the internet started preferring ISP-B for the return leg of flows the office initiates. The office egress still prefers R1-A, so a flow's SYN creates conntrack state on R1-A while its SYN/ACK arrives at R1-B, which has never seen the flow and therefore has no ESTABLISHED entry to match. R1-B's WAN-facing chain does exactly what it was written to do: the established/related accept does not match, none of the explicit permits match an inbound TCP segment from port 443 to an ephemeral port, and the packet is dropped by the chain's default action. Nothing is misconfigured in the sense the incident channel was looking for — the firewall is correct, the routing is correct, and BGP is behaving exactly as multi-homing is supposed to behave. What is wrong is a design assumption that nobody wrote down: that both directions of a flow traverse the same router. The failure is partial rather than total because the return path is chosen per remote network, so destinations whose operators prefer ISP-A stay symmetric and keep working, which is what makes the symptom set look like a problem at the far end. ICMP survives because that chain's ICMP accept was moved above the invalid drop years ago, for a monitoring vendor, and was written without a state clause — so it matches an echo reply that belongs to no tracked flow and admits it. Ping is not testing the same thing as TCP, and on this router it never was.

Remediation

Decide first whether you are restoring the invariant or removing the dependency on it, because they are different changes with different costs. Restoring the invariant is the fast path: make the return traffic come back to R1-A by making R1-B's advertisement less attractive — AS-path prepending towards ISP-B, or the provider community that marks the announcement as backup — so both directions meet on one router again. That costs you the inbound load sharing you just bought and leaves ISP-B as a failover path rather than an active one, which is a real reduction in what the second uplink is worth and should be recorded as such. Removing the dependency is the larger piece of work: either both directions must be steered to one router per flow, or both routers must hold the same state. Do not reach for conntrack state synchronisation as though it solved this. State replication between a pair of routers is built for failover, where the standby needs yesterday's state; it does not help here, because the packet you need state for is the SYN/ACK, and it lands on the other router one round trip after the SYN created the entry. Do not "fix" it by relaxing the return chain to stateless. A rule that admits inbound TCP without a state clause admits every out-of-state segment an attacker cares to send, which is precisely the exposure the chain exists to prevent, and it will be permanent. If the change window is not now, the honest hold is to shut the ISP-B eBGP session or withdraw the prefix from it: one command, an immediate return to the known-good single-return-path state, a rollback everybody understands, and a named owner with an end time to come back and do the design properly.

Verification

Verify at the flow, not at the configuration. Reproduce the failing case first — a TCP connection from an office client to one of the destinations that was timing out — and require it to complete a handshake, not merely to answer a ping. Then read the state: sudo conntrack -L on R1-A must show that flow ESTABLISHED with non-zero counters in both directions and the [ASSURED] flag, where before it sat in SYN_SENT marked [UNREPLIED]. On R1-B, the catch-all deny counter must stop climbing at the connection-attempt rate and the firewall log must stop showing SYN/ACK segments addressed to office clients. Confirm the routing change actually took by capturing on R1-B's uplink and seeing no return traffic for office prefixes arrive there at all, rather than by reading the prepend back out of the configuration. Finally run the negative test, because the failure mode of this fix is that somebody restores service by opening the firewall: from an external position, an unsolicited TCP segment to a client address must still be dropped. A fix that makes the traffic work and also makes that test pass has not fixed anything.

Prevention

Treat "which router will the return packet land on" as an input to the design rather than an emergent property of somebody else's best-path selection. Any change that adds a second place for traffic to enter the estate — a second uplink, a second announcement, a new peering, an ECMP path — needs the same question asked before it is committed: does any stateful device now sit on one direction of a flow and not the other. Put that question in the multi-homing turn-up runbook as a gate, not as a note. Make the leading indicator visible: the rate of default-deny drops on each edge router is the cheapest signal this class of fault produces, and it moved two nights before anyone opened a ticket. Alert on the rate, and route the alert to the network team rather than to the security queue, because it will look like a scan every time. Give every rule an explicit state clause, including the ICMP accepts — not because the stateless ICMP rule caused this, but because it is why four people concluded the network was healthy and closed the ticket. Finally, write the assumption down. "Forward and return traverse the same edge router" is load-bearing, it was true for years, and it stopped being true during a change that was reviewed, executed cleanly and signed off as successful.

Reported symptoms

The estate is the course reference topology: DC-East with two edge routers, R1-A on ISP-A and R1-B on ISP-B, both advertising the site’s own 203.0.113.0/24 and both carrying a full WAN-facing rule set. The office network is publicly addressed out of that /24 and is deliberately not NATed, which is normal for a BGP-multi-homed site and turns out to matter a great deal.

The ISP-B session on R1-B was turned up on Tuesday night. It came up clean, the prefix was accepted, the change was signed off, and nothing broke.

On Thursday the service desk had four tickets that nobody connected:

  • A SaaS vendor’s portal times out from every office machine. Another vendor, linked from the same intranet page, loads instantly. The split follows no pattern anybody can describe.
  • The nightly off-site backup to a partner endpoint has failed three nights running with a connection timeout. It has run for two years.
  • Two engineers independently ran ping and traceroute to the failing destinations. Both succeeded, both times, with normal latency. Both tickets were closed as an application problem at the far end.
  • The on-call engineer opened a security ticket, because R1-B had gone from a handful of default-deny log lines a minute to several thousand, all sourced from a large cloud provider. It reads exactly like a scan.

Monitoring agrees that everything is fine. The synthetic checks that reach those endpoints are ICMP, and they are green.

The one thing that does line up, and that nobody noticed for a day and a half, is that long-lived sessions to the affected destinations all died inside the same minute — and sessions to everything else were untouched.

Evidence provided

Read-only / SafeR1-A: the flow exists and has never had a reply
$ sudo conntrack -L -d 198.51.100.25 -p tcp
tcp 6 112 SYN_SENT src=203.0.113.61 dst=198.51.100.25 sport=51344 dport=443 [UNREPLIED] src=198.51.100.25 dst=203.0.113.61 sport=443 dport=51344 mark=0 use=1
conntrack v1.4.7 (conntrack-tools): 1 flow entries have been shown.

Illustrative output

Read-only / SafeR1-B: the same query, on the other edge router
$ sudo conntrack -L -d 198.51.100.25 -p tcp
conntrack v1.4.7 (conntrack-tools): 0 flow entries have been shown.

Illustrative output

Read-only / SafeR1-A ISP-A uplink: SYN out, retransmit, retransmit, nothing back
$ sudo tcpdump -ni eth0 'host 198.51.100.25 and tcp port 443'
14:02:11.418823 IP 203.0.113.61.51344 > 198.51.100.25.443: Flags [S], seq 2948117, win 64240, length 0
14:02:12.441907 IP 203.0.113.61.51344 > 198.51.100.25.443: Flags [S], seq 2948117, win 64240, length 0
14:02:14.481930 IP 203.0.113.61.51344 > 198.51.100.25.443: Flags [S], seq 2948117, win 64240, length 0

Illustrative output

Read-only / SafeR1-B ISP-B uplink: the SYN/ACK is arriving here, on time
$ sudo tcpdump -ni eth0 'host 198.51.100.25 and tcp port 443'
14:02:11.463102 IP 198.51.100.25.443 > 203.0.113.61.51344: Flags [S.], seq 771204, ack 2948118, win 65160, length 0
14:02:12.487551 IP 198.51.100.25.443 > 203.0.113.61.51344: Flags [S.], seq 771204, ack 2948118, win 65160, length 0

Illustrative output

Read-only / SafeR1-B: the drop, described from the far side of the flow
$ show log firewall | grep 203.0.113.61 | tail -2
Read-only / SafeR1-B chain WAN-IN: established flat, ICMP busy, catch-all enormous
$ show firewall
Rule     Action   Packets      Bytes   Description
----     ------   -------      -----   -----------
10       accept   4218        312K     Established/related
15       accept   88214       6.1M     ICMP (monitoring)
20       drop     31          2.4K     Invalid
30       accept   0           0        SSH from jump host
9999     drop     2841177     198M     Default deny

Illustrative output

Read-only / SafeR1-B
$ show ip bgp summary

Work the evidence before reading on

Four facts are in tension, and reconciling them is the whole exercise.

  1. R1-A has a conntrack entry for the flow and it is [UNREPLIED]. R1-B has no entry for the same flow at all. Both routers are telling the truth. What does each one’s answer tell you about which packets it has seen?
  2. The SYN leaves on one router’s uplink and the SYN/ACK arrives on the other’s, 45 milliseconds later. Neither packet is lost. Which device is deciding where the reply lands, and is it a device you own?
  3. Rule 10 accepts established and related traffic and its counter is flat, while rule 9999 is doing millions of packets. What has to be true about the SYN/ACK for rule 10 not to match it?
  4. ping works and TCP does not, to the same address, over the same path. Read rule 15 and rule 20 in the chain above and work out why. The ordering is the answer, and it is older than this incident.

Before continuing: which router created the state, and which router is being asked to use it?

Root cause

1. Connection state is per-router, and there are now two routers

nf_conntrack is a table in one kernel’s memory. When the office client sends its SYN, the packet leaves through R1-A, and R1-A writes an entry for that four-tuple. Nothing about that entry exists anywhere else. R1-B has never seen the flow.

When the SYN/ACK arrives at R1-B, the kernel there looks the four-tuple up, finds nothing, and hands the packet to the chain without an ESTABLISHED state. Rule 10 matches state established and state related, so it does not fire. Rules 30 and above are explicit permits for services the site publishes — none of them describe an inbound TCP segment from port 443 to an ephemeral port. The packet falls off the end of the chain and the default action drops it.

Every component behaved correctly. That is what makes this hard: there is no misconfiguration to find, and the four people who went looking for one spent a day and a half not finding it.

2. BGP made the return path somebody else’s decision

Both edge routers advertise 203.0.113.0/24. Before Tuesday, only R1-A advertised it, so every remote network on the internet had exactly one way back and it was symmetric by construction. From Tuesday there are two, and each remote network picks its own — by its own best-path selection, over which you have no direct control.

This is also why the failure is partial. Destinations whose operators prefer ISP-A stay symmetric and keep working. Destinations whose operators prefer ISP-B break. That distribution is stable, arbitrary from the office’s point of view, and looks exactly like “some vendors are having a bad week”.

The absence of NAT is what exposes the estate to this. A NATed office would carry R1-A’s own address on the wire, and the reply would be addressed to a host route inside ISP-A’s assignment — no choice for the remote network to get wrong. Public addressing plus two announcements is what puts the decision in someone else’s routing table.

3. ICMP survived, and that is why nobody believed the network was broken

Rule 15 accepts protocol icmp and carries no state clause. It was moved above the invalid drop years ago because a monitoring vendor complained about dropped latency probes, and it has worked perfectly ever since.

An echo reply arriving at R1-B for a request that left via R1-A belongs to no tracked flow. Rule 10 cannot match it. But rule 15 does not ask about state — it asks about protocol — so it matches, and the reply is admitted and forwarded to the client. Ping works end to end across a split path that TCP cannot cross.

Two engineers used that as proof the network was healthy, and it was a reasonable thing to believe. Rule 15 is the difference between a stateful check and a protocol check, and the page is testing whichever one it happens to use.

Resolution

  1. Stop the security investigation. The drops on R1-B are your own return traffic, and every hour spent on the scan hypothesis is an hour the office is half offline. Say so in the channel explicitly, with the tcpdump from both uplinks as the evidence.
  2. Decide whether you are restoring path symmetry or removing the dependency on it. These are different changes with different costs and the choice belongs to whoever owns the multi-homing design, not to the person holding the pager.
  3. If restoring symmetry: make R1-B less attractive as a return path — AS-path prepending towards ISP-B, or the provider community that marks an announcement as backup — so both directions meet on R1-A again. Record what this costs: inbound load sharing is gone and ISP-B becomes a failover path rather than an active one.
  4. Make the change with commit-confirm so a mistake on a WAN-facing router undoes itself rather than requiring a console. Confirm it only after the verification below has passed.
  5. If holding rather than fixing: shut the ISP-B eBGP session or withdraw the prefix from it. That is one command, it returns the estate to the state it was in on Monday, and everyone understands the rollback. Give the hold a named owner and an end time, because a temporary shutdown of a redundant uplink is a quiet way to lose the redundancy permanently.
  6. Do not relax the return chain to stateless to make the traffic flow. A rule admitting inbound TCP with no state clause admits every out-of-state segment anybody sends you, it will never be removed, and it converts an availability incident into a standing exposure.
  7. Do not deploy conntrack state synchronisation expecting it to fix this. State replication exists so a standby can take over a live flow; the packet you need here arrives on the other router one round trip after the state was created, which is not a race replication is designed to win.
  8. Once service is restored, open the design question properly: either steer both directions of a flow to one router, or build an edge that holds no per-flow state that only one router has.

Verification

  1. The failing case completes. Open a TCP connection from an office client to a destination that was timing out and require a completed handshake — not a successful ping, which was green throughout and proved nothing.
  2. The state looks right on R1-A. sudo conntrack -L shows that flow ESTABLISHED, with non-zero counters in both directions and the [ASSURED] flag, where before it sat in SYN_SENT marked [UNREPLIED].
  3. The drops stop on R1-B. The catch-all deny counter stops climbing at the connection-attempt rate, and show log firewall stops showing TCP segments addressed to office client addresses.
  4. The routing change actually took. Capture on R1-B uplink and confirm no return traffic for office prefixes arrives there at all. Read this from the wire, not from the configuration — the configuration was never the thing in doubt.
  5. The negative test still passes. From an external position, an unsolicited TCP segment to an office client address must still be dropped. A fix that restores the traffic and also makes this test pass has restored service by removing the firewall.
  6. The monitoring can now see the fault. Re-check that the default-deny drop rate on both edge routers is graphed and alerting, and confirm the alert would have fired on Tuesday night.

Prevention

  • Ask the return-path question before every change that adds a second way into the estate — a second uplink, a second announcement, a new peering, an ECMP path. Does any stateful device now sit on one direction of a flow and not the other? Put it in the multi-homing turn-up runbook as a gate with a signature, not as a note in the appendix.
  • Alert on the per-router default-deny drop rate. It is the cheapest signal this fault produces and it moved two nights before the first ticket. Route the alert to the network team: it will look like a scan every single time, and sending it to the security queue guarantees a day of investigating the wrong thing.
  • Give every rule an explicit state clause, ICMP included. The stateless ICMP accept did not cause this incident, but it is why four people concluded the network was healthy.
  • Do not let ICMP be the only synthetic check on a path that carries TCP. A probe that opens a real connection would have failed on Tuesday night and named the destination.
  • Write the assumption down. “Forward and return traverse the same edge router” was true for years, is load-bearing, and stopped being true during a change that was reviewed, executed cleanly and signed off as successful. Unwritten invariants are only discovered when they break.