Loading content...
Simple split horizon prevents loops by omitting routes learned from an interface. But silence can be ambiguous—when neighbor B doesn't receive route X from A, does A have no path to X, or is A just being quiet about it?
Poison reverse removes this ambiguity with explicit communication: instead of omitting routes, actively advertise them with an infinite metric (16 in RIP). This tells neighbors definitively: "I do not have an independent path to this destination—don't rely on me."
This explicit poisoning accelerates convergence in several ways:
Poison reverse is particularly valuable during topology changes, when the network is most vulnerable to forming loops. By loudly declaring which routes are not available, routers can make better decisions faster.
By the end of this page, you will understand poison reverse mechanics in detail, how it interacts with triggered updates and hold-down timers, implementation considerations, convergence time analysis, and when to prefer poison reverse over simple split horizon.
Let's formalize the poison reverse technique and understand its behavior in detail.
Formal Definition:
When building a routing update for interface I, for each route R:
- If R was learned from interface I: advertise R with metric = ∞ (16)
- Otherwise: advertise R with its actual metric
Comparison with Split Horizon:
| Scenario | Simple Split Horizon | Poison Reverse |
|---|---|---|
| Route X learned from eth0 | Omit X from eth0 updates | Include X with metric 16 |
| Update size | Smaller | Larger |
| Information conveyed | Implicit "no path" | Explicit "no path" |
| Ambiguity | Possible | None |
Why the Difference Matters:
Consider a scenario where updates cross in transit:
With simple split horizon:
With poison reverse:
The Explicit Signal Advantage:
Poison reverse's explicit "I can't help you" message:
With poison reverse, A advertising "X = 16" to B doesn't mean A thinks X is globally unreachable—just that A cannot reach X through a path that doesn't involve B. A may still successfully route to X via other neighbors. The poison is interface-specific, not table-wide.
Let's trace through a network failure with poison reverse enabled, comparing behavior at each step.
Topology:
A ——— B ——— C ——— X
eth0 eth1 eth0 eth1
All links have cost 1.
Initial Converged State:
| Router | Destination | Metric | Via | Learned From |
|---|---|---|---|---|
| A | X | 3 | B | eth0 |
| B | X | 2 | C | eth0 |
| C | X | 1 | X | eth0 (direct) |
Updates Sent (Poison Reverse Enabled):
Router A sends on eth0 (to B):
Router B sends on eth0 (to C):
Router B sends on eth1 (to A):
Event: Link C-X Fails at time T=0
T=0: C detects failure
T=0.1s: B receives triggered update
T=0.2s: A receives triggered update
Network converged in ~0.3 seconds!
| Time | Event | A's Route to X | B's Route to X | C's Route to X |
|---|---|---|---|---|
| 0.0s | C-X link fails | 3 (via B) | 2 (via C) | ∞ |
| 0.1s | B receives poison from C | 3 (via B) | ∞ | ∞ |
| 0.2s | A receives poison from B | ∞ | ∞ | ∞ |
| 0.3s | All updates exchanged | ∞ (converged) | ∞ (converged) | ∞ (converged) |
With poison reverse and triggered updates, this failure converged in fractions of a second. Without these mechanisms, count-to-infinity would take 7+ minutes. The poison from A to B (pre-failure) ensured B never considered using A as an alternate path, and the poison from B to A ensured A immediately knew B couldn't help.
Poison reverse doesn't operate in isolation—it works alongside other distance vector mechanisms to achieve robust convergence.
Poison Reverse + Triggered Updates:
The combination is powerful:
Link fails → Triggered update with poison
↓
Neighbor receives, updates, sends triggered update with poison
↓
Next neighbor receives, updates, triggers...
↓
Entire network converged (milliseconds to seconds)
Poison Reverse + Hold-Down Timers:
Hold-down timers prevent "flapping"—rapid route changes when a link is unstable. The interaction with poison reverse requires care:
Caution: Hold-down can slow convergence when valid alternate routes exist. Some implementations reduce or eliminate hold-down when poison reverse is enabled.
Poison Reverse + Route Poisoning:
Terminology can be confusing:
They're complementary:
C-X fails:
C does ROUTE POISONING: advertises X=∞ to all neighbors
A does POISON REVERSE: already advertising X=∞ to B (learned from B)
| Technique A | Technique B | Interaction | Effect |
|---|---|---|---|
| Poison Reverse | Triggered Updates | Synergistic | Fast poison propagation |
| Poison Reverse | Hold-Down | Can conflict | Hold-down may delay valid routes |
| Poison Reverse | Route Poisoning | Complementary | Both advertise infinity for different reasons |
| Poison Reverse | Split Horizon | Alternative | Choose one or the other |
| Poison Reverse | Max Hop Count | Independent | Both limit loop duration |
"Poison reverse" specifically means advertising infinity for routes learned from an interface. "Route poisoning" means advertising infinity when you lose a route. "Poisoned route" means any route with infinite metric. Context determines which concept is being discussed.
Implementing poison reverse requires careful attention to update construction and table management.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
from dataclasses import dataclassfrom typing import Dict, List, Set, Optionalimport time @dataclassclass RouteEntry: destination: str metric: int next_hop: str learned_interface: str last_update: float def is_reachable(self, infinity: int = 16) -> bool: return self.metric < infinity class PoisonReverseRouter: """ Complete router implementation with poison reverse. Demonstrates all aspects of poison reverse operation. """ INFINITY = 16 UPDATE_INTERVAL = 30.0 TRIGGERED_UPDATE_DELAY = 1.0 # Min delay between triggered updates def __init__(self, router_id: str, interfaces: Dict[str, int]): """ Args: router_id: Unique router identifier interfaces: {interface_name: link_cost} """ self.router_id = router_id self.interfaces = interfaces self.interface_neighbors: Dict[str, str] = {} # Routing table: {destination: RouteEntry} self.routing_table: Dict[str, RouteEntry] = {} # Add self-route self.routing_table[router_id] = RouteEntry( destination=router_id, metric=0, next_hop=router_id, learned_interface="local", last_update=time.time() ) # Pending triggered update destinations self.triggered_pending: Set[str] = set() self.last_triggered_update = 0.0 def add_neighbor(self, interface: str, neighbor_id: str): """Register which neighbor is reachable via which interface.""" self.interface_neighbors[interface] = neighbor_id # Add direct route to neighbor self.routing_table[neighbor_id] = RouteEntry( destination=neighbor_id, metric=self.interfaces[interface], next_hop=neighbor_id, learned_interface=interface, last_update=time.time() ) def build_update(self, interface: str, full_update: bool = True) -> Dict[str, int]: """ Build routing update for an interface with POISON REVERSE. Args: interface: Outgoing interface name full_update: If True, include all routes. If False, only triggered. Returns: {destination: metric} to advertise """ update = {} destinations = self.routing_table.keys() if full_update else self.triggered_pending for dest in destinations: if dest not in self.routing_table: continue route = self.routing_table[dest] # POISON REVERSE: routes learned from this interface get infinity if route.learned_interface == interface: update[dest] = self.INFINITY else: update[dest] = route.metric return update def receive_update(self, source_interface: str, source_neighbor: str, distance_vector: Dict[str, int]) -> List[str]: """ Process received update with poison reverse awareness. Returns: List of destinations that changed (for triggered updates) """ changed = [] current_time = time.time() link_cost = self.interfaces[source_interface] for dest, received_metric in distance_vector.items(): # Compute effective metric new_metric = min(received_metric + link_cost, self.INFINITY) if dest not in self.routing_table: # New destination if new_metric < self.INFINITY: self.routing_table[dest] = RouteEntry( destination=dest, metric=new_metric, next_hop=source_neighbor, learned_interface=source_interface, last_update=current_time ) changed.append(dest) print(f" [{self.router_id}] NEW: {dest} via {source_neighbor}, " f"metric {new_metric}") else: existing = self.routing_table[dest] # Case 1: Update from current next-hop - MUST accept if existing.next_hop == source_neighbor: if new_metric != existing.metric: old = existing.metric existing.metric = new_metric existing.last_update = current_time changed.append(dest) print(f" [{self.router_id}] UPDATED: {dest} " f"{old} -> {new_metric} (same next-hop)") else: # Just refresh timer existing.last_update = current_time # Case 2: Better route from different next-hop elif new_metric < existing.metric: old_nh = existing.next_hop old_metric = existing.metric existing.metric = new_metric existing.next_hop = source_neighbor existing.learned_interface = source_interface existing.last_update = current_time changed.append(dest) print(f" [{self.router_id}] BETTER: {dest} via {source_neighbor} " f"(metric {new_metric}) replaces via {old_nh} " f"(metric {old_metric})") # Queue triggered updates self.triggered_pending.update(changed) return changed def should_send_triggered_update(self) -> bool: """Check if triggered update should be sent.""" if not self.triggered_pending: return False current_time = time.time() if current_time - self.last_triggered_update < self.TRIGGERED_UPDATE_DELAY: return False return True def get_triggered_update(self, interface: str) -> Optional[Dict[str, int]]: """Get triggered update for interface if one is pending.""" if not self.should_send_triggered_update(): return None update = self.build_update(interface, full_update=False) return update if update else None def clear_triggered_pending(self): """Clear pending triggers after sending updates.""" self.triggered_pending.clear() self.last_triggered_update = time.time() def print_routing_table(self): """Display current routing table.""" print(f"\n=== Router {self.router_id} Routing Table ===") print(f"{'Dest':<6} {'Metric':<8} {'Next Hop':<10} {'Interface':<12}") print("-" * 40) for dest, route in sorted(self.routing_table.items()): metric_str = str(route.metric) if route.metric < self.INFINITY else "∞" print(f"{dest:<6} {metric_str:<8} {route.next_hop:<10} " f"{route.learned_interface:<12}") def print_updates_all_interfaces(self): """Show what this router advertises on each interface.""" print(f"\n=== Router {self.router_id} Outgoing Updates ===") for iface in self.interfaces: neighbor = self.interface_neighbors.get(iface, "unknown") update = self.build_update(iface) print(f" Interface {iface} (to {neighbor}):") for dest, metric in sorted(update.items()): metric_str = str(metric) if metric < self.INFINITY else "∞ (poison)" print(f" {dest}: {metric_str}") def demonstrate_poison_reverse(): """Demonstrate poison reverse behavior during failure.""" print("=" * 60) print("POISON REVERSE DEMONSTRATION") print("Topology: A --- B --- C --- X") print("=" * 60) # Create routers router_a = PoisonReverseRouter("A", {"eth0": 1}) router_b = PoisonReverseRouter("B", {"eth0": 1, "eth1": 1}) router_c = PoisonReverseRouter("C", {"eth0": 1, "eth1": 1}) # Configure neighbors router_a.add_neighbor("eth0", "B") router_b.add_neighbor("eth1", "A") router_b.add_neighbor("eth0", "C") router_c.add_neighbor("eth1", "B") # C has direct route to X router_c.routing_table["X"] = RouteEntry( "X", 1, "X", "eth0", time.time() ) print("\n--- Initial State: Manually setting converged routes ---") # Manually set converged state router_b.routing_table["X"] = RouteEntry( "X", 2, "C", "eth0", time.time() ) router_a.routing_table["X"] = RouteEntry( "X", 3, "B", "eth0", time.time() ) for router in [router_a, router_b, router_c]: router.print_routing_table() print("\n--- Show Poison Reverse in Updates ---") for router in [router_a, router_b, router_c]: router.print_updates_all_interfaces() print("\n" + "=" * 60) print("EVENT: Link C-X fails!") print("=" * 60) # C detects failure print("\n[C] Detects link failure to X") router_c.routing_table["X"].metric = 16 router_c.triggered_pending.add("X") # C sends triggered update to B print("\n[C -> B] Sending triggered update...") update_to_b = {"X": 16} # Route poison changes = router_b.receive_update("eth0", "C", update_to_b) print(f" B changed routes: {changes}") # B sends triggered update to A print("\n[B -> A] Sending triggered update...") update_to_a = router_b.build_update("eth1", full_update=False) print(f" B's triggered update to A: {update_to_a}") changes = router_a.receive_update("eth0", "B", update_to_a) print(f" A changed routes: {changes}") print("\n--- Final State ---") for router in [router_a, router_b, router_c]: router.print_routing_table() print("\n✓ Converged! All routers know X is unreachable.") demonstrate_poison_reverse()Poison reverse increases update message size because poisoned routes are included rather than omitted. For networks with many routes, this can significantly increase bandwidth usage. Monitor routing protocol traffic in production environments.
Let's analyze how poison reverse affects convergence time in various scenarios.
| Scenario | No Split Horizon | Simple Split Horizon | Poison Reverse |
|---|---|---|---|
| Two-node loop (A-B) | ~7 minutes | Instant | Instant |
| Three-node loop (A-B-C) | ~7 minutes | ~7 minutes | ~7 minutes* |
| Linear chain failure | ~7 minutes | Seconds** | Milliseconds** |
| Updates cross in transit | May cause temp loop | May cause temp loop | No confusion |
| Missed update packet | Depends on next update | Depends on next update | Next update confirms |
* Poison reverse doesn't prevent multi-node loops, but may slightly reduce their duration. ** With triggered updates enabled.
Why Poison Reverse Helps Linear Chains:
In a linear topology A — B — C — X:
Mathematical Analysis:
Let:
With triggered updates + poison reverse:
Convergence time ≈ t_fail + (n-1) × p
≈ milliseconds to seconds
Without (counting to infinity):
Convergence time ≈ (∞ - initial_distance) × update_interval
≈ 14 × 30s = 7 minutes
Race Condition Resolution:
Consider this timeline:
T=0.0s: Link fails at C
T=0.0s: A sends periodic update (doesn't know about failure yet)
T=0.1s: B receives A's update
T=0.1s: B receives C's triggered update
With simple split horizon:
With poison reverse:
Poison reverse alone doesn't speed convergence—it must be combined with triggered updates. Without triggers, you still wait up to 30 seconds between information propagation steps. Poison reverse removes ambiguity; triggered updates remove waiting.
Bandwidth Overhead Analysis:
For a router with:
Simple split horizon:
Poison reverse:
When This Matters:
Use poison reverse when: convergence speed is critical, network topology is complex (risk of updates crossing), or debugging ease is important. Use simple split horizon when: bandwidth is constrained, routing tables are large, or the network is stable with rare changes.
Poison reverse configuration varies by platform. Here's how to configure and verify on common implementations.
Cisco IOS Configuration:
! View current split horizon setting
Router# show ip interface fastethernet0/0 | include split
Split horizon is enabled
ICMP redirects are always sent
! Enable split horizon (default)
Router(config)# interface fastethernet0/0
Router(config-if)# ip split-horizon
! Disable split horizon (rarely needed)
Router(config-if)# no ip split-horizon
! Note: Cisco IOS RIP uses split horizon with poison reverse by default
! on most interface types. The 'ip split-horizon' command controls
! whether split horizon is applied at all.
! Enable RIP debugging to see poison reverse behavior
Router# debug ip rip
RIP: sending v2 update to 224.0.0.9 via FastEthernet0/0
10.1.0.0/24 -> 0.0.0.0, metric 16, tag 0 <- Poison reverse!
10.2.0.0/24 -> 0.0.0.0, metric 2, tag 0 <- Normal entry
Verification Commands:
! Check interface RIP settings
Router# show ip protocols
Routing Protocol is "rip"
Outgoing update filter list for all interfaces is not set
Incoming update filter list for all interfaces is not set
Sending updates every 30 seconds, next due in 18 seconds
Invalid after 180 seconds, hold down 180, flushed after 240
! Monitor actual updates
Router# debug ip rip events
! Observe metrics in updates - 16 indicates poison
! Check routing table for infinity routes
Router# show ip route rip
! Routes with metric 16 will show as 'possibly down'
Packet Capture Analysis:
When analyzing poison reverse in packet captures (Wireshark):
ripExample Wireshark Filter:
rip.metric == 16
This shows all poison entries in RIP traffic.
| Platform | Default Split Horizon | Poison Reverse |
|---|---|---|
| Cisco IOS | Enabled | Enabled by default with split horizon |
| Juniper JunOS | Enabled | Configurable |
| Linux (Quagga/FRR) | Enabled | Configurable per interface |
| Mikrotik | Enabled | Enabled by default |
| VyOS | Enabled | Configurable |
Disabling split horizon or poison reverse for testing can cause routing loops in production networks. Always test in isolated lab environments. If you must test in production, do so during maintenance windows with monitoring in place.
We've completed our exploration of poison reverse, the final piece of the distance vector routing puzzle. Let's consolidate both this page's content and the entire module's learning.
Module Summary: Distance Vector Routing Mastered
Across this module, we've built a comprehensive understanding of distance vector routing:
Bellman-Ford Algorithm: The mathematical foundation—iterative relaxation converging to shortest paths
Routing Table Exchange: How routers share distance vectors, the timer mechanisms, and update processing
Count-to-Infinity Problem: The fundamental limitation—circular dependencies causing slow convergence and routing loops
Split Horizon: The primary countermeasure—don't advertise routes back to where you learned them
Poison Reverse: Explicit signaling—advertise infinity for routes through the learning interface
The Complete Picture:
Distance vector routing represents an elegant trade-off:
| Aspect | Characteristic | Implication |
|---|---|---|
| Information shared | Distance to destinations | Simple but limited visibility |
| Computation | Local Bellman-Ford application | Low CPU requirements |
| Memory | One entry per destination | Scales with network destinations |
| Convergence (good news) | Fast (triggered updates) | New routes propagate quickly |
| Convergence (bad news) | Slow (count-to-infinity) | Failures take time to resolve |
| Loop prevention | Split horizon + poison reverse | Effective for two-node loops |
| Scalability | Limited (15 hops typical) | Suitable for smaller networks |
Where Distance Vector Fits Today:
What's Next in Your Learning Journey:
With distance vector mastered, you're ready to explore:
Congratulations! You've completed the Distance Vector Routing module. You now possess a Principal Engineer-level understanding of how distance vector protocols work—from Bellman-Ford mathematics through practical loop prevention mechanisms. This knowledge forms the foundation for understanding all routing protocol categories and making informed network design decisions.