Lab implementing core routing algorithms in Python: longest prefix matching for route selection, Dijkstra’s algorithm for link-state shortest paths (OSPF), and Bellman-Ford with split horizon for distance-vector convergence (RIP). Includes comparing OSPF vs RIP based on their algorithmic foundations.
14.1 Learning Objectives
By the end of this chapter, you will be able to:
Construct a longest prefix matching implementation to select the most specific route for a destination
Execute Dijkstra’s algorithm to compute shortest paths in link-state routing protocols
Simulate distance vector convergence using Bellman-Ford algorithm with split horizon
Evaluate routing protocols (OSPF vs RIP) based on their algorithmic foundations
Diagnose route selection issues by tracing administrative distance and metric calculations
For Beginners: Routing Algorithms Lab
Routing algorithms are the mathematical recipes that networks use to find the best path for data. In these labs, you will experiment with different algorithms and see how they make decisions – like comparing different GPS apps that might choose different routes based on distance, traffic, or toll costs.
Sensor Squad: Building Algorithms from Scratch!
“In this lab, we implement routing algorithms in Python,” said Max the Microcontroller. “You will code longest prefix matching, Dijkstra’s algorithm for OSPF, and Bellman-Ford for distance-vector protocols like RIP.”
“Dijkstra’s algorithm finds the shortest path through a graph,” explained Sammy the Sensor. “It is like solving a maze – you explore all nearby intersections first, marking the shortest distance from the start, then gradually expand outward until you reach the destination.”
“Bellman-Ford is different – it is what RIP uses,” added Lila the LED. “Each router shares its distance table with neighbors, and over multiple rounds, everyone gradually learns the best routes. Split horizon prevents telling a neighbor about routes you learned from them – that would create loops!”
“Comparing the two algorithms shows exactly why OSPF converges faster than RIP,” said Bella the Battery. “Dijkstra runs once on the complete topology and gets the answer immediately. Bellman-Ford needs many iterations to converge, and each round only updates routes one hop further. It is the fundamental trade-off between speed and simplicity.”
14.3 Implementation 1: Routing Table Simulator with Longest Prefix Matching
This implementation demonstrates how routers select the most specific route using longest prefix matching.
14.3.1 Understanding Longest Prefix Match
Figure 14.1: Longest Prefix Match: When multiple routes match a destination, the route with the longest matching prefix (most specific) is selected. For 172.16.50.100, the /24 route beats /16 and default /0 routes.
14.3.2 Python Implementation
"""Routing Table Simulator with Longest Prefix MatchingDemonstrates how routers select routes based on prefix length"""import ipaddressfrom dataclasses import dataclassfrom typing import Optional, List, Tuple@dataclassclass Route:"""Represents a routing table entry""" destination: str# Network in CIDR notation next_hop: str# Next hop IP or 'Connected' interface: str# Outgoing interface metric: int# Route cost route_type: str# 'C'=Connected, 'S'=Static, 'D'=Dynamic, 'G'=Default admin_distance: int# Administrative distancedef __post_init__(self):# Parse network for matchingself.network = ipaddress.ip_network(self.destination, strict=False)def matches(self, ip: str) ->bool:"""Check if IP matches this route"""try: addr = ipaddress.ip_address(ip)return addr inself.networkexceptValueError:returnFalse@propertydef prefix_length(self) ->int:"""Get prefix length for longest match comparison"""returnself.network.prefixlenclass RoutingTable:"""Simulates a router's routing table with longest prefix matching"""def__init__(self):self.routes: List[Route] = []def add_route(self, route: Route):"""Add a route to the table"""self.routes.append(route)# Sort by prefix length (descending) for efficient lookupself.routes.sort(key=lambda r: r.prefix_length, reverse=True)def lookup(self, destination: str) -> Tuple[Optional[Route], List[str]]:""" Find best route for destination using longest prefix matching Returns (best_route, list_of_all_matches) """ matches = [] best_route =Nonefor route inself.routes:if route.matches(destination): matches.append(route.destination)if best_route isNone: # First match is longest (sorted) best_route = routereturn best_route, matches[1:] if matches else []def display(self):"""Print routing table in standard format"""print("="*72)print("ROUTING TABLE")print("="*72)print(f"{'Destination':<20}{'Next Hop':<16}{'Interface':<10} "f"{'Metric':<8}{'Type':<5}{'AD'}")print("-"*72)for route insorted(self.routes, key=lambda r: r.prefix_length):print(f"{route.destination:<20}{route.next_hop:<16} "f"{route.interface:<10}{route.metric:<8} "f"{route.route_type:<5}{route.admin_distance}")def demonstrate_routing():"""Demonstrate routing table lookup with various scenarios"""# Create routing table table = RoutingTable()# Add routes (order doesn't matter - sorted by prefix length) table.add_route(Route( destination="192.168.1.0/24", next_hop="Connected", interface="eth0", metric=0, route_type="C", admin_distance=0 )) table.add_route(Route( destination="10.0.0.0/8", next_hop="Connected", interface="eth1", metric=0, route_type="C", admin_distance=0 )) table.add_route(Route( destination="172.16.0.0/16", next_hop="10.0.0.2", interface="eth1", metric=10, route_type="S", admin_distance=1 )) table.add_route(Route( destination="172.16.50.0/24", next_hop="10.0.0.5", interface="eth1", metric=5, route_type="S", admin_distance=1 )) table.add_route(Route( destination="192.168.100.0/24", next_hop="10.0.0.8", interface="eth1", metric=20, route_type="D", admin_distance=110# OSPF )) table.add_route(Route( destination="0.0.0.0/0", next_hop="192.168.1.1", interface="eth0", metric=1, route_type="G", admin_distance=1 ))# Display routing table table.display()# Test lookupsprint("\n"+"="*72)print("ROUTE LOOKUPS (Longest Prefix Matching)")print("="*72) test_destinations = ["192.168.1.50", # Local network"172.16.50.100", # More specific /24 route"172.16.30.100", # Less specific /16 route"8.8.8.8", # Default route"192.168.100.50", # Dynamic route ]for dest in test_destinations: route, other_matches = table.lookup(dest)print(f"\n{dest}:")if route:print(f" Matched route: {route.destination}")if route.next_hop =="Connected":print(f" Next hop: Direct delivery")else:print(f" Next hop: {route.next_hop}")print(f" Interface: {route.interface}") type_names = {'C': 'CONNECTED','S': 'STATIC','D': 'DYNAMIC','G': 'DEFAULT' }print(f" Type: {type_names.get(route.route_type, route.route_type)}")if other_matches:print(f" Other matches: {other_matches}")else:print(" No route found!")if__name__=="__main__": demonstrate_routing()
14.3.3 Expected Output
========================================================================
ROUTING TABLE
========================================================================
Destination Next Hop Interface Metric Type AD
------------------------------------------------------------------------
172.16.50.0/24 10.0.0.5 eth1 5 S 1
192.168.1.0/24 Connected eth0 0 C 0
192.168.100.0/24 10.0.0.8 eth1 20 D 110
172.16.0.0/16 10.0.0.2 eth1 10 S 1
10.0.0.0/8 Connected eth1 0 C 0
0.0.0.0/0 192.168.1.1 eth0 1 G 1
========================================================================
ROUTE LOOKUPS (Longest Prefix Matching)
========================================================================
192.168.1.50:
Matched route: 192.168.1.0/24
Next hop: Direct delivery
Interface: eth0
Type: CONNECTED
172.16.50.100:
Matched route: 172.16.50.0/24
Next hop: 10.0.0.5
Interface: eth1
Type: STATIC
Other matches: ['172.16.0.0/16', '0.0.0.0/0']
172.16.30.100:
Matched route: 172.16.0.0/16
Next hop: 10.0.0.2
Interface: eth1
Type: STATIC
Other matches: ['0.0.0.0/0']
8.8.8.8:
Matched route: 0.0.0.0/0
Next hop: 192.168.1.1
Interface: eth0
Type: DEFAULT
192.168.100.50:
Matched route: 192.168.100.0/24
Next hop: 10.0.0.8
Interface: eth1
Type: DYNAMIC
14.3.4 Key Concepts Demonstrated
Longest Prefix Matching: 172.16.50.100 matches both /24 and /16 routes, but /24 is selected
Administrative Distance: When multiple protocols learn same route, lower AD wins
Default Route: Matches all destinations when no specific route exists
Connected Routes: Direct delivery on local network
Try It: Longest Prefix Match Explorer
Explore how routers select the most specific route. Adjust the destination IP octet and prefix lengths to see which route wins.
This implementation shows how OSPF and other link-state protocols compute shortest paths.
14.4.1 Understanding Dijkstra’s Algorithm
Figure 14.2: Dijkstra’s Algorithm: Starts from source with distance 0, all others infinity. Each step visits the closest unvisited node and updates neighbor distances if a shorter path is found.
14.4.2 Python Implementation
"""Dijkstra's Shortest Path AlgorithmDemonstrates link-state routing protocol path computation"""import heapqfrom collections import defaultdictfrom typing import Dict, List, Tuple, Optionalclass LinkStateRouter:"""Simulates a router with complete network topology knowledge"""def__init__(self):# Adjacency list: router -> [(neighbor, cost), ...]self.topology: Dict[str, List[Tuple[str, int]]] = defaultdict(list)def add_link(self, router1: str, router2: str, cost: int):"""Add a bidirectional link to the topology"""self.topology[router1].append((router2, cost))self.topology[router2].append((router1, cost))def remove_link(self, router1: str, router2: str):"""Remove a link (simulate failure)"""self.topology[router1] = [ (n, c) for n, c inself.topology[router1] if n != router2 ]self.topology[router2] = [ (n, c) for n, c inself.topology[router2] if n != router1 ]def dijkstra(self, source: str) -> Tuple[Dict[str, int], Dict[str, str]]:""" Compute shortest paths from source to all destinations Returns: (distances, predecessors) """# Initialize distances distances = {router: float('inf') for router inself.topology} distances[source] =0# Track path predecessors predecessors: Dict[str, Optional[str]] = { router: Nonefor router inself.topology }# Priority queue: (distance, router) pq = [(0, source)] visited =set()while pq: current_dist, current = heapq.heappop(pq)if current in visited:continue visited.add(current)# Explore neighborsfor neighbor, cost inself.topology[current]:if neighbor in visited:continue new_dist = current_dist + costif new_dist < distances[neighbor]: distances[neighbor] = new_dist predecessors[neighbor] = current heapq.heappush(pq, (new_dist, neighbor))return distances, predecessorsdef get_path(self, source: str, destination: str, predecessors: Dict[str, str]) -> List[str]:"""Reconstruct path from source to destination""" path = [] current = destinationwhile current isnotNone: path.append(current) current = predecessors.get(current) path.reverse()if path[0] != source:return [] # No path existsreturn pathdef compute_routing_table(self, source: str) -> Dict[str, Tuple[str, int]]:""" Compute routing table for a router Returns: {destination: (next_hop, total_cost)} """ distances, predecessors =self.dijkstra(source) routing_table = {}for destination inself.topology:if destination == source:continue path =self.get_path(source, destination, predecessors)iflen(path) >=2: next_hop = path[1] # First hop after source routing_table[destination] = (next_hop, distances[destination])return routing_tabledef display_topology(self):"""Print the network topology"""print("\nNetwork Topology:")print("="*50)for router insorted(self.topology.keys()):print(f"\n{router}:")for neighbor, cost insorted(self.topology[router]):print(f" -> {neighbor} (cost: {cost})")def demonstrate_dijkstra():"""Demonstrate Dijkstra's algorithm with a sample network"""# Create network network = LinkStateRouter()# Build topology## R1 ----1---- R2# | |# 5 2# | |# R3 ----1---- R4 ----3---- R5# | |# 2 10# | |# R6 --------5------------- R7# network.add_link('R1', 'R2', 1) network.add_link('R1', 'R3', 5) network.add_link('R2', 'R4', 2) network.add_link('R3', 'R4', 1) network.add_link('R3', 'R6', 2) network.add_link('R4', 'R5', 3) network.add_link('R4', 'R7', 10) network.add_link('R6', 'R7', 5) network.display_topology()# Compute shortest paths from R1print("\n"+"="*50)print("Shortest Paths from R1 (using Dijkstra's algorithm)")print("="*50) distances, predecessors = network.dijkstra('R1')for dest insorted(distances.keys()):if dest !='R1': path = network.get_path('R1', dest, predecessors) path_str =" -> ".join(path)print(f"{dest}: {path_str} (cost: {distances[dest]}, "f"hops: {len(path)-1})")# Display routing tableprint("\n"+"="*50)print("Routing Table for R1")print("="*50)print(f"{'Destination':<12}{'Next Hop':<10}{'Total Cost'}")print("-"*40) routing_table = network.compute_routing_table('R1')for dest insorted(routing_table.keys()): next_hop, cost = routing_table[dest]print(f"{dest:<12}{next_hop:<10}{cost}")# Simulate link failureprint("\n"+"="*50)print("Link Failure Analysis: R1-R2 link fails")print("="*50) network.remove_link('R1', 'R2') distances, predecessors = network.dijkstra('R1')for dest in ['R2', 'R4', 'R5']: path = network.get_path('R1', dest, predecessors)if path: path_str =" -> ".join(path)print(f"{dest}: {path_str} (cost: {distances[dest]})")else:print(f"{dest}: No path available")if__name__=="__main__": demonstrate_dijkstra()
This implementation demonstrates the Bellman-Ford algorithm used by RIP and similar protocols.
14.5.1 Understanding Distance Vector
Figure 14.3: Distance Vector Exchange: Routers periodically share their routing tables with neighbors. Each router adds the cost to reach the neighbor and updates routes if a shorter path is found.
14.5.2 Python Implementation
"""Distance Vector Routing Protocol SimulationDemonstrates Bellman-Ford algorithm with split horizon"""from collections import defaultdictfrom typing import Dict, List, Tuple, Optionalimport copyclass DistanceVectorRouter:"""Simulates a router running distance vector protocol""" INFINITY =16# RIP uses 16 as infinitydef__init__(self, name: str):self.name = nameself.neighbors: Dict[str, int] = {} # neighbor -> costself.routing_table: Dict[str, Tuple[int, str]] = {} # dest -> (cost, next_hop)# Initialize route to selfself.routing_table[name] = (0, name)def add_neighbor(self, neighbor: str, cost: int):"""Add a directly connected neighbor"""self.neighbors[neighbor] = costself.routing_table[neighbor] = (cost, neighbor)def remove_neighbor(self, neighbor: str):"""Remove a neighbor (link failure)"""if neighbor inself.neighbors:delself.neighbors[neighbor]# Invalidate routes through this neighborfor dest, (cost, next_hop) inlist(self.routing_table.items()):if next_hop == neighbor and dest !=self.name:delself.routing_table[dest]def get_update_for(self, neighbor: str) -> Dict[str, int]:""" Get routing update to send to a specific neighbor Implements split horizon: don't advertise routes back """ update = {}for dest, (cost, next_hop) inself.routing_table.items():# Split horizon: don't advertise route via the neighbor it was learned fromif next_hop != neighbor: update[dest] = costelse:# Poison reverse: advertise with infinity to prevent loops update[dest] =self.INFINITYreturn updatedef process_update(self, from_neighbor: str, update: Dict[str, int]) ->bool:""" Process routing update from a neighbor Returns True if routing table changed """ changed =False cost_to_neighbor =self.neighbors.get(from_neighbor)if cost_to_neighbor isNone:returnFalse# Not a neighborfor dest, dest_cost in update.items():if dest ==self.name:continue# Skip routes to self new_cost = cost_to_neighbor + dest_cost# Cap at infinityif new_cost >=self.INFINITY: new_cost =self.INFINITY current =self.routing_table.get(dest)# Update if: new destination OR better cost OR same next_hop with new costif current isNone:if new_cost <self.INFINITY:self.routing_table[dest] = (new_cost, from_neighbor) changed =Trueelif new_cost < current[0]:self.routing_table[dest] = (new_cost, from_neighbor) changed =Trueelif current[1] == from_neighbor and new_cost != current[0]:# Same next hop, update cost (even if worse)if new_cost >=self.INFINITY:delself.routing_table[dest]else:self.routing_table[dest] = (new_cost, from_neighbor) changed =Truereturn changeddef display_table(self):"""Print routing table"""print(f"\nRouting Table for {self.name}:")print("="*60)print(f"{'Destination':<15}{'Cost':<8}{'Next Hop':<15}{'Status'}")print("-"*60)for dest insorted(self.routing_table.keys()): cost, next_hop =self.routing_table[dest] status ="Valid"if cost <self.INFINITY else"Invalid" cost_str =str(cost) if cost <self.INFINITY else"inf"print(f"{dest:<15}{cost_str:<8}{next_hop:<15}{status}")class DistanceVectorNetwork:"""Simulates a network of distance vector routers"""def__init__(self):self.routers: Dict[str, DistanceVectorRouter] = {}def add_router(self, name: str):"""Add a router to the network"""self.routers[name] = DistanceVectorRouter(name)def add_link(self, router1: str, router2: str, cost: int):"""Add a bidirectional link"""self.routers[router1].add_neighbor(router2, cost)self.routers[router2].add_neighbor(router1, cost)def remove_link(self, router1: str, router2: str):"""Remove a link (simulate failure)"""self.routers[router1].remove_neighbor(router2)self.routers[router2].remove_neighbor(router1)def run_convergence(self, max_rounds: int=20) ->int:""" Run distance vector convergence Returns number of rounds to converge """for round_num inrange(max_rounds):print(f"\n{'='*60}")print(f"Round {round_num}")print(f"{'='*60}") any_change =False# Collect all updates updates: Dict[str, Dict[str, Dict[str, int]]] = {}for name, router inself.routers.items(): updates[name] = {}for neighbor in router.neighbors: updates[name][neighbor] = router.get_update_for(neighbor)# Apply updatesfor name, router inself.routers.items():for neighbor in router.neighbors: neighbor_update = updates[neighbor].get(name, {})if router.process_update(neighbor, neighbor_update): any_change =True# Display current statefor router inself.routers.values(): router.display_table()ifnot any_change and round_num >0:print(f"\nConverged in {round_num} rounds")return round_numprint(f"\nDid not converge in {max_rounds} rounds")return max_roundsdef demonstrate_distance_vector():"""Demonstrate distance vector protocol with convergence and failure"""# Create network network = DistanceVectorNetwork()# Add routersfor name in ['R1', 'R2', 'R3', 'R4']: network.add_router(name)# Add links## R1 ---- 1 ---- R2# | |# 2 1# | |# R3 ---- 3 ---- R4# network.add_link('R1', 'R2', 1) network.add_link('R1', 'R3', 2) network.add_link('R2', 'R4', 1) network.add_link('R3', 'R4', 3)print("="*60)print("Initial Network Topology")print("="*60)print(""" R1 ---- 1 ---- R2 | | 2 1 | | R3 ---- 3 ---- R4 """)print("\n"+"="*60)print("Running Distance Vector Protocol (convergence)")print("="*60) rounds = network.run_convergence()# Simulate link failureprint("\n"+"="*60)print("Link Failure: R1-R2 link goes down")print("="*60) network.remove_link('R1', 'R2')print("\nImmediate effect (before reconvergence):")for router in network.routers.values(): router.display_table()print("\n"+"="*60)print("Reconverging after link failure")print("="*60) network.run_convergence()if__name__=="__main__": demonstrate_distance_vector()
14.5.3 Expected Output
============================================================
Initial Network Topology
============================================================
R1 ---- 1 ---- R2
| |
2 1
| |
R3 ---- 3 ---- R4
============================================================
Running Distance Vector Protocol (convergence)
============================================================
============================================================
Round 0
============================================================
Routing Table for R1:
============================================================
Destination Cost Next Hop Status
------------------------------------------------------------
R1 0 R1 Valid
R2 1 R2 Valid
R3 2 R3 Valid
[... continues through convergence ...]
Converged in 3 rounds
============================================================
Link Failure: R1-R2 link goes down
============================================================
Immediate effect (before reconvergence):
Routing Table for R1:
============================================================
Destination Cost Next Hop Status
------------------------------------------------------------
R1 0 R1 Valid
R3 2 R3 Valid
[... reconvergence ...]
Split Horizon: Don’t advertise routes back to the router you learned them from
Poison Reverse: Advertise unreachable routes with infinite cost
Convergence: Network takes multiple rounds to learn all routes
Count-to-Infinity Problem: Solved by maximum hop count (16 for RIP)
Try It: Distance Vector Convergence Simulator
See how a 3-router chain converges using the Bellman-Ford algorithm. Adjust link costs and observe how many rounds are needed for all routers to learn complete routes.
Figure 14.4: Algorithm Comparison: Link-state protocols (Dijkstra) use complete topology knowledge and converge fast at the cost of more memory, while distance vector protocols (Bellman-Ford) use only neighbor information and converge more slowly but are memory-efficient.
Aspect
Link-State (OSPF)
Distance Vector (RIP)
Knowledge
Complete topology
Neighbor routes only
Algorithm
Dijkstra’s
Bellman-Ford
Convergence
Fast (seconds)
Slow (minutes)
Memory
High (store topology)
Low (store routes)
Bandwidth
High (flood LSAs)
Low (periodic updates)
Scalability
Excellent
Limited (15 hop max)
Loop Prevention
SPF calculation
Split horizon, poison reverse
Use Case
Enterprise, ISP
Small networks, IoT
Try It: Algorithm Complexity Calculator
Compare the computational complexity of Dijkstra (OSPF) vs Bellman-Ford (RIP) as network size changes. See how the performance gap widens with scale.
Real-world impact: In a campus network with 1,000 routers, OSPF reconverges in < 1 ms after link failure (users don’t notice). RIP takes 50 ms per iteration × 15 hops max = 750 ms (noticeable lag during VoIP calls). This is why OSPF dominates large networks despite higher protocol complexity!
Match the Routing Algorithm Concept
Order the Dijkstra Algorithm Steps
Common Pitfalls
1. Implementing Dijkstra Without Understanding Negative-Weight Constraints
Dijkstra’s algorithm assumes non-negative edge weights. Routing metrics that can decrease (like rapidly improving link quality) can produce incorrect results with naive Dijkstra. Use Bellman-Ford for metrics that can decrease.
2. Not Testing Algorithm Correctness With Multiple Graph Topologies
A routing algorithm that works for a chain topology may fail for a grid or mesh topology. Test algorithms with at least: chain, star, mesh, and ring topologies with various failure conditions.
3. Confusing Algorithm Complexity With Network Performance
O(n²) complexity of Dijkstra means computation scales poorly for large networks, but ‘large’ means thousands of nodes in IP networks. IoT networks with 50–200 nodes rarely hit algorithmic scalability limits — other bottlenecks appear first.
🏷️ Label the Diagram
Code Challenge
14.7 Summary
Longest prefix matching ensures routers select the most specific route for a destination, with /24 routes taking precedence over /16 and default /0 routes
Dijkstra’s algorithm (link-state) computes optimal paths using complete topology knowledge, enabling fast convergence
Bellman-Ford algorithm (distance vector) uses only neighbor information, trading faster setup for slower convergence
Split horizon and poison reverse prevent routing loops in distance vector protocols
Administrative distance determines which routing source to trust when multiple protocols provide routes to the same destination
Link failure recovery demonstrates how both algorithms recompute paths when network topology changes
14.8 Knowledge Check
Quiz: Routing Algorithms
Worked Example: Debugging Routing Loops with Algorithm Analysis
Scenario: A 5-node IoT test network experiences intermittent connectivity issues. Occasionally, packets get “stuck” in the network, circulating between routers until TTL expires. You need to identify whether the routing algorithm implementation has a flaw.
Network Topology:
R1 ---- R2 ---- R3
| | |
+------ R4 -----+
Distance Vector Routing Tables (After Convergence - Should Be Stable):
R1: dest R3 -> next R2, cost 2
R2: dest R3 -> next R3, cost 1
R3: dest R1 -> next R2, cost 2
R4: dest R3 -> next R2, cost 2
Problem Report: Packets from R1 to R3 sometimes loop R1→R2→R4→R1 repeatedly
Step 1: Trace Routing Updates
Enable debug logging on R1, R2, R4 to capture routing table exchanges:
# R2 sends update to R4R2 → R4: {'R1': 1, 'R3': 1, 'R4': 0} # R2's distances to all nodes# R4 processes update (Bellman-Ford)R4 calculates: Distance to R3 via R2 =1 (R2_to_R3) +1 (R4_to_R2) =2 ✓R4 updates: dest R3 ->next R2, cost 2# BUT THEN: R1-R2 link temporarily fails!
Step 2: Analyze Failure Behavior (The Bug Appears)
When R1-R2 link fails:
R1 routing table:
dest R3 -> next R2, cost 2 (STALE! R2 unreachable now)
R1 doesn't immediately remove stale route because:
- No "poison reverse" implemented (split horizon only)
- No triggered updates on link down
- Relies on periodic updates (30s timer)
Meanwhile, R4 sends periodic update to R1:
R4 → R1: {'R3': 2, ...} # R4 can reach R3 in 2 hops
R1 processes:
Distance to R3 via R4 = 2 (R4_to_R3) + 1 (R1_to_R4) = 3
Current route: R3 via R2 cost 2 (but R2 down!)
R1 KEEPS stale route (cost 2 < 3) -- BUG!
Step 3: The Routing Loop
Packet from R1 to R3:
1. R1 forwards to R2 (stale route)
2. R1-R2 link down, packet goes nowhere...
Wait, implementation has fallback:
"If primary next-hop unreachable, try any neighbor"
3. R1 sends to R4 (fallback)
4. R4 forwards to R2 (its route)
5. R2 forwards back to R1 (thinks R1 closer)
6. R1→R4→R2→R1→R4→R2... (LOOP!)