From c5bffa0dba10631738fc0de05850ff55487ef2ba Mon Sep 17 00:00:00 2001 From: zigpy-review-bot <286747149+zigpy-review-bot@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:00:20 +0200 Subject: [PATCH] Clear End Device Initiator when relaying an end device child's frame Zigbee R23.2 3.3.1.1.8: after validating the source, a router parent SHALL clear the End Device Initiator bit when relaying a message sent by one of its end device children. Ziggurat relayed these frames with the bit still set, so other routers, which find no end device with that source in their own neighbor table, drop them (3.6.2.2). A group command an end device remote broadcasts through its ziggurat parent was therefore dropped by the routers that received the relayed copy, the bound bulbs included. Clear the bit for frames whose NWK source is one of our end device children in both relay paths: the broadcast relay (the over-the-air copy, its retransmits, and the indirect copies fanned out to sleepy children) and the unicast relay (sender retries and the indirect queue). --- .../ziggurat-driver/src/zigbee_stack/nwk.rs | 21 ++++- crates/ziggurat-zigbee/src/nwk/neighbors.rs | 86 +++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/crates/ziggurat-driver/src/zigbee_stack/nwk.rs b/crates/ziggurat-driver/src/zigbee_stack/nwk.rs index 89bfc13..a7ac906 100644 --- a/crates/ziggurat-driver/src/zigbee_stack/nwk.rs +++ b/crates/ziggurat-driver/src/zigbee_stack/nwk.rs @@ -1666,6 +1666,13 @@ impl ZigbeeStack { return; } + // Spec 3.3.1.1.8: a frame from one of our end device children is relayed with + // its End Device Initiator bit cleared + self.core() + .nib + .neighbors + .clear_end_device_initiator_for_relay(&mut nwk_frame.nwk_header); + let destination = nwk_frame.nwk_header.destination; // Spec 3.6.4.5.5: each relay appends its network address to a transiting route @@ -1827,16 +1834,24 @@ impl ZigbeeStack { return; } + let mut relayed_frame = nwk_frame.clone(); + + // Spec 3.3.1.1.8: a broadcast from one of our end device children is relayed + // with its End Device Initiator bit cleared, both over the air and in the + // copies queued for sleepy children + self.core() + .nib + .neighbors + .clear_end_device_initiator_for_relay(&mut relayed_frame.nwk_header); + // Spec 3.6.6: deliver another device's 0xFFFF broadcast to our own sleepy // children as MAC unicasts (a no-op for non-0xFFFF destinations). self.maybe_fan_out_broadcast_to_sleepy_children( - nwk_frame, + &relayed_frame, NwkSecurityMode::NetworkKey, TrafficClass::Forwarding, ); - let mut relayed_frame = nwk_frame.clone(); - relayed_frame.nwk_header.radius = relayed_frame.nwk_header.radius.saturating_sub(1); if relayed_frame.nwk_header.radius == 0 { tracing::debug!("Not relaying broadcast, radius is exhausted"); diff --git a/crates/ziggurat-zigbee/src/nwk/neighbors.rs b/crates/ziggurat-zigbee/src/nwk/neighbors.rs index 5b4bc21..72f9f4f 100644 --- a/crates/ziggurat-zigbee/src/nwk/neighbors.rs +++ b/crates/ziggurat-zigbee/src/nwk/neighbors.rs @@ -4,6 +4,7 @@ use core::cmp; use crate::Instant; use crate::nwk::commands::{NwkLinkStatus, NwkLinkStatusCommand}; +use crate::nwk::frame::NwkHeader; use crate::ring_buffer::RingBuffer; use core::time::Duration; use ziggurat_ieee_802154::types::{Eui64, Nwk}; @@ -242,6 +243,22 @@ impl Neighbors { }) } + /// Spec 3.3.1.1.8: after validating the source (3.6.2.2), a router parent SHALL + /// clear the End Device Initiator bit when relaying a frame sent by one of its end + /// device children. Otherwise the next router searches its own neighbor table for + /// the source, finds no end device there, and drops the frame (3.6.2.2). Frames + /// from any other source are left untouched. Returns whether the bit was cleared. + pub fn clear_end_device_initiator_for_relay(&self, header: &mut NwkHeader) -> bool { + if !header.frame_control.end_device_initiator + || self.end_device_child_eui64(header.source).is_none() + { + return false; + } + + header.frame_control.end_device_initiator = false; + true + } + /// The EUI64 of the sleepy (rx-off-when-idle) child with the given network /// address, if one exists. Frames to sleepy children cannot be transmitted /// directly and must go through the indirect transaction queue. @@ -721,3 +738,72 @@ impl Neighbors { lost_link.then_some(neighbor_entry.network_address) } } + +#[cfg(test)] +mod test { + use super::*; + use hex_literal::hex; + + const CHILD_NWK: Nwk = Nwk(0x1234); + + fn neighbors_with(device_type: NwkDeviceType) -> Neighbors { + let mut neighbors = Neighbors::default(); + neighbors.upsert_child( + ChildDescriptor { + eui64: Eui64::from_hex("00:11:22:33:44:55:66:77"), + network_address: CHILD_NWK, + device_type, + rx_on_when_idle: false, + device_timeout: Duration::from_secs(60), + relationship: Relationship::Child, + }, + Instant::from_micros(0), + ); + neighbors + } + + /// A group command broadcast to 0xFFFD by end device 0x1234: data frame, route + /// discovery enabled, secured, End Device Initiator set (frame control 0x2248). + fn end_device_broadcast_header() -> NwkHeader { + let (header, rest) = NwkHeader::deserialize(&hex!("4822fdff34121e05")).unwrap(); + assert!(rest.is_empty()); + assert!(header.frame_control.end_device_initiator); + header + } + + #[test] + fn test_relay_clears_end_device_initiator_of_end_device_child() { + let neighbors = neighbors_with(NwkDeviceType::EndDevice); + let mut header = end_device_broadcast_header(); + + assert!(neighbors.clear_end_device_initiator_for_relay(&mut header)); + assert!(!header.frame_control.end_device_initiator); + + // Only the End Device Initiator bit changes: frame control 0x2248 -> 0x0248 + let mut bytes = Vec::new(); + header.serialize_into(&mut bytes); + assert_eq!(bytes, hex!("4802fdff34121e05")); + } + + #[test] + fn test_relay_keeps_end_device_initiator_of_other_sources() { + // Not an end device in our table: the bit is not ours to clear + for neighbors in [Neighbors::default(), neighbors_with(NwkDeviceType::Router)] { + let mut header = end_device_broadcast_header(); + + assert!(!neighbors.clear_end_device_initiator_for_relay(&mut header)); + assert!(header.frame_control.end_device_initiator); + } + } + + #[test] + fn test_relay_without_end_device_initiator_is_unchanged() { + let neighbors = neighbors_with(NwkDeviceType::EndDevice); + let mut header = end_device_broadcast_header(); + header.frame_control.end_device_initiator = false; + let expected = header.clone(); + + assert!(!neighbors.clear_end_device_initiator_for_relay(&mut header)); + assert_eq!(header, expected); + } +}