Skip to main content
RunBook Academy

← All break/fix scenarios in VyOS

intermediatevyos-static~30 min

Recursive Next Hop Unreachable

Reported symptoms

  • Every application in DC-A that talks to the partner estate (10.0.0.0/8) started timing out on Tuesday morning. They hang rather than being refused, which reads as a far-end problem
  • The partner reports their side healthy, and their edge firewall shows no inbound connection attempts from us at all — not blocked ones, none
  • Security opened a separate ticket the same morning: the internet edge router is logging a steady stream of dropped egress packets from DC-A hosts to 10.x destinations, which looks like beaconing to private space
  • `show ip route 10.0.0.0/8` on `core1` prints the static route. Three engineers have now confirmed "the route is there"
  • `ping 192.0.2.201` from `core1` succeeds, so the next hop is up
  • `show system commit` on `core1` shows no change for six weeks, and the routing team is certain nothing was touched
  • Everything else is fine: OSPF adjacencies stable, BGP Established, internal DC-A traffic normal, internet access normal

Evidence

  • · `show ip route 10.0.0.0/8` — the static is present, carries no `*`, and ends in the word `inactive`
  • · `show ip route static` — the other four statics on the box all show `S>*`; only this one does not
  • · `ping -c 3 192.0.2.201` — three replies, sub-millisecond, from the next-hop address the inactive route points at
  • · `show ip route 192.0.2.201` — the longest match for the next hop is `O>* 0.0.0.0/0`, and nothing more specific exists
  • · `show ip ospf database summary` — exactly one Type-3 LSA in the area, for 0.0.0.0/0. A capture from the same command a month ago listed sixty-one
  • · `ip route get 10.4.19.20` — the kernel resolves partner traffic to `dev eth1` via the default gateway, not via the static
  • · `show system commit` on the area border router `abr1` — one line committed Tuesday 06:40 by the platform team: `set protocols ospf area 10 area-type stub no-summary`
  • · The internet edge egress filter log — dropped packets from DC-A source addresses to 10.0.0.0/8 destinations, starting Tuesday 06:41
Diagnosis and resolutionclick to reveal

Root cause

The static route for the partner supernet is deliberately recursive: it points at 192.0.2.201, a loopback on the partner-facing router `pe1` in area 0, so that the traffic follows whichever of the two transit links the IGP currently prefers rather than being pinned to one interface. Resolving that next hop requires a route in `core1`'s table that covers 192.0.2.201, and until Tuesday there was one — an inter-area OSPF route for the loopback, flooded into area 10 as a Type-3 summary. On Tuesday morning the platform team converted area 10 from a stub area to a totally stubby area with one line on the area border router, to shrink the link-state database on the branch routers in the same area. That is a legitimate change and it is legal to make on the ABR alone. Its effect is that the ABR stops flooding every Type-3 summary into the area and floods exactly one: the default route. From that moment the only thing in `core1`'s table covering 192.0.2.201 was 0.0.0.0/0, and zebra will not resolve a recursive next hop through a default route unless it is explicitly told to. The static went inactive, and inactive means present in the RIB and absent from the FIB. What makes this so durable is the ping. Forwarding a packet to 192.0.2.201 and resolving 192.0.2.201 as a next hop are two different operations with two different rules: forwarding is a longest-prefix match against the FIB, and the default route serves it perfectly well, so the next hop answers every probe anybody sends it. The check everybody ran to prove the next hop was reachable was the one check that could not detect the fault. Meanwhile the traffic did not stop — with the /8 out of the FIB, partner traffic fell to the same default route and left the estate through the internet edge, where an egress filter dropped it and logged it into somebody else's ticket.

Remediation

Restore a route that covers the next hop, or remove the dependency on one. The immediate option is to revert the ABR to a plain stub area, which puts the inter-area summaries back and makes the static resolve within a convergence interval; it is one line, it is reversible, and it undoes a change that was made for a real reason, so it needs the platform team's agreement rather than a unilateral revert. The durable option is to stop resolving a next hop across a boundary that is allowed to summarise it away: move the partner supernet static onto the ABR itself, which sits in area 0, can resolve the loopback, and is already the exit that area 10's default route points at. Area 10 keeps its small database, `core1` keeps no static at all, and the recursion happens where the information exists. Refuse three fixes that will be proposed under pressure. Pinning the route to an interface, or adding `on-link`, forces it active by asserting that the next hop is directly attached when it is a loopback two hops away; the route installs, traffic still does not arrive, and now the evidence is gone. Adding a static for 192.0.2.201 itself makes the recursion resolve, but it hard-codes one transit link at administrative distance 1, which is precisely the pinning the recursive design existed to avoid, and it will keep the supernet active and black-holing when that link is up and `pe1` is not. And relaxing FRR's next-hop resolution so that recursion may use the default route would fix this static and quietly arm every other one on the box, converting "route inactive" — loud, local, diagnosable — into "traffic silently leaves the estate", which is the symptom that is already costing you a security ticket.

