← All posts

Hop-by-Hop Truth

Your path tool draws a corridor. Routers forward hop by hop. Here is the 4,032-pair measurement of the gap.

Michel Wijnberg

Almost every network path tool I have used (including, until recently, mine) works like this:

  1. Take the source router.
  2. Run Dijkstra over the topology.
  3. Draw the resulting shortest path.

That is a source-rooted corridor, and it is a subtly wrong model of IP forwarding. Real forwarding has no corridor. Every router independently matches the destination address against its own routing table and picks a next hop. The two answers agree only as long as every intermediate router’s decision rule and input set match the source’s.

In OSPF, they routinely do not.


One packet that never arrives

Here is a case from my lab, measured on every router involved with show ip route, not modelled but read off the boxes:

RouterSays about the destinationForwards to
dfw1-gw2metric 20031, type inter areadfw1-cr1
dfw1-cr1metric 30001, type intra areaord1-cr1not dfw2-cr1
ord1-cr1metric 20001, type intra areajfk1-cr1
jfk1-cr1metric 10001, type intra arealhr1-cr1
lhr1-cr1metric 1, connecteddelivered

The source-rooted corridor drew this:

dfw1-gw2 → dfw1-cr1 → dfw2-cr1 → iad2-cr1 → ams1-cr1 → lhr1-cr1

The packet never reaches dfw2-cr1. The corridor is a cheaper path. It is just not a path any router will build. Two independent RFC 2328 rules make dfw1-cr1 refuse it:

§16 step (3) / §16.2 ¶1: the backbone gate. A router attached to multiple areas examines only backbone summary-LSAs. dfw2-cr1’s cheaper cost-20011 summary genuinely is in dfw1-cr1’s area-4.6.23.0 database (I confirmed it with show ip ospf database summary), but it is not a candidate, because dfw1-cr1 is an ABR and that summary did not arrive via area 0. RFC 3509 §2.1/§2.2 (the Cisco ABR behaviour actually running on these boxes) conditions this on having an active backbone connection, which dfw1-cr1 has.

§16.2 step (6): intra beats inter, full stop. “If the paths present in the table are intra-area paths, do nothing with the LSA (intra-area paths are always preferred).” dfw1-cr1 already holds an intra-area path at metric 30001. The 20011 summary loses to it. Metric is irrelevant: this is a rule about path type, and a cost comparison never happens.

Either rule alone kills the corridor. A tool that runs one SPF from the source cannot see either, because it never asks dfw1-cr1 what dfw1-cr1 thinks.


How often does this actually matter?

This is the part I care about, because “your model is theoretically imprecise” is a much weaker claim than a number.

So I read the full OSPF routing table off all 64 routers in AS 200 and compared every one of the 4,032 ordered source/destination pairs:

MeasurementResult
Inter-area pairs where the corridor drew ≥1 hop no router would take360 of 2,839 (12.68%)
Intra-area pairs with the same defect0 of 1,193
Overall360 of 4,032 (8.93%)

Zero for intra-area is the sanity check. Within a single area every router runs SPF over an identical LSDB, and a shortest path stays shortest from every point along it, so the two models have every reason to coincide. The point is that they were measured coinciding rather than assumed to: 1,193 pairs, no disagreement. The moment an ABR and a summary-LSA enter the picture, one path in eight is fiction.

Not “slightly suboptimal”. Fiction, containing a router the packet provably never visits. If you are using that path to plan a maintenance window or to explain an incident, you are reasoning about a route that does not exist.


The fix: ask every router

Osprey now walks the chain. At each hop it evaluates that router’s own installed route toward the destination address, using that router’s own LSDB, its own area memberships, its own ABR status and its own administrative distance mix, then follows the next hop it actually selects, and repeats.

Validated against the same 4,032 pairs:

CheckResult
Router’s own metric reproduced exactly4,032 / 4,032
Path type (intra / inter / external) exact4,032 / 4,032
Full installed ECMP next-hop set identical4,032 / 4,032
Chain terminates delivered at the requested destination4,032 / 4,032

Not “close”. Identical, on every pair, including the complete equal-cost next-hop set rather than one arbitrarily chosen member.

Here is what that looks like on the canvas: forward path in orange, reverse in blue, everything not on the path dimmed to a corridor:

An OSPF path drawn across the topology with forward and reverse overlays and four equal-cost paths
An OSPF path drawn across the topology with forward and reverse overlays and four equal-cost paths

