Reported symptoms
The service desk starts taking calls at 01:40. The reports are useless in a specific and recognisable way: some things work and some things do not, and the people reporting cannot tell you which is which. One team cannot reach a supplier portal. Another cannot pull container images. A third is fine. Nobody can find the pattern because there is no pattern in the customer-visible facts.
The network on-call arrives and finds a healthy estate:
- Every BGP session is Established, with the same uptime as yesterday, and the per-peer prefix counts are unchanged.
- OSPF is fully converged on all five routers. Every adjacency is Full. No LSA churn, no alarm, nothing in the log.
- From either border router, everything is reachable. Curl works, ping works, the transit providers are visibly healthy.
traceroutefrom a client dies at the first core router — which is the box the network team is most confident about, because they just looked at it and it is perfect.
The only change anywhere in the window is a configuration-management
run at 01:14 whose diff removed a handful of static routes annotated
as unused. It was reviewed by two people. It removed nothing that any
show command on any router refers to.
The estate
AS 64512, five VyOS 1.5 LTS routers, a shape you will recognise:
rtr-edge-1— eBGP to ISP-A. External link192.0.2.0/30; the router holds192.0.2.2, ISP-A holds192.0.2.1.rtr-edge-2— eBGP to ISP-B. External link10.64.0.0/30, a privately numbered handoff, which is common and will matter.rtr-core-1,rtr-core-2,rtr-core-3— no external sessions.- Full-mesh iBGP between all five. OSPF area 0 carries loopbacks out
of
10.255.0.0/24and the internal LANs out of10.0.0.0/8.
No router in the estate sets next-hop-self. That is not a mistake
anyone made; it is a thing nobody did, which is a different and more
durable kind of problem.
Evidence provided
vyos@rtr-core-1:~$ show ip bgp 198.51.100.0/24BGP routing table entry for 198.51.100.0/24
Paths: (1 available, best #1)
Path 1 (best):
192.0.2.2 from 10.255.0.1 (10.255.0.1)
Origin IGP, metric 0, localpref 100, weight 0, valid, internalIllustrative output
vyos@rtr-core-1:~$ show ip route 198.51.100.0/24vyos@rtr-core-1:~$ show ip route 192.0.2.2vyos@rtr-core-1:~$ show ip bgp 203.0.113.0/24BGP routing table entry for 203.0.113.0/24
Paths: (1 available, best #1)
Path 1 (best):
10.64.0.2 from 10.255.0.2 (10.255.0.2)
Origin IGP, metric 0, localpref 100, weight 0, valid, internalIllustrative output
vyos@rtr-core-1:~$ show ip route ospf | match '192.0.2|10.64.0'O 10.64.0.0/30 [110/20] via 10.255.0.2, eth1, 21d04h11mIllustrative output
vyos@rtr-core-1:~$ show system commit0 2026-08-18 01:14:09 by ansible via api cleanup: remove unused static routes
1 2026-07-28 10:31:55 by vyos via cli ospf: stop redistributing WAN transit links
2 2026-07-14 08:55:31 by vyos via cli monitoring: add snmp communityIllustrative output
Work the evidence before reading on
The BGP table is not the forwarding table. Everything above is a statement about which of the two you are looking at.
- Read the two
show ip bgpoutputs side by side and find the one field that differs between the broken prefix and the working one. It is not an attribute, a metric or a peer. - Ask what has to be true before a router can install a route whose next hop is not directly connected, and then check whether it is true here.
- The OSPF change is three weeks old and the outage is one hour old. Resist discarding the older change: ask what was holding the estate up in between, and why nothing broke on the day of the OSPF change.
- The overnight diff removed static routes that no
showcommand refers to. Consider whether “referred to” and “depended on” are the same test. - Two of the three core routers are broken and two of five destinations work. Decide which of those two ratios is a coincidence.
Before continuing: why does this router forward to 203.0.113.0/24 and not to 198.51.100.0/24, when its BGP table is equally happy about both?
Root cause
1. The next hop is an external address, carried unchanged across iBGP
When rtr-edge-1 learns 198.51.100.0/24 from ISP-A, the NEXT_HOP
attribute is 192.0.2.2 — the address on rtr-edge-1’s own external
link. When rtr-edge-1 re-advertises that path over iBGP, the NEXT_HOP
is passed on unchanged. That is the default: an iBGP speaker does not
rewrite NEXT_HOP unless next-hop-self tells it to.
So every core router holds a path whose usability depends on a
question that has nothing to do with BGP: can I reach 192.0.2.2?
If the answer is no, the path stays in the BGP table and never
reaches the kernel. show ip bgp looks perfect, show ip route
returns nothing, and the router silently drops the traffic it just
told everyone else it could carry.
2. The IGP stopped carrying the external link three weeks ago — correctly
The change on 28 July replaced a bare redistribute connected in
OSPF with a route-map permitting only loopbacks and the internal
supernet. This is a good change. Transit handoff subnets have no
business in your IGP: they are addressed by someone else, they change
without your involvement, and carrying them expands the IGP for no
routing benefit.
It removed 192.0.2.0/30 from OSPF, and nothing broke, so the change
was recorded as a success. Three weeks later nobody connects it to
anything.
Note which link survived. rtr-edge-2’s handoff is numbered
10.64.0.0/30, inside the 10.0.0.0/8 supernet the route-map
permits, so it is still redistributed. The correctly hardened link is
the one that broke; the one that slipped through the filter is the
one still working. That is the entire explanation for “two in five
destinations” — the split is simply which border router wins the best
path.
3. A static route nobody could account for was the load-bearing wall
Between 28 July and last night, 192.0.2.2 resolved through a static
route for 192.0.2.0/24 — a summary left over from a decommissioned
addressing plan, present on the core routers, referenced by nothing,
explained by no one.
The overnight automation removed static routes annotated as unused, and by every test available it was unused: no other route pointed at it, no interface used it, no policy matched it. The test that would have caught it is not “is anything referring to this route” but “is any BGP next hop resolving through it”, and that test is one command per next hop.
Two changes, three weeks apart, each correct in isolation, neither sufficient alone. That is the normal shape of a serious outage, and it is why the review of the second change could not have found the problem by reading the second change.
Resolution
- Capture the state before touching anything:
show ip bgp 198.51.100.0/24,show ip route 198.51.100.0/24andshow ip route 192.0.2.2on each core router. The third is the one that will look like nothing and is the one that matters. - Establish the true blast radius from the next hop, not from the complaints. Every prefix whose next hop is 192.0.2.2 is affected, whether or not anybody has noticed yet;
show ip bgpfiltered by that next hop gives you the list. - Decide, explicitly and with a name attached, between the structural fix and the hold. Structural:
next-hop-selfon rtr-edge-1 iBGP sessions. Hold: re-admit 192.0.2.0/30 to the IGP now and schedule the structural fix. - If you take the hold, take it deliberately: adjust the OSPF redistribution route-map to permit 192.0.2.0/30 specifically, not to permit transit links generally, and write the replacement date into the ticket before you commit.
- If you take the structural fix, apply it on rtr-edge-1 - the router with no symptom - and expect a convergence event, because the next hop changes for every prefix it advertises internally.
- Either way,
compareand thencommit-confirm 10. This is a change to next-hop resolution for a large part of the table; if it is wrong you want the router to undo it without you. - Watch the next hop resolve before you look at any prefix:
show ip route 192.0.2.2on a core router, thenshow ip route 198.51.100.0/24. The order is the diagnosis in miniature. - Do not re-add the static summary. It restores service, it is invisible to review, and it rebuilds the exact dependency that turned a routine cleanup into an outage.
- Confirm and save on every router, then close the loop on the automation: the removed static was correctly identified as unreferenced and was still load-bearing, and that gap belongs in the change process rather than in this ticket.
Verification
- The next hop resolves.
show ip route 192.0.2.2returns a route on every core router. Check all of them - the failure was never local to the box that reported it. - The prefix is installed.
show ip route 198.51.100.0/24shows a BGP route with a usable next hop, andshow ip route summaryshows the BGP count back where it belongs. - Forwarding works from a client, not from the router. The reported symptom was reachability; a FIB entry is necessary and not sufficient.
- The return path works. A return route that resolves through the same missing next hop fails identically and is invisible to a one-way test.
- If next-hop-self was the fix, the rewrite actually happened:
show ip bgp 198.51.100.0/24on a core router shows rtr-edge-1 loopback as the next hop, not 192.0.2.2. Applied to the wrong session it commits cleanly and changes nothing. - The hardening survived.
show ip route ospfmust not have regained a set of transit links nobody asked for; if you took the hold, exactly one /30 came back and it is named in the route-map. - The whole affected set recovered, not just the prefixes in the tickets. Re-run the next-hop query from step two of the resolution and confirm every prefix in that list is now installed.
Prevention
- Make
next-hop-selfan estate rule on every border router’s iBGP sessions. It converts a dependency on somebody else’s link addressing into a dependency on your own loopbacks, which the IGP carries deliberately and which nobody removes as tidying. - Alert on the gap between the BGP table and the FIB. The counts
from
show ip bgp summaryandshow ip route summaryshould track each other. A divergence is precisely this class of fault, and it is visible at the moment of breakage rather than forty minutes later through a service-desk call. - Redefine “unused” for routing configuration. A static route is
unused only when you can name what resolves through it. For every
BGP next hop in the table,
show ip route <next-hop>answers the question in advance, and the answer belongs in the change record. - Validate hardening changes against what they might be propping up. The OSPF change was correct and left the estate one commit from an outage. The review that would have caught it asks not “is this change right” but “what currently depends on the thing this removes”.
- Write down which next hop each prefix source resolves through. It is a short table, it changes rarely, and it turns a two-hour diagnosis into a one-command check.