Verification

`show ip route 10.0.0.0/8` must show `S>*` with a resolved egress interface after the `via`, not merely a line of output containing the prefix. The `*` is the whole test: it is the marker that says the route reached the FIB, and its absence is what six people read past. Confirm the forwarding decision independently with `ip route get` for a real partner address and check that the egress is the transit interface rather than the default gateway, because the kernel's answer is the one that decides where packets go. Prove the check can fail rather than trusting that it passed: in the lab, withdraw the route covering the next hop and watch the static drop back to `inactive`, so you know what the failing state looks like on the command you are relying on. Then check the two things that are not on this router. Partner traffic must reach the partner — ask them to confirm inbound sessions, since their firewall counters are the only evidence independent of the estate that produced the fault. And the internet edge egress filter must stop logging drops to 10.0.0.0/8; that log going quiet is the proof that the leak closed. Finally, exercise the property the recursion existed for: fail one transit link in a window and confirm the supernet follows the IGP to the other one. A recursive route that has not been watched move is a design intention, not a verified behaviour.

Prevention

Alert on inactive statics. Every static route on a production router is meant to forward traffic, so one sitting in the RIB without a `*` is a defect whatever caused it, and it is a one-line check against `show ip route static` that would have raised this at 06:41 instead of at 09:20. Treat a change to an OSPF area type as a forwarding change on every router in the area, not as database hygiene on the ABR: `no-summary` is legal to apply to the ABR alone and its blast radius is every route every router in the area was resolving through an inter-area prefix, so the pre-change capture belongs on the internal routers and the post-change comparison is a count of inter-area routes on each of them. Do not let a recursive next hop cross a boundary whose summarisation you do not own — either resolve it inside the area, or put the route on the router that lives where the next hop is visible. Egress filters at the internet edge should alert, not merely drop: internal destinations arriving there means something upstream has lost a route, and that signal was available two hours before anybody correlated it. And separate the two questions permanently in your own habits and in your runbooks — "can I reach it" and "can the router resolve it" are answered by different commands, and only the second one is about the fault.

Reported symptoms

core1 is the DC-A distribution router. It sits in OSPF area 10 with two transit links towards the core, and it carries one static route that matters here: the partner supernet 10.0.0.0/8, pointed at 192.0.2.201. That address is a loopback on pe1, the partner-facing router in area 0. The static is recursive on purpose — pointing at a loopback rather than at an interface means the traffic follows whichever transit link the IGP currently prefers, and survives losing either one.

On Tuesday at about 06:45, every application in DC-A that talks to the partner estate began timing out. Timing out, not being refused: the sessions hang until the client gives up, which is what a far-end problem looks like.

Three more facts arrived over the next two hours and none of them pointed here:

  • The partner says their side is healthy. More usefully, their edge firewall shows no inbound connection attempts from us at all. Not blocked ones — none.
  • Security opened an unrelated ticket the same morning. The internet edge router is logging a steady stream of dropped egress packets from DC-A hosts to 10.x destinations. Read cold, that looks like something in DC-A beaconing to private address space.
  • Nothing changed on core1. show system commit goes back six weeks, and the routing team is confident, correctly, that they did not touch it.

And on the router itself, two checks were run early and both passed. The route is in show ip route 10.0.0.0/8. The next hop answers ping. By 09:00 the working theory was a partner-side or transit-side problem, because the local routing had been confirmed twice.

Evidence provided

Read-only / Safethe route everybody confirmed was present — read the end of the line
vyos@core1:~$ show ip route 10.0.0.0/8
Codes: K - kernel route, C - connected, S - static, R - RIP,
     O - OSPF, B - BGP, > - selected route, * - FIB route

S   10.0.0.0/8 [1/0] via 192.0.2.201 inactive

Illustrative output

Read-only / Safethree statics carry the selected-and-installed markers; one does not
vyos@core1:~$ show ip route static
S>* 172.16.40.0/22 [1/0] via 198.51.100.1, eth1, 41w2d
S>* 172.16.44.0/24 [1/0] via 198.51.100.1, eth1, 41w2d
S>* 192.168.90.0/24 [1/0] via 172.16.8.9, eth3, 22w4d
S   10.0.0.0/8 [1/0] via 192.0.2.201 inactive

Illustrative output

