Browse Source

Implement IGMPv1/v2 processing.

Closes: #178
Approved by: whitequark
Astro 7 năm trước cách đây
mục cha
commit
58a54730db
9 tập tin đã thay đổi với 593 bổ sung35 xóa
  1. 2 2
      .travis.yml
  2. 7 2
      Cargo.toml
  3. 10 0
      README.md
  4. 113 0
      examples/multicast.rs
  5. 6 0
      examples/utils.rs
  6. 441 23
      src/iface/ethernet.rs
  7. 4 4
      src/socket/raw.rs
  8. 8 2
      src/wire/ipv4.rs
  9. 2 2
      src/wire/mod.rs

+ 2 - 2
.travis.yml

@@ -16,7 +16,7 @@ matrix:
     - rust: nightly
       env: FEATURES='std phy-tap_interface proto-ipv6 socket-udp' MODE='test'
     - rust: nightly
-      env: FEATURES='std proto-ipv4 socket-raw' MODE='test'
+      env: FEATURES='std proto-ipv4 proto-igmp socket-raw' MODE='test'
     - rust: nightly
       env: FEATURES='std proto-ipv6 socket-udp' MODE='test'
     - rust: nightly
@@ -33,7 +33,7 @@ matrix:
       env: FEATURES='proto-ipv4 proto-ipv6 socket-raw socket-udp socket-tcp socket-icmp alloc'
         MODE='test'
     - rust: nightly
-      env: FEATURES='proto-ipv4 proto-ipv6 socket-raw socket-udp socket-tcp socket-icmp'
+      env: FEATURES='proto-ipv4 proto-ipv6 proto-igmp socket-raw socket-udp socket-tcp socket-icmp'
         MODE='build'
     - rust: nightly
       env: MODE='fuzz run' ARGS='packet_parser -- -max_len=1536 -max_total_time=30'

+ 7 - 2
Cargo.toml

@@ -34,6 +34,7 @@ verbose = []
 "phy-raw_socket" = ["std", "libc"]
 "phy-tap_interface" = ["std", "libc"]
 "proto-ipv4" = []
+"proto-igmp" = ["proto-ipv4"]
 "proto-ipv6" = []
 "socket-raw" = []
 "socket-udp" = []
@@ -43,7 +44,7 @@ verbose = []
 default = [
   "std", "log", # needed for `cargo test --no-default-features --features default` :/
   "phy-raw_socket", "phy-tap_interface",
-  "proto-ipv4", "proto-ipv6",
+  "proto-ipv4", "proto-igmp", "proto-ipv6",
   "socket-raw", "socket-icmp", "socket-udp", "socket-tcp"
 ]
 
@@ -76,9 +77,13 @@ required-features = ["std", "phy-tap_interface", "proto-ipv4", "socket-tcp", "so
 name = "loopback"
 required-features = ["log", "proto-ipv4", "socket-tcp"]
 
+[[example]]
+name = "multicast"
+required-features = ["std", "phy-tap_interface", "proto-ipv4", "proto-igmp", "socket-udp"]
+
 [[example]]
 name = "benchmark"
-required-features = ["std", "phy-tap_interface", "proto-ipv4", "socket-tcp"]
+required-features = ["std", "phy-tap_interface", "proto-ipv4", "socket-raw", "socket-udp"]
 
 [profile.release]
 debug = 2

+ 10 - 0
README.md

@@ -47,6 +47,16 @@ The only supported medium is Ethernet.
   * IPv6 default gateway is **not** supported.
   * IPv6 extension headers are **not** supported.
 
+### IP multicast
+
+#### IGMP
+
+The IGMPv1 and IGMPv2 protocols are supported, and IPv4 multicast is available.
+
+  * Membership reports are sent in response to membership queries at
+    equal intervals equal to the maximum response time divided by the
+    number of groups to be reported.
+
 ### ICMP layer
 
 #### ICMPv4

+ 113 - 0
examples/multicast.rs

@@ -0,0 +1,113 @@
+#[macro_use]
+extern crate log;
+extern crate env_logger;
+extern crate getopts;
+extern crate smoltcp;
+extern crate byteorder;
+
+mod utils;
+
+use std::collections::BTreeMap;
+use std::os::unix::io::AsRawFd;
+use smoltcp::phy::wait as phy_wait;
+use smoltcp::wire::{EthernetAddress, IpVersion, IpProtocol, IpAddress, IpCidr, Ipv4Address,
+                    Ipv4Packet, IgmpPacket, IgmpRepr};
+use smoltcp::iface::{NeighborCache, EthernetInterfaceBuilder};
+use smoltcp::socket::{SocketSet,
+                      RawSocket, RawSocketBuffer, RawPacketMetadata,
+                      UdpSocket, UdpSocketBuffer, UdpPacketMetadata};
+use smoltcp::time::Instant;
+
+const MDNS_PORT: u16 = 5353;
+const MDNS_GROUP: [u8; 4] = [224, 0, 0, 251];
+
+fn main() {
+    utils::setup_logging("warn");
+
+    let (mut opts, mut free) = utils::create_options();
+    utils::add_tap_options(&mut opts, &mut free);
+    utils::add_middleware_options(&mut opts, &mut free);
+
+    let mut matches = utils::parse_options(&opts, free);
+    let device = utils::parse_tap_options(&mut matches);
+    let fd = device.as_raw_fd();
+    let device = utils::parse_middleware_options(&mut matches,
+                                                 device,
+                                                 /*loopback=*/
+                                                 false);
+    let neighbor_cache = NeighborCache::new(BTreeMap::new());
+
+    let local_addr = Ipv4Address::new(192, 168, 69, 2);
+
+    let ethernet_addr = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]);
+    let ip_addr = IpCidr::new(IpAddress::from(local_addr), 24);
+    let mut ipv4_multicast_storage = [None; 1];
+    let mut iface = EthernetInterfaceBuilder::new(device)
+            .ethernet_addr(ethernet_addr)
+            .neighbor_cache(neighbor_cache)
+            .ip_addrs([ip_addr])
+            .ipv4_multicast_groups(&mut ipv4_multicast_storage[..])
+            .finalize();
+
+    let now = Instant::now();
+    // Join a multicast group to receive mDNS traffic
+    iface.join_multicast_group(Ipv4Address::from_bytes(&MDNS_GROUP), now).unwrap();
+
+    let mut sockets = SocketSet::new(vec![]);
+
+    // Must fit at least one IGMP packet
+    let raw_rx_buffer = RawSocketBuffer::new(vec![RawPacketMetadata::EMPTY; 2], vec![0; 512]);
+    // Will not send IGMP
+    let raw_tx_buffer = RawSocketBuffer::new(vec![], vec![]);
+    let raw_socket = RawSocket::new(
+        IpVersion::Ipv4, IpProtocol::Igmp,
+        raw_rx_buffer, raw_tx_buffer
+    );
+    let raw_handle = sockets.add(raw_socket);
+
+    // Must fit mDNS payload of at least one packet
+    let udp_rx_buffer = UdpSocketBuffer::new(vec![UdpPacketMetadata::EMPTY; 4], vec![0; 1024]);
+    // Will not send mDNS
+    let udp_tx_buffer = UdpSocketBuffer::new(vec![UdpPacketMetadata::EMPTY], vec![0; 0]);
+    let udp_socket = UdpSocket::new(udp_rx_buffer, udp_tx_buffer);
+    let udp_handle = sockets.add(udp_socket);
+
+    loop {
+        let timestamp = Instant::now();
+        match iface.poll(&mut sockets, timestamp) {
+            Ok(_) => {},
+            Err(e) => {
+                debug!("poll error: {}",e);
+            }
+        }
+
+        {
+            let mut socket = sockets.get::<RawSocket>(raw_handle);
+
+            if socket.can_recv() {
+                // For display purposes only - normally we wouldn't process incoming IGMP packets
+                // in the application layer
+                socket.recv()
+                    .and_then(|payload| Ipv4Packet::new_checked(payload))
+                    .and_then(|ipv4_packet| IgmpPacket::new_checked(ipv4_packet.payload()))
+                    .and_then(|igmp_packet| IgmpRepr::parse(&igmp_packet))
+                    .map(|igmp_repr| println!("IGMP packet: {:?}", igmp_repr))
+                    .unwrap_or_else(|e| println!("Recv IGMP error: {:?}", e));
+            }
+        }
+        {
+            let mut socket = sockets.get::<UdpSocket>(udp_handle);
+            if !socket.is_open() {
+                socket.bind(MDNS_PORT).unwrap()
+            }
+
+            if socket.can_recv() {
+                socket.recv()
+                    .map(|(data, sender)| println!("mDNS traffic: {} UDP bytes from {}", data.len(), sender))
+                    .unwrap_or_else(|e| println!("Recv UDP error: {:?}", e));
+            }
+        }
+
+        phy_wait(fd, iface.poll_delay(&sockets, timestamp)).expect("wait error");
+    }
+}

+ 6 - 0
examples/utils.rs

@@ -18,6 +18,7 @@ use smoltcp::phy::{Device, EthernetTracer, FaultInjector};
 #[cfg(feature = "phy-tap_interface")]
 use smoltcp::phy::TapInterface;
 use smoltcp::phy::{PcapWriter, PcapSink, PcapMode, PcapLinkType};
+use smoltcp::phy::RawSocket;
 use smoltcp::time::{Duration, Instant};
 
 #[cfg(feature = "log")]
@@ -87,6 +88,11 @@ pub fn parse_tap_options(matches: &mut Matches) -> TapInterface {
     TapInterface::new(&interface).unwrap()
 }
 
+pub fn parse_raw_socket_options(matches: &mut Matches) -> RawSocket {
+    let interface = matches.free.remove(0);
+    RawSocket::new(&interface).unwrap()
+}
+
 pub fn add_middleware_options(opts: &mut Options, _free: &mut Vec<&str>) {
     opts.optopt("", "pcap", "Write a packet capture file", "FILE");
     opts.optopt("", "drop-chance", "Chance of dropping a packet (%)", "CHANCE");

+ 441 - 23
src/iface/ethernet.rs

@@ -4,6 +4,8 @@
 
 use core::cmp;
 use managed::{ManagedSlice, ManagedMap};
+#[cfg(not(feature = "proto-igmp"))]
+use core::marker::PhantomData;
 
 use {Error, Result};
 use phy::{Device, DeviceCapabilities, RxToken, TxToken};
@@ -14,11 +16,13 @@ use wire::{IpAddress, IpProtocol, IpRepr, IpCidr};
 #[cfg(feature = "proto-ipv6")]
 use wire::{Ipv6Address, Ipv6Packet, Ipv6Repr, IPV6_MIN_MTU};
 #[cfg(feature = "proto-ipv4")]
-use wire::{Ipv4Packet, Ipv4Repr, IPV4_MIN_MTU};
+use wire::{Ipv4Address, Ipv4Packet, Ipv4Repr, IPV4_MIN_MTU};
 #[cfg(feature = "proto-ipv4")]
 use wire::{ArpPacket, ArpRepr, ArpOperation};
 #[cfg(feature = "proto-ipv4")]
 use wire::{Icmpv4Packet, Icmpv4Repr, Icmpv4DstUnreachable};
+#[cfg(feature = "proto-igmp")]
+use wire::{IgmpPacket, IgmpRepr, IgmpVersion};
 #[cfg(feature = "proto-ipv6")]
 use wire::{Icmpv6Packet, Icmpv6Repr, Icmpv6ParamProblem};
 #[cfg(all(feature = "socket-icmp", any(feature = "proto-ipv4", feature = "proto-ipv6")))]
@@ -70,17 +74,29 @@ struct InterfaceInner<'b, 'c, 'e> {
     ethernet_addr:          EthernetAddress,
     ip_addrs:               ManagedSlice<'c, IpCidr>,
     routes:                 Routes<'e>,
+    #[cfg(feature = "proto-igmp")]
+    ipv4_multicast_groups:  ManagedMap<'e, Ipv4Address, ()>,
+    #[cfg(not(feature = "proto-igmp"))]
+    _ipv4_multicast_groups: PhantomData<&'e ()>,
+    /// When to report for (all or) the next multicast group membership via IGMP
+    #[cfg(feature = "proto-igmp")]
+    igmp_report_state:      IgmpReportState,
     device_capabilities:    DeviceCapabilities,
 }
 
 /// A builder structure used for creating a Ethernet network
 /// interface.
 pub struct InterfaceBuilder <'b, 'c, 'e, DeviceT: for<'d> Device<'d>> {
-    device:              DeviceT,
-    ethernet_addr:       Option<EthernetAddress>,
-    neighbor_cache:      Option<NeighborCache<'b>>,
-    ip_addrs:            ManagedSlice<'c, IpCidr>,
-    routes:              Routes<'e>,
+    device:                 DeviceT,
+    ethernet_addr:          Option<EthernetAddress>,
+    neighbor_cache:         Option<NeighborCache<'b>>,
+    ip_addrs:               ManagedSlice<'c, IpCidr>,
+    routes:                 Routes<'e>,
+    /// Does not share storage with `ipv6_multicast_groups` to avoid IPv6 size overhead.
+    #[cfg(feature = "proto-igmp")]
+    ipv4_multicast_groups:  ManagedMap<'e, Ipv4Address, ()>,
+    #[cfg(not(feature = "proto-igmp"))]
+    _ipv4_multicast_groups: PhantomData<&'e ()>,
 }
 
 impl<'b, 'c, 'e, DeviceT> InterfaceBuilder<'b, 'c, 'e, DeviceT>
@@ -110,13 +126,17 @@ impl<'b, 'c, 'e, DeviceT> InterfaceBuilder<'b, 'c, 'e, DeviceT>
     ///         .ip_addrs(ip_addrs)
     ///         .finalize();
     /// ```
-    pub fn new(device: DeviceT) -> InterfaceBuilder<'b, 'c, 'e, DeviceT> {
+    pub fn new(device: DeviceT) -> Self {
         InterfaceBuilder {
             device:              device,
             ethernet_addr:       None,
             neighbor_cache:      None,
             ip_addrs:            ManagedSlice::Borrowed(&mut []),
             routes:              Routes::new(ManagedMap::Borrowed(&mut [])),
+            #[cfg(feature = "proto-igmp")]
+            ipv4_multicast_groups:   ManagedMap::Borrowed(&mut []),
+            #[cfg(not(feature = "proto-igmp"))]
+            _ipv4_multicast_groups:  PhantomData,
         }
     }
 
@@ -127,7 +147,7 @@ impl<'b, 'c, 'e, DeviceT> InterfaceBuilder<'b, 'c, 'e, DeviceT>
     /// This function panics if the address is not unicast.
     ///
     /// [ethernet_addr]: struct.EthernetInterface.html#method.ethernet_addr
-    pub fn ethernet_addr(mut self, addr: EthernetAddress) -> InterfaceBuilder<'b, 'c, 'e, DeviceT> {
+    pub fn ethernet_addr(mut self, addr: EthernetAddress) -> Self {
         InterfaceInner::check_ethernet_addr(&addr);
         self.ethernet_addr = Some(addr);
         self
@@ -140,7 +160,7 @@ impl<'b, 'c, 'e, DeviceT> InterfaceBuilder<'b, 'c, 'e, DeviceT>
     /// This function panics if any of the addresses are not unicast.
     ///
     /// [ip_addrs]: struct.EthernetInterface.html#method.ip_addrs
-    pub fn ip_addrs<T>(mut self, ip_addrs: T) -> InterfaceBuilder<'b, 'c, 'e, DeviceT>
+    pub fn ip_addrs<T>(mut self, ip_addrs: T) -> Self
         where T: Into<ManagedSlice<'c, IpCidr>>
     {
         let ip_addrs = ip_addrs.into();
@@ -160,9 +180,26 @@ impl<'b, 'c, 'e, DeviceT> InterfaceBuilder<'b, 'c, 'e, DeviceT>
         self
     }
 
+    /// Provide storage for multicast groups.
+    ///
+    /// Join multicast groups by calling [`join_multicast_group()`] on an `Interface`.
+    /// Using [`join_multicast_group()`] will send initial membership reports.
+    ///
+    /// A previously destroyed interface can be recreated by reusing the multicast group
+    /// storage, i.e. providing a non-empty storage to `ipv4_multicast_groups()`.
+    /// Note that this way initial membership reports are **not** sent.
+    ///
+    /// [`join_multicast_group()`]: struct.EthernetInterface.html#method.join_multicast_group
+    #[cfg(feature = "proto-igmp")]
+    pub fn ipv4_multicast_groups<T>(mut self, ipv4_multicast_groups: T) -> Self
+        where T: Into<ManagedMap<'e, Ipv4Address, ()>>
+    {
+        self.ipv4_multicast_groups = ipv4_multicast_groups.into();
+        self
+    }
+
     /// Set the Neighbor Cache the interface will use.
-    pub fn neighbor_cache(mut self, neighbor_cache: NeighborCache<'b>) ->
-                         InterfaceBuilder<'b, 'c, 'e, DeviceT> {
+    pub fn neighbor_cache(mut self, neighbor_cache: NeighborCache<'b>) -> Self {
         self.neighbor_cache = Some(neighbor_cache);
         self
     }
@@ -182,12 +219,19 @@ impl<'b, 'c, 'e, DeviceT> InterfaceBuilder<'b, 'c, 'e, DeviceT>
         match (self.ethernet_addr, self.neighbor_cache) {
             (Some(ethernet_addr), Some(neighbor_cache)) => {
                 let device_capabilities = self.device.capabilities();
+
                 Interface {
                     device: self.device,
                     inner: InterfaceInner {
                         ethernet_addr, device_capabilities, neighbor_cache,
                         ip_addrs: self.ip_addrs,
                         routes: self.routes,
+                        #[cfg(feature = "proto-igmp")]
+                        ipv4_multicast_groups: self.ipv4_multicast_groups,
+                        #[cfg(not(feature = "proto-igmp"))]
+                        _ipv4_multicast_groups:  PhantomData,
+                        #[cfg(feature = "proto-igmp")]
+                        igmp_report_state: IgmpReportState::Inactive,
                     }
                 }
             },
@@ -203,6 +247,8 @@ enum Packet<'a> {
     Arp(ArpRepr),
     #[cfg(feature = "proto-ipv4")]
     Icmpv4((Ipv4Repr, Icmpv4Repr<'a>)),
+    #[cfg(feature = "proto-igmp")]
+    Igmp((Ipv4Repr, IgmpRepr)),
     #[cfg(feature = "proto-ipv6")]
     Icmpv6((Ipv6Repr, Icmpv6Repr<'a>)),
     #[cfg(feature = "socket-raw")]
@@ -221,6 +267,8 @@ impl<'a> Packet<'a> {
             &Packet::Arp(_) => None,
             #[cfg(feature = "proto-ipv4")]
             &Packet::Icmpv4((ref ipv4_repr, _)) => Some(ipv4_repr.dst_addr.into()),
+            #[cfg(feature = "proto-igmp")]
+            &Packet::Igmp((ref ipv4_repr, _)) => Some(ipv4_repr.dst_addr.into()),
             #[cfg(feature = "proto-ipv6")]
             &Packet::Icmpv6((ref ipv6_repr, _)) => Some(ipv6_repr.dst_addr.into()),
             #[cfg(feature = "socket-raw")]
@@ -246,6 +294,22 @@ fn icmp_reply_payload_len(len: usize, mtu: usize, header_len: usize) -> usize {
     cmp::min(len, mtu - header_len * 2 - 8)
 }
 
+#[cfg(feature = "proto-igmp")]
+enum IgmpReportState {
+    Inactive,
+    ToGeneralQuery {
+        version:    IgmpVersion,
+        timeout:    Instant,
+        interval:   Duration,
+        next_index: usize
+    },
+    ToSpecificQuery {
+        version:    IgmpVersion,
+        timeout:    Instant,
+        group:      Ipv4Address
+    },
+}
+
 impl<'b, 'c, 'e, DeviceT> Interface<'b, 'c, 'e, DeviceT>
         where DeviceT: for<'d> Device<'d> {
     /// Get the Ethernet address of the interface.
@@ -262,6 +326,65 @@ impl<'b, 'c, 'e, DeviceT> Interface<'b, 'c, 'e, DeviceT>
         InterfaceInner::check_ethernet_addr(&self.inner.ethernet_addr);
     }
 
+    /// 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<T: Into<IpAddress>>(&mut self, addr: T, _timestamp: Instant) -> Result<bool> {
+        match addr.into() {
+            #[cfg(feature = "proto-igmp")]
+            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 = self.device.transmit().ok_or(Error::Exhausted)?;
+                    self.inner.dispatch(tx_token, _timestamp, pkt)?;
+                    Ok(true)
+                } else {
+                    Ok(false)
+                }
+            }
+            // Multicast is not yet implemented for other address families
+            _ => 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<T: Into<IpAddress>>(&mut self, addr: T, _timestamp: Instant) -> Result<bool> {
+        match addr.into() {
+            #[cfg(feature = "proto-igmp")]
+            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 = self.device.transmit().ok_or(Error::Exhausted)?;
+                    self.inner.dispatch(tx_token, _timestamp, pkt)?;
+                    Ok(true)
+                } else {
+                    Ok(false)
+                }
+            }
+            // Multicast is not yet implemented for other address families
+            _ => 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)
+    }
+
     /// Get the IP addresses of the interface.
     pub fn ip_addrs(&self) -> &[IpCidr] {
         self.inner.ip_addrs.as_ref()
@@ -281,6 +404,12 @@ impl<'b, 'c, 'e, DeviceT> Interface<'b, 'c, 'e, DeviceT>
         self.inner.has_ip_addr(addr)
     }
 
+    /// Get the first IPv4 address of the interface.
+    #[cfg(feature = "proto-ipv4")]
+    pub fn ipv4_address(&self) -> Option<Ipv4Address> {
+        self.inner.ipv4_address()
+    }
+
     pub fn routes(&self) -> &Routes<'e> {
         &self.inner.routes
     }
@@ -311,6 +440,10 @@ impl<'b, 'c, 'e, DeviceT> Interface<'b, 'c, 'e, DeviceT>
         loop {
             let processed_any = self.socket_ingress(sockets, timestamp)?;
             let emitted_any   = self.socket_egress(sockets, timestamp)?;
+
+            #[cfg(feature = "proto-igmp")]
+            self.igmp_egress(timestamp)?;
+
             if processed_any || emitted_any {
                 readiness_may_have_changed = true;
             } else {
@@ -463,6 +596,54 @@ impl<'b, 'c, 'e, DeviceT> Interface<'b, 'c, 'e, DeviceT>
         }
         Ok(emitted_any)
     }
+
+    /// Depending on `igmp_report_state` and the therein contained
+    /// timeouts, send IGMP membership reports.
+    #[cfg(feature = "proto-igmp")]
+    fn igmp_egress(&mut self, timestamp: Instant) -> Result<bool> {
+        match self.inner.igmp_report_state {
+            IgmpReportState::ToSpecificQuery { version, timeout, group }
+                    if timestamp >= timeout => {
+                if let Some(pkt) = self.inner.igmp_report_packet(version, group) {
+                    // Send initial membership report
+                    let tx_token = self.device.transmit().ok_or(Error::Exhausted)?;
+                    self.inner.dispatch(tx_token, timestamp, pkt)?;
+                }
+
+                self.inner.igmp_report_state = IgmpReportState::Inactive;
+                Ok(true)
+            }
+            IgmpReportState::ToGeneralQuery { version, timeout, interval, next_index }
+                    if timestamp >= 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 = self.device.transmit().ok_or(Error::Exhausted)?;
+                            self.inner.dispatch(tx_token, timestamp, pkt)?;
+                        }
+
+                        let next_timeout = (timeout + interval).max(timestamp);
+                        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<'b, 'c, 'e> InterfaceInner<'b, 'c, 'e> {
@@ -505,16 +686,44 @@ impl<'b, 'c, 'e> InterfaceInner<'b, 'c, 'e> {
         self.ip_addrs.iter().any(|probe| probe.address() == addr)
     }
 
+    /// Get the first IPv4 address of the interface.
+    #[cfg(feature = "proto-ipv4")]
+    pub fn ipv4_address(&self) -> Option<Ipv4Address> {
+        self.ip_addrs.iter()
+            .filter_map(
+                |addr| match addr {
+                    &IpCidr::Ipv4(cidr) => Some(cidr.address()),
+                    _ => None,
+                })
+            .next()
+    }
+
+    /// 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() {
+            #[cfg(feature = "proto-igmp")]
+            IpAddress::Ipv4(key) =>
+                key == Ipv4Address::MULTICAST_ALL_SYSTEMS ||
+                self.ipv4_multicast_groups.get(&key).is_some(),
+            _ =>
+                false,
+        }
+    }
+
     fn process_ethernet<'frame, T: AsRef<[u8]>>
                        (&mut self, sockets: &mut SocketSet, timestamp: Instant, frame: &'frame T) ->
                        Result<Packet<'frame>>
     {
         let eth_frame = EthernetFrame::new_checked(frame)?;
 
-        // Ignore any packets not directed to our hardware address.
+        // Ignore any packets not directed to our hardware address or any of the multicast groups.
         if !eth_frame.dst_addr().is_broadcast() &&
            !eth_frame.dst_addr().is_multicast() &&
-                eth_frame.dst_addr() != self.ethernet_addr {
+           eth_frame.dst_addr() != self.ethernet_addr
+        {
             return Ok(Packet::None)
         }
 
@@ -705,10 +914,8 @@ impl<'b, 'c, 'e> InterfaceInner<'b, 'c, 'e> {
         #[cfg(feature = "socket-raw")]
         let handled_by_raw_socket = self.raw_socket_filter(sockets, &ip_repr, ip_payload);
 
-        if !ipv4_repr.dst_addr.is_broadcast() &&
-           !ipv4_repr.dst_addr.is_multicast() &&
-           !self.has_ip_addr(ipv4_repr.dst_addr) {
-            // Ignore IP packets not directed at us.
+        if !self.has_ip_addr(ipv4_repr.dst_addr) && !self.has_multicast_group(ipv4_repr.dst_addr) {
+            // Ignore IP packets not directed at us or any of the multicast groups
             return Ok(Packet::None)
         }
 
@@ -716,6 +923,10 @@ impl<'b, 'c, 'e> InterfaceInner<'b, 'c, 'e> {
             IpProtocol::Icmp =>
                 self.process_icmpv4(sockets, ip_repr, ip_payload),
 
+            #[cfg(feature = "proto-igmp")]
+            IpProtocol::Igmp =>
+                self.process_igmp(timestamp, ipv4_repr, ip_payload),
+
             #[cfg(feature = "socket-udp")]
             IpProtocol::Udp =>
                 self.process_udp(sockets, ip_repr, ip_payload),
@@ -742,6 +953,60 @@ impl<'b, 'c, 'e> InterfaceInner<'b, 'c, 'e> {
         }
     }
 
+    /// 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.
+    #[cfg(feature = "proto-igmp")]
+    fn process_igmp<'frame>(&mut self, timestamp: Instant, ipv4_repr: Ipv4Repr,
+                            ip_payload: &'frame [u8]) -> Result<Packet<'frame>> {
+        let igmp_packet = IgmpPacket::new_checked(ip_payload)?;
+        let igmp_repr = 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: timestamp + 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: timestamp + timeout, group: group_addr
+                        };
+                    }
+                }
+            },
+            // Ignore membership reports
+            IgmpRepr::MembershipReport { .. } => (),
+            // Ignore hosts leaving groups
+            IgmpRepr::LeaveGroup{ .. } => (),
+        }
+
+        Ok(Packet::None)
+    }
+
     #[cfg(feature = "proto-ipv6")]
     fn process_icmpv6<'frame>(&mut self, _sockets: &mut SocketSet, timestamp: Instant,
                               ip_repr: IpRepr, ip_payload: &'frame [u8]) -> Result<Packet<'frame>>
@@ -1087,6 +1352,12 @@ impl<'b, 'c, 'e> InterfaceInner<'b, 'c, 'e> {
                     icmpv4_repr.emit(&mut Icmpv4Packet::new_unchecked(payload), &checksum_caps);
                 })
             }
+            #[cfg(feature = "proto-igmp")]
+            Packet::Igmp((ipv4_repr, igmp_repr)) => {
+                self.dispatch_ip(tx_token, timestamp, IpRepr::Ipv4(ipv4_repr), |_ip_repr, payload| {
+                    igmp_repr.emit(&mut IgmpPacket::new_unchecked(payload));
+                })
+            }
             #[cfg(feature = "proto-ipv6")]
             Packet::Icmpv6((ipv6_repr, icmpv6_repr)) => {
                 self.dispatch_ip(tx_token, timestamp, IpRepr::Ipv6(ipv6_repr),
@@ -1167,7 +1438,7 @@ impl<'b, 'c, 'e> InterfaceInner<'b, 'c, 'e> {
     fn route(&self, addr: &IpAddress, timestamp: Instant) -> Result<IpAddress> {
         // Send directly.
         if self.in_same_network(addr) || addr.is_broadcast() {
-            return Ok(addr.clone())
+            return Ok(*addr)
         }
 
         // Route via a router.
@@ -1319,16 +1590,55 @@ impl<'b, 'c, 'e> InterfaceInner<'b, 'c, 'e> {
             f(ip_repr, payload)
         })
     }
+
+    #[cfg(feature = "proto-igmp")]
+    fn igmp_report_packet<'any>(&self, version: IgmpVersion, group_addr: Ipv4Address) -> Option<Packet<'any>> {
+        let iface_addr = self.ipv4_address()?;
+        let igmp_repr = IgmpRepr::MembershipReport {
+            group_addr,
+            version,
+        };
+        let pkt = Packet::Igmp((Ipv4Repr {
+            src_addr:    iface_addr,
+            // Send to the group being reported
+            dst_addr:    group_addr,
+            protocol:    IpProtocol::Igmp,
+            payload_len: igmp_repr.buffer_len(),
+            hop_limit:   1,
+            // TODO: add Router Alert IPv4 header option. See
+            // [#183](https://github.com/m-labs/smoltcp/issues/183).
+        }, igmp_repr));
+        Some(pkt)
+    }
+
+    #[cfg(feature = "proto-igmp")]
+    fn igmp_leave_packet<'any>(&self, group_addr: Ipv4Address) -> Option<Packet<'any>> {
+        self.ipv4_address().map(|iface_addr| {
+            let igmp_repr = IgmpRepr::LeaveGroup { group_addr };
+            let pkt = Packet::Igmp((Ipv4Repr {
+                src_addr:    iface_addr,
+                dst_addr:    Ipv4Address::MULTICAST_ALL_ROUTERS,
+                protocol:    IpProtocol::Igmp,
+                payload_len: igmp_repr.buffer_len(),
+                hop_limit:   1,
+            }, igmp_repr));
+            pkt
+        })
+    }
 }
 
 #[cfg(test)]
 mod test {
+    #[cfg(feature = "proto-igmp")]
+    use std::vec::Vec;
     use std::collections::BTreeMap;
     use {Result, Error};
 
     use super::InterfaceBuilder;
     use iface::{NeighborCache, EthernetInterface};
     use phy::{self, Loopback, ChecksumCapabilities};
+    #[cfg(feature = "proto-igmp")]
+    use phy::{Device, RxToken, TxToken};
     use time::Instant;
     use socket::SocketSet;
     #[cfg(feature = "proto-ipv4")]
@@ -1337,8 +1647,12 @@ mod test {
     use wire::{IpAddress, IpCidr, IpProtocol, IpRepr};
     #[cfg(feature = "proto-ipv4")]
     use wire::{Ipv4Address, Ipv4Repr};
+    #[cfg(feature = "proto-igmp")]
+    use wire::Ipv4Packet;
     #[cfg(feature = "proto-ipv4")]
     use wire::{Icmpv4Repr, Icmpv4DstUnreachable};
+    #[cfg(feature = "proto-igmp")]
+    use wire::{IgmpPacket, IgmpRepr, IgmpVersion};
     #[cfg(all(feature = "socket-udp", any(feature = "proto-ipv4", feature = "proto-ipv6")))]
     use wire::{UdpPacket, UdpRepr};
     #[cfg(feature = "proto-ipv6")]
@@ -1365,15 +1679,31 @@ mod test {
             IpCidr::new(IpAddress::v6(0xfdbe, 0, 0, 0, 0, 0, 0, 1), 64),
         ];
 
-        let iface = InterfaceBuilder::new(device)
-                .ethernet_addr(EthernetAddress::default())
-                .neighbor_cache(NeighborCache::new(BTreeMap::new()))
-                .ip_addrs(ip_addrs)
-                .finalize();
+        let iface_builder = InterfaceBuilder::new(device)
+            .ethernet_addr(EthernetAddress::default())
+            .neighbor_cache(NeighborCache::new(BTreeMap::new()))
+            .ip_addrs(ip_addrs);
+        #[cfg(feature = "proto-igmp")]
+        let iface_builder = iface_builder
+            .ipv4_multicast_groups(BTreeMap::new());
+        let iface = iface_builder
+            .finalize();
 
         (iface, SocketSet::new(vec![]))
     }
 
+    #[cfg(feature = "proto-igmp")]
+    fn recv_all<'b>(iface: &mut EthernetInterface<'static, 'b, 'static, Loopback>, timestamp: Instant) -> Vec<Vec<u8>> {
+        let mut pkts = Vec::new();
+        while let Some((rx, _tx)) = iface.device.receive() {
+            rx.consume(timestamp, |pkt| {
+                pkts.push(pkt.iter().cloned().collect());
+                Ok(())
+            }).unwrap();
+        }
+        pkts
+    }
+
     #[derive(Debug, PartialEq)]
     struct MockTxToken;
 
@@ -2043,4 +2373,92 @@ mod test {
             &IpAddress::Ipv6(remote_ip_addr)),
             Ok((remote_hw_addr, MockTxToken)));
     }
+
+    #[test]
+    #[cfg(feature = "proto-igmp")]
+    fn test_handle_igmp() {
+        fn recv_igmp<'b>(mut iface: &mut EthernetInterface<'static, 'b, 'static, Loopback>, timestamp: Instant) -> Vec<(Ipv4Repr, IgmpRepr)> {
+            let checksum_caps = &iface.device.capabilities().checksum;
+            recv_all(&mut iface, timestamp)
+                .iter()
+                .filter_map(|frame| {
+                    let eth_frame = EthernetFrame::new_checked(frame).ok()?;
+                    let ipv4_packet = Ipv4Packet::new_checked(eth_frame.payload()).ok()?;
+                    let ipv4_repr = Ipv4Repr::parse(&ipv4_packet, &checksum_caps).ok()?;
+                    let ip_payload = ipv4_packet.payload();
+                    let igmp_packet = IgmpPacket::new_checked(ip_payload).ok()?;
+                    let igmp_repr = IgmpRepr::parse(&igmp_packet).ok()?;
+                    Some((ipv4_repr, igmp_repr))
+                })
+                .collect::<Vec<_>>()
+        }
+
+        let groups = [
+            Ipv4Address::new(224, 0, 0, 22),
+            Ipv4Address::new(224, 0, 0, 56),
+        ];
+
+        let (mut iface, mut socket_set) = create_loopback();
+
+        // Join multicast groups
+        let timestamp = Instant::now();
+        for group in &groups {
+            iface.join_multicast_group(*group, timestamp)
+                .unwrap();
+        }
+
+        let reports = recv_igmp(&mut iface, timestamp);
+        assert_eq!(reports.len(), 2);
+        for (i, group_addr) in groups.iter().enumerate() {
+            assert_eq!(reports[i].0.protocol, IpProtocol::Igmp);
+            assert_eq!(reports[i].0.dst_addr, *group_addr);
+            assert_eq!(reports[i].1, IgmpRepr::MembershipReport {
+                group_addr: *group_addr,
+                version: IgmpVersion::Version2,
+            });
+        }
+
+        // General query
+        let timestamp = Instant::now();
+        const GENERAL_QUERY_BYTES: &[u8] = &[
+            0x01, 0x00, 0x5e, 0x00, 0x00, 0x01, 0x0a, 0x14,
+            0x48, 0x01, 0x21, 0x01, 0x08, 0x00, 0x46, 0xc0,
+            0x00, 0x24, 0xed, 0xb4, 0x00, 0x00, 0x01, 0x02,
+            0x47, 0x43, 0xac, 0x16, 0x63, 0x04, 0xe0, 0x00,
+            0x00, 0x01, 0x94, 0x04, 0x00, 0x00, 0x11, 0x64,
+            0xec, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0c,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00
+        ];
+        {
+            // Transmit GENERAL_QUERY_BYTES into loopback
+            let tx_token = iface.device.transmit().unwrap();
+            tx_token.consume(
+                timestamp, GENERAL_QUERY_BYTES.len(),
+                |buffer| {
+                    buffer.copy_from_slice(GENERAL_QUERY_BYTES);
+                    Ok(())
+                }).unwrap();
+        }
+        // Trigger processing until all packets received through the
+        // loopback have been processed, including responses to
+        // GENERAL_QUERY_BYTES. Therefore `recv_all()` would return 0
+        // pkts that could be checked.
+        iface.socket_ingress(&mut socket_set, timestamp).unwrap();
+
+        // Leave multicast groups
+        let timestamp = Instant::now();
+        for group in &groups {
+            iface.leave_multicast_group(group.clone(), timestamp)
+                .unwrap();
+        }
+
+        let leaves = recv_igmp(&mut iface, timestamp);
+        assert_eq!(leaves.len(), 2);
+        for (i, group_addr) in groups.iter().cloned().enumerate() {
+            assert_eq!(leaves[i].0.protocol, IpProtocol::Igmp);
+            assert_eq!(leaves[i].0.dst_addr, Ipv4Address::MULTICAST_ALL_ROUTERS);
+            assert_eq!(leaves[i].1, IgmpRepr::LeaveGroup { group_addr });
+        }
+    }
 }

+ 4 - 4
src/socket/raw.rs

@@ -154,13 +154,13 @@ impl<'a, 'b> RawSocket<'a, 'b> {
                              Result<()>
             where F: FnOnce((IpRepr, &[u8])) -> Result<()> {
         fn prepare<'a>(protocol: IpProtocol, buffer: &'a mut [u8],
-                   checksum_caps: &ChecksumCapabilities) -> Result<(IpRepr, &'a [u8])> {
+                   _checksum_caps: &ChecksumCapabilities) -> Result<(IpRepr, &'a [u8])> {
             match IpVersion::of_packet(buffer.as_ref())? {
                 #[cfg(feature = "proto-ipv4")]
                 IpVersion::Ipv4 => {
                     let mut packet = Ipv4Packet::new_checked(buffer.as_mut())?;
                     if packet.protocol() != protocol { return Err(Error::Unaddressable) }
-                    if checksum_caps.ipv4.tx() {
+                    if _checksum_caps.ipv4.tx() {
                         packet.fill_checksum();
                     } else {
                         // make sure we get a consistently zeroed checksum,
@@ -168,8 +168,8 @@ impl<'a, 'b> RawSocket<'a, 'b> {
                         packet.set_checksum(0);
                     }
 
-                    let packet = Ipv4Packet::new_unchecked(&*packet.into_inner());
-                    let ipv4_repr = Ipv4Repr::parse(&packet, checksum_caps)?;
+                    let packet = Ipv4Packet::new_checked(&*packet.into_inner())?;
+                    let ipv4_repr = Ipv4Repr::parse(&packet, _checksum_caps)?;
                     Ok((IpRepr::Ipv4(ipv4_repr), packet.payload()))
                 }
                 #[cfg(feature = "proto-ipv6")]

+ 8 - 2
src/wire/ipv4.rs

@@ -27,10 +27,16 @@ pub struct Address(pub [u8; 4]);
 
 impl Address {
     /// An unspecified address.
-    pub const UNSPECIFIED: Address = Address([0x00; 4]);
+    pub const UNSPECIFIED:           Address = Address([0x00; 4]);
 
     /// The broadcast address.
-    pub const BROADCAST:   Address = Address([0xff; 4]);
+    pub const BROADCAST:             Address = Address([0xff; 4]);
+
+    /// All multicast-capable nodes
+    pub const MULTICAST_ALL_SYSTEMS: Address = Address([224, 0, 0, 1]);
+
+    /// All multicast-capable routers
+    pub const MULTICAST_ALL_ROUTERS: Address = Address([224, 0, 0, 2]);
 
     /// Construct an IPv4 address from parts.
     pub fn new(a0: u8, a1: u8, a2: u8, a3: u8) -> Address {

+ 2 - 2
src/wire/mod.rs

@@ -99,7 +99,7 @@ mod icmpv4;
 mod icmpv6;
 #[cfg(any(feature = "proto-ipv4", feature = "proto-ipv6"))]
 mod icmp;
-#[cfg(feature = "proto-ipv4")]
+#[cfg(feature = "proto-igmp")]
 mod igmp;
 #[cfg(feature = "proto-ipv6")]
 mod ndisc;
@@ -173,7 +173,7 @@ pub use self::icmpv4::{Message as Icmpv4Message,
                        Packet as Icmpv4Packet,
                        Repr as Icmpv4Repr};
 
-#[cfg(feature = "proto-ipv4")]
+#[cfg(feature = "proto-igmp")]
 pub use self::igmp::{Packet as IgmpPacket,
                      Repr as IgmpRepr,
                      IgmpVersion};