smoltcp/iface/interface/
multicast.rs

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/// Error type for `join_multicast_group`, `leave_multicast_group`.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[cfg_attr(feature = "defmt", derive(defmt::Format))]
14pub enum MulticastError {
15    /// The table of joined multicast groups is already full.
16    GroupTableFull,
17    /// Cannot join/leave the given multicast group.
18    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 group, we have to send the join packet.
52    Joining,
53    /// We've already sent the join packet, we have nothing to do.
54    Joined,
55    /// We want to leave the group, we have to send a leave packet.
56    Leaving,
57}
58
59pub(crate) struct State {
60    groups: LinearMap<IpAddress, GroupState, IFACE_MAX_MULTICAST_GROUP_COUNT>,
61    /// When to report for (all or) the next multicast group membership via IGMP
62    #[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        // Return false if we don't have the multicast group,
81        // or we're leaving it.
82        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    /// Add an address to a list of subscribed multicast IP addresses.
105    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    /// Remove an address from the subscribed multicast IP addresses.
131    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    /// Check whether the interface listens to given destination multicast IP address.
155    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        // Remove old solicited-node multicast addresses
162        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    /// Do multicast egress.
183    ///
184    /// - Send join/leave packets according to the multicast group state.
185    /// - Depending on `igmp_report_state` and the therein contained
186    ///   timeouts, send IGMP membership reports.
187    pub(crate) fn multicast_egress(&mut self, device: &mut (impl Device + ?Sized)) {
188        // Process multicast joins.
189        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                        // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery.
205                        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                        // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery.
221                        self.inner
222                            .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter)
223                            .unwrap();
224                    }
225                }
226            }
227
228            // NOTE(unwrap): this is always replacing an existing entry, so it can't fail due to the map being full.
229            self.inner
230                .multicast
231                .groups
232                .insert(addr, GroupState::Joined)
233                .unwrap();
234        }
235
236        // Process multicast leaves.
237        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                        // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery.
253                        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                        // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery.
269                        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                    // Send initial membership report
288                    if let Some(tx_token) = device.transmit(self.inner.now) {
289                        // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery.
290                        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                            // Send initial membership report
319                            if let Some(tx_token) = device.transmit(self.inner.now) {
320                                // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery.
321                                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                        // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery.
379                        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    /// Host duties of the **IGMPv2** protocol.
393    ///
394    /// Sets up `igmp_report_state` for responding to IGMP general/specific membership queries.
395    /// Membership must not be reported immediately in order to avoid flooding the network
396    /// after a query is broadcasted by a router; this is not currently done.
397    #[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        // FIXME: report membership after a delay
409        match igmp_repr {
410            IgmpRepr::MembershipQuery {
411                group_addr,
412                version,
413                max_resp_time,
414            } => {
415                // General query
416                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                    // Are we member in any groups?
425                    if ipv4_multicast_group_count != 0 {
426                        let interval = match version {
427                            IgmpVersion::Version1 => Duration::from_millis(100),
428                            IgmpVersion::Version2 => {
429                                // No dependence on a random generator
430                                // (see [#24](https://github.com/m-labs/smoltcp/issues/24))
431                                // but at least spread reports evenly across max_resp_time.
432                                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                    // Group-specific query
445                    if self.has_multicast_group(group_addr) && ipv4_repr.dst_addr == group_addr {
446                        // Don't respond immediately
447                        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            // Ignore membership reports
457            IgmpRepr::MembershipReport { .. } => (),
458            // Ignore hosts leaving groups
459            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                // Send to the group being reported
480                dst_addr: group_addr,
481                next_header: IpProtocol::Igmp,
482                payload_len: igmp_repr.buffer_len(),
483                hop_limit: 1,
484                // [#183](https://github.com/m-labs/smoltcp/issues/183).
485            },
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    /// Host duties of the **MLDv2** protocol.
509    ///
510    /// Sets up `mld_report_state` for responding to MLD general/specific membership queries.
511    /// Membership must not be reported immediately in order to avoid flooding the network
512    /// after a query is broadcasted by a router; Currently the delay is fixed and not randomized.
513    #[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                // Do not respont immediately to the query, but wait a random time
526                let delay = crate::time::Duration::from_millis(
527                    (self.rand.rand_u16() % max_resp_code).into(),
528                );
529                // General query
530                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}