Read-only / Safethe check that closed the investigation for two hours
vyos@core1:~$ ping -c 3 192.0.2.201
PING 192.0.2.201 (192.0.2.201) 56(84) bytes of data.
64 bytes from 192.0.2.201: icmp_seq=1 ttl=63 time=0.784 ms
64 bytes from 192.0.2.201: icmp_seq=2 ttl=63 time=0.731 ms
64 bytes from 192.0.2.201: icmp_seq=3 ttl=63 time=0.744 ms

--- 192.0.2.201 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss

Illustrative output

Read-only / Safethe longest match for the next hop, and there is nothing more specific
vyos@core1:~$ show ip route 192.0.2.201
Routing entry for 0.0.0.0/0
Known via "ospf", distance 110, metric 10, best
Last update 02:41:19 ago
* 198.51.100.1, via eth1

Illustrative output

Read-only / Safeone Type-3 LSA in the area; the archived capture from last month listed sixty-one
vyos@core1:~$ show ip ospf database summary
       Summary Link States (Area 0.0.0.10)

Link ID         ADV Router      Age  Seq#       CkSum  Route
0.0.0.0         198.51.100.1    141  80000001   0x4d2a 0.0.0.0/0

Illustrative output

Read-only / Safewhere partner traffic is actually going, according to the thing that decides
vyos@core1:~$ ip route get 10.4.19.20
10.4.19.20 via 198.51.100.1 dev eth1 src 198.51.100.2 uid 1000
  cache

Illustrative output

Read-only / Safea different router, a different team, one minute before the first timeout
vyos@abr1:~$ show system commit | head -3
0   2026-08-18 06:40:22 by platform via cli   PLT-2291 shrink area 10 LSDB
1   2026-07-29 15:11:07 by platform via cli   ntp source change
2   2026-07-14 09:02:44 by netops via cli     add area 10 interface eth7

Illustrative output

Work the evidence before reading on

Two commands disagree about the same address, and both are correct.

  1. Look at the first output again, character by character. What do the > and * markers mean, which of them is on the partner supernet line, and what is the last word on that line? “The route is in show ip route” answers a question — is it the question that was asked?
  2. ping 192.0.2.201 succeeds and show ip route 192.0.2.201 returns the default route. Both are true at once. What operation does ping perform on that address, what operation does a recursive static perform on it, and are they the same operation?
  3. The area’s link-state database went from sixty-one summary LSAs to one. Which routes did core1 lose, and which single route replaced all of them?
  4. Traffic to the partner did not stop being generated. If the supernet is not in the FIB, what does a packet to 10.4.19.20 match instead, and does that explain the security ticket that arrived at 06:41?

Then the question that resolves it: the router can clearly send a packet to 192.0.2.201, so why will it not use 192.0.2.201 as a next hop?

Root cause

1. The area became totally stubby, and that is an ABR-only change

Area 10 has been a stub area for a long time; every router in it agrees on that, which is what the E-bit in the Hello packets negotiates. On Tuesday the platform team added no-summary on the area border router to shrink the link-state database on the branch routers that share the area.

That converts a stub area into a totally stubby area, and it is legal to configure on the ABR alone — the internal routers need no change and their adjacencies are unaffected, which is exactly why nobody thought of it as touching core1. What it does is stop the ABR flooding Type-3 summary LSAs into the area, with one exception: the default route. Sixty-one inter-area prefixes became one.

Among the sixty-one was 192.0.2.201/32, the loopback the partner supernet’s static route points at.

2. Resolution and reachability are different operations

A recursive static route has a next hop and no egress interface. Zebra has to find the egress by looking the next hop up in the routing table, and it applies a rule that surprises people the first time they meet it: it will not resolve a next hop through a default route.

That rule is deliberate. A default route says “everything I do not otherwise know about goes this way”, which is a statement about last resort, not about where a specific host is. Allowing recursion through it would mean a static route whose next hop is a typo — or a loopback that has been withdrawn — resolves anyway, installs anyway, and quietly forwards its traffic at the default gateway. The router refuses, marks the route inactive, and leaves it in the RIB where you can see it.

Forwarding does none of that. When you type ping 192.0.2.201, the kernel does a longest-prefix match against the FIB, matches 0.0.0.0/0, and sends the packet to the default gateway, which is in area 0 and knows exactly where pe1 is. The replies come back in under a millisecond. Every packet the next-hop test sends arrives, because the default route is a perfectly good route — for forwarding.

So the ping was not a lucky false negative. It was a correct answer to a different question. Reachability was never in doubt; resolvability was the whole fault.

3. inactive means in the RIB and not in the FIB

The route stayed in show ip route because the RIB holds what the router has been told. It never reached the FIB because zebra could not resolve it, and show ip route says so twice: the > and * markers are missing, and the line ends in the word inactive.