Note the 4 equal-cost paths selector and the per-flow hash label. Osprey enumerates the complete ECMP set rather than picking one member and presenting it as the path, because your traffic is hashed across all four, and a troubleshooting session that examines only one of them will find nothing wrong three times out of four.

And here is the same path opened up:

A 10-hop OSPF path with per-hop table, ABR badges, ingress/egress ports and the numbered explanation
A 10-hop OSPF path with per-hop table, ABR badges, ingress/egress ports and the numbered explanation

Every hop carries the router that made the decision, the area it made it in, the physical ingress and egress interfaces with their addresses, and the cost. ABR badges are derived from the LSDBs, not from hostnames. The area ribbon underneath shows the path crossing 12.1.24.00.0.0.06.18.1.0, which is the backbone transit RFC 2328 requires for inter-area traffic.

And the total cost shown, 30061, is the source’s own installed metric. It is literally the number show ip route prints on lax1-gw1, stub cost included, not a sum Osprey computed by adding up links it liked.

The explanation is generated, not decorative:

  1. Route type: O IA (inter-area): Source 172.16.2.33 and destination 172.16.5.23 have no shared area, routing via backbone (area 0.0.0.0)
  2. Path traverses: area 12.1.24.0 (cost 10) → area 0.0.0.0 (cost 30040) → area 6.18.1.0 (cost 10)
  3. Total cost: 30061 via 10 hops
  4. ECMP: 4 equal-cost paths available

Across the AS boundary

Inside one IGP the chain is computable because the LSDB is complete. Across an AS boundary, nothing is. There is no shared database, no shared metric space, and no shared administrative-distance policy.

The honest answer is not to give up, and it is definitely not to pretend the metrics compose. It is to stitch evidence.

A 15-hop path crossing from AS 200 into AS 300, with per-segment costs and an eBGP transition
A 15-hop path crossing from AS 200 into AS 300, with per-segment costs and an eBGP transition

Fifteen hops from lax1-gw1 in AS 200 (tenant Harrier-Broadband, OSPF) to i-sin1-gw1 in AS 300 (tenant Merlin-Carrier, IS-IS). Read what it does:

  • Hop 8 jfk1-gw1 carries a BGP badge: this is where the IGP stops and the BGP decision takes over.
  • Hop 9 i-jfk1-gw1 carries an IS-IS badge: a completely different protocol, different address plan, different tenant.
  • The footer says “Per-segment costs (never summed) | 15 hops”. An OSPF cost of 20051 and an IS-IS cost of 1320 are numbers in unrelated metric spaces. Adding them would produce 21371, which is not a quantity that exists. Osprey refuses to print it.

The generated explanation names its evidence at every step:

  1. BGP border selection: jfk1-gw1 holds 10.66.0.0/15 for 10.66.15.3 (AS path: 300)
  2. AS 200: IGP segment: lax1-gw1jfk1-gw1, cost 20051
  3. eBGP transition 200 → 300: jfk1-gw1 Et1/1 → i-jfk1-gw1 Et1/2 (bmp-peer+l2; ip-bound)
  4. AS 300: IGP segment: i-jfk1-gw1i-sin1-gw1, cost 1320

Step 3 is my favourite line in the product. It does not say “these two routers are probably connected somehow”. It names physical ports on both ends, and then it shows its work in that parenthesis:

  • bmp-peer: an established eBGP session between them, seen via BMP
  • +l2: the far side was matched through the borders’ LLDP/CDP adjacency
  • ip-bound: both session addresses resolved to those specific ports through the devices’ IP-MIB bindings

Three independent pieces of evidence agreeing. When they do not agree, the product says so instead, which is the entire subject of the next post.


Why I care about this more than about features

A path drawing is a claim about what your network will do with a packet. If the claim is wrong one time in eight and nothing on screen distinguishes the wrong ones from the right ones, the drawing is worse than useless. It is confidently useless, and it will be believed in exactly the moment when being wrong is expensive.

Getting from “plausible” to “reproduces all 64 routing tables exactly, 4,032 out of 4,032” took a validation harness, a lab I can read ground truth from, and a willingness to find out that my own tool had been drawing fiction 360 times.

That is what the lab is for.


Next, the other half of the same coin: the places where Osprey cannot compute the answer, and says so: What Osprey Refuses to Guess.