diff options
Diffstat (limited to 'src/iface/interface/igmp.rs')
-rw-r--r-- | src/iface/interface/igmp.rs | 275 |
1 files changed, 275 insertions, 0 deletions
diff --git a/src/iface/interface/igmp.rs b/src/iface/interface/igmp.rs new file mode 100644 index 0000000..14856ca --- /dev/null +++ b/src/iface/interface/igmp.rs @@ -0,0 +1,275 @@ +use super::*; + +use crate::phy::{Device, PacketMeta}; +use crate::time::{Duration, Instant}; +use crate::wire::*; + +use core::result::Result; + +/// Error type for `join_multicast_group`, `leave_multicast_group`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MulticastError { + /// The hardware device transmit buffer is full. Try again later. + Exhausted, + /// The table of joined multicast groups is already full. + GroupTableFull, + /// IPv6 multicast is not yet supported. + Ipv6NotSupported, +} + +impl core::fmt::Display for MulticastError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match self { + MulticastError::Exhausted => write!(f, "Exhausted"), + MulticastError::GroupTableFull => write!(f, "GroupTableFull"), + MulticastError::Ipv6NotSupported => write!(f, "Ipv6NotSupported"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for MulticastError {} + +impl Interface { + /// Add an address to a list of subscribed multicast IP addresses. + /// + /// Returns `Ok(announce_sent)` if the address was added successfully, where `announce_sent` + /// indicates whether an initial immediate announcement has been sent. + pub fn join_multicast_group<D, T: Into<IpAddress>>( + &mut self, + device: &mut D, + addr: T, + timestamp: Instant, + ) -> Result<bool, MulticastError> + where + D: Device + ?Sized, + { + self.inner.now = timestamp; + + match addr.into() { + IpAddress::Ipv4(addr) => { + let is_not_new = self + .inner + .ipv4_multicast_groups + .insert(addr, ()) + .map_err(|_| MulticastError::GroupTableFull)? + .is_some(); + if is_not_new { + Ok(false) + } else if let Some(pkt) = self.inner.igmp_report_packet(IgmpVersion::Version2, addr) + { + // Send initial membership report + let tx_token = device + .transmit(timestamp) + .ok_or(MulticastError::Exhausted)?; + + // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery. + self.inner + .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter) + .unwrap(); + + Ok(true) + } else { + Ok(false) + } + } + // Multicast is not yet implemented for other address families + #[allow(unreachable_patterns)] + _ => Err(MulticastError::Ipv6NotSupported), + } + } + + /// Remove an address from the subscribed multicast IP addresses. + /// + /// Returns `Ok(leave_sent)` if the address was removed successfully, where `leave_sent` + /// indicates whether an immediate leave packet has been sent. + pub fn leave_multicast_group<D, T: Into<IpAddress>>( + &mut self, + device: &mut D, + addr: T, + timestamp: Instant, + ) -> Result<bool, MulticastError> + where + D: Device + ?Sized, + { + self.inner.now = timestamp; + + match addr.into() { + IpAddress::Ipv4(addr) => { + let was_not_present = self.inner.ipv4_multicast_groups.remove(&addr).is_none(); + if was_not_present { + Ok(false) + } else if let Some(pkt) = self.inner.igmp_leave_packet(addr) { + // Send group leave packet + let tx_token = device + .transmit(timestamp) + .ok_or(MulticastError::Exhausted)?; + + // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery. + self.inner + .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter) + .unwrap(); + + Ok(true) + } else { + Ok(false) + } + } + // Multicast is not yet implemented for other address families + #[allow(unreachable_patterns)] + _ => Err(MulticastError::Ipv6NotSupported), + } + } + + /// Check whether the interface listens to given destination multicast IP address. + pub fn has_multicast_group<T: Into<IpAddress>>(&self, addr: T) -> bool { + self.inner.has_multicast_group(addr) + } + + /// Depending on `igmp_report_state` and the therein contained + /// timeouts, send IGMP membership reports. + pub(crate) fn igmp_egress<D>(&mut self, device: &mut D) -> bool + where + D: Device + ?Sized, + { + match self.inner.igmp_report_state { + IgmpReportState::ToSpecificQuery { + version, + timeout, + group, + } if self.inner.now >= timeout => { + if let Some(pkt) = self.inner.igmp_report_packet(version, group) { + // Send initial membership report + if let Some(tx_token) = device.transmit(self.inner.now) { + // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery. + self.inner + .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter) + .unwrap(); + } else { + return false; + } + } + + self.inner.igmp_report_state = IgmpReportState::Inactive; + true + } + IgmpReportState::ToGeneralQuery { + version, + timeout, + interval, + next_index, + } if self.inner.now >= timeout => { + let addr = self + .inner + .ipv4_multicast_groups + .iter() + .nth(next_index) + .map(|(addr, ())| *addr); + + match addr { + Some(addr) => { + if let Some(pkt) = self.inner.igmp_report_packet(version, addr) { + // Send initial membership report + if let Some(tx_token) = device.transmit(self.inner.now) { + // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery. + self.inner + .dispatch_ip( + tx_token, + PacketMeta::default(), + pkt, + &mut self.fragmenter, + ) + .unwrap(); + } else { + return false; + } + } + + let next_timeout = (timeout + interval).max(self.inner.now); + self.inner.igmp_report_state = IgmpReportState::ToGeneralQuery { + version, + timeout: next_timeout, + interval, + next_index: next_index + 1, + }; + true + } + + None => { + self.inner.igmp_report_state = IgmpReportState::Inactive; + false + } + } + } + _ => false, + } + } +} + +impl InterfaceInner { + /// Host duties of the **IGMPv2** protocol. + /// + /// Sets up `igmp_report_state` for responding to IGMP general/specific membership queries. + /// Membership must not be reported immediately in order to avoid flooding the network + /// after a query is broadcasted by a router; this is not currently done. + pub(super) fn process_igmp<'frame>( + &mut self, + ipv4_repr: Ipv4Repr, + ip_payload: &'frame [u8], + ) -> Option<Packet<'frame>> { + let igmp_packet = check!(IgmpPacket::new_checked(ip_payload)); + let igmp_repr = check!(IgmpRepr::parse(&igmp_packet)); + + // FIXME: report membership after a delay + match igmp_repr { + IgmpRepr::MembershipQuery { + group_addr, + version, + max_resp_time, + } => { + // General query + if group_addr.is_unspecified() + && ipv4_repr.dst_addr == Ipv4Address::MULTICAST_ALL_SYSTEMS + { + // Are we member in any groups? + if self.ipv4_multicast_groups.iter().next().is_some() { + let interval = match version { + IgmpVersion::Version1 => Duration::from_millis(100), + IgmpVersion::Version2 => { + // No dependence on a random generator + // (see [#24](https://github.com/m-labs/smoltcp/issues/24)) + // but at least spread reports evenly across max_resp_time. + let intervals = self.ipv4_multicast_groups.len() as u32 + 1; + max_resp_time / intervals + } + }; + self.igmp_report_state = IgmpReportState::ToGeneralQuery { + version, + timeout: self.now + interval, + interval, + next_index: 0, + }; + } + } else { + // Group-specific query + if self.has_multicast_group(group_addr) && ipv4_repr.dst_addr == group_addr { + // Don't respond immediately + let timeout = max_resp_time / 4; + self.igmp_report_state = IgmpReportState::ToSpecificQuery { + version, + timeout: self.now + timeout, + group: group_addr, + }; + } + } + } + // Ignore membership reports + IgmpRepr::MembershipReport { .. } => (), + // Ignore hosts leaving groups + IgmpRepr::LeaveGroup { .. } => (), + } + + None + } +} |