Reported symptoms
edge2 is the router for a regional site with two transit providers. ISP-A on
eth0 is the contracted primary and sends a default over eBGP. ISP-B on eth1
is the backup and sends one too, held back by an inbound route-map. The site
announces its own prefix to both.
The ticket did not come from the network team. Finance opened it, because ISP-B has spent two months billing above its 95th-percentile commit while ISP-A — the circuit the site pays a primary’s price for — bills at close to nothing.
Two more complaints arrived separately and were logged as separate problems:
- A partner’s API started refusing the site two days ago with an IP-allowlist error. The partner had just tightened their allowlist to the single address the site documented to them years ago.
- A handful of long-lived sessions through a stateful middlebox drop intermittently. Inbound traffic still arrives over ISP-A, so those flows are asymmetric: out one provider, back the other.
The confounding facts are what kept this open. BGP is clean — both sessions Established for weeks, prefix counts as expected, no flaps. And it has already been fixed once: a week ago an engineer raised the local-preference on the ISP-A default to 200, confirmed the change was live in the BGP table, and closed the ticket. Nothing moved.
Evidence provided
vyos@edge2:~$ show ip route 0.0.0.0/0Codes: K - kernel route, C - connected, S - static, R - RIP,
O - OSPF, B - BGP, > - selected route, * - FIB route
S>* 0.0.0.0/0 [1/0] via 198.51.100.1, eth1, weight 1, 06w0d05h
B 0.0.0.0/0 [20/0] via 203.0.113.1, eth0, weight 1, 05w6d22hIllustrative output
vyos@edge2:~$ show ip bgp 0.0.0.0/0BGP routing table entry for 0.0.0.0/0
Paths: (2 available, best #1, table default, not in RIB)
64510
203.0.113.1 from 203.0.113.1 (203.0.113.254)
Origin IGP, localpref 200, valid, external, best (Local Pref)
64520
198.51.100.1 from 198.51.100.1 (198.51.100.254)
Origin IGP, localpref 50, valid, externalIllustrative output
vyos@edge2:~$ show configuration commands | match "static route 0.0.0.0/0"set protocols static route 0.0.0.0/0 next-hop 198.51.100.1Illustrative output
vyos@edge2:~$ show system commit | head -60 2026-08-11 09:22:04 by admin via cli raise localpref on ISP-A default
1 2026-08-04 14:05:51 by admin via cli firewall rule 40 for monitoring
2 2026-07-30 11:47:19 by admin via cli ntp server change
3 2026-07-07 22:14:38 by admin via cli temp default during ISP-A window
4 2026-07-07 21:58:02 by admin via cli shutdown ISP-A session for windowIllustrative output
vyos@edge2:~$ traceroute -n 198.18.0.1traceroute to 198.18.0.1 (198.18.0.1), 30 hops max, 60 byte packets
1 198.51.100.1 1.412 ms 1.388 ms 1.401 ms
2 198.51.100.254 2.907 ms 2.884 ms 2.910 msIllustrative output
Work the evidence before reading on
The BGP table and the routing table disagree, and both are telling the truth.
- Read the two lines of
show ip route 0.0.0.0/0again. Which marker means “selected”, and which of the two candidates has it? What decides between two routes for the same prefix from two different sources? show ip bgp 0.0.0.0/0saysbestnext to the ISP-A path, and it also says something about the RIB. What is BGP claiming to be best among?- The local-preference change is live, visible and correct. If a knob is set in the right place and has no effect, what does that tell you about which layer the decision is actually being made in?
- Revision 3 in the commit log was a sensible change when it was made, and the session it was made for is listed one line below it. What was supposed to happen to revision 3, and what event would have made anybody notice that it had not?
Then the question that resolves it: if you raised the ISP-A local-preference to 4,294,967,295, what would change? Your answer tells you where the fault is.
Root cause
1. A static default outlived the window it was made for
Revision 4 in the commit log shut the ISP-A session for a maintenance window. Revision 3, sixteen minutes later, added a static default toward ISP-B to keep the site online while it was down. That was correct, and it worked.
The window closed. The ISP-A session came back. The static did not go away.
2. Administrative distance decided the rest, and it knows nothing about intent
When two routes cover the same prefix, the router picks the one whose source it trusts more, and that trust is a fixed number: connected 0, static 1, eBGP 20, OSPF 110, iBGP 200. Lower wins. A static route at distance 1 beats an eBGP-learned route at distance 20 every time, in every direction, regardless of what the operator meant.
So from the moment the session recovered, the site’s entire egress moved to the backup provider. Nothing broke, because ISP-B is a working transit. There was a default; it was selected; traffic flowed. No monitoring anywhere in the estate asks whether the right traffic is leaving by the right door, so the fault surfaced through an invoice and a partner’s allowlist, eight weeks later, as two unrelated tickets.
3. Local-preference was the right idea in the wrong layer
This is the part that cost a week, and it is the most transferable thing in the scenario.
Local-preference is a BGP path attribute. It orders paths within BGP, and its job is to decide which of several BGP paths for a prefix becomes BGP’s best path. Having decided, BGP hands the RIB exactly one candidate — and that candidate arrives carrying administrative distance 20.
The contest that determines the selected route is between that one BGP candidate and the static route at distance 1. Local-preference is not an input to that contest and never sees it. Set to 200 or to four billion, it changes which BGP path is best and cannot change which route is selected.
The line to notice in the evidence is not in RIB, sitting in the same output
that says best. Both are accurate. BGP is reporting that it has chosen a best
path and that the RIB did not take it.
Resolution
- Establish what now depends on the wrong path before you correct it. Six weeks of egress via ISP-B is long enough for dependencies to accrete: firewall or NAT rules written against
eth1, allowlists at third parties updated to the ISP-B address, VPN peers with a pinned local address. Correcting the route without checking turns one broken partner into several. - Decide whether this is a window change or an immediate one, and say which out loud. Removing the static re-translates every source-NATed flow to the ISP-A address on its next packet, so sessions in flight break. That cost is real, and it is smaller than the allowlist rejection that is already happening — but it is a decision, not a side effect.
- Remove the static default and the inert fix together, under
commit-confirm 5:delete protocols static route 0.0.0.0/0, anddelete policy route-map RM-FROM-ISP-A rule 10 set local-preferencefor the knob added a week ago. Leaving the local-preference behind is worse than useless, because the next engineer will read it as evidence that path preference is managed here. - Confirm the selection moved before the confirm timer expires.
show ip route 0.0.0.0/0must show the BGP route selected. If it does not, let the auto-rollback fire rather than adding another change on top. - If ISP-B must remain primary for commercial reasons, do not re-add the static. Express it in BGP by raising local-preference on the ISP-B default, so the path is still tied to the liveness of the BGP session and still fails over.
- Re-run the partner call from the site and confirm the source address they see is the documented one. That is the symptom the business raised, and the routing table is not evidence for it.
- Close the loop on the window that started this: find the change record for revision 3 and record what the removal step should have said.
Verification
show ip route 0.0.0.0/0shows the eBGP default via 203.0.113.1 selected, and shows no static candidate at all. Not an inactive static — no static. A leftover inactive route is the next incident, because it also outranks the correct one the moment its next-hop becomes reachable again.show ip bgp 0.0.0.0/0is unchanged. The BGP table was correct throughout, and a fix that changed it would mean you fixed something else.- Both resolvers agree.
traceroute -nfrom the router and from a host behind it must both leave via 203.0.113.1. Router and host resolve the route by different paths, and agreement across the two is the evidence, not either one alone. - The outside world sees the right address. Check the observed public source address from an external service, and re-run the partner call that was being rejected. This is the only check independent of the router that produced the fault.
- Egress has moved.
show interfaces ethernet eth0andeth1show the byte counters crossing over within a few minutes. - Failover works. In a window, fail the ISP-A session and confirm the default moves to ISP-B and returns on recovery. For six weeks this site believed it had a redundant edge and had a single static route; the only way to know that has been repaired is to exercise it.
Prevention
- Give temporary configuration an expiry. A change made to survive a window is not done when it works; it is done when it is gone. Put the removal in the same checklist, with the same owner, and do not close the window until the removal is verified.
- When a temporary default really is needed, make it floating — a static at a distance above the dynamic source, which is inert while BGP holds the default and takes over automatically when BGP withdraws it:
set protocols static route 0.0.0.0/0 next-hop 198.51.100.1 distance 210
Left behind, that line is harmless. Left behind, the same line without
distance 210 is this incident. One habit removes the entire failure mode.
- Alert on the identity of the selected default, not its existence. “A default
exists and traffic flows” was true for eight weeks. The check that would have
caught this is that the selected
0.0.0.0/0has source BGP and next-hop 203.0.113.1. - Monitor egress bytes per WAN against design intent, and monitor the public source address the estate presents. Third parties contract on the address, not on the routing table, and the address is observable from outside.
- Diff the running configuration against the intended configuration on a schedule. A line nobody remembers adding should be found by a comparison, not by an invoice.