|
@@ -0,0 +1,233 @@
|
|
|
+use super::{check, IgmpReportState, Interface, InterfaceInner, IpPacket};
|
|
|
+use crate::phy::Device;
|
|
|
+use crate::time::{Duration, Instant};
|
|
|
+use crate::wire::*;
|
|
|
+use crate::{Error, Result};
|
|
|
+
|
|
|
+impl<'a> Interface<'a> {
|
|
|
+ /// Add an address to a list of subscribed multicast IP addresses.
|
|
|
+ ///
|
|
|
+ /// Returns `Ok(announce_sent)` if the address was added successfully, where `annouce_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>
|
|
|
+ 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(|_| Error::Exhausted)?
|
|
|
+ .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(Error::Exhausted)?;
|
|
|
+ self.inner.dispatch_ip(tx_token, pkt, None)?;
|
|
|
+ Ok(true)
|
|
|
+ } else {
|
|
|
+ Ok(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Multicast is not yet implemented for other address families
|
|
|
+ #[allow(unreachable_patterns)]
|
|
|
+ _ => Err(Error::Unaddressable),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 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>
|
|
|
+ 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(Error::Exhausted)?;
|
|
|
+ self.inner.dispatch_ip(tx_token, pkt, None)?;
|
|
|
+ Ok(true)
|
|
|
+ } else {
|
|
|
+ Ok(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Multicast is not yet implemented for other address families
|
|
|
+ #[allow(unreachable_patterns)]
|
|
|
+ _ => Err(Error::Unaddressable),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 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) -> Result<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
|
|
|
+ let tx_token = device.transmit(self.inner.now).ok_or(Error::Exhausted)?;
|
|
|
+ self.inner.dispatch_ip(tx_token, pkt, None)?;
|
|
|
+ }
|
|
|
+
|
|
|
+ self.inner.igmp_report_state = IgmpReportState::Inactive;
|
|
|
+ Ok(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
|
|
|
+ let tx_token =
|
|
|
+ device.transmit(self.inner.now).ok_or(Error::Exhausted)?;
|
|
|
+ self.inner.dispatch_ip(tx_token, pkt, None)?;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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,
|
|
|
+ };
|
|
|
+ Ok(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ None => {
|
|
|
+ self.inner.igmp_report_state = IgmpReportState::Inactive;
|
|
|
+ Ok(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ _ => Ok(false),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<'a> InterfaceInner<'a> {
|
|
|
+ /// Check whether the interface listens to given destination multicast IP address.
|
|
|
+ ///
|
|
|
+ /// If built without feature `proto-igmp` this function will
|
|
|
+ /// always return `false`.
|
|
|
+ pub fn has_multicast_group<T: Into<IpAddress>>(&self, addr: T) -> bool {
|
|
|
+ match addr.into() {
|
|
|
+ IpAddress::Ipv4(key) => {
|
|
|
+ key == Ipv4Address::MULTICAST_ALL_SYSTEMS
|
|
|
+ || self.ipv4_multicast_groups.get(&key).is_some()
|
|
|
+ }
|
|
|
+ #[allow(unreachable_patterns)]
|
|
|
+ _ => false,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 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<IpPacket<'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
|
|
|
+ }
|
|
|
+}
|