From 063cbbb9b9043279fdf1fc5ddf2fe5962e73e0d0 Mon Sep 17 00:00:00 2001 From: Igor Yamolov Date: Thu, 12 Nov 2026 17:03:51 +0100 Subject: [PATCH] Drop the route record on any network status route failure A relay reporting LinkFailure, LegacyLinkFailure or LegacyNoRouteAvailable for a destination removed the routing table entry but kept its route record; only SourceRouteFailure dropped the record. Since route_to prefers the record, every later send to that destination took the same dead path until the record was replaced or the stack restarted. This happens when the concentrator's own transmission succeeds but the next device cannot forward it, e.g. a direct (relay-less) record for a router that moved out of range while another device still acks the frame. Routing::forget_route_to now drops both the route and the record, and handle_network_status uses it for every route failure status, so the next send falls back to route discovery. Co-Authored-By: Claude Opus 5.5 --- .../ziggurat-driver/src/zigbee_stack/route.rs | 24 +++---- crates/ziggurat-zigbee/src/nwk/routing.rs | 66 +++++++++++++++++++ 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/crates/ziggurat-driver/src/zigbee_stack/route.rs b/crates/ziggurat-driver/src/zigbee_stack/route.rs index 9904657..db8ed19 100644 --- a/crates/ziggurat-driver/src/zigbee_stack/route.rs +++ b/crates/ziggurat-driver/src/zigbee_stack/route.rs @@ -400,26 +400,18 @@ impl ZigbeeStack { | NwkNetworkStatus::LegacyLinkFailure | NwkNetworkStatus::LinkFailure | NwkNetworkStatus::SourceRouteFailure => { - let mut core = self.core(); - - core.nib + // The reported path is broken whatever it was: a source route, or a + // next hop or direct route record gone stale when the device moved. + // The next transmission discovers until a fresh route record arrives + let removed_record = self + .core() + .nib .routing - .remove_route(network_status_cmd.network_address); - - // A relay reported the stored path broken; the next transmission - // falls back to discovery until a fresh route record arrives - let removed_record = network_status_cmd.status_code - == NwkNetworkStatus::SourceRouteFailure - && core - .nib - .routing - .remove_route_record(network_status_cmd.network_address); - - drop(core); + .forget_route_to(network_status_cmd.network_address); if removed_record { tracing::info!( - "Removed failed source route to {:?}", + "Removed failed route record to {:?}", network_status_cmd.network_address ); } diff --git a/crates/ziggurat-zigbee/src/nwk/routing.rs b/crates/ziggurat-zigbee/src/nwk/routing.rs index f3b294f..d927f0c 100644 --- a/crates/ziggurat-zigbee/src/nwk/routing.rs +++ b/crates/ziggurat-zigbee/src/nwk/routing.rs @@ -332,6 +332,18 @@ impl Routing { self.route_record_table.remove(&destination).is_some() } + /// Spec 3.6.4.8.1: a router reported it could not deliver a frame we originated + /// toward `destination` (`LinkFailure`, `SourceRouteFailure` or a legacy + /// equivalent). Both the routing entry and the stored route record go: a record + /// that reached the failing router as a plain next hop, such as a direct record + /// (no relays) for a device that moved away, is as stale as a broken source route, + /// and only its removal lets the next transmission discover a fresh route. + /// Returns whether a route record was removed. + pub fn forget_route_to(&mut self, destination: Nwk) -> bool { + self.route_table.remove(&destination); + self.route_record_table.remove(&destination).is_some() + } + /// Spec 3.6.4.3.2: relaying a source-routed frame proves the concentrator that /// originated it holds a route record for the frame's source; no route record /// has to precede the next frame toward it (unless it keeps no route cache). @@ -699,3 +711,57 @@ impl Routing { } } } + +#[cfg(test)] +mod test { + use super::*; + use alloc::vec; + + const DESTINATION: Nwk = Nwk(0x0391); + const RELAY: Nwk = Nwk(0xEF2F); + const MAX_SOURCE_ROUTE: u8 = 12; + + #[test] + fn test_forget_route_to_drops_a_direct_route_record() { + let mut routing = Routing::default(); + routing.store_route_record(DESTINATION, vec![]); + assert_eq!( + routing.route_to(DESTINATION, MAX_SOURCE_ROUTE), + Some(Route::NextHop(DESTINATION)) + ); + + assert!(routing.forget_route_to(DESTINATION)); + assert_eq!(routing.route_to(DESTINATION, MAX_SOURCE_ROUTE), None); + } + + #[test] + fn test_forget_route_to_drops_a_source_route_record() { + let mut routing = Routing::default(); + routing.store_route_record(DESTINATION, vec![RELAY]); + + assert!(routing.forget_route_to(DESTINATION)); + assert_eq!(routing.route_to(DESTINATION, MAX_SOURCE_ROUTE), None); + } + + #[test] + fn test_forget_route_to_drops_the_routing_entry() { + let mut routing = Routing::default(); + routing.restore_route(DESTINATION, RELAY, 1); + assert_eq!(routing.next_hop(DESTINATION), Some(RELAY)); + + assert!(!routing.forget_route_to(DESTINATION)); + assert_eq!(routing.next_hop(DESTINATION), None); + } + + #[test] + fn test_forget_route_to_keeps_other_destinations() { + let mut routing = Routing::default(); + routing.store_route_record(DESTINATION, vec![RELAY]); + + routing.forget_route_to(DESTINATION); + assert_eq!( + routing.route_to(RELAY, MAX_SOURCE_ROUTE), + Some(Route::NextHop(RELAY)) + ); + } +}