1use core::result::Result;
2use heapless::{LinearMap, Vec};
3
4#[cfg(any(feature = "proto-ipv4", feature = "proto-ipv6"))]
5use super::{check, IpPayload, Packet};
6use super::{Interface, InterfaceInner};
7use crate::config::{IFACE_MAX_ADDR_COUNT, IFACE_MAX_MULTICAST_GROUP_COUNT};
8use crate::phy::{Device, PacketMeta};
9use crate::wire::*;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[cfg_attr(feature = "defmt", derive(defmt::Format))]
14pub enum MulticastError {
15 GroupTableFull,
17 Unaddressable,
19}
20
21#[cfg(feature = "proto-ipv4")]
22pub(crate) enum IgmpReportState {
23 Inactive,
24 ToGeneralQuery {
25 version: IgmpVersion,
26 timeout: crate::time::Instant,
27 interval: crate::time::Duration,
28 next_index: usize,
29 },
30 ToSpecificQuery {
31 version: IgmpVersion,
32 timeout: crate::time::Instant,
33 group: Ipv4Address,
34 },
35}
36
37#[cfg(feature = "proto-ipv6")]
38pub(crate) enum MldReportState {
39 Inactive,
40 ToGeneralQuery {
41 timeout: crate::time::Instant,
42 },
43 ToSpecificQuery {
44 group: Ipv6Address,
45 timeout: crate::time::Instant,
46 },
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50enum GroupState {
51 Joining,
53 Joined,
55 Leaving,
57}
58
59pub(crate) struct State {
60 groups: LinearMap<IpAddress, GroupState, IFACE_MAX_MULTICAST_GROUP_COUNT>,
61 #[cfg(feature = "proto-ipv4")]
63 igmp_report_state: IgmpReportState,
64 #[cfg(feature = "proto-ipv6")]
65 mld_report_state: MldReportState,
66}
67
68impl State {
69 pub(crate) fn new() -> Self {
70 Self {
71 groups: LinearMap::new(),
72 #[cfg(feature = "proto-ipv4")]
73 igmp_report_state: IgmpReportState::Inactive,
74 #[cfg(feature = "proto-ipv6")]
75 mld_report_state: MldReportState::Inactive,
76 }
77 }
78
79 pub(crate) fn has_multicast_group<T: Into<IpAddress>>(&self, addr: T) -> bool {
80 match self.groups.get(&addr.into()) {
83 None => false,
84 Some(GroupState::Joining) => true,
85 Some(GroupState::Joined) => true,
86 Some(GroupState::Leaving) => false,
87 }
88 }
89}
90
91impl core::fmt::Display for MulticastError {
92 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
93 match self {
94 MulticastError::GroupTableFull => write!(f, "GroupTableFull"),
95 MulticastError::Unaddressable => write!(f, "Unaddressable"),
96 }
97 }
98}
99
100#[cfg(feature = "std")]
101impl std::error::Error for MulticastError {}
102
103impl Interface {
104 pub fn join_multicast_group<T: Into<IpAddress>>(
106 &mut self,
107 addr: T,
108 ) -> Result<(), MulticastError> {
109 let addr = addr.into();
110 if !addr.is_multicast() {
111 return Err(MulticastError::Unaddressable);
112 }
113
114 if let Some(state) = self.inner.multicast.groups.get_mut(&addr) {
115 *state = match state {
116 GroupState::Joining => GroupState::Joining,
117 GroupState::Joined => GroupState::Joined,
118 GroupState::Leaving => GroupState::Joined,
119 };
120 } else {
121 self.inner
122 .multicast
123 .groups
124 .insert(addr, GroupState::Joining)
125 .map_err(|_| MulticastError::GroupTableFull)?;
126 }
127 Ok(())
128 }
129
130 pub fn leave_multicast_group<T: Into<IpAddress>>(
132 &mut self,
133 addr: T,
134 ) -> Result<(), MulticastError> {
135 let addr = addr.into();
136 if !addr.is_multicast() {
137 return Err(MulticastError::Unaddressable);
138 }
139
140 if let Some(state) = self.inner.multicast.groups.get_mut(&addr) {
141 let delete;
142 (*state, delete) = match state {
143 GroupState::Joining => (GroupState::Joined, true),
144 GroupState::Joined => (GroupState::Leaving, false),
145 GroupState::Leaving => (GroupState::Leaving, false),
146 };
147 if delete {
148 self.inner.multicast.groups.remove(&addr);
149 }
150 }
151 Ok(())
152 }
153
154 pub fn has_multicast_group<T: Into<IpAddress>>(&self, addr: T) -> bool {
156 self.inner.has_multicast_group(addr)
157 }
158
159 #[cfg(feature = "proto-ipv6")]
160 pub(super) fn update_solicited_node_groups(&mut self) {
161 let removals: Vec<_, IFACE_MAX_MULTICAST_GROUP_COUNT> = self
163 .inner
164 .multicast
165 .groups
166 .keys()
167 .cloned()
168 .filter(|a| matches!(a, IpAddress::Ipv6(a) if a.is_solicited_node_multicast() && !self.inner.has_solicited_node(*a)))
169 .collect();
170 for removal in removals {
171 let _ = self.leave_multicast_group(removal);
172 }
173
174 let cidrs: Vec<IpCidr, IFACE_MAX_ADDR_COUNT> = Vec::from_slice(self.ip_addrs()).unwrap();
175 for cidr in cidrs {
176 if let IpCidr::Ipv6(cidr) = cidr {
177 let _ = self.join_multicast_group(cidr.address().solicited_node());
178 }
179 }
180 }
181
182 pub(crate) fn multicast_egress(&mut self, device: &mut (impl Device + ?Sized)) {
188 while let Some((&addr, _)) = self
190 .inner
191 .multicast
192 .groups
193 .iter()
194 .find(|(_, &state)| state == GroupState::Joining)
195 {
196 match addr {
197 #[cfg(feature = "proto-ipv4")]
198 IpAddress::Ipv4(addr) => {
199 if let Some(pkt) = self.inner.igmp_report_packet(IgmpVersion::Version2, addr) {
200 let Some(tx_token) = device.transmit(self.inner.now) else {
201 break;
202 };
203
204 self.inner
206 .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter)
207 .unwrap();
208 }
209 }
210 #[cfg(feature = "proto-ipv6")]
211 IpAddress::Ipv6(addr) => {
212 if let Some(pkt) = self.inner.mldv2_report_packet(&[MldAddressRecordRepr::new(
213 MldRecordType::ChangeToInclude,
214 addr,
215 )]) {
216 let Some(tx_token) = device.transmit(self.inner.now) else {
217 break;
218 };
219
220 self.inner
222 .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter)
223 .unwrap();
224 }
225 }
226 }
227
228 self.inner
230 .multicast
231 .groups
232 .insert(addr, GroupState::Joined)
233 .unwrap();
234 }
235
236 while let Some((&addr, _)) = self
238 .inner
239 .multicast
240 .groups
241 .iter()
242 .find(|(_, &state)| state == GroupState::Leaving)
243 {
244 match addr {
245 #[cfg(feature = "proto-ipv4")]
246 IpAddress::Ipv4(addr) => {
247 if let Some(pkt) = self.inner.igmp_leave_packet(addr) {
248 let Some(tx_token) = device.transmit(self.inner.now) else {
249 break;
250 };
251
252 self.inner
254 .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter)
255 .unwrap();
256 }
257 }
258 #[cfg(feature = "proto-ipv6")]
259 IpAddress::Ipv6(addr) => {
260 if let Some(pkt) = self.inner.mldv2_report_packet(&[MldAddressRecordRepr::new(
261 MldRecordType::ChangeToExclude,
262 addr,
263 )]) {
264 let Some(tx_token) = device.transmit(self.inner.now) else {
265 break;
266 };
267
268 self.inner
270 .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter)
271 .unwrap();
272 }
273 }
274 }
275
276 self.inner.multicast.groups.remove(&addr);
277 }
278
279 #[cfg(feature = "proto-ipv4")]
280 match self.inner.multicast.igmp_report_state {
281 IgmpReportState::ToSpecificQuery {
282 version,
283 timeout,
284 group,
285 } if self.inner.now >= timeout => {
286 if let Some(pkt) = self.inner.igmp_report_packet(version, group) {
287 if let Some(tx_token) = device.transmit(self.inner.now) {
289 self.inner
291 .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter)
292 .unwrap();
293 self.inner.multicast.igmp_report_state = IgmpReportState::Inactive;
294 }
295 }
296 }
297 IgmpReportState::ToGeneralQuery {
298 version,
299 timeout,
300 interval,
301 next_index,
302 } if self.inner.now >= timeout => {
303 let addr = self
304 .inner
305 .multicast
306 .groups
307 .iter()
308 .filter_map(|(addr, _)| match addr {
309 IpAddress::Ipv4(addr) => Some(*addr),
310 #[allow(unreachable_patterns)]
311 _ => None,
312 })
313 .nth(next_index);
314
315 match addr {
316 Some(addr) => {
317 if let Some(pkt) = self.inner.igmp_report_packet(version, addr) {
318 if let Some(tx_token) = device.transmit(self.inner.now) {
320 self.inner
322 .dispatch_ip(
323 tx_token,
324 PacketMeta::default(),
325 pkt,
326 &mut self.fragmenter,
327 )
328 .unwrap();
329
330 let next_timeout = (timeout + interval).max(self.inner.now);
331 self.inner.multicast.igmp_report_state =
332 IgmpReportState::ToGeneralQuery {
333 version,
334 timeout: next_timeout,
335 interval,
336 next_index: next_index + 1,
337 };
338 }
339 }
340 }
341 None => {
342 self.inner.multicast.igmp_report_state = IgmpReportState::Inactive;
343 }
344 }
345 }
346 _ => {}
347 }
348 #[cfg(feature = "proto-ipv6")]
349 match self.inner.multicast.mld_report_state {
350 MldReportState::ToGeneralQuery { timeout } if self.inner.now >= timeout => {
351 let records = self
352 .inner
353 .multicast
354 .groups
355 .iter()
356 .filter_map(|(addr, _)| match addr {
357 IpAddress::Ipv6(addr) => Some(MldAddressRecordRepr::new(
358 MldRecordType::ModeIsExclude,
359 *addr,
360 )),
361 #[allow(unreachable_patterns)]
362 _ => None,
363 })
364 .collect::<heapless::Vec<_, IFACE_MAX_MULTICAST_GROUP_COUNT>>();
365 if let Some(pkt) = self.inner.mldv2_report_packet(&records) {
366 if let Some(tx_token) = device.transmit(self.inner.now) {
367 self.inner
368 .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter)
369 .unwrap();
370 };
371 };
372 self.inner.multicast.mld_report_state = MldReportState::Inactive;
373 }
374 MldReportState::ToSpecificQuery { group, timeout } if self.inner.now >= timeout => {
375 let record = MldAddressRecordRepr::new(MldRecordType::ModeIsExclude, group);
376 if let Some(pkt) = self.inner.mldv2_report_packet(&[record]) {
377 if let Some(tx_token) = device.transmit(self.inner.now) {
378 self.inner
380 .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter)
381 .unwrap();
382 }
383 }
384 self.inner.multicast.mld_report_state = MldReportState::Inactive;
385 }
386 _ => {}
387 }
388 }
389}
390
391impl InterfaceInner {
392 #[cfg(feature = "proto-ipv4")]
398 pub(super) fn process_igmp<'frame>(
399 &mut self,
400 ipv4_repr: Ipv4Repr,
401 ip_payload: &'frame [u8],
402 ) -> Option<Packet<'frame>> {
403 use crate::time::Duration;
404
405 let igmp_packet = check!(IgmpPacket::new_checked(ip_payload));
406 let igmp_repr = check!(IgmpRepr::parse(&igmp_packet));
407
408 match igmp_repr {
410 IgmpRepr::MembershipQuery {
411 group_addr,
412 version,
413 max_resp_time,
414 } => {
415 if group_addr.is_unspecified() && ipv4_repr.dst_addr == IPV4_MULTICAST_ALL_SYSTEMS {
417 let ipv4_multicast_group_count = self
418 .multicast
419 .groups
420 .keys()
421 .filter(|a| matches!(a, IpAddress::Ipv4(_)))
422 .count();
423
424 if ipv4_multicast_group_count != 0 {
426 let interval = match version {
427 IgmpVersion::Version1 => Duration::from_millis(100),
428 IgmpVersion::Version2 => {
429 let intervals = ipv4_multicast_group_count as u32 + 1;
433 max_resp_time / intervals
434 }
435 };
436 self.multicast.igmp_report_state = IgmpReportState::ToGeneralQuery {
437 version,
438 timeout: self.now + interval,
439 interval,
440 next_index: 0,
441 };
442 }
443 } else {
444 if self.has_multicast_group(group_addr) && ipv4_repr.dst_addr == group_addr {
446 let timeout = max_resp_time / 4;
448 self.multicast.igmp_report_state = IgmpReportState::ToSpecificQuery {
449 version,
450 timeout: self.now + timeout,
451 group: group_addr,
452 };
453 }
454 }
455 }
456 IgmpRepr::MembershipReport { .. } => (),
458 IgmpRepr::LeaveGroup { .. } => (),
460 }
461
462 None
463 }
464
465 #[cfg(feature = "proto-ipv4")]
466 fn igmp_report_packet<'any>(
467 &self,
468 version: IgmpVersion,
469 group_addr: Ipv4Address,
470 ) -> Option<Packet<'any>> {
471 let iface_addr = self.ipv4_addr()?;
472 let igmp_repr = IgmpRepr::MembershipReport {
473 group_addr,
474 version,
475 };
476 let pkt = Packet::new_ipv4(
477 Ipv4Repr {
478 src_addr: iface_addr,
479 dst_addr: group_addr,
481 next_header: IpProtocol::Igmp,
482 payload_len: igmp_repr.buffer_len(),
483 hop_limit: 1,
484 },
486 IpPayload::Igmp(igmp_repr),
487 );
488 Some(pkt)
489 }
490
491 #[cfg(feature = "proto-ipv4")]
492 fn igmp_leave_packet<'any>(&self, group_addr: Ipv4Address) -> Option<Packet<'any>> {
493 self.ipv4_addr().map(|iface_addr| {
494 let igmp_repr = IgmpRepr::LeaveGroup { group_addr };
495 Packet::new_ipv4(
496 Ipv4Repr {
497 src_addr: iface_addr,
498 dst_addr: IPV4_MULTICAST_ALL_ROUTERS,
499 next_header: IpProtocol::Igmp,
500 payload_len: igmp_repr.buffer_len(),
501 hop_limit: 1,
502 },
503 IpPayload::Igmp(igmp_repr),
504 )
505 })
506 }
507
508 #[cfg(feature = "proto-ipv6")]
514 pub(super) fn process_mldv2<'frame>(
515 &mut self,
516 ip_repr: Ipv6Repr,
517 repr: MldRepr<'frame>,
518 ) -> Option<Packet<'frame>> {
519 match repr {
520 MldRepr::Query {
521 mcast_addr,
522 max_resp_code,
523 ..
524 } => {
525 let delay = crate::time::Duration::from_millis(
527 (self.rand.rand_u16() % max_resp_code).into(),
528 );
529 if mcast_addr.is_unspecified()
531 && (ip_repr.dst_addr == IPV6_LINK_LOCAL_ALL_NODES
532 || self.has_ip_addr(ip_repr.dst_addr))
533 {
534 let ipv6_multicast_group_count = self
535 .multicast
536 .groups
537 .keys()
538 .filter(|a| matches!(a, IpAddress::Ipv6(_)))
539 .count();
540 if ipv6_multicast_group_count != 0 {
541 self.multicast.mld_report_state = MldReportState::ToGeneralQuery {
542 timeout: self.now + delay,
543 };
544 }
545 }
546 if self.has_multicast_group(mcast_addr) && ip_repr.dst_addr == mcast_addr {
547 self.multicast.mld_report_state = MldReportState::ToSpecificQuery {
548 group: mcast_addr,
549 timeout: self.now + delay,
550 };
551 }
552 None
553 }
554 MldRepr::Report { .. } => None,
555 MldRepr::ReportRecordReprs { .. } => None,
556 }
557 }
558}