blob: 14856ca07e805dda21ef056510aa0c4ba0aad77f [file] [log] [blame]
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
}
}