diff --git a/.travis.yml b/.travis.yml index 686470f..266c70b 100644 --- a/.travis.yml +++ b/.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' diff --git a/Cargo.toml b/Cargo.toml index 75a9f64..3d6e4c6 100644 --- a/Cargo.toml +++ b/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 diff --git a/README.md b/README.md index bae63b7..c19c5f9 100644 --- a/README.md +++ b/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 diff --git a/examples/multicast.rs b/examples/multicast.rs new file mode 100644 index 0000000..81a606f --- /dev/null +++ b/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::(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::(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"); + } +} diff --git a/examples/utils.rs b/examples/utils.rs index 7707e7d..a816125 100644 --- a/examples/utils.rs +++ b/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"); diff --git a/src/iface/ethernet.rs b/src/iface/ethernet.rs index 60d3156..dbb4522 100644 --- a/src/iface/ethernet.rs +++ b/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, - neighbor_cache: Option>, - ip_addrs: ManagedSlice<'c, IpCidr>, - routes: Routes<'e>, + device: DeviceT, + ethernet_addr: Option, + neighbor_cache: Option>, + 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(mut self, ip_addrs: T) -> InterfaceBuilder<'b, 'c, 'e, DeviceT> + pub fn ip_addrs(mut self, ip_addrs: T) -> Self where T: Into> { 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(mut self, ipv4_multicast_groups: T) -> Self + where T: Into> + { + 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>(&mut self, addr: T, _timestamp: Instant) -> Result { + 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>(&mut self, addr: T, _timestamp: Instant) -> Result { + 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>(&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 { + 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 { + 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 { + 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>(&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> { 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> { + 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> @@ -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 { // 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> { + 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> { + 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> { + 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::>() + } + + 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 }); + } + } } diff --git a/src/socket/raw.rs b/src/socket/raw.rs index 5f0583d..f246ffb 100644 --- a/src/socket/raw.rs +++ b/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")] diff --git a/src/wire/ipv4.rs b/src/wire/ipv4.rs index 6176af4..95a8958 100644 --- a/src/wire/ipv4.rs +++ b/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 { diff --git a/src/wire/mod.rs b/src/wire/mod.rs index 20b6a34..20fb9ce 100644 --- a/src/wire/mod.rs +++ b/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};