blob: 2ee5c5ccf42c64d09a5935e9f91cb09928b68196 [file] [log] [blame]
// 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.
//! Internet Group Management Protocol (IGMP).
//!
//! IGMP is a communications protocol used by hosts and adjacent routers on IPv4
//! networks to establish multicast group memberships.
use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::time::Duration;
use failure::Fail;
use log::{debug, error};
use net_types::ip::{AddrSubnet, Ipv4Addr};
use net_types::{MulticastAddr, SpecifiedAddr, SpecifiedAddress, Witness};
use packet::{BufferMut, 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;
use crate::wire::igmp::{
messages::{IgmpLeaveGroup, IgmpMembershipReportV1, IgmpMembershipReportV2, IgmpPacket},
IgmpMessage, IgmpPacketBuilder, MessageType,
};
use crate::Instant;
/// Metadata for sending an IGMP packet.
///
/// `IgmpPacketMetadata` is used by [`IgmpContext`]'s [`FrameContext`] bound.
/// When [`FrameContext::send_frame`] is called with an `IgmpPacketMetadata`,
/// the body will be encapsulated in an IP packet with a TTL of 1 and with the
/// "Router Alert" option set.
pub(crate) struct IgmpPacketMetadata<D> {
pub(crate) device: D,
pub(crate) src_ip: SpecifiedAddr<Ipv4Addr>,
pub(crate) dst_ip: MulticastAddr<Ipv4Addr>,
}
impl<D> IgmpPacketMetadata<D> {
fn new(
device: D,
src_ip: SpecifiedAddr<Ipv4Addr>,
dst_ip: MulticastAddr<Ipv4Addr>,
) -> IgmpPacketMetadata<D> {
IgmpPacketMetadata { device, src_ip, dst_ip }
}
}
/// The execution context for the Internet Group Management Protocol (IGMP).
pub(crate) trait IgmpContext:
IpDeviceIdContext
+ TimerContext<IgmpTimerId<<Self as IpDeviceIdContext>::DeviceId>>
+ RngContext
+ StateContext<
<Self as IpDeviceIdContext>::DeviceId,
IgmpInterface<<Self as InstantContext>::Instant>,
> + FrameContext<EmptyBuf, IgmpPacketMetadata<<Self as IpDeviceIdContext>::DeviceId>>
{
/// Gets an IP address and subnet associated with this device.
fn get_ip_addr_subnet(&self, device: Self::DeviceId) -> Option<AddrSubnet<Ipv4Addr>>;
}
/// Receive an IGMP message in an IP packet.
pub(crate) fn receive_igmp_packet<C: IgmpContext, B: BufferMut>(
ctx: &mut C,
device: C::DeviceId,
src_ip: Ipv4Addr,
dst_ip: SpecifiedAddr<Ipv4Addr>,
mut buffer: B,
) {
let packet = match buffer.parse_with::<_, IgmpPacket<&[u8]>>(()) {
Ok(packet) => packet,
Err(err) => {
debug!("Cannot parse the incoming IGMP packet, dropping.");
return;
} // TODO: Do something else here?
};
if let Err(e) = match packet {
IgmpPacket::MembershipQueryV2(msg) => {
let now = ctx.now();
handle_igmp_message(ctx, device, msg, |rng, state, msg| {
state.query_received(rng, msg.max_response_time().into(), now)
})
}
IgmpPacket::MembershipReportV1(msg) => {
handle_igmp_message(ctx, device, msg, |_, state, _| state.report_received())
}
IgmpPacket::MembershipReportV2(msg) => {
handle_igmp_message(ctx, device, msg, |_, state, _| state.report_received())
}
IgmpPacket::LeaveGroup(_) => {
debug!("Hosts are not interested in Leave Group messages");
return;
}
_ => {
debug!("We do not support IGMPv3 yet");
return;
}
} {
error!("Error occurred when handling IGMPv2 message: {}", e);
}
}
fn handle_igmp_message<C: IgmpContext, B: ByteSlice, M, F>(
ctx: &mut C,
device: C::DeviceId,
msg: IgmpMessage<B, M>,
handler: F,
) -> IgmpResult<(), C::DeviceId>
where
M: MessageType<B, FixedHeader = Ipv4Addr>,
F: Fn(
&mut XorShiftRng,
&mut IgmpGroupState<C::Instant>,
&IgmpMessage<B, M>,
) -> Actions<Igmpv2ProtocolSpecific>,
{
// 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 = msg.group_addr();
if !group_addr.is_specified() {
let mut addr_and_actions = ctx
.get_state_mut(device)
.groups
.iter_mut()
.map(|(addr, state)| (addr.clone(), handler(&mut rng, state, &msg)))
.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, &msg),
None => return Err(IgmpError::NotAMember { addr: *group_addr }),
};
// `group_addr` here must be a multicast address for similar reasons
run_actions(ctx, device, actions, group_addr);
Ok(())
} else {
Err(IgmpError::NotAMember { addr: group_addr })
}
}
#[derive(Debug, Fail)]
pub(crate) enum IgmpError<D: Display + Debug + Send + Sync + 'static> {
/// 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: Ipv4Addr },
/// Failed to send an IGMP packet.
#[fail(display = "failed to send out an IGMP packet to address: {}", addr)]
SendFailure { addr: Ipv4Addr },
/// The given device does not have an assigned IP address.
#[fail(display = "no ip address is associated with the device: {}", device)]
NoIpAddress { device: D },
}
pub(crate) type IgmpResult<T, D> = Result<T, IgmpError<D>>;
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub(crate) enum IgmpTimerId<D> {
/// The timer used to switch a host from Delay Member state to Idle Member
/// state.
ReportDelay { device: D, group_addr: MulticastAddr<Ipv4Addr> },
/// The timer used to determine whether there is a router speaking IGMPv1.
V1RouterPresent { device: D },
}
impl<D> IgmpTimerId<D> {
fn new_report_delay(device: D, group_addr: MulticastAddr<Ipv4Addr>) -> IgmpTimerId<D> {
IgmpTimerId::ReportDelay { device, group_addr }
}
fn new_v1_router_present(device: D) -> IgmpTimerId<D> {
IgmpTimerId::V1RouterPresent { device }
}
}
pub(crate) fn handle_timeout<C: IgmpContext>(ctx: &mut C, timer: IgmpTimerId<C::DeviceId>) {
match timer {
IgmpTimerId::ReportDelay { device, group_addr } => {
let actions = match ctx.get_state_mut(device).groups.get_mut(&group_addr) {
Some(state) => state.report_timer_expired(),
None => {
error!("Not already a member");
return;
}
};
run_actions(ctx, device, actions, group_addr);
}
IgmpTimerId::V1RouterPresent { device } => {
for (_, state) in ctx.get_state_mut(device).groups.iter_mut() {
state.v1_router_present_timer_expired();
}
}
}
}
fn send_igmp_message<C: IgmpContext, M>(
ctx: &mut C,
device: C::DeviceId,
group_addr: MulticastAddr<Ipv4Addr>,
dst_ip: MulticastAddr<Ipv4Addr>,
max_resp_time: M::MaxRespTime,
) -> IgmpResult<(), C::DeviceId>
where
M: MessageType<EmptyBuf, FixedHeader = Ipv4Addr, VariableBody = ()>,
{
let src_ip = match ctx.get_ip_addr_subnet(device) {
Some(addr_subnet) => addr_subnet.addr(),
None => return Err(IgmpError::NoIpAddress { device }),
};
let body =
IgmpPacketBuilder::<EmptyBuf, M>::new_with_resp_time(group_addr.get(), max_resp_time);
ctx.send_frame(IgmpPacketMetadata::new(device, src_ip, dst_ip), body.into_serializer())
.map_err(|_| IgmpError::SendFailure { addr: *group_addr })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Igmpv2ProtocolSpecific {
v1_router_present: bool,
}
impl Default for Igmpv2ProtocolSpecific {
fn default() -> Self {
Igmpv2ProtocolSpecific { v1_router_present: false }
}
}
#[derive(PartialEq, Eq, Debug)]
enum Igmpv2Actions {
ScheduleV1RouterPresentTimer(Duration),
}
struct Igmpv2HostConfig {
// When a host wants to send a report not because
// of a query, this value is used as the delay timer.
unsolicited_report_interval: Duration,
// When this option is true, the host can send a leave
// message even when it is not the last one in the multicast
// group.
send_leave_anyway: bool,
// Default timer value for Version 1 Router Present Timeout.
v1_router_present_timeout: Duration,
}
/// The default value for `unsolicited_report_interval` as per [RFC 2236 section 8.10].
///
/// [RFC 2236 section 8.10]: https://tools.ietf.org/html/rfc2236#section-8.10
const DEFAULT_UNSOLICITED_REPORT_INTERVAL: Duration = Duration::from_secs(10);
/// The default value for `v1_router_present_timeout` as per [RFC 2236 section 8.11].
///
/// [RFC 2236 section 8.11]: https://tools.ietf.org/html/rfc2236#section-8.11
const DEFAULT_V1_ROUTER_PRESENT_TIMEOUT: Duration = Duration::from_secs(400);
/// The default value for the `MaxRespTime` if the query is a V1 query, whose
/// `MaxRespTime` field is 0 in the packet. Please refer to [RFC 2236 section 4]
///
/// [RFC 2236 section 4]: https://tools.ietf.org/html/rfc2236#section-4
const DEFAULT_V1_QUERY_MAX_RESP_TIME: Duration = Duration::from_secs(10);
impl Default for Igmpv2HostConfig {
fn default() -> Self {
Igmpv2HostConfig {
unsolicited_report_interval: DEFAULT_UNSOLICITED_REPORT_INTERVAL,
send_leave_anyway: false,
v1_router_present_timeout: DEFAULT_V1_ROUTER_PRESENT_TIMEOUT,
}
}
}
impl ProtocolSpecific for Igmpv2ProtocolSpecific {
type Action = Igmpv2Actions;
type Config = Igmpv2HostConfig;
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 {
if resp_time.as_micros() == 0 {
DEFAULT_V1_QUERY_MAX_RESP_TIME
} else {
resp_time
}
}
fn do_query_received_specific(
cfg: &Self::Config,
actions: &mut Actions<Self>,
max_resp_time: Duration,
old: Igmpv2ProtocolSpecific,
) -> Igmpv2ProtocolSpecific {
// IGMPv2 hosts should be compatible with routers that only speak IGMPv1.
// When an IGMPv2 host receives an IGMPv1 query (whose `MaxRespCode` is 0),
// it should set up a timer and only respond with IGMPv1 responses before
// the timer expires. Please refer to https://tools.ietf.org/html/rfc2236#section-4
// for details.
let new_ps = Igmpv2ProtocolSpecific { v1_router_present: max_resp_time.as_micros() == 0 };
if new_ps.v1_router_present {
actions.push_specific(Igmpv2Actions::ScheduleV1RouterPresentTimer(
cfg.v1_router_present_timeout,
));
}
new_ps
}
}
type IgmpGroupState<I> = GmpStateMachine<I, Igmpv2ProtocolSpecific>;
impl<I: Instant> IgmpGroupState<I> {
fn v1_router_present_timer_expired(&mut self) {
self.update_with_protocol_specific(Igmpv2ProtocolSpecific { v1_router_present: false });
}
}
/// This is used to represent the groups that an IGMP host is interested in.
pub(crate) struct IgmpInterface<I: Instant> {
groups: HashMap<MulticastAddr<Ipv4Addr>, IgmpGroupState<I>>,
}
impl<I: Instant> Default for IgmpInterface<I> {
fn default() -> Self {
IgmpInterface { groups: HashMap::new() }
}
}
fn run_actions<C: IgmpContext>(
ctx: &mut C,
device: C::DeviceId,
actions: Actions<Igmpv2ProtocolSpecific>,
group_addr: MulticastAddr<Ipv4Addr>,
) {
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
fn run_action<C: IgmpContext>(
ctx: &mut C,
device: C::DeviceId,
action: Action<Igmpv2ProtocolSpecific>,
group_addr: MulticastAddr<Ipv4Addr>,
) -> IgmpResult<(), C::DeviceId> {
match action {
Action::Generic(GmpAction::ScheduleReportTimer(duration)) => {
ctx.schedule_timer(duration, IgmpTimerId::new_report_delay(device, group_addr));
}
Action::Generic(GmpAction::StopReportTimer) => {
ctx.cancel_timer(IgmpTimerId::new_report_delay(device, group_addr));
}
Action::Generic(GmpAction::SendLeave) => {
send_igmp_message::<_, IgmpLeaveGroup>(
ctx,
device,
group_addr,
MulticastAddr::new(crate::ip::IPV4_ALL_ROUTERS).unwrap(),
(),
)?;
}
Action::Generic(GmpAction::SendReport(Igmpv2ProtocolSpecific { v1_router_present })) => {
if v1_router_present {
send_igmp_message::<_, IgmpMembershipReportV1>(
ctx,
device,
group_addr,
group_addr,
(),
)?;
} else {
send_igmp_message::<_, IgmpMembershipReportV2>(
ctx,
device,
group_addr,
group_addr,
(),
)?;
}
}
Action::Specific(Igmpv2Actions::ScheduleV1RouterPresentTimer(duration)) => {
ctx.schedule_timer(duration, IgmpTimerId::new_v1_router_present(device));
}
}
Ok(())
}
impl<I: Instant> IgmpInterface<I> {
fn join_group<R: Rng>(
&mut self,
rng: &mut R,
addr: MulticastAddr<Ipv4Addr>,
now: I,
) -> Actions<Igmpv2ProtocolSpecific> {
self.groups.entry(addr).or_insert(GmpStateMachine::default()).join_group(rng, now)
}
fn leave_group<D: Display + Debug + Send + Sync>(
&mut self,
addr: MulticastAddr<Ipv4Addr>,
) -> IgmpResult<Actions<Igmpv2ProtocolSpecific>, D> {
match self.groups.remove(&addr).as_mut() {
Some(state) => Ok(state.leave_group()),
None => Err(IgmpError::NotAMember { addr: addr.get() }),
}
}
}
/// Make our host join a multicast group.
pub(crate) fn igmp_join_group<C: IgmpContext>(
ctx: &mut C,
device: C::DeviceId,
group_addr: MulticastAddr<Ipv4Addr>,
) {
let now = ctx.now();
// 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 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 igmp_leave_group<C: IgmpContext>(
ctx: &mut C,
device: C::DeviceId,
group_addr: MulticastAddr<Ipv4Addr>,
) -> IgmpResult<(), C::DeviceId> {
let actions = ctx.get_state_mut(device).leave_group(group_addr)?;
run_actions(ctx, device, actions, group_addr);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::convert::TryInto;
use std::time;
use net_types::ethernet::Mac;
use net_types::ip::AddrSubnet;
use packet::serialize::{Buf, Serializer};
use crate::device::ethernet::*;
use crate::device::DeviceId;
use crate::ip::gmp::{Action, GmpAction, MemberState};
use crate::testutil::{self, *};
use crate::wire::igmp::messages::IgmpMembershipQueryV2;
use crate::{Context, EventDispatcher, StackStateBuilder};
fn at_least_one_action(
actions: Actions<Igmpv2ProtocolSpecific>,
action: Action<Igmpv2ProtocolSpecific>,
) -> bool {
actions.into_iter().any(|a| a == action)
}
#[test]
fn test_igmp_state_with_igmpv1_router() {
let mut rng = new_rng(0);
let mut s = IgmpGroupState::default();
s.join_group(&mut rng, time::Instant::now());
s.query_received(&mut rng, Duration::from_secs(0), time::Instant::now());
let actions = s.report_timer_expired();
at_least_one_action(
actions,
Action::<Igmpv2ProtocolSpecific>::Generic(GmpAction::SendReport(
Igmpv2ProtocolSpecific { v1_router_present: true },
)),
);
}
#[test]
fn test_igmp_state_igmpv1_router_present_timer_expires() {
let mut s = IgmpGroupState::default();
let mut rng = new_rng(0);
s.join_group(&mut rng, time::Instant::now());
s.query_received(&mut rng, Duration::from_secs(0), time::Instant::now());
match s.get_inner() {
MemberState::Delaying(state) => {
assert!(state.get_protocol_specific().v1_router_present);
}
_ => panic!("Wrong State!"),
}
s.v1_router_present_timer_expired();
match s.get_inner() {
MemberState::Delaying(state) => {
assert!(!state.get_protocol_specific().v1_router_present);
}
_ => panic!("Wrong State!"),
}
s.query_received(&mut rng, Duration::from_secs(0), time::Instant::now());
s.report_received();
s.v1_router_present_timer_expired();
match s.get_inner() {
MemberState::Idle(state) => {
assert!(!state.get_protocol_specific().v1_router_present);
}
_ => panic!("Wrong State!"),
}
}
const MY_ADDR: SpecifiedAddr<Ipv4Addr> =
unsafe { SpecifiedAddr::new_unchecked(Ipv4Addr::new([192, 168, 0, 2])) };
const ROUTER_ADDR: Ipv4Addr = Ipv4Addr::new([192, 168, 0, 1]);
const OTHER_HOST_ADDR: Ipv4Addr = Ipv4Addr::new([192, 168, 0, 3]);
const GROUP_ADDR: Ipv4Addr = Ipv4Addr::new([224, 0, 0, 3]);
const GROUP_ADDR_2: Ipv4Addr = Ipv4Addr::new([224, 0, 0, 4]);
fn receive_igmp_query<C: IgmpContext>(ctx: &mut C, device: C::DeviceId, resp_time: Duration) {
let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipQueryV2>::new_with_resp_time(
GROUP_ADDR,
resp_time.try_into().unwrap(),
);
let mut buff = ser.into_serializer().serialize_vec_outer().unwrap();
receive_igmp_packet(ctx, device, ROUTER_ADDR, MY_ADDR, buff);
}
fn receive_igmp_general_query<C: IgmpContext>(
ctx: &mut C,
device: C::DeviceId,
resp_time: Duration,
) {
let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipQueryV2>::new_with_resp_time(
Ipv4Addr::new([0, 0, 0, 0]),
resp_time.try_into().unwrap(),
);
let mut buff = ser.into_serializer().serialize_vec_outer().unwrap();
receive_igmp_packet(ctx, device, ROUTER_ADDR, MY_ADDR, buff);
}
fn receive_igmp_report<D: EventDispatcher>(ctx: &mut Context<D>, device: DeviceId) {
let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipReportV2>::new(GROUP_ADDR);
let mut buff = ser.into_serializer().serialize_vec_outer().unwrap();
receive_igmp_packet(ctx, device, OTHER_HOST_ADDR, MY_ADDR, buff);
}
fn setup_simple_test_environment() -> (Context<DummyEventDispatcher>, DeviceId) {
let mut stack_builder = StackStateBuilder::default();
// Most tests do not need NDP's DAD or router solicitation so disable it here.
let mut ndp_configs = crate::device::ndp::NdpConfigurations::default();
ndp_configs.set_dup_addr_detect_transmits(None);
ndp_configs.set_max_router_solicitations(None);
stack_builder.device_builder().set_default_ndp_configs(ndp_configs);
let mut ctx = Context::new(stack_builder.build(), DummyEventDispatcher::default());
let dev_id = ctx.state.add_ethernet_device(Mac::new([1, 2, 3, 4, 5, 6]), 1500);
crate::device::initialize_device(&mut ctx, dev_id);
add_ip_addr_subnet(&mut ctx, dev_id.id(), AddrSubnet::new(MY_ADDR.get(), 24).unwrap());
(ctx, dev_id)
}
fn ensure_ttl_ihl_rtr(ctx: &mut Context<DummyEventDispatcher>) {
for (_, frame) in ctx.dispatcher.frames_sent() {
assert_eq!(frame[22], 1); // TTL,
assert_eq!(&frame[34..38], &[148, 4, 0, 0]); // RTR
assert_eq!(frame[14], 0x46); // IHL
}
}
#[test]
fn test_igmp_simple_integration() {
let (mut ctx, dev_id) = setup_simple_test_environment();
igmp_join_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap());
receive_igmp_query(&mut ctx, dev_id, Duration::from_secs(10));
assert!(testutil::trigger_next_timer(&mut ctx));
// we should get two Igmpv2 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);
}
#[test]
fn test_igmp_integration_fallback_from_idle() {
let (mut ctx, dev_id) = setup_simple_test_environment();
igmp_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_igmp_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
.ipv4
.get_igmp_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);
let mac: [u8; 6] = [0x01, 0x00, 0x5e, 0, 0, 3];
for (_, frame) in ctx.dispatcher.frames_sent() {
assert_eq!(frame[0..6], mac[..]);
}
ensure_ttl_ihl_rtr(&mut ctx);
}
#[test]
fn test_igmp_integration_igmpv1_router_present() {
let (mut ctx, dev_id) = setup_simple_test_environment();
igmp_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();
receive_igmp_query(&mut ctx, dev_id, Duration::from_secs(0));
assert_eq!(ctx.dispatcher.frames_sent().len(), 1);
// Since we have heard from the v1 router, we should have set our flag
let group_state = ctx
.state
.ipv4
.get_igmp_state_mut(dev_id.id())
.groups
.get(&MulticastAddr::new(GROUP_ADDR).unwrap())
.unwrap();
match group_state.get_inner() {
MemberState::Delaying(state) => {
assert!(state.get_protocol_specific().v1_router_present)
}
_ => panic!("Wrong State!"),
}
assert_eq!(ctx.dispatcher.frames_sent().len(), 1);
// Two timers: one for the delayed report, one for the v1 router timer
assert_eq!(ctx.dispatcher.timer_events().count(), 2);
let instant2 = ctx.dispatcher.timer_events().nth(0).unwrap().0.clone();
assert_eq!(instant1, instant2);
assert!(testutil::trigger_next_timer(&mut ctx));
// After the first timer, we send out our V1 report.
assert_eq!(ctx.dispatcher.frames_sent().len(), 2);
// the last frame being sent should be a V1 report.
let (_, frame) = ctx.dispatcher.frames_sent().last().unwrap();
// 34 and 0x12 are hacky but they can quickly tell it is a V1 report.
assert_eq!(frame[38], 0x12);
assert!(testutil::trigger_next_timer(&mut ctx));
// After the second timer, we should reset our flag for v1 routers.
let group_state = ctx
.state
.ipv4
.get_igmp_state_mut(dev_id.id())
.groups
.get(&MulticastAddr::new(GROUP_ADDR).unwrap())
.unwrap();
match group_state.get_inner() {
MemberState::Idle(state) => assert!(!state.get_protocol_specific().v1_router_present),
_ => panic!("Wrong State!"),
}
receive_igmp_query(&mut ctx, dev_id, Duration::from_secs(10));
assert!(testutil::trigger_next_timer(&mut ctx));
assert_eq!(ctx.dispatcher.frames_sent().len(), 3);
// Now we should get V2 report
assert_eq!(ctx.dispatcher.frames_sent().last().unwrap().1[38], 0x16);
ensure_ttl_ihl_rtr(&mut ctx);
}
// TODO(zeling): add this test back once we can have a reliable and
// deterministic way to make `duration` longer than 100ms.
#[test]
#[ignore]
fn test_igmp_integration_delay_reset_timer() {
let (mut ctx, dev_id) = setup_simple_test_environment();
igmp_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 = Duration::from_micros(((instant1 - start).as_micros() / 2) as u64);
if duration.as_millis() < 100 {
return;
}
receive_igmp_query(&mut ctx, dev_id, duration);
assert_eq!(ctx.dispatcher.frames_sent().len(), 1);
let instant2 = ctx.dispatcher.timer_events().nth(0).unwrap().0.clone();
// because of the message, our timer should be reset to a nearer future
assert!(instant2 <= instant1);
assert!(trigger_next_timer(&mut ctx));
assert!(ctx.dispatcher.now() - start <= duration);
assert_eq!(ctx.dispatcher.frames_sent().len(), 2);
// make sure it is a V2 report
assert_eq!(ctx.dispatcher.frames_sent().last().unwrap().1[38], 0x16);
ensure_ttl_ihl_rtr(&mut ctx);
}
#[test]
fn test_igmp_integration_last_send_leave() {
let (mut ctx, dev_id) = setup_simple_test_environment();
igmp_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!(igmp_leave_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap()).is_ok());
// our leave message
assert_eq!(ctx.dispatcher.frames_sent().len(), 3);
let leave_frame = &ctx.dispatcher.frames_sent().last().unwrap().1;
// make sure it is a leave message
assert_eq!(leave_frame[38], 0x17);
// and the destination is ALL-ROUTERS (224.0.0.2)
assert_eq!(leave_frame[30], 224);
assert_eq!(leave_frame[31], 0);
assert_eq!(leave_frame[32], 0);
assert_eq!(leave_frame[33], 2);
ensure_ttl_ihl_rtr(&mut ctx);
}
#[test]
fn test_igmp_integration_not_last_dont_send_leave() {
let (mut ctx, dev_id) = setup_simple_test_environment();
igmp_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_igmp_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!(igmp_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);
ensure_ttl_ihl_rtr(&mut ctx);
}
#[test]
fn test_receive_general_query() {
let (mut ctx, dev_id) = setup_simple_test_environment();
igmp_join_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR).unwrap());
igmp_join_group(&mut ctx, dev_id, MulticastAddr::new(GROUP_ADDR_2).unwrap());
assert_eq!(ctx.dispatcher.timer_events().count(), 2);
// The initial unsolicited report
assert_eq!(ctx.dispatcher.frames_sent().len(), 2);
assert!(trigger_next_timer(&mut ctx));
assert!(trigger_next_timer(&mut ctx));
assert_eq!(ctx.dispatcher.frames_sent().len(), 4);
receive_igmp_general_query(&mut ctx, dev_id, Duration::from_secs(10));
// Two new timers should be there.
assert_eq!(ctx.dispatcher.timer_events().count(), 2);
assert!(trigger_next_timer(&mut ctx));
assert!(trigger_next_timer(&mut ctx));
// Two new reports should be sent
assert_eq!(ctx.dispatcher.frames_sent().len(), 6);
ensure_ttl_ihl_rtr(&mut ctx);
}
}