Three engineers looked at that output and read the prefix, which was the thing they were looking for. The markers are two characters wide and sit where the eye is not.

4. The traffic did not stop — it left

This is the part with the second ticket attached.

With the supernet out of the FIB, a packet to 10.4.19.20 no longer matches a /8. It matches the next-longest thing in the table, which is now the freshly injected default, and follows it towards the core and out through the internet edge. There the egress filter drops it as private-space destined traffic and logs it, which is correct behaviour and is why the estate did not actually leak partner traffic onto the internet.

It also means the fault presented in two places at once, in two tickets, owned by two teams, with no shared field between them except a timestamp.

Resolution

  1. Correlate the two tickets before you change anything. The security ticket and the partner ticket are the same incident, and knowing that tells you the traffic is leaving the estate rather than being dropped locally — which is what decides whether this waits for a window.
  2. Establish what else in the area depended on an inter-area prefix. core1 is one router in area 10, and every other router in it lost the same sixty summaries at the same moment. Check their static routes and any next hop that is not on a connected segment before you declare the blast radius to be one prefix.
  3. Decide with the platform team, not instead of them. Reverting area-type stub no-summary on abr1 restores the summaries and fixes this within a convergence interval, but it undoes a change that was made for a real reason. That is a conversation, and it is a short one.
  4. If the immediate revert is the agreed path, apply it on abr1 under commit-confirm, and watch show ip ospf database summary on core1 repopulate before you confirm. The routers inside the area need no change at all.
  5. For the durable fix, move the dependency to where the information lives: carry the partner supernet static on abr1, which is in area 0, resolves 192.0.2.201 natively, and is already the router that area 10 default-routes towards. Area 10 keeps its small database and core1 carries no static at all.
  6. Refuse the interface pin. next-hop 192.0.2.201 interface eth1, or on-link, asserts that the next hop is directly attached to that segment. It is a loopback two hops away. The route will install, the traffic will still not arrive, and the diagnostic evidence will be gone.
  7. Refuse the helper static. Adding a route for 192.0.2.201/32 makes the recursion resolve, and it pins the supernet to one transit link at administrative distance 1 — precisely the pinning the recursive design existed to avoid. It will hold the supernet active and black-holing whenever that link is up and pe1 is not.
  8. Refuse relaxing next-hop resolution. Permitting recursion via the default route would fix this static and arm every other one on the box, turning a loud local failure into silent egress. That is the symptom already costing you a security ticket, applied estate-wide.
  9. Close the loop on the change record for PLT-2291, naming what the area-type change actually affected. The next person to shrink an LSDB should find this.

Verification

  1. show ip route 10.0.0.0/8 shows S>* with a resolved egress interface after the via. The markers are the test; a line of output containing the prefix is not.
  2. ip route get for a real partner address returns the transit interface, not the default gateway. The kernel decides where packets go, so the kernel is what you ask.
  3. The check can fail. In the lab, withdraw the route covering the next hop and watch the static drop back to inactive, so you know what the failing state looks like on the command you are now relying on.
  4. The partner sees inbound sessions. Their firewall counters are the only evidence independent of the estate that produced the fault, and "no inbound attempts at all" was the sharpest symptom in the whole incident.
  5. The internet edge egress filter stops logging drops to 10.0.0.0/8. That log going quiet is the proof the leak closed, and it is observable from a router nobody touched.
  6. Every other static on every router in area 10 shows S>*. One inactive route was the visible fault; the area change had the same reach on all of them.
  7. The recursion still does its job. In a window, fail one transit link and confirm the supernet follows the IGP to the other. A recursive route nobody has watched move is an intention, not a verified behaviour.

Prevention

  • Alert on inactive statics. A static route exists to forward traffic, so one sitting in the RIB without a * is a defect whatever caused it. It is a single check against show ip route static, and it would have raised this at 06:41 rather than at 09:20.
  • Treat an OSPF area-type change as a forwarding change on every router in the area. no-summary is legal on the ABR alone and its blast radius is every route every internal router was resolving through an inter-area prefix, so the pre-change capture belongs on the internal routers and the post-change check is a count of inter-area routes on each of them.
  • Do not resolve a next hop across a boundary whose summarisation you do not own. Either the next hop is visible from inside the area, or the route belongs on a router where it is.
  • Make egress filters alert, not merely drop. Internal destinations arriving at the internet edge means something upstream has lost a route, and that signal existed two hours before anybody correlated the two tickets.
  • Separate the two questions permanently, in habit and in runbooks. “Can I reach it” and “can the router resolve it” are different commands, and only the second one is about this class of fault.