Просмотр исходного кода

multicast: use single Cargo feature for both ipv4 and ipv6.

The multicast feature was somewhat broken before, there was no way to do IPv6-only multicast
since a lot of multicast code was gated behind `proto-igmp` which force-enables ipv4.

Solution:

- Added feature `multicast` which enables multicast for ipv4 and/or ipv6, whatever the user has enabled.
- Removed feature `proto-igmp`. IGMP support in `wire` is always built if ipv4 is enabled. This causes no bloat if unused, and the increase in compile time is negligible. `iface` only uses it if the `multicast` feature is enabled.
Dario Nieuwenhuis 7 месяцев назад
Родитель
Сommit
07f09911bc

+ 5 - 4
Cargo.toml

@@ -50,7 +50,6 @@ defmt = ["dep:defmt", "heapless/defmt-03"]
 
 "proto-ipv4" = []
 "proto-ipv4-fragmentation" = ["proto-ipv4", "_proto-fragmentation"]
-"proto-igmp" = ["proto-ipv4"]
 "proto-dhcpv4" = ["proto-ipv4"]
 "proto-ipv6" = []
 "proto-ipv6-hbh" = ["proto-ipv6"]
@@ -64,6 +63,8 @@ defmt = ["dep:defmt", "heapless/defmt-03"]
 "proto-ipsec-ah" = []
 "proto-ipsec-esp" = []
 
+"multicast" = []
+
 "socket" = []
 "socket-raw" = ["socket"]
 "socket-udp" = ["socket"]
@@ -96,10 +97,10 @@ default = [
   "std", "log", # needed for `cargo test --no-default-features --features default` :/
   "medium-ethernet", "medium-ip", "medium-ieee802154",
   "phy-raw_socket", "phy-tuntap_interface",
-  "proto-ipv4", "proto-igmp", "proto-dhcpv4", "proto-ipv6", "proto-dns",
+  "proto-ipv4",  "proto-dhcpv4", "proto-ipv6", "proto-dns",
   "proto-ipv4-fragmentation", "proto-sixlowpan-fragmentation",
   "socket-raw", "socket-icmp", "socket-udp", "socket-tcp", "socket-dhcpv4", "socket-dns", "socket-mdns",
-  "packetmeta-id", "async"
+  "packetmeta-id", "async", "multicast"
 ]
 
 # Private features
@@ -301,7 +302,7 @@ required-features = ["std", "log", "medium-ethernet", "proto-ipv4", "socket-tcp"
 
 [[example]]
 name = "multicast"
-required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "proto-igmp", "socket-udp"]
+required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "multicast", "socket-udp"]
 
 [[example]]
 name = "multicast6"

+ 6 - 6
ci.sh

@@ -18,10 +18,10 @@ FEATURES_TEST=(
     "std,medium-ethernet,phy-raw_socket,proto-ipv6,socket-udp,socket-dns"
     "std,medium-ethernet,phy-tuntap_interface,proto-ipv6,socket-udp"
     "std,medium-ethernet,proto-ipv4,proto-ipv4-fragmentation,socket-raw,socket-dns"
-    "std,medium-ethernet,proto-ipv4,proto-igmp,socket-raw,socket-dns"
+    "std,medium-ethernet,proto-ipv4,multicast,socket-raw,socket-dns"
     "std,medium-ethernet,proto-ipv4,socket-udp,socket-tcp,socket-dns"
     "std,medium-ethernet,proto-ipv4,proto-dhcpv4,socket-udp"
-    "std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv6,proto-igmp,proto-rpl,socket-udp,socket-dns"
+    "std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv6,multicast,proto-rpl,socket-udp,socket-dns"
     "std,medium-ethernet,proto-ipv6,socket-tcp"
     "std,medium-ethernet,medium-ip,proto-ipv4,socket-icmp,socket-tcp"
     "std,medium-ip,proto-ipv6,socket-icmp,socket-tcp"
@@ -29,7 +29,7 @@ FEATURES_TEST=(
     "std,medium-ieee802154,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp"
     "std,medium-ieee802154,proto-rpl,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp"
     "std,medium-ip,proto-ipv4,proto-ipv6,socket-tcp,socket-udp"
-    "std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv4,proto-ipv6,proto-igmp,proto-rpl,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
+    "std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv4,proto-ipv6,multicast,proto-rpl,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
     "std,medium-ieee802154,medium-ip,proto-ipv4,socket-raw"
     "std,medium-ethernet,proto-ipv4,proto-ipsec,socket-raw"
 )
@@ -39,9 +39,9 @@ FEATURES_TEST_NIGHTLY=(
 )
 
 FEATURES_CHECK=(
-    "medium-ip,medium-ethernet,medium-ieee802154,proto-ipv6,proto-ipv6,proto-igmp,proto-dhcpv4,proto-ipsec,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
-    "defmt,medium-ip,medium-ethernet,proto-ipv6,proto-ipv6,proto-igmp,proto-dhcpv4,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
-    "defmt,alloc,medium-ip,medium-ethernet,proto-ipv6,proto-ipv6,proto-igmp,proto-dhcpv4,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
+    "medium-ip,medium-ethernet,medium-ieee802154,proto-ipv6,proto-ipv6,multicast,proto-dhcpv4,proto-ipsec,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
+    "defmt,medium-ip,medium-ethernet,proto-ipv6,proto-ipv6,multicast,proto-dhcpv4,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
+    "defmt,alloc,medium-ip,medium-ethernet,proto-ipv6,proto-ipv6,multicast,proto-dhcpv4,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
 )
 
 test() {

+ 1 - 44
src/iface/interface/ipv4.rs

@@ -209,7 +209,7 @@ impl InterfaceInner {
         match ipv4_repr.next_header {
             IpProtocol::Icmp => self.process_icmpv4(sockets, ipv4_repr, ip_payload),
 
-            #[cfg(feature = "proto-igmp")]
+            #[cfg(feature = "multicast")]
             IpProtocol::Igmp => self.process_igmp(ipv4_repr, ip_payload),
 
             #[cfg(any(feature = "socket-udp", feature = "socket-dns"))]
@@ -464,47 +464,4 @@ impl InterfaceInner {
             frag.ipv4.frag_offset += payload_len as u16;
         })
     }
-
-    #[cfg(feature = "proto-igmp")]
-    pub(super) fn igmp_report_packet<'any>(
-        &self,
-        version: IgmpVersion,
-        group_addr: Ipv4Address,
-    ) -> Option<Packet<'any>> {
-        let iface_addr = self.ipv4_addr()?;
-        let igmp_repr = IgmpRepr::MembershipReport {
-            group_addr,
-            version,
-        };
-        let pkt = Packet::new_ipv4(
-            Ipv4Repr {
-                src_addr: iface_addr,
-                // Send to the group being reported
-                dst_addr: group_addr,
-                next_header: IpProtocol::Igmp,
-                payload_len: igmp_repr.buffer_len(),
-                hop_limit: 1,
-                // [#183](https://github.com/m-labs/smoltcp/issues/183).
-            },
-            IpPayload::Igmp(igmp_repr),
-        );
-        Some(pkt)
-    }
-
-    #[cfg(feature = "proto-igmp")]
-    pub(super) fn igmp_leave_packet<'any>(&self, group_addr: Ipv4Address) -> Option<Packet<'any>> {
-        self.ipv4_addr().map(|iface_addr| {
-            let igmp_repr = IgmpRepr::LeaveGroup { group_addr };
-            Packet::new_ipv4(
-                Ipv4Repr {
-                    src_addr: iface_addr,
-                    dst_addr: Ipv4Address::MULTICAST_ALL_ROUTERS,
-                    next_header: IpProtocol::Igmp,
-                    payload_len: igmp_repr.buffer_len(),
-                    hop_limit: 1,
-                },
-                IpPayload::Igmp(igmp_repr),
-            )
-        })
-    }
 }

+ 20 - 56
src/iface/interface/mod.rs

@@ -17,20 +17,17 @@ mod ipv6;
 #[cfg(feature = "proto-sixlowpan")]
 mod sixlowpan;
 
-#[cfg(feature = "proto-igmp")]
-mod igmp;
+#[cfg(feature = "multicast")]
+pub(crate) mod multicast;
 #[cfg(feature = "socket-tcp")]
 mod tcp;
 #[cfg(any(feature = "socket-udp", feature = "socket-dns"))]
 mod udp;
 
-#[cfg(feature = "proto-igmp")]
-pub use igmp::MulticastError;
-
 use super::packet::*;
 
 use core::result::Result;
-use heapless::{LinearMap, Vec};
+use heapless::Vec;
 
 #[cfg(feature = "_proto-fragmentation")]
 use super::fragmentation::FragKey;
@@ -41,10 +38,7 @@ use super::fragmentation::{Fragmenter, FragmentsBuffer};
 #[cfg(any(feature = "medium-ethernet", feature = "medium-ieee802154"))]
 use super::neighbor::{Answer as NeighborAnswer, Cache as NeighborCache};
 use super::socket_set::SocketSet;
-use crate::config::{
-    IFACE_MAX_ADDR_COUNT, IFACE_MAX_MULTICAST_GROUP_COUNT,
-    IFACE_MAX_SIXLOWPAN_ADDRESS_CONTEXT_COUNT,
-};
+use crate::config::{IFACE_MAX_ADDR_COUNT, IFACE_MAX_SIXLOWPAN_ADDRESS_CONTEXT_COUNT};
 use crate::iface::Routes;
 use crate::phy::PacketMeta;
 use crate::phy::{ChecksumCapabilities, Device, DeviceCapabilities, Medium, RxToken, TxToken};
@@ -82,16 +76,6 @@ pub struct Interface {
     fragmenter: Fragmenter,
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum MulticastGroupState {
-    /// Joining group, we have to send the join packet.
-    Joining,
-    /// We've already sent the join packet, we have nothing to do.
-    Joined,
-    /// We want to leave the group, we have to send a leave packet.
-    Leaving,
-}
-
 /// The device independent part of an Ethernet network interface.
 ///
 /// Separating the device from the data required for processing and dispatching makes
@@ -121,11 +105,8 @@ pub struct InterfaceInner {
     ip_addrs: Vec<IpCidr, IFACE_MAX_ADDR_COUNT>,
     any_ip: bool,
     routes: Routes,
-    #[cfg(any(feature = "proto-igmp", feature = "proto-ipv6"))]
-    multicast_groups: LinearMap<IpAddress, MulticastGroupState, IFACE_MAX_MULTICAST_GROUP_COUNT>,
-    /// When to report for (all or) the next multicast group membership via IGMP
-    #[cfg(feature = "proto-igmp")]
-    igmp_report_state: IgmpReportState,
+    #[cfg(feature = "multicast")]
+    multicast: multicast::State,
 }
 
 /// Configuration structure used for creating a network interface.
@@ -234,10 +215,8 @@ impl Interface {
                 routes: Routes::new(),
                 #[cfg(any(feature = "medium-ethernet", feature = "medium-ieee802154"))]
                 neighbor_cache: NeighborCache::new(),
-                #[cfg(any(feature = "proto-igmp", feature = "proto-ipv6"))]
-                multicast_groups: LinearMap::new(),
-                #[cfg(feature = "proto-igmp")]
-                igmp_report_state: IgmpReportState::Inactive,
+                #[cfg(feature = "multicast")]
+                multicast: multicast::State::new(),
                 #[cfg(feature = "medium-ieee802154")]
                 sequence_no,
                 #[cfg(feature = "medium-ieee802154")]
@@ -445,10 +424,8 @@ impl Interface {
         let mut readiness_may_have_changed = self.socket_ingress(device, sockets);
         readiness_may_have_changed |= self.socket_egress(device, sockets);
 
-        #[cfg(feature = "proto-igmp")]
-        {
-            readiness_may_have_changed |= self.multicast_egress(device);
-        }
+        #[cfg(feature = "multicast")]
+        self.multicast_egress(device);
 
         readiness_may_have_changed
     }
@@ -755,36 +732,23 @@ impl InterfaceInner {
     }
 
     /// Check whether the interface listens to given destination multicast IP address.
-    ///
-    /// If built without feature `proto-igmp` this function will
-    /// always return `false` when using IPv4.
     fn has_multicast_group<T: Into<IpAddress>>(&self, addr: T) -> bool {
-        /// Return false if we don't have the multicast group,
-        /// or we're leaving it.
-        fn wanted_state(x: Option<&MulticastGroupState>) -> bool {
-            match x {
-                None => false,
-                Some(MulticastGroupState::Joining) => true,
-                Some(MulticastGroupState::Joined) => true,
-                Some(MulticastGroupState::Leaving) => false,
-            }
+        let addr = addr.into();
+
+        #[cfg(feature = "multicast")]
+        if self.multicast.has_multicast_group(addr) {
+            return true;
         }
 
-        let addr = addr.into();
         match addr {
-            #[cfg(feature = "proto-igmp")]
-            IpAddress::Ipv4(key) => {
-                key == Ipv4Address::MULTICAST_ALL_SYSTEMS
-                    || wanted_state(self.multicast_groups.get(&addr))
-            }
+            #[cfg(feature = "proto-ipv4")]
+            IpAddress::Ipv4(key) => key == Ipv4Address::MULTICAST_ALL_SYSTEMS,
+            #[cfg(feature = "proto-rpl")]
+            IpAddress::Ipv6(Ipv6Address::LINK_LOCAL_ALL_RPL_NODES) => true,
             #[cfg(feature = "proto-ipv6")]
             IpAddress::Ipv6(key) => {
-                key == Ipv6Address::LINK_LOCAL_ALL_NODES
-                    || self.has_solicited_node(key)
-                    || wanted_state(self.multicast_groups.get(&addr))
+                key == Ipv6Address::LINK_LOCAL_ALL_NODES || self.has_solicited_node(key)
             }
-            #[cfg(feature = "proto-rpl")]
-            IpAddress::Ipv6(Ipv6Address::LINK_LOCAL_ALL_RPL_NODES) => true,
             #[allow(unreachable_patterns)]
             _ => false,
         }

+ 154 - 45
src/iface/interface/igmp.rs → src/iface/interface/multicast.rs

@@ -1,4 +1,12 @@
-use super::*;
+use core::result::Result;
+use heapless::LinearMap;
+
+#[cfg(feature = "proto-ipv4")]
+use super::{check, IpPayload, Packet};
+use super::{Interface, InterfaceInner};
+use crate::config::IFACE_MAX_MULTICAST_GROUP_COUNT;
+use crate::phy::{Device, PacketMeta};
+use crate::wire::*;
 
 /// Error type for `join_multicast_group`, `leave_multicast_group`.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -10,6 +18,60 @@ pub enum MulticastError {
     Unaddressable,
 }
 
+#[cfg(feature = "proto-ipv4")]
+pub(crate) enum IgmpReportState {
+    Inactive,
+    ToGeneralQuery {
+        version: IgmpVersion,
+        timeout: crate::time::Instant,
+        interval: crate::time::Duration,
+        next_index: usize,
+    },
+    ToSpecificQuery {
+        version: IgmpVersion,
+        timeout: crate::time::Instant,
+        group: Ipv4Address,
+    },
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum GroupState {
+    /// Joining group, we have to send the join packet.
+    Joining,
+    /// We've already sent the join packet, we have nothing to do.
+    Joined,
+    /// We want to leave the group, we have to send a leave packet.
+    Leaving,
+}
+
+pub(crate) struct State {
+    groups: LinearMap<IpAddress, GroupState, IFACE_MAX_MULTICAST_GROUP_COUNT>,
+    /// When to report for (all or) the next multicast group membership via IGMP
+    #[cfg(feature = "proto-ipv4")]
+    igmp_report_state: IgmpReportState,
+}
+
+impl State {
+    pub(crate) fn new() -> Self {
+        Self {
+            groups: LinearMap::new(),
+            #[cfg(feature = "proto-ipv4")]
+            igmp_report_state: IgmpReportState::Inactive,
+        }
+    }
+
+    pub(crate) fn has_multicast_group<T: Into<IpAddress>>(&self, addr: T) -> bool {
+        // Return false if we don't have the multicast group,
+        // or we're leaving it.
+        match self.groups.get(&addr.into()) {
+            None => false,
+            Some(GroupState::Joining) => true,
+            Some(GroupState::Joined) => true,
+            Some(GroupState::Leaving) => false,
+        }
+    }
+}
+
 impl core::fmt::Display for MulticastError {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
         match self {
@@ -33,16 +95,17 @@ impl Interface {
             return Err(MulticastError::Unaddressable);
         }
 
-        if let Some(state) = self.inner.multicast_groups.get_mut(&addr) {
+        if let Some(state) = self.inner.multicast.groups.get_mut(&addr) {
             *state = match state {
-                MulticastGroupState::Joining => MulticastGroupState::Joining,
-                MulticastGroupState::Joined => MulticastGroupState::Joined,
-                MulticastGroupState::Leaving => MulticastGroupState::Joined,
+                GroupState::Joining => GroupState::Joining,
+                GroupState::Joined => GroupState::Joined,
+                GroupState::Leaving => GroupState::Joined,
             };
         } else {
             self.inner
-                .multicast_groups
-                .insert(addr, MulticastGroupState::Joining)
+                .multicast
+                .groups
+                .insert(addr, GroupState::Joining)
                 .map_err(|_| MulticastError::GroupTableFull)?;
         }
         Ok(())
@@ -58,15 +121,15 @@ impl Interface {
             return Err(MulticastError::Unaddressable);
         }
 
-        if let Some(state) = self.inner.multicast_groups.get_mut(&addr) {
+        if let Some(state) = self.inner.multicast.groups.get_mut(&addr) {
             let delete;
             (*state, delete) = match state {
-                MulticastGroupState::Joining => (MulticastGroupState::Joined, true),
-                MulticastGroupState::Joined => (MulticastGroupState::Leaving, false),
-                MulticastGroupState::Leaving => (MulticastGroupState::Leaving, false),
+                GroupState::Joining => (GroupState::Joined, true),
+                GroupState::Joined => (GroupState::Leaving, false),
+                GroupState::Leaving => (GroupState::Leaving, false),
             };
             if delete {
-                self.inner.multicast_groups.remove(&addr);
+                self.inner.multicast.groups.remove(&addr);
             }
         }
         Ok(())
@@ -82,18 +145,20 @@ impl Interface {
     /// - Send join/leave packets according to the multicast group state.
     /// - Depending on `igmp_report_state` and the therein contained
     ///   timeouts, send IGMP membership reports.
-    pub(crate) fn multicast_egress<D>(&mut self, device: &mut D) -> bool
+    pub(crate) fn multicast_egress<D>(&mut self, device: &mut D)
     where
         D: Device + ?Sized,
     {
         // Process multicast joins.
         while let Some((&addr, _)) = self
             .inner
-            .multicast_groups
+            .multicast
+            .groups
             .iter()
-            .find(|(_, &state)| state == MulticastGroupState::Joining)
+            .find(|(_, &state)| state == GroupState::Joining)
         {
             match addr {
+                #[cfg(feature = "proto-ipv4")]
                 IpAddress::Ipv4(addr) => {
                     if let Some(pkt) = self.inner.igmp_report_packet(IgmpVersion::Version2, addr) {
                         let Some(tx_token) = device.transmit(self.inner.now) else {
@@ -126,19 +191,22 @@ impl Interface {
 
             // NOTE(unwrap): this is always replacing an existing entry, so it can't fail due to the map being full.
             self.inner
-                .multicast_groups
-                .insert(addr, MulticastGroupState::Joined)
+                .multicast
+                .groups
+                .insert(addr, GroupState::Joined)
                 .unwrap();
         }
 
         // Process multicast leaves.
         while let Some((&addr, _)) = self
             .inner
-            .multicast_groups
+            .multicast
+            .groups
             .iter()
-            .find(|(_, &state)| state == MulticastGroupState::Leaving)
+            .find(|(_, &state)| state == GroupState::Leaving)
         {
             match addr {
+                #[cfg(feature = "proto-ipv4")]
                 IpAddress::Ipv4(addr) => {
                     if let Some(pkt) = self.inner.igmp_leave_packet(addr) {
                         let Some(tx_token) = device.transmit(self.inner.now) else {
@@ -169,10 +237,11 @@ impl Interface {
                 }
             }
 
-            self.inner.multicast_groups.remove(&addr);
+            self.inner.multicast.groups.remove(&addr);
         }
 
-        match self.inner.igmp_report_state {
+        #[cfg(feature = "proto-ipv4")]
+        match self.inner.multicast.igmp_report_state {
             IgmpReportState::ToSpecificQuery {
                 version,
                 timeout,
@@ -185,13 +254,9 @@ impl Interface {
                         self.inner
                             .dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter)
                             .unwrap();
-                    } else {
-                        return false;
+                        self.inner.multicast.igmp_report_state = IgmpReportState::Inactive;
                     }
                 }
-
-                self.inner.igmp_report_state = IgmpReportState::Inactive;
-                true
             }
             IgmpReportState::ToGeneralQuery {
                 version,
@@ -201,7 +266,8 @@ impl Interface {
             } if self.inner.now >= timeout => {
                 let addr = self
                     .inner
-                    .multicast_groups
+                    .multicast
+                    .groups
                     .iter()
                     .filter_map(|(addr, _)| match addr {
                         IpAddress::Ipv4(addr) => Some(*addr),
@@ -224,28 +290,24 @@ impl Interface {
                                         &mut self.fragmenter,
                                     )
                                     .unwrap();
-                            } else {
-                                return false;
+
+                                let next_timeout = (timeout + interval).max(self.inner.now);
+                                self.inner.multicast.igmp_report_state =
+                                    IgmpReportState::ToGeneralQuery {
+                                        version,
+                                        timeout: next_timeout,
+                                        interval,
+                                        next_index: next_index + 1,
+                                    };
                             }
                         }
-
-                        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
+                        self.inner.multicast.igmp_report_state = IgmpReportState::Inactive;
                     }
                 }
             }
-            _ => false,
+            _ => {}
         }
     }
 }
@@ -256,11 +318,14 @@ impl InterfaceInner {
     /// 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.
+    #[cfg(feature = "proto-ipv4")]
     pub(super) fn process_igmp<'frame>(
         &mut self,
         ipv4_repr: Ipv4Repr,
         ip_payload: &'frame [u8],
     ) -> Option<Packet<'frame>> {
+        use crate::time::Duration;
+
         let igmp_packet = check!(IgmpPacket::new_checked(ip_payload));
         let igmp_repr = check!(IgmpRepr::parse(&igmp_packet));
 
@@ -276,7 +341,8 @@ impl InterfaceInner {
                     && ipv4_repr.dst_addr == Ipv4Address::MULTICAST_ALL_SYSTEMS
                 {
                     let ipv4_multicast_group_count = self
-                        .multicast_groups
+                        .multicast
+                        .groups
                         .keys()
                         .filter(|a| matches!(a, IpAddress::Ipv4(_)))
                         .count();
@@ -293,7 +359,7 @@ impl InterfaceInner {
                                 max_resp_time / intervals
                             }
                         };
-                        self.igmp_report_state = IgmpReportState::ToGeneralQuery {
+                        self.multicast.igmp_report_state = IgmpReportState::ToGeneralQuery {
                             version,
                             timeout: self.now + interval,
                             interval,
@@ -305,7 +371,7 @@ impl InterfaceInner {
                     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 {
+                        self.multicast.igmp_report_state = IgmpReportState::ToSpecificQuery {
                             version,
                             timeout: self.now + timeout,
                             group: group_addr,
@@ -321,4 +387,47 @@ impl InterfaceInner {
 
         None
     }
+
+    #[cfg(feature = "proto-ipv4")]
+    fn igmp_report_packet<'any>(
+        &self,
+        version: IgmpVersion,
+        group_addr: Ipv4Address,
+    ) -> Option<Packet<'any>> {
+        let iface_addr = self.ipv4_addr()?;
+        let igmp_repr = IgmpRepr::MembershipReport {
+            group_addr,
+            version,
+        };
+        let pkt = Packet::new_ipv4(
+            Ipv4Repr {
+                src_addr: iface_addr,
+                // Send to the group being reported
+                dst_addr: group_addr,
+                next_header: IpProtocol::Igmp,
+                payload_len: igmp_repr.buffer_len(),
+                hop_limit: 1,
+                // [#183](https://github.com/m-labs/smoltcp/issues/183).
+            },
+            IpPayload::Igmp(igmp_repr),
+        );
+        Some(pkt)
+    }
+
+    #[cfg(feature = "proto-ipv4")]
+    fn igmp_leave_packet<'any>(&self, group_addr: Ipv4Address) -> Option<Packet<'any>> {
+        self.ipv4_addr().map(|iface_addr| {
+            let igmp_repr = IgmpRepr::LeaveGroup { group_addr };
+            Packet::new_ipv4(
+                Ipv4Repr {
+                    src_addr: iface_addr,
+                    dst_addr: Ipv4Address::MULTICAST_ALL_ROUTERS,
+                    next_header: IpProtocol::Igmp,
+                    payload_len: igmp_repr.buffer_len(),
+                    hop_limit: 1,
+                },
+                IpPayload::Igmp(igmp_repr),
+            )
+        })
+    }
 }

+ 2 - 2
src/iface/interface/tests/ipv4.rs

@@ -659,9 +659,9 @@ fn test_icmpv4_socket(#[case] medium: Medium) {
 
 #[rstest]
 #[case(Medium::Ip)]
-#[cfg(all(feature = "proto-igmp", feature = "medium-ip"))]
+#[cfg(all(feature = "multicast", feature = "medium-ip"))]
 #[case(Medium::Ethernet)]
-#[cfg(all(feature = "proto-igmp", feature = "medium-ethernet"))]
+#[cfg(all(feature = "multicast", feature = "medium-ethernet"))]
 fn test_handle_igmp(#[case] medium: Medium) {
     fn recv_igmp(
         device: &mut crate::tests::TestingDevice,

+ 2 - 2
src/iface/interface/tests/mod.rs

@@ -5,7 +5,7 @@ mod ipv6;
 #[cfg(feature = "proto-sixlowpan")]
 mod sixlowpan;
 
-#[cfg(feature = "proto-igmp")]
+#[allow(unused)]
 use std::vec::Vec;
 
 use crate::tests::setup;
@@ -27,7 +27,7 @@ fn fill_slice(s: &mut [u8], val: u8) {
     }
 }
 
-#[cfg(feature = "proto-igmp")]
+#[allow(unused)]
 fn recv_all(device: &mut crate::tests::TestingDevice, timestamp: Instant) -> Vec<Vec<u8>> {
     let mut pkts = Vec::new();
     while let Some((rx, _tx)) = device.receive(timestamp) {

+ 2 - 2
src/iface/mod.rs

@@ -16,8 +16,8 @@ mod socket_set;
 
 mod packet;
 
-#[cfg(feature = "proto-igmp")]
-pub use self::interface::MulticastError;
+#[cfg(feature = "multicast")]
+pub use self::interface::multicast::MulticastError;
 pub use self::interface::{Config, Interface, InterfaceInner as Context};
 
 pub use self::route::{Route, RouteTableFull, Routes};

+ 3 - 19
src/iface/packet.rs

@@ -81,7 +81,7 @@ impl<'p> Packet<'p> {
             IpPayload::Icmpv4(icmpv4_repr) => {
                 icmpv4_repr.emit(&mut Icmpv4Packet::new_unchecked(payload), &caps.checksum)
             }
-            #[cfg(feature = "proto-igmp")]
+            #[cfg(all(feature = "proto-ipv4", feature = "multicast"))]
             IpPayload::Igmp(igmp_repr) => igmp_repr.emit(&mut IgmpPacket::new_unchecked(payload)),
             #[cfg(feature = "proto-ipv6")]
             IpPayload::Icmpv6(icmpv6_repr) => {
@@ -207,7 +207,7 @@ pub(crate) struct PacketV6<'p> {
 pub(crate) enum IpPayload<'p> {
     #[cfg(feature = "proto-ipv4")]
     Icmpv4(Icmpv4Repr<'p>),
-    #[cfg(feature = "proto-igmp")]
+    #[cfg(all(feature = "proto-ipv4", feature = "multicast"))]
     Igmp(IgmpRepr),
     #[cfg(feature = "proto-ipv6")]
     Icmpv6(Icmpv6Repr<'p>),
@@ -235,7 +235,7 @@ impl<'p> IpPayload<'p> {
             Self::Icmpv6(_) => SixlowpanNextHeader::Uncompressed(IpProtocol::Icmpv6),
             #[cfg(feature = "proto-ipv6")]
             Self::HopByHopIcmpv6(_, _) => unreachable!(),
-            #[cfg(feature = "proto-igmp")]
+            #[cfg(all(feature = "proto-ipv4", feature = "multicast"))]
             Self::Igmp(_) => unreachable!(),
             #[cfg(feature = "socket-tcp")]
             Self::Tcp(_) => SixlowpanNextHeader::Uncompressed(IpProtocol::Tcp),
@@ -259,19 +259,3 @@ pub(crate) fn icmp_reply_payload_len(len: usize, mtu: usize, header_len: usize)
     // <min mtu> - IP Header Size * 2 - ICMPv4 DstUnreachable hdr size
     len.min(mtu - header_len * 2 - 8)
 }
-
-#[cfg(feature = "proto-igmp")]
-pub(crate) enum IgmpReportState {
-    Inactive,
-    ToGeneralQuery {
-        version: IgmpVersion,
-        timeout: crate::time::Instant,
-        interval: crate::time::Duration,
-        next_index: usize,
-    },
-    ToSpecificQuery {
-        version: IgmpVersion,
-        timeout: crate::time::Instant,
-        group: Ipv4Address,
-    },
-}

+ 2 - 2
src/wire/mod.rs

@@ -93,7 +93,7 @@ mod icmpv4;
 mod icmpv6;
 #[cfg(feature = "medium-ieee802154")]
 pub mod ieee802154;
-#[cfg(feature = "proto-igmp")]
+#[cfg(feature = "proto-ipv4")]
 mod igmp;
 pub(crate) mod ip;
 #[cfg(feature = "proto-ipv4")]
@@ -225,7 +225,7 @@ pub use self::icmpv4::{
     TimeExceeded as Icmpv4TimeExceeded,
 };
 
-#[cfg(feature = "proto-igmp")]
+#[cfg(feature = "proto-ipv4")]
 pub use self::igmp::{IgmpVersion, Packet as IgmpPacket, Repr as IgmpRepr};
 
 #[cfg(feature = "proto-ipv6")]