Skip to main content
RunBook Academy

← All break/fix scenarios in VyOS

advancedvyos-ecmp~30 min

ECMP Unexpectedly Changes Traffic Path

Reported symptoms

  • At 20:15, within the same second, a large fraction of established sessions across the whole site dropped — including sessions that never touched the circuit being added
  • Two partner sites report that the source address of traffic from this site changed mid-session; one partner allowlist rejected calls for about ninety seconds
  • A monitoring probe that traceroutes an external target every minute has shown a different hop set on almost every run since the change, which reads as path instability
  • The new ISP-C circuit is carrying far less than a third of egress bytes, so the capacity project looks like it failed as well
  • By 20:22 everything is healthy: three next-hops installed, all interfaces up, no drops, sessions re-established
  • Nobody accepts that the change caused it, because the change was additive. Nothing was removed and no existing next-hop was touched

Evidence

  • · `show ip route 0.0.0.0/0` — three equal-cost next-hops, all selected, all installed
  • · `ip route show 0.0.0.0/0` — three `nexthop` lines, each `weight 1`
  • · `show system commit` — one revision at 20:15, "add ISP-C default (additive, low risk)"
  • · The firewall counter for invalid-state drops shows a sharp spike beginning at 20:15 and returning to baseline within about two minutes
  • · `sudo conntrack -L --src-nat` grouped by the reply-direction destination, which is the translated source address — the three WAN addresses hold roughly equal numbers of flows
  • · `show interfaces ethernet eth2` against `eth0` and `eth1` — ISP-C carries a small share of bytes despite carrying a third of the flows
  • · `traceroute -n` run three times in succession from the same host — three different second hops
Diagnosis and resolutionclick to reveal

Root cause

Linux ECMP is stateless. Nothing on the router remembers which path a flow used; the next-hop is recomputed for every packet from a hash of the flow tuple against the current set of members. Flow affinity is not a stored fact, it is an emergent property of feeding the same inputs into the same function — which means the member set is an input. Adding a third next-hop to a group that had two changes that input for every flow on the router, so most existing flows were remapped onto a different WAN even though their 5-tuple never changed and their traffic had no relationship to the new circuit. Roughly two thirds of them moved, not the one third that the "additive" framing implies. Remapping is what turned a routing change into an outage, because the site source-NATs per WAN behind a stateful firewall: a remapped flow leaves a different interface, is translated to a different public address, and its return traffic arrives at a provider holding no conntrack entry for it, so the firewall discards it and the session resets. The two remaining complaints are not faults at all. Traceroute emits a new destination port per probe, so each probe is a distinct flow that per-flow ECMP may legitimately send a different way, and the hash distributes flows rather than bytes, so a handful of large transfers pinned to two paths will leave a third path carrying its share of sessions and very little traffic.

Remediation

Start by recognising that the disruption has already been paid and that the obvious response would charge it again. Removing the third next-hop is not an undo: it changes the member set a second time, remaps most flows a second time, and produces a second wave of resets identical to the first. For that reason the usual right answer here is to hold — keep the new path, record the one-off cost, and stop classifying ECMP membership changes as low-risk — with a named owner and a review time rather than an implicit decision to leave it. Take the path back out only if the circuit itself is faulty, and then do it as a scheduled change with service owners told to expect session resets, not as an emergency rollback. The structural fix addresses the coupling rather than the hash: traffic whose source address is part of a contract has no business being subject to a hash at all, so move it off ECMP with policy routing into a table holding a single default, and it becomes immune to every future change of WAN membership. Do that for the two partner integrations first, since they are the flows with a third party's allowlist attached to them.

Verification

Verify recovery first, then verify that the next change will not repeat this. The invalid-state drop counter must be back at baseline and flat over several minutes, and conntrack must show a stable population of established flows rather than a churn of new ones. Check the ECMP distribution by flow count and not by bytes: group `conntrack -L` by translated source address and confirm the three WANs hold comparable numbers of flows. Only if the flow counts are also skewed is there a hashing question to answer, and the byte imbalance on its own is expected behaviour rather than evidence. For the traffic you have pinned off ECMP, sample its conntrack entries and confirm every one shows the same translated source address — a policy route that has never been observed to constrain anything has not been tested. Re-run the partner call that failed and confirm the address they see is the documented one. Do not use traceroute for any of this: it cannot answer which path a session takes, because its probes are not that session.

Prevention

Treat a change to the membership of an ECMP group as a session-resetting change, in the same category as a reboot. It gets a window, an owner and a notification, and the word "additive" gets no weight, because adding a member disturbs every flow in the group and not only the flows that will use the new member. Pin address-dependent traffic off ECMP permanently: anything with a partner allowlist, a licence check, or a mail reputation attached to its source address belongs in a policy-routed table with one default, where no hash can reach it. Baseline the hash distribution by flow count during normal operation so that "the new link is not taking its share" is measured against something known, rather than against an assumption that flows and bytes distribute alike. Keep an inventory of every dependency on the estate's public source addresses and review it before any WAN change, since that inventory is what turns this from an outage into a notification. And retire traceroute as evidence about per-flow paths: `conntrack -L` filtered on the exact tuple, or a capture on each candidate egress, answers the question traceroute only appears to answer.

Reported symptoms

edge3 fronts a site with two WAN uplinks, ISP-A on eth0 and ISP-B on eth1, carrying two static default routes at equal distance — an ECMP pair. Each WAN has its own masquerade rule, so a flow’s public source address is whichever interface it leaves by. A stateful firewall sits in front of both.

A capacity project added a third circuit. The change was written up as additive and low risk: one more default route, same distance, nothing removed. It went in at 20:15.

Within the same second, a large fraction of established sessions across the entire site dropped. Not the sessions that would have used the new circuit — the helpdesk logged about a minute of “everything froze”, from users doing work that had nothing to do with ISP-C and could not have.

Three more reports followed, and each was filed as its own problem:

  • Two partner sites saw the source address of traffic from this site change mid-session. One partner’s allowlist rejected calls for roughly ninety seconds.
  • The monitoring probe that traceroutes an external target every minute has returned a different hop set on almost every run since 20:15. That reads as path instability, and it is still doing it.
  • ISP-C is carrying nowhere near a third of egress bytes. The capacity project appears to have failed on top of everything else.

By 20:22, when the first engineer logged in, the router looked perfect. Three next-hops installed and selected, every interface up, no drops, sessions re-established. The change had been additive, so nobody on the bridge accepted that it was responsible.

Evidence provided

Read-only / Safethree members, equal weight — this is the input that changed
vyos@edge3:~$ ip route show 0.0.0.0/0
default proto static metric 20
nexthop via 203.0.113.1 dev eth0 weight 1
nexthop via 198.51.100.1 dev eth1 weight 1
nexthop via 192.0.2.1 dev eth2 weight 1

Illustrative output

Read-only / Safethe timestamp matches the helpdesk calls to the second
vyos@edge3:~$ show system commit | head -3
0   2026-08-18 20:15:07 by admin via cli   add ISP-C default (additive, low risk)
1   2026-08-14 10:31:44 by admin via cli   dhcp pool extension for floor 3
2   2026-08-11 16:02:20 by admin via cli   snmp acl update

Illustrative output

Read-only / Safein a source-NATed entry the translated address is the reply tuple's destination
vyos@edge3:~$ sudo conntrack -L --src-nat | grep -oE 'dst=(192.0.2|198.51.100|203.0.113)[0-9.]+' | sort | uniq -c
   1174 dst=192.0.2.20
 1206 dst=198.51.100.20
 1189 dst=203.0.113.20

Illustrative output

Read-only / Safesame target, same host, two different first hops, seconds apart
vyos@edge3:~$ traceroute -n 198.18.0.1; traceroute -n 198.18.0.1
traceroute to 198.18.0.1 (198.18.0.1), 30 hops max, 60 byte packets
1  192.0.2.1  1.702 ms  1.688 ms  1.694 ms
traceroute to 198.18.0.1 (198.18.0.1), 30 hops max, 60 byte packets
1  203.0.113.1  1.221 ms  1.402 ms  1.198 ms

Illustrative output

Work the evidence before reading on

There are four complaints on the ticket. Before reading on, decide how many of them are faults.

  1. The change added a member to an ECMP group. Where on the router is the record that flow X was using ISP-A before the change? If you cannot name the place it is stored, what happens to that flow when the group changes?
  2. Two thirds of sessions dropped, not one third. What arithmetic produces two thirds when you go from two members to three, and what does that say about which flows were affected?
  3. A dropped session is not a routing failure by itself — a packet leaving by a different WAN still reaches the internet. What else is configured on this router that makes “different WAN” mean “different flow” to the far end?
  4. Traceroute returns a different first hop on consecutive runs. What does traceroute vary between probes, and is that one flow or many?

Then the question that separates the fault from the noise: which of these four symptoms would still be reported if the site did no source NAT at all?

Root cause

1. Linux ECMP is stateless, so there is no flow-to-path memory

Nothing on this router remembers which path a flow was using. There is no flow table for forwarding, no per-session pin, nothing to consult. For every packet, the kernel hashes the tuple and selects a member of the group from the result.

Flow affinity — all packets of a TCP session leaving by the same WAN — is real, but it is not a stored decision. It is an emergent property of feeding identical inputs into a deterministic function and getting an identical answer. The session stays on one path because its tuple does not change.

2. The member set is one of those inputs, so membership changes remap flows

The selection is a function of the hash and the current set of members. Change the set and you change the answer for tuples that were untouched.

The scale is the part that gets underestimated. Selecting a member from a hash is a modulo-style operation over the group, so going from two members to three changes the result for about two thirds of hash values. Only by coincidence does a given flow keep its previous path. “Additive” describes the configuration diff; it does not describe the effect, and there is no additive change to the membership of an ECMP group.

This is the behaviour named in the title. The traffic path changed unexpectedly — for flows that had nothing to do with the circuit that was added, which is why the correlation with the change was rejected on the bridge.

3. Per-WAN source NAT turns a path change into a session break

A remapped flow on its own is survivable: another WAN still reaches the internet. What makes it fatal here is what the router does to the packet on the way out.

Each WAN has its own masquerade rule, so the source address is whichever interface the packet leaves by. When a flow is remapped mid-session it is translated to a different public address, the far end sees traffic from an address it has no session for, and the return traffic comes back to a provider whose firewall holds no conntrack entry for it. The state check fails, the packet is discarded, and the session resets. That is the invalid-state spike in the firewall counters, and it is why it ended after two minutes: everything had finished reconnecting on its new path.

The partner allowlist rejection is the same mechanism seen from outside. The site’s traffic arrived from an address the partner had never been told about.

4. Two of the four complaints are not faults

Traceroute varying between runs. Traceroute sends a new destination port on each probe, so every probe is a distinct flow that per-flow ECMP is entitled to hash somewhere else. The output is correct behaviour rendered by a tool that cannot express the question being asked of it. It was doing this before the change too; nobody was reading it before the change.

ISP-C carrying a small share of bytes. The hash distributes flows, not bytes. The conntrack evidence shows flows split almost exactly in thirds, which is the group working as designed. A handful of large transfers happened to hash to the other two paths, and each is pinned to its path for its lifetime, so the byte counters are lopsided while the flow counters are not.

Resolution

  1. Do not roll back reflexively. Removing the third next-hop changes the member set again, remaps most flows again, and produces a second wave of resets identical to the first. An undo here costs exactly what the original change cost.
  2. Confirm the site has actually recovered before deciding anything: the invalid-state drop counter back at baseline and flat, and conntrack showing a stable population of established flows rather than continuous new ones. If the disruption is over, the expensive part of this incident is behind you.
  3. Default to holding. Keep ISP-C, record that the one-off disruption has been paid, and reclassify ECMP membership as a disruptive change. Hold is a decision, so it takes a named owner and a review time — otherwise it is just nobody doing anything.
  4. Remove the path only if the circuit itself is faulty. Then schedule it, tell service owners to expect session resets, and do it in a window, using commit-confirm so a mistake on a live path backs itself out.
  5. Fix the coupling rather than the hash. Move the two partner integrations off ECMP into a policy-routed table with a single default, so no future WAN change can move them: set protocols static table 100 route 0.0.0.0/0 next-hop 203.0.113.1 interface eth0, then a route-map matching those destinations bound to the ingress interface with set interfaces ethernet eth1 policy route-map RM-PARTNER-PIN.
  6. Tell the two partners what happened and confirm which address they should have allowlisted, since one of them has already tightened their list against an address the site no longer reliably uses.
  7. Close the traceroute complaint explicitly rather than leaving it open. The monitoring probe is not detecting instability; it is detecting ECMP. Point it at a per-flow measurement or accept its output as expected.

Verification

  1. Recovery is real. The firewall invalid-state counter is at baseline and flat across several minutes, not merely lower than it was during the spike.
  2. The group is distributing as designed. Group sudo conntrack -L --src-nat by the reply-direction destination and confirm the three WANs hold comparable numbers of flows. Flow count is the measure; byte count is not, and treating it as one is what turned expected behaviour into a fourth ticket.
  3. The byte imbalance has an explanation, not a fix. If flow counts are even and bytes are not, the hash is working and a few large transfers are responsible. Only skewed flow counts justify investigating the hash.
  4. The pinned traffic is genuinely pinned. Sample conntrack entries for the partner integrations and confirm every one shows the same translated source address. A policy route that has never been observed to constrain anything has not been tested.
  5. The partner call works from the documented address. This is the only check that does not depend on the router that caused the incident.
  6. The decision is recorded. Whether you held or scheduled a removal, the owner and the review time exist somewhere other than the incident channel.
  7. No verification step in this list uses traceroute. If one of yours does, it is measuring the tool rather than the network.

Prevention

  • Reclassify ECMP membership changes as session-resetting, alongside reboots and failovers. They get a window, an owner and a notification. “Additive” earns no reduction in category, because adding a member disturbs every flow in the group.
  • Pin address-dependent traffic off ECMP for good. Anything carrying a partner allowlist, a licence check or a mail reputation on its source address belongs in a policy-routed table with one default, where the hash cannot reach it. The cost is a little configuration; the benefit is that WAN changes stop being customer-visible for that traffic.
  • Baseline the flow distribution across the WANs while things are normal, so a future “the new link is not taking its share” is compared against a known number rather than against the assumption that flows and bytes distribute alike.
  • Keep an inventory of everything that depends on the estate’s public source addresses, and read it before any WAN change. That inventory is the difference between an outage and a notification sent in advance.
  • Where the design permits it, remove the coupling entirely: a site that announces its own address space and does not source-NAT per WAN can move a flow between providers without changing what the far end sees. That is a redesign rather than a remediation, but it is the reason larger sites do not have this failure mode.