| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! Multicast Listener Discovery (MLD). |
| //! |
| //! MLD is derived from version 2 of IPv4's Internet Group Management Protocol, |
| //! IGMPv2. One important difference to note is that MLD uses ICMPv6 (IP |
| //! Protocol 58) message types, rather than IGMP (IP Protocol 2) message types. |
| |
| use std::collections::HashMap; |
| use std::time::Duration; |
| |
| use failure::Fail; |
| use log::{debug, error}; |
| use net_types::ip::{Ip, Ipv6, Ipv6Addr}; |
| use net_types::{LinkLocalAddr, MulticastAddr, SpecifiedAddr, SpecifiedAddress, Witness}; |
| use packet::serialize::Serializer; |
| use packet::{EmptyBuf, InnerPacketBuilder}; |
| use rand::Rng; |
| use rand_xorshift::XorShiftRng; |
| use zerocopy::ByteSlice; |
| |
| use crate::context::{ |
| FrameContext, InstantContext, RngContext, RngContextExt, StateContext, TimerContext, |
| }; |
| use crate::ip::gmp::{Action, Actions, GmpAction, GmpStateMachine, ProtocolSpecific}; |
| use crate::ip::{IpDeviceIdContext, IpProto}; |
| use crate::wire::icmp::mld::{ |
| IcmpMldv1MessageType, Mldv1Body, Mldv1MessageBuilder, MulticastListenerDone, |
| MulticastListenerReport, |
| }; |
| use crate::wire::icmp::{IcmpPacketBuilder, IcmpUnusedCode, Icmpv6Packet}; |
| use crate::wire::ipv6::ext_hdrs::{ |
| ExtensionHeaderOptionAction, HopByHopOption, HopByHopOptionData, |
| }; |
| use crate::wire::ipv6::{Ipv6PacketBuilder, Ipv6PacketBuilderWithHBHOptions}; |
| use crate::Instant; |
| |
| /// Metadata for sending an MLD packet in an IP packet. |
| /// |
| /// `MldFrameMetadata` is used by [`MldContext`]'s [`FrameContext`] bound. When |
| /// [`FrameContext::send_frame`] is called with an `MldFrameMetadata`, the body |
| /// contains an MLD packet in an IP packet. It is encapsulated in a link-layer |
| /// frame, and sent to the link-layer address corresponding to the given local |
| /// IP address. |
| pub(crate) struct MldFrameMetadata<D> { |
| pub(crate) device: D, |
| pub(crate) local_ip: MulticastAddr<Ipv6Addr>, |
| } |
| |
| impl<D> MldFrameMetadata<D> { |
| fn new(device: D, local_ip: MulticastAddr<Ipv6Addr>) -> MldFrameMetadata<D> { |
| MldFrameMetadata { device, local_ip } |
| } |
| } |
| |
| /// The execution context for the Multicast Listener Discovery (MLD) protocol. |
| pub(crate) trait MldContext: IpDeviceIdContext |
| + TimerContext< |
| MldReportDelay<<Self as IpDeviceIdContext>::DeviceId>, |
| > + RngContext |
| + StateContext< |
| <Self as IpDeviceIdContext>::DeviceId, |
| MldInterface<<Self as InstantContext>::Instant>, |
| > + FrameContext< |
| EmptyBuf, |
| MldFrameMetadata<<Self as IpDeviceIdContext>::DeviceId>, |
| > |
| { |
| /// Gets the IPv6 link local address on `device`. |
| fn get_ipv6_link_local_addr(&self, device: Self::DeviceId) -> Option<LinkLocalAddr<Ipv6Addr>>; |
| } |
| |
| /// A handler for incoming MLD packets. |
| /// |
| /// `MldHandler` is implemented by any type which also implements |
| /// [`MldContext`], and it can also be mocked for use in testing. |
| pub(crate) trait MldHandler: IpDeviceIdContext { |
| /// Receive an MLD packet. |
| /// |
| /// # Panics |
| /// |
| /// `receive_mld_packet` panics if `packet` is not one of |
| /// `MulticastListenerQuery`, `MulticastListenerReport`, or |
| /// `MulticastListenerDone`. |
| fn receive_mld_packet<B: ByteSlice>( |
| &mut self, |
| device: Self::DeviceId, |
| src_ip: Ipv6Addr, |
| dst_ip: SpecifiedAddr<Ipv6Addr>, |
| packet: Icmpv6Packet<B>, |
| ); |
| } |
| |
| impl<C: MldContext> MldHandler for C { |
| fn receive_mld_packet<B: ByteSlice>( |
| &mut self, |
| device: Self::DeviceId, |
| src_ip: Ipv6Addr, |
| dst_ip: SpecifiedAddr<Ipv6Addr>, |
| packet: Icmpv6Packet<B>, |
| ) { |
| if let Err(e) = match packet { |
| Icmpv6Packet::MulticastListenerQuery(msg) => { |
| let now = self.now(); |
| let max_response_delay: Duration = msg.body().max_response_delay(); |
| handle_mld_message(self, device, msg.body(), |rng, state| { |
| state.query_received(rng, max_response_delay, now) |
| }) |
| } |
| Icmpv6Packet::MulticastListenerReport(msg) => { |
| handle_mld_message(self, device, msg.body(), |_, state| state.report_received()) |
| } |
| Icmpv6Packet::MulticastListenerDone(_) => { |
| debug!("Hosts are not interested in Done messages"); |
| return; |
| } |
| _ => { |
| unreachable!("It is not an MLD packet"); |
| } |
| } { |
| error!("Error occurred when handling MLD message: {}", e); |
| } |
| } |
| } |
| |
| fn handle_mld_message<C: MldContext, B: ByteSlice, F>( |
| ctx: &mut C, |
| device: C::DeviceId, |
| body: &Mldv1Body<B>, |
| handler: F, |
| ) -> MldResult<()> |
| where |
| F: Fn(&mut XorShiftRng, &mut MldGroupState<C::Instant>) -> Actions<MldProtocolSpecific>, |
| { |
| // TODO(joshlf): Once we figure out how to access the RNG and the state at |
| // the same time, get rid of this hack. For the time being, this is probably |
| // fine because, while the `XorShiftRng` isn't cryptographically secure, its |
| // seed is, which means that, at worst, an attacker will be able to |
| // correlate events generated during this one function call. |
| let mut rng = ctx.new_xorshift_rng(); |
| let group_addr = body.group_addr; |
| if !group_addr.is_specified() { |
| let addr_and_actions = ctx |
| .get_state_mut(device) |
| .groups |
| .iter_mut() |
| .map(|(addr, state)| (addr.clone(), handler(&mut rng, state))) |
| .collect::<Vec<_>>(); |
| // `addr` must be a multicast address, otherwise it will not have |
| // an associated state in the first place |
| for (addr, actions) in addr_and_actions { |
| run_actions(ctx, device, actions, addr); |
| } |
| Ok(()) |
| } else if let Some(group_addr) = MulticastAddr::new(group_addr) { |
| let actions = match ctx.get_state_mut(device).groups.get_mut(&group_addr) { |
| Some(state) => handler(&mut rng, state), |
| None => return Err(MldError::NotAMember { addr: group_addr.get() }), |
| }; |
| run_actions(ctx, device, actions, group_addr); |
| Ok(()) |
| } else { |
| Err(MldError::NotAMember { addr: group_addr }) |
| } |
| } |
| |
| #[derive(Debug, Fail)] |
| pub(crate) enum MldError { |
| /// The host is trying to operate on an group address of which the host is not a member. |
| #[fail(display = "the host has not already been a member of the address: {}", addr)] |
| NotAMember { addr: Ipv6Addr }, |
| /// Failed to send an IGMP packet. |
| #[fail(display = "failed to send out an IGMP packet to address: {}", addr)] |
| SendFailure { addr: Ipv6Addr }, |
| } |
| |
| pub(crate) type MldResult<T> = Result<T, MldError>; |
| |
| #[derive(PartialEq, Eq, Clone, Copy, Default, Debug)] |
| pub(crate) struct MldProtocolSpecific; |
| |
| pub(crate) struct MldConfig { |
| unsolicited_report_interval: Duration, |
| send_leave_anyway: bool, |
| } |
| |
| #[derive(PartialEq, Eq, Clone, Copy, Debug)] |
| pub(crate) struct ImmediateIdleState; |
| |
| /// The default value for `unsolicited_report_interval` [RFC 2710 section 7.10] |
| /// |
| /// [RFC 2710 section 7.10] https://tools.ietf.org/html/rfc2710#section-7.10 |
| const DEFAULT_UNSOLICITED_REPORT_INTERVAL: Duration = Duration::from_secs(10); |
| |
| impl Default for MldConfig { |
| fn default() -> Self { |
| MldConfig { |
| unsolicited_report_interval: DEFAULT_UNSOLICITED_REPORT_INTERVAL, |
| send_leave_anyway: false, |
| } |
| } |
| } |
| |
| impl ProtocolSpecific for MldProtocolSpecific { |
| /// The action to turn an MLD host to Idle state immediately. |
| type Action = ImmediateIdleState; |
| type Config = MldConfig; |
| |
| fn cfg_unsolicited_report_interval(cfg: &Self::Config) -> Duration { |
| cfg.unsolicited_report_interval |
| } |
| |
| fn cfg_send_leave_anyway(cfg: &Self::Config) -> bool { |
| cfg.send_leave_anyway |
| } |
| |
| fn get_max_resp_time(resp_time: Duration) -> Duration { |
| resp_time |
| } |
| |
| fn do_query_received_specific( |
| cfg: &Self::Config, |
| actions: &mut Actions<Self>, |
| max_resp_time: Duration, |
| old: Self, |
| ) -> Self { |
| if max_resp_time.as_millis() == 0 { |
| actions.push_specific(ImmediateIdleState); |
| } |
| old |
| } |
| } |
| |
| /// The state on a multicast address. |
| pub(crate) type MldGroupState<I> = GmpStateMachine<I, MldProtocolSpecific>; |
| |
| /// The States on all the interested multicast address of an interface. |
| pub(crate) struct MldInterface<I: Instant> { |
| groups: HashMap<MulticastAddr<Ipv6Addr>, MldGroupState<I>>, |
| } |
| |
| impl<I: Instant> Default for MldInterface<I> { |
| fn default() -> Self { |
| MldInterface { groups: HashMap::new() } |
| } |
| } |
| |
| impl<I: Instant> MldInterface<I> { |
| fn join_group<R: Rng>( |
| &mut self, |
| rng: &mut R, |
| addr: MulticastAddr<Ipv6Addr>, |
| now: I, |
| ) -> Actions<MldProtocolSpecific> { |
| self.groups.entry(addr).or_insert(MldGroupState::default()).join_group(rng, now) |
| } |
| |
| fn leave_group( |
| &mut self, |
| addr: MulticastAddr<Ipv6Addr>, |
| ) -> MldResult<Actions<MldProtocolSpecific>> { |
| match self.groups.remove(&addr).as_mut() { |
| Some(state) => Ok(state.leave_group()), |
| None => Err(MldError::NotAMember { addr: addr.get() }), |
| } |
| } |
| |
| fn report_timer_expired(&mut self, addr: MulticastAddr<Ipv6Addr>) -> MldResult<()> { |
| match self.groups.get_mut(&addr) { |
| Some(state) => { |
| state.report_timer_expired(); |
| Ok(()) |
| } |
| None => Err(MldError::NotAMember { addr: addr.get() }), |
| } |
| } |
| } |
| |
| /// Make our host join a multicast group. |
| pub(crate) fn mld_join_group<C: MldContext>( |
| ctx: &mut C, |
| device: C::DeviceId, |
| group_addr: MulticastAddr<Ipv6Addr>, |
| ) { |
| // TODO(joshlf): Once we figure out how to access the RNG and the state at |
| // the same time, get rid of this hack. For the time being, this is probably |
| // fine because, while the `XorShiftRng` isn't cryptographically secure, its |
| // seed is, which means that, at worst, an attacker will be able to |
| // correlate events generated during this one function call. |
| let mut rng = ctx.new_xorshift_rng(); |
| let now = ctx.now(); |
| let actions = ctx.get_state_mut(device).join_group(&mut rng, group_addr, now); |
| // actions will be `Nothing` if the the host is not in the `NonMember` state. |
| run_actions(ctx, device, actions, group_addr); |
| } |
| |
| /// Make our host leave the multicast group. |
| /// |
| /// If our host is not already a member of the given address, this will result |
| /// in the `IgmpError::NotAMember` error. |
| pub(crate) fn mld_leave_group<C: MldContext>( |
| ctx: &mut C, |
| device: C::DeviceId, |
| group_addr: MulticastAddr<Ipv6Addr>, |
| ) -> MldResult<()> { |
| let actions = ctx.get_state_mut(device).leave_group(group_addr)?; |
| run_actions(ctx, device, actions, group_addr); |
| Ok(()) |
| } |
| |
| /// The timer to delay the MLD report. |
| #[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)] |
| pub(crate) struct MldReportDelay<D> { |
| device: D, |
| group_addr: MulticastAddr<Ipv6Addr>, |
| } |
| |
| impl<D> MldReportDelay<D> { |
| fn new(device: D, group_addr: MulticastAddr<Ipv6Addr>) -> MldReportDelay<D> { |
| MldReportDelay { device, group_addr }.into() |
| } |
| } |
| |
| fn run_actions<C: MldContext>( |
| ctx: &mut C, |
| device: C::DeviceId, |
| actions: Actions<MldProtocolSpecific>, |
| group_addr: MulticastAddr<Ipv6Addr>, |
| ) { |
| for action in actions { |
| if let Err(err) = run_action(ctx, device, action, group_addr) { |
| error!("Error performing action on {} on device {}: {}", group_addr, device, err); |
| } |
| } |
| } |
| |
| /// Interpret the actions generated by the state machine. |
| fn run_action<C: MldContext>( |
| ctx: &mut C, |
| device: C::DeviceId, |
| action: Action<MldProtocolSpecific>, |
| group_addr: MulticastAddr<Ipv6Addr>, |
| ) -> MldResult<()> { |
| match action { |
| Action::Generic(GmpAction::ScheduleReportTimer(delay)) => { |
| ctx.schedule_timer(delay, MldReportDelay::new(device, group_addr)); |
| Ok(()) |
| } |
| Action::Generic(GmpAction::StopReportTimer) => { |
| ctx.cancel_timer(MldReportDelay::new(device, group_addr)); |
| Ok(()) |
| } |
| Action::Generic(GmpAction::SendLeave) => send_mld_packet::<_, &[u8], _>( |
| ctx, |
| device, |
| MulticastAddr::new(crate::ip::IPV6_ALL_ROUTERS).unwrap(), |
| MulticastListenerDone, |
| group_addr, |
| (), |
| ), |
| Action::Generic(GmpAction::SendReport(_)) => send_mld_packet::<_, &[u8], _>( |
| ctx, |
| device, |
| group_addr, |
| MulticastListenerReport, |
| group_addr, |
| (), |
| ), |
| Action::Specific(ImmediateIdleState) => { |
| ctx.cancel_timer(MldReportDelay::new(device, group_addr)); |
| ctx.get_state_mut(device).report_timer_expired(group_addr) |
| } |
| } |
| } |
| |
| /// Send an MLD packet. |
| /// |
| /// The MLD packet being sent should have its `hop_limit` to be 1 and |
| /// a `RouterAlert` option in its Hop-by-Hop Options extensions header. |
| fn send_mld_packet<C: MldContext, B: ByteSlice, M: IcmpMldv1MessageType<B>>( |
| ctx: &mut C, |
| device: C::DeviceId, |
| dst_ip: MulticastAddr<Ipv6Addr>, |
| msg: M, |
| group_addr: M::GroupAddr, |
| max_resp_delay: M::MaxRespDelay, |
| ) -> MldResult<()> { |
| // According to https://tools.ietf.org/html/rfc3590#section-4, |
| // if a valid link-local address is not available for the device (e.g., one |
| // has not been configured), the message is sent with the unspecified |
| // address (::) as the IPv6 source address. |
| |
| let src_ip = |
| ctx.get_ipv6_link_local_addr(device).map_or(Ipv6::UNSPECIFIED_ADDRESS, |x| x.get()); |
| |
| let body = Mldv1MessageBuilder::<M>::new_with_max_resp_delay(group_addr, max_resp_delay) |
| .into_serializer() |
| .encapsulate(IcmpPacketBuilder::new(src_ip, dst_ip.get(), IcmpUnusedCode, msg)) |
| .encapsulate( |
| Ipv6PacketBuilderWithHBHOptions::new( |
| Ipv6PacketBuilder::new(src_ip, dst_ip.get(), 1, IpProto::Icmpv6), |
| &[HopByHopOption { |
| action: ExtensionHeaderOptionAction::SkipAndContinue, |
| mutable: false, |
| data: HopByHopOptionData::RouterAlert { data: 0 }, |
| }], |
| ) |
| .unwrap(), |
| ); |
| ctx.send_frame(MldFrameMetadata::new(device, dst_ip), body) |
| .map_err(|_| MldError::SendFailure { addr: group_addr.into() }) |
| } |
| |
| pub(crate) fn handle_timeout<C: MldContext>(ctx: &mut C, timer: MldReportDelay<C::DeviceId>) { |
| let MldReportDelay { device, group_addr } = timer; |
| send_mld_packet::<_, &[u8], _>( |
| ctx, |
| device, |
| group_addr, |
| MulticastListenerReport, |
| group_addr, |
| (), |
| ); |
| if let Err(e) = ctx.get_state_mut(device).report_timer_expired(group_addr) { |
| error!("MLD timer fired, but an error has occurred: {}", e); |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use std::convert::TryInto; |
| use std::time::Instant; |
| |
| use net_types::ethernet::Mac; |
| use net_types::ip::AddrSubnet; |
| |
| use super::*; |
| use crate::device::{add_ip_addr_subnet, DeviceId}; |
| use crate::ip::gmp::{Action, GmpAction, MemberState}; |
| use crate::ip::icmp::receive_icmpv6_packet; |
| use crate::ip::IPV6_ALL_ROUTERS; |
| use crate::testutil; |
| use crate::testutil::new_rng; |
| use crate::testutil::{DummyEventDispatcher, DummyEventDispatcherBuilder}; |
| use crate::wire::icmp::mld::MulticastListenerQuery; |
| use crate::{Context, EventDispatcher}; |
| |
| #[test] |
| fn test_mld_immediate_report() { |
| // Most of the test surface is covered by the GMP implementation, |
| // MLD specific part is mostly passthrough. This test case is here |
| // because MLD allows a router to ask for report immediately, by |
| // specifying the `MaxRespDelay` to be 0. If this is the case, the |
| // host should send the report immediately instead of setting a timer. |
| let mut s = MldGroupState::default(); |
| let mut rng = new_rng(0); |
| s.join_group(&mut rng, Instant::now()); |
| let actions = s.query_received(&mut rng, Duration::from_secs(0), Instant::now()); |
| let vec = actions.into_iter().collect::<Vec<Action<_>>>(); |
| assert_eq!(vec.len(), 2); |
| assert_eq!(vec[0], Action::Generic(GmpAction::SendReport(MldProtocolSpecific))); |
| assert_eq!(vec[1], Action::Specific(ImmediateIdleState)); |
| } |
| |
| const MY_MAC: Mac = Mac::new([1, 2, 3, 4, 5, 6]); |
| const ROUTER_MAC: Mac = Mac::new([6, 5, 4, 3, 2, 1]); |
| const GROUP_ADDR: Ipv6Addr = |
| Ipv6Addr::new([0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3]); |
| |
| fn receive_mld_query( |
| ctx: &mut Context<DummyEventDispatcher>, |
| device: DeviceId, |
| resp_time: Duration, |
| ) { |
| let my_addr = MY_MAC.to_ipv6_link_local().into_specified(); |
| let router_addr = ROUTER_MAC.to_ipv6_link_local().get(); |
| let buffer = Mldv1MessageBuilder::<MulticastListenerQuery>::new_with_max_resp_delay( |
| GROUP_ADDR, |
| resp_time.try_into().unwrap(), |
| ) |
| .into_serializer() |
| .encapsulate(IcmpPacketBuilder::<_, &[u8], _>::new( |
| router_addr, |
| my_addr, |
| IcmpUnusedCode, |
| MulticastListenerQuery, |
| )) |
| .serialize_vec_outer() |
| .unwrap(); |
| receive_icmpv6_packet(ctx, Some(device), router_addr, my_addr, buffer); |
| } |
| |
| fn receive_mld_report(ctx: &mut Context<DummyEventDispatcher>, device: DeviceId) { |
| let my_addr = MY_MAC.to_ipv6_link_local().into_specified(); |
| let router_addr = ROUTER_MAC.to_ipv6_link_local().get(); |
| let buffer = Mldv1MessageBuilder::<MulticastListenerReport>::new( |
| MulticastAddr::new(GROUP_ADDR).unwrap(), |
| ) |
| .into_serializer() |
| .encapsulate(IcmpPacketBuilder::<_, &[u8], _>::new( |
| router_addr, |
| my_addr, |
| IcmpUnusedCode, |
| MulticastListenerReport, |
| )) |
| .serialize_vec_outer() |
| .unwrap(); |
| receive_icmpv6_packet(ctx, Some(device), router_addr, my_addr, buffer); |
| } |
| |
| fn setup_simple_test_environment() -> (Context<DummyEventDispatcher>, DeviceId) { |
| let mut ctx = DummyEventDispatcherBuilder::default().build(); |
| let dev_id = ctx.state.add_ethernet_device(MY_MAC, 1500); |
| crate::device::initialize_device(&mut ctx, dev_id); |
| add_ip_addr_subnet( |
| &mut ctx, |
| dev_id, |
| AddrSubnet::new(MY_MAC.to_ipv6_link_local().get(), 128).unwrap(), |
| ); |
| (ctx, dev_id) |
| } |
| |
| // ensure the ttl is 1. |
| fn ensure_ttl(frame: &[u8]) { |
| assert_eq!(frame[21], 1); |
| } |
| |
| fn ensure_slice_addr(frame: &[u8], start: usize, end: usize, ip: Ipv6Addr) { |
| let mut bytes = [0u8; 16]; |
| bytes.copy_from_slice(&frame[start..end]); |
| assert_eq!(Ipv6Addr::new(bytes), ip); |
| } |
| |
| // ensure the destination address field in the ICMPv6 packet is correct. |
| fn ensure_dst_addr(frame: &[u8], ip: Ipv6Addr) { |
| ensure_slice_addr(frame, 38, 54, ip); |
| } |
| |
| // ensure the multicast address field in the MLD packet is correct. |
| fn ensure_multicast_addr(frame: &[u8], ip: Ipv6Addr) { |
| ensure_slice_addr(frame, 70, 86, ip); |
| } |
| |
| // ensure a sent frame meets the requirement |
| fn ensure_frame(frame: &[u8], op: u8, dst: Ipv6Addr, multicast: Ipv6Addr) { |
| ensure_ttl(frame); |
| assert_eq!(frame[62], op); |
| // Ensure the length our payload is 32 = 8 (hbh_ext_hdr) + 24 (mld) |
| assert_eq!(frame[19], 32); |
| // Ensure the next header is our HopByHop Extension Header. |
| assert_eq!(frame[20], 0); |
| // Ensure the hop limit is 1. |
| assert_eq!(frame[21], 1); |
| // Ensure there is a RouterAlert HopByHopOption in our sent frame |
| assert_eq!(&frame[54..62], &[58, 0, 5, 2, 0, 0, 1, 0]); |
| ensure_ttl(&frame[..]); |
| ensure_dst_addr(&frame[..], dst); |
| ensure_multicast_addr(&frame[..], multicast); |
| } |
| |
| #[test] |
| fn test_mld_simple_integration() { |
| let (mut ctx, dev_id) = setup_simple_test_environment(); |
| mld_join_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap()); |
| |
| receive_mld_query(&mut ctx, dev_id, Duration::from_secs(10)); |
| assert!(testutil::trigger_next_timer(&mut ctx)); |
| |
| let frame = &ctx.dispatcher.frames_sent().get(0).unwrap().1; |
| |
| // we should get two MLD reports, one for the unsolicited one for the host |
| // to turn into Delay Member state and the other one for the timer being fired. |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 2); |
| // The frames are all reports. |
| for (_, frame) in ctx.dispatcher.frames_sent() { |
| ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR); |
| } |
| } |
| |
| #[test] |
| fn test_mld_immediate_query() { |
| testutil::set_logger_for_test(); |
| let (mut ctx, dev_id) = setup_simple_test_environment(); |
| mld_join_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap()); |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 1); |
| |
| receive_mld_query(&mut ctx, dev_id, Duration::from_secs(0)); |
| // the query says that it wants to hear from us immediately |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 2); |
| // there should be no timers set |
| assert!(!testutil::trigger_next_timer(&mut ctx)); |
| // The frames are all reports. |
| for (_, frame) in ctx.dispatcher.frames_sent() { |
| ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR); |
| } |
| } |
| |
| #[test] |
| fn test_mld_integration_fallback_from_idle() { |
| let (mut ctx, dev_id) = setup_simple_test_environment(); |
| mld_join_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap()); |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 1); |
| |
| assert!(testutil::trigger_next_timer(&mut ctx)); |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 2); |
| |
| receive_mld_query(&mut ctx, dev_id, Duration::from_secs(10)); |
| |
| // We have received a query, hence we are falling back to Delay Member state. |
| let group_state = ctx |
| .state |
| .ipv6 |
| .get_mld_state_mut(dev_id.id()) |
| .groups |
| .get(&MulticastAddr::new(GROUP_ADDR).unwrap()) |
| .unwrap(); |
| match group_state.get_inner() { |
| MemberState::Delaying(_) => {} |
| _ => panic!("Wrong State!"), |
| } |
| |
| assert!(testutil::trigger_next_timer(&mut ctx)); |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 3); |
| // The frames are all reports. |
| for (_, frame) in ctx.dispatcher.frames_sent() { |
| ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR); |
| } |
| } |
| |
| #[test] |
| fn test_mld_integration_immediate_query_wont_fallback() { |
| let (mut ctx, dev_id) = setup_simple_test_environment(); |
| mld_join_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap()); |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 1); |
| |
| assert!(testutil::trigger_next_timer(&mut ctx)); |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 2); |
| |
| receive_mld_query(&mut ctx, dev_id, Duration::from_secs(0)); |
| |
| // Since it is an immediate query, we will send a report immediately and turn |
| // into Idle state again |
| let group_state = ctx |
| .state |
| .ipv6 |
| .get_mld_state_mut(dev_id.id()) |
| .groups |
| .get(&MulticastAddr::new(GROUP_ADDR).unwrap()) |
| .unwrap(); |
| match group_state.get_inner() { |
| MemberState::Idle(_) => {} |
| _ => panic!("Wrong State!"), |
| } |
| |
| // No timers! |
| assert!(!testutil::trigger_next_timer(&mut ctx)); |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 3); |
| // The frames are all reports. |
| for (_, frame) in ctx.dispatcher.frames_sent() { |
| ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR); |
| } |
| } |
| |
| // TODO: It seems like the RNG is always giving us quite small values, so the test logic |
| // for the timers doesn't hold. Ignore the test for now until we have a way to reliably |
| // generate a larger value for duration. |
| #[test] |
| #[ignore] |
| fn test_mld_integration_delay_reset_timer() { |
| let (mut ctx, dev_id) = setup_simple_test_environment(); |
| mld_join_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap()); |
| assert_eq!(ctx.dispatcher.timer_events().count(), 1); |
| let instant1 = ctx.dispatcher.timer_events().nth(0).unwrap().0.clone(); |
| let start = ctx.dispatcher.now(); |
| let duration = instant1 - start; |
| |
| receive_mld_query(&mut ctx, dev_id, duration); |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 2); |
| // The frames are all reports. |
| for (_, frame) in ctx.dispatcher.frames_sent() { |
| ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR); |
| } |
| } |
| |
| #[test] |
| fn test_mld_integration_last_send_leave() { |
| let (mut ctx, dev_id) = setup_simple_test_environment(); |
| mld_join_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap()); |
| assert_eq!(ctx.dispatcher.timer_events().count(), 1); |
| // The initial unsolicited report |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 1); |
| assert!(testutil::trigger_next_timer(&mut ctx)); |
| // The report after the delay |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 2); |
| assert!(mld_leave_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap()).is_ok()); |
| // Our leave message |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 3); |
| // The first two messages should be reports |
| ensure_frame(&ctx.dispatcher().frames_sent()[0].1, 131, GROUP_ADDR, GROUP_ADDR); |
| ensure_frame(&ctx.dispatcher().frames_sent()[1].1, 131, GROUP_ADDR, GROUP_ADDR); |
| // The last one should be the done message whose destination is all routers. |
| ensure_frame(&ctx.dispatcher().frames_sent()[2].1, 132, IPV6_ALL_ROUTERS, GROUP_ADDR); |
| } |
| |
| #[test] |
| fn test_mld_integration_not_last_dont_send_leave() { |
| let (mut ctx, dev_id) = setup_simple_test_environment(); |
| mld_join_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap()); |
| assert_eq!(ctx.dispatcher.timer_events().count(), 1); |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 1); |
| receive_mld_report(&mut ctx, dev_id); |
| assert_eq!(ctx.dispatcher.timer_events().count(), 0); |
| // The report should be discarded because we have received from someone else. |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 1); |
| assert!(mld_leave_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap()).is_ok()); |
| // A leave message is not sent |
| assert_eq!(ctx.dispatcher.frames_sent().len(), 1); |
| // The frames are all reports. |
| for (_, frame) in ctx.dispatcher.frames_sent() { |
| ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR); |
| } |
| } |
| |
| #[test] |
| fn test_mld_unspecified_src_no_addr() { |
| let mut ctx = DummyEventDispatcherBuilder::default().build(); |
| let dev_id = ctx.state.add_ethernet_device(MY_MAC, 1500); |
| crate::device::initialize_device(&mut ctx, dev_id); |
| // The IP address of the device is intentionally unspecified. |
| mld_join_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap()); |
| assert!(testutil::trigger_next_timer(&mut ctx)); |
| for (_, frame) in ctx.dispatcher.frames_sent() { |
| ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR); |
| ensure_slice_addr(&frame, 22, 38, MY_MAC.to_ipv6_link_local().get()); |
| } |
| } |
| |
| #[test] |
| fn test_mld_unspecified_src_not_link_local() { |
| let mut ctx = DummyEventDispatcherBuilder::default().build(); |
| let dev_id = ctx.state.add_ethernet_device(MY_MAC, 1500); |
| crate::device::initialize_device(&mut ctx, dev_id); |
| add_ip_addr_subnet( |
| &mut ctx, |
| dev_id, |
| AddrSubnet::new( |
| // set up a site-local address |
| Ipv6Addr::new([0xfe, 0xf0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]), |
| 128, |
| ) |
| .unwrap(), |
| ); |
| // The IP address of the device is intentionally unspecified. |
| mld_join_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap()); |
| assert!(testutil::trigger_next_timer(&mut ctx)); |
| for (_, frame) in ctx.dispatcher.frames_sent() { |
| ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR); |
| ensure_slice_addr(&frame, 22, 38, MY_MAC.to_ipv6_link_local().get()); |
| } |
| } |
| } |