use core::fmt::Debug;
use core::time::Duration;
use log::{debug, error};
use net_declare::net_ip_v4;
use net_types::ip::{AddrSubnet, Ip as _, Ipv4, Ipv4Addr};
use net_types::{MulticastAddr, SpecifiedAddr, Witness};
use netstack3_base::{
AnyDevice, CoreTimerContext, DeviceIdContext, ErrorAndSerializer, HandleableTimer,
Ipv4DeviceAddr, TimerContext, WeakDeviceIdentifier,
};
use packet::{BufferMut, EmptyBuf, InnerPacketBuilder, PacketBuilder, Serializer};
use packet_formats::gmp::GmpReportGroupRecord;
use packet_formats::igmp::messages::{
IgmpLeaveGroup, IgmpMembershipQueryV2, IgmpMembershipQueryV3, IgmpMembershipReportV1,
IgmpMembershipReportV2, IgmpMembershipReportV3Builder, IgmpPacket,
};
use packet_formats::igmp::{IgmpMessage, IgmpPacketBuilder, MessageType};
use packet_formats::ip::Ipv4Proto;
use packet_formats::ipv4::options::Ipv4Option;
use packet_formats::ipv4::{
Ipv4OptionsTooLongError, Ipv4PacketBuilder, Ipv4PacketBuilderWithOptions,
};
use packet_formats::utils::NonZeroDuration;
use thiserror::Error;
use zerocopy::SplitByteSlice;
use crate::internal::base::{IpDeviceMtuContext, IpLayerHandler, IpPacketDestination};
use crate::internal::gmp::{
self, v2, GmpBindingsContext, GmpBindingsTypes, GmpContext, GmpContextInner, GmpGroupState,
GmpMode, GmpStateContext, GmpStateRef, GmpTimerId, GmpTypeLayout, IpExt, MulticastGroupSet,
NotAMemberErr,
};
const ALL_IGMPV3_CAPABLE_ROUTERS: MulticastAddr<Ipv4Addr> =
unsafe { MulticastAddr::new_unchecked(net_ip_v4!("224.0.0.22")) };
pub trait IgmpBindingsTypes: GmpBindingsTypes {}
impl<BT> IgmpBindingsTypes for BT where BT: GmpBindingsTypes {}
pub trait IgmpBindingsContext: GmpBindingsContext + 'static {}
impl<BC> IgmpBindingsContext for BC where BC: GmpBindingsContext + 'static {}
pub struct IgmpState<BT: IgmpBindingsTypes> {
v1_router_present_timer: BT::Timer,
v1_router_present: bool,
}
impl<BC: IgmpBindingsTypes + TimerContext> IgmpState<BC> {
pub fn new<D: WeakDeviceIdentifier, CC: CoreTimerContext<IgmpTimerId<D>, BC>>(
bindings_ctx: &mut BC,
device: D,
) -> Self {
Self {
v1_router_present_timer: CC::new_timer(
bindings_ctx,
IgmpTimerId::V1RouterPresent { device },
),
v1_router_present: false,
}
}
}
pub trait IgmpContextMarker {}
pub trait IgmpStateContext<BT: IgmpBindingsTypes>:
DeviceIdContext<AnyDevice> + IgmpContextMarker
{
fn with_igmp_state<O, F: FnOnce(&MulticastGroupSet<Ipv4Addr, GmpGroupState<Ipv4, BT>>) -> O>(
&mut self,
device: &Self::DeviceId,
cb: F,
) -> O;
}
pub trait IgmpSendContext<BT: IgmpBindingsTypes>:
DeviceIdContext<AnyDevice> + IpLayerHandler<Ipv4, BT> + IpDeviceMtuContext<Ipv4>
{
fn get_ip_addr_subnet(
&mut self,
device: &Self::DeviceId,
) -> Option<AddrSubnet<Ipv4Addr, Ipv4DeviceAddr>>;
}
pub trait IgmpContext<BT: IgmpBindingsTypes>:
DeviceIdContext<AnyDevice> + IgmpContextMarker
{
type SendContext<'a>: IgmpSendContext<BT, DeviceId = Self::DeviceId> + 'a;
fn with_igmp_state_mut<
O,
F: for<'a> FnOnce(
Self::SendContext<'a>,
GmpStateRef<'a, Ipv4, Self, BT>,
&'a mut IgmpState<BT>,
) -> O,
>(
&mut self,
device: &Self::DeviceId,
cb: F,
) -> O;
}
pub trait IgmpPacketHandler<BC, DeviceId> {
fn receive_igmp_packet<B: BufferMut>(
&mut self,
bindings_ctx: &mut BC,
device: &DeviceId,
src_ip: Ipv4Addr,
dst_ip: SpecifiedAddr<Ipv4Addr>,
buffer: B,
);
}
impl<BC: IgmpBindingsContext, CC: IgmpContext<BC>> IgmpPacketHandler<BC, CC::DeviceId> for CC {
fn receive_igmp_packet<B: BufferMut>(
&mut self,
bindings_ctx: &mut BC,
device: &CC::DeviceId,
_src_ip: Ipv4Addr,
_dst_ip: SpecifiedAddr<Ipv4Addr>,
mut buffer: B,
) {
let packet = match buffer.parse_with::<_, IgmpPacket<&[u8]>>(()) {
Ok(packet) => packet,
Err(_) => {
debug!("Cannot parse the incoming IGMP packet, dropping.");
return;
}
};
let result = match packet {
IgmpPacket::MembershipQueryV2(msg) => {
gmp::v1::handle_query_message(self, bindings_ctx, device, &msg).map_err(Into::into)
}
IgmpPacket::MembershipQueryV3(msg) => {
gmp::v2::handle_query_message(self, bindings_ctx, device, &msg).map_err(Into::into)
}
IgmpPacket::MembershipReportV1(msg) => {
let addr = msg.group_addr();
MulticastAddr::new(addr).map_or(Err(IgmpError::NotAMember { addr }), |group_addr| {
gmp::v1::handle_report_message(self, bindings_ctx, device, group_addr)
.map_err(Into::into)
})
}
IgmpPacket::MembershipReportV2(msg) => {
let addr = msg.group_addr();
MulticastAddr::new(addr).map_or(Err(IgmpError::NotAMember { addr }), |group_addr| {
gmp::v1::handle_report_message(self, bindings_ctx, device, group_addr)
.map_err(Into::into)
})
}
IgmpPacket::LeaveGroup(_) => {
debug!("Hosts are not interested in Leave Group messages");
return;
}
IgmpPacket::MembershipReportV3(_) => {
debug!("Hosts are not interested in IGMPv3 report messages");
return;
}
};
result.unwrap_or_else(|e| {
debug!("Error occurred when handling IGMPv2 message: {}", e);
})
}
}
impl<B: SplitByteSlice> gmp::v1::QueryMessage<Ipv4> for IgmpMessage<B, IgmpMembershipQueryV2> {
fn group_addr(&self) -> Ipv4Addr {
self.group_addr()
}
fn max_response_time(&self) -> Duration {
self.max_response_time().into()
}
}
impl<B: SplitByteSlice> gmp::v2::QueryMessage<Ipv4> for IgmpMessage<B, IgmpMembershipQueryV3> {
fn as_v1(&self) -> impl gmp::v1::QueryMessage<Ipv4> + '_ {
self.as_v2_query()
}
fn robustness_variable(&self) -> u8 {
self.header().querier_robustness_variable()
}
fn query_interval(&self) -> Duration {
self.header().querier_query_interval()
}
fn group_address(&self) -> Ipv4Addr {
self.header().group_address()
}
fn max_response_time(&self) -> Duration {
self.max_response_time().into()
}
fn sources(&self) -> impl Iterator<Item = Ipv4Addr> + '_ {
self.body().iter().copied()
}
}
impl IpExt for Ipv4 {
fn should_perform_gmp(addr: MulticastAddr<Ipv4Addr>) -> bool {
addr != Ipv4::ALL_SYSTEMS_MULTICAST_ADDRESS
}
}
impl<BT: IgmpBindingsTypes, CC: DeviceIdContext<AnyDevice> + IgmpContextMarker>
GmpTypeLayout<Ipv4, BT> for CC
{
type Actions = Igmpv2Actions;
type Config = IgmpConfig;
}
impl<BT: IgmpBindingsTypes, CC: IgmpStateContext<BT>> GmpStateContext<Ipv4, BT> for CC {
fn with_gmp_state<O, F: FnOnce(&MulticastGroupSet<Ipv4Addr, GmpGroupState<Ipv4, BT>>) -> O>(
&mut self,
device: &Self::DeviceId,
cb: F,
) -> O {
self.with_igmp_state(device, cb)
}
}
impl<BC: IgmpBindingsContext, CC: IgmpContext<BC>> GmpContext<Ipv4, BC> for CC {
type Inner<'a> = IgmpContextInner<'a, CC::SendContext<'a>, BC>;
fn with_gmp_state_mut_and_ctx<
O,
F: FnOnce(Self::Inner<'_>, GmpStateRef<'_, Ipv4, Self, BC>) -> O,
>(
&mut self,
device: &Self::DeviceId,
cb: F,
) -> O {
self.with_igmp_state_mut(device, |core_ctx, state_ref, igmp_state| {
let inner = IgmpContextInner { igmp_state, core_ctx };
cb(inner, state_ref)
})
}
}
pub struct IgmpContextInner<'a, CC, BT: IgmpBindingsTypes> {
igmp_state: &'a mut IgmpState<BT>,
core_ctx: CC,
}
impl<CC, BT: IgmpBindingsTypes> GmpTypeLayout<Ipv4, BT> for IgmpContextInner<'_, CC, BT>
where
CC: DeviceIdContext<AnyDevice>,
{
type Actions = Igmpv2Actions;
type Config = IgmpConfig;
}
impl<BT, CC> DeviceIdContext<AnyDevice> for IgmpContextInner<'_, CC, BT>
where
CC: DeviceIdContext<AnyDevice>,
BT: IgmpBindingsTypes,
{
type DeviceId = CC::DeviceId;
type WeakDeviceId = CC::WeakDeviceId;
}
impl<BC, CC> GmpContextInner<Ipv4, BC> for IgmpContextInner<'_, CC, BC>
where
CC: IgmpSendContext<BC>,
BC: IgmpBindingsContext,
{
fn send_message_v1(
&mut self,
bindings_ctx: &mut BC,
device: &Self::DeviceId,
group_addr: MulticastAddr<Ipv4Addr>,
msg_type: gmp::v1::GmpMessageType,
) {
let Self { igmp_state: IgmpState { v1_router_present, .. }, core_ctx } = self;
let result = match msg_type {
gmp::v1::GmpMessageType::Report => {
if *v1_router_present {
send_igmp_v2_message::<_, _, IgmpMembershipReportV1>(
core_ctx,
bindings_ctx,
device,
group_addr,
group_addr,
(),
)
} else {
send_igmp_v2_message::<_, _, IgmpMembershipReportV2>(
core_ctx,
bindings_ctx,
device,
group_addr,
group_addr,
(),
)
}
}
gmp::v1::GmpMessageType::Leave => send_igmp_v2_message::<_, _, IgmpLeaveGroup>(
core_ctx,
bindings_ctx,
device,
group_addr,
Ipv4::ALL_ROUTERS_MULTICAST_ADDRESS,
(),
),
};
match result {
Ok(()) => {}
Err(err) => debug!(
"error sending IGMP message ({msg_type:?}) on device {device:?} for group \
{group_addr}: {err}",
),
}
}
fn send_report_v2(
&mut self,
bindings_ctx: &mut BC,
device: &Self::DeviceId,
groups: impl Iterator<Item: GmpReportGroupRecord<Ipv4Addr> + Clone> + Clone,
) {
let Self { core_ctx, igmp_state: _ } = self;
let dst_ip = ALL_IGMPV3_CAPABLE_ROUTERS;
let header = new_ip_header_builder(core_ctx, device, dst_ip);
let avail_len =
usize::from(core_ctx.get_mtu(device)).saturating_sub(header.constraints().header_len());
let reports = match IgmpMembershipReportV3Builder::new(groups).with_len_limits(avail_len) {
Ok(msg) => msg,
Err(e) => {
error!("MTU too small to send IGMP reports: {e:?}");
return;
}
};
for report in reports {
let destination = IpPacketDestination::Multicast(dst_ip);
let ip_frame = report.into_serializer().encapsulate(header.clone());
IpLayerHandler::send_ip_frame(core_ctx, bindings_ctx, device, destination, ip_frame)
.unwrap_or_else(|ErrorAndSerializer { error, .. }| {
debug!("failed to send IGMPv3 report over {device:?}: {error:?}")
});
}
}
fn run_actions(
&mut self,
bindings_ctx: &mut BC,
_device: &Self::DeviceId,
actions: Igmpv2Actions,
) {
let Self {
igmp_state: IgmpState { v1_router_present_timer, v1_router_present, .. },
core_ctx: _,
} = self;
match actions {
Igmpv2Actions::ScheduleV1RouterPresentTimer(duration) => {
*v1_router_present = true;
let _: Option<BC::Instant> =
bindings_ctx.schedule_timer(duration, v1_router_present_timer);
}
}
}
fn handle_mode_change(
&mut self,
bindings_ctx: &mut BC,
_device: &Self::DeviceId,
new_mode: GmpMode,
) {
match new_mode {
GmpMode::V1 { .. } => {}
GmpMode::V2 => {
let Self {
igmp_state: IgmpState { v1_router_present_timer, v1_router_present },
core_ctx: _,
} = self;
*v1_router_present = false;
let _: Option<_> = bindings_ctx.cancel_timer(v1_router_present_timer);
}
}
}
}
#[derive(Debug, Error)]
pub(crate) enum IgmpError {
#[error("the host has not already been a member of the address: {}", addr)]
NotAMember { addr: Ipv4Addr },
#[error("failed to send out an IGMP packet to address: {}", addr)]
SendFailure { addr: Ipv4Addr },
#[error("IGMP is disabled on interface")]
Disabled,
}
impl From<NotAMemberErr<Ipv4>> for IgmpError {
fn from(NotAMemberErr(addr): NotAMemberErr<Ipv4>) -> Self {
Self::NotAMember { addr }
}
}
impl From<v2::QueryError<Ipv4>> for IgmpError {
fn from(err: v2::QueryError<Ipv4>) -> Self {
match err {
v2::QueryError::NotAMember(addr) => Self::NotAMember { addr },
v2::QueryError::Disabled => Self::Disabled,
}
}
}
pub(crate) type IgmpResult<T> = Result<T, IgmpError>;
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum IgmpTimerId<D: WeakDeviceIdentifier> {
Gmp(GmpTimerId<Ipv4, D>),
#[allow(missing_docs)]
V1RouterPresent { device: D },
}
impl<D: WeakDeviceIdentifier> IgmpTimerId<D> {
pub(crate) fn device_id(&self) -> &D {
match self {
Self::Gmp(id) => id.device_id(),
Self::V1RouterPresent { device } => device,
}
}
#[cfg(any(test, feature = "testutils"))]
pub fn new_delayed_report(device: D) -> Self {
Self::Gmp(GmpTimerId { device, _marker: Default::default() })
}
}
impl<D: WeakDeviceIdentifier> From<GmpTimerId<Ipv4, D>> for IgmpTimerId<D> {
fn from(id: GmpTimerId<Ipv4, D>) -> IgmpTimerId<D> {
IgmpTimerId::Gmp(id)
}
}
impl<BC: IgmpBindingsContext, CC: IgmpContext<BC>> HandleableTimer<CC, BC>
for IgmpTimerId<CC::WeakDeviceId>
{
fn handle(self, core_ctx: &mut CC, bindings_ctx: &mut BC, _: BC::UniqueTimerId) {
match self {
IgmpTimerId::Gmp(id) => gmp::handle_timer(core_ctx, bindings_ctx, id),
IgmpTimerId::V1RouterPresent { device } => {
let Some(device) = device.upgrade() else {
return;
};
IgmpContext::with_igmp_state_mut(
core_ctx,
&device,
|_core_ctx, GmpStateRef { .. }, IgmpState { v1_router_present, .. }| {
*v1_router_present = false;
},
)
}
}
}
}
#[derive(Debug, Clone, Default)]
struct IgmpIpOptions(bool);
impl Iterator for IgmpIpOptions {
type Item = Ipv4Option<'static>;
fn next(&mut self) -> Option<Self::Item> {
let Self(yielded) = self;
if core::mem::replace(yielded, true) {
None
} else {
Some(Ipv4Option::RouterAlert { data: 0 })
}
}
}
const IGMP_IP_TTL: u8 = 1;
fn new_ip_header_builder<BC: IgmpBindingsContext, CC: IgmpSendContext<BC>>(
core_ctx: &mut CC,
device: &CC::DeviceId,
dst_ip: MulticastAddr<Ipv4Addr>,
) -> Ipv4PacketBuilderWithOptions<'static, IgmpIpOptions> {
let src_ip =
core_ctx.get_ip_addr_subnet(device).map_or(Ipv4::UNSPECIFIED_ADDRESS, |a| a.addr().get());
Ipv4PacketBuilderWithOptions::new(
Ipv4PacketBuilder::new(src_ip, dst_ip, IGMP_IP_TTL, Ipv4Proto::Igmp),
IgmpIpOptions::default(),
)
.unwrap_or_else(|Ipv4OptionsTooLongError| unreachable!("router alert always fits"))
}
fn send_igmp_v2_message<BC: IgmpBindingsContext, CC: IgmpSendContext<BC>, M>(
core_ctx: &mut CC,
bindings_ctx: &mut BC,
device: &CC::DeviceId,
group_addr: MulticastAddr<Ipv4Addr>,
dst_ip: MulticastAddr<Ipv4Addr>,
max_resp_time: M::MaxRespTime,
) -> IgmpResult<()>
where
M: MessageType<EmptyBuf, FixedHeader = Ipv4Addr, VariableBody = ()>,
{
let header = new_ip_header_builder(core_ctx, device, dst_ip);
let body =
IgmpPacketBuilder::<EmptyBuf, M>::new_with_resp_time(group_addr.get(), max_resp_time);
let body = body.into_serializer().encapsulate(header);
let destination = IpPacketDestination::Multicast(dst_ip);
IpLayerHandler::send_ip_frame(core_ctx, bindings_ctx, &device, destination, body)
.map_err(|_| IgmpError::SendFailure { addr: *group_addr })
}
#[derive(PartialEq, Eq, Debug)]
pub enum Igmpv2Actions {
ScheduleV1RouterPresentTimer(Duration),
}
#[derive(Debug)]
pub struct IgmpConfig {
unsolicited_report_interval: Duration,
send_leave_anyway: bool,
v1_router_present_timeout: Duration,
}
pub const IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL: Duration = Duration::from_secs(10);
const DEFAULT_V1_ROUTER_PRESENT_TIMEOUT: Duration = Duration::from_secs(400);
const DEFAULT_V1_QUERY_MAX_RESP_TIME: Duration = Duration::from_secs(10);
impl Default for IgmpConfig {
fn default() -> Self {
IgmpConfig {
unsolicited_report_interval: IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL,
send_leave_anyway: false,
v1_router_present_timeout: DEFAULT_V1_ROUTER_PRESENT_TIMEOUT,
}
}
}
impl gmp::v1::ProtocolConfig for IgmpConfig {
fn unsolicited_report_interval(&self) -> Duration {
self.unsolicited_report_interval
}
fn send_leave_anyway(&self) -> bool {
self.send_leave_anyway
}
fn get_max_resp_time(&self, resp_time: Duration) -> Option<NonZeroDuration> {
Some(NonZeroDuration::new(resp_time).unwrap_or_else(|| {
const_unwrap::const_unwrap_option(NonZeroDuration::new(DEFAULT_V1_QUERY_MAX_RESP_TIME))
}))
}
type QuerySpecificActions = Igmpv2Actions;
fn do_query_received_specific(&self, max_resp_time: Duration) -> Option<Igmpv2Actions> {
let v1_router_present = max_resp_time.as_micros() == 0;
v1_router_present
.then(|| Igmpv2Actions::ScheduleV1RouterPresentTimer(self.v1_router_present_timeout))
}
}
impl gmp::v2::ProtocolConfig for IgmpConfig {
fn query_response_interval(&self) -> NonZeroDuration {
gmp::v2::DEFAULT_QUERY_RESPONSE_INTERVAL
}
fn unsolicited_report_interval(&self) -> NonZeroDuration {
gmp::v2::DEFAULT_UNSOLICITED_REPORT_INTERVAL
}
}
#[cfg(test)]
mod tests {
use core::cell::RefCell;
use alloc::rc::Rc;
use alloc::vec;
use alloc::vec::Vec;
use assert_matches::assert_matches;
use net_types::ip::{Ip, IpVersionMarker, Mtu};
use netstack3_base::testutil::{
assert_empty, new_rng, run_with_many_seeds, FakeDeviceId, FakeInstant, FakeTimerCtxExt,
FakeWeakDeviceId, TestIpExt as _,
};
use netstack3_base::{CtxPair, InstantContext as _, IntoCoreTimerCtx, SendFrameContext as _};
use packet::serialize::Buf;
use packet::{ParsablePacket as _, ParseBuffer};
use packet_formats::gmp::GroupRecordType;
use packet_formats::igmp::messages::IgmpMembershipQueryV2;
use packet_formats::ipv4::{Ipv4Header, Ipv4Packet};
use packet_formats::testutil::parse_ip_packet;
use test_case::test_case;
use super::*;
use crate::internal::base::{IpPacketDestination, IpSendFrameError, SendIpPacketMeta};
use crate::internal::fragmentation::FragmentableIpSerializer;
use crate::internal::gmp::{GmpHandler as _, GmpState, GroupJoinResult, GroupLeaveResult};
#[derive(Debug, PartialEq)]
pub(crate) struct IgmpPacketMetadata<D> {
pub(crate) device: D,
pub(crate) dst_ip: MulticastAddr<Ipv4Addr>,
}
impl<D> IgmpPacketMetadata<D> {
fn new(device: D, dst_ip: MulticastAddr<Ipv4Addr>) -> IgmpPacketMetadata<D> {
IgmpPacketMetadata { device, dst_ip }
}
}
struct FakeIgmpCtx {
igmp_enabled: bool,
shared: Rc<RefCell<Shared>>,
addr_subnet: Option<AddrSubnet<Ipv4Addr, Ipv4DeviceAddr>>,
}
struct Shared {
groups: MulticastGroupSet<Ipv4Addr, GmpGroupState<Ipv4, FakeBindingsCtx>>,
igmp_state: IgmpState<FakeBindingsCtx>,
gmp_state: GmpState<Ipv4, FakeBindingsCtx>,
config: IgmpConfig,
}
impl FakeIgmpCtx {
fn gmp_state(&mut self) -> &mut GmpState<Ipv4, FakeBindingsCtx> {
&mut Rc::get_mut(&mut self.shared).unwrap().get_mut().gmp_state
}
fn groups(
&mut self,
) -> &mut MulticastGroupSet<Ipv4Addr, GmpGroupState<Ipv4, FakeBindingsCtx>> {
&mut Rc::get_mut(&mut self.shared).unwrap().get_mut().groups
}
fn igmp_state(&mut self) -> &mut IgmpState<FakeBindingsCtx> {
&mut Rc::get_mut(&mut self.shared).unwrap().get_mut().igmp_state
}
}
type FakeCtx = CtxPair<FakeCoreCtx, FakeBindingsCtx>;
type FakeCoreCtx = netstack3_base::testutil::FakeCoreCtx<
FakeIgmpCtx,
IgmpPacketMetadata<FakeDeviceId>,
FakeDeviceId,
>;
type FakeBindingsCtx = netstack3_base::testutil::FakeBindingsCtx<
IgmpTimerId<FakeWeakDeviceId<FakeDeviceId>>,
(),
(),
(),
>;
impl IgmpContextMarker for FakeCoreCtx {}
impl IgmpStateContext<FakeBindingsCtx> for FakeCoreCtx {
fn with_igmp_state<
O,
F: FnOnce(&MulticastGroupSet<Ipv4Addr, GmpGroupState<Ipv4, FakeBindingsCtx>>) -> O,
>(
&mut self,
&FakeDeviceId: &FakeDeviceId,
cb: F,
) -> O {
cb(&self.state.shared.borrow().groups)
}
}
impl IgmpContext<FakeBindingsCtx> for FakeCoreCtx {
type SendContext<'a> = &'a mut Self;
fn with_igmp_state_mut<
O,
F: for<'a> FnOnce(
Self::SendContext<'a>,
GmpStateRef<'a, Ipv4, Self, FakeBindingsCtx>,
&'a mut IgmpState<FakeBindingsCtx>,
) -> O,
>(
&mut self,
&FakeDeviceId: &FakeDeviceId,
cb: F,
) -> O {
let FakeIgmpCtx { igmp_enabled, shared, .. } = &mut self.state;
let enabled = *igmp_enabled;
let shared = Rc::clone(shared);
let mut shared = shared.borrow_mut();
let Shared { igmp_state, gmp_state, groups, config } = &mut *shared;
cb(self, GmpStateRef { enabled, groups, gmp: gmp_state, config }, igmp_state)
}
}
impl IgmpSendContext<FakeBindingsCtx> for &mut FakeCoreCtx {
fn get_ip_addr_subnet(
&mut self,
_device: &FakeDeviceId,
) -> Option<AddrSubnet<Ipv4Addr, Ipv4DeviceAddr>> {
self.state.addr_subnet
}
}
impl IpDeviceMtuContext<Ipv4> for &mut FakeCoreCtx {
fn get_mtu(&mut self, _device: &FakeDeviceId) -> Mtu {
Mtu::new(1500)
}
}
impl IpLayerHandler<Ipv4, FakeBindingsCtx> for &mut FakeCoreCtx {
fn send_ip_packet_from_device<S>(
&mut self,
_bindings_ctx: &mut FakeBindingsCtx,
_meta: SendIpPacketMeta<
Ipv4,
&Self::DeviceId,
Option<SpecifiedAddr<<Ipv4 as Ip>::Addr>>,
>,
_body: S,
) -> Result<(), IpSendFrameError<S>>
where
S: Serializer,
S::Buffer: BufferMut,
{
unimplemented!();
}
fn send_ip_frame<S>(
&mut self,
bindings_ctx: &mut FakeBindingsCtx,
device: &Self::DeviceId,
destination: IpPacketDestination<Ipv4, &Self::DeviceId>,
body: S,
) -> Result<(), IpSendFrameError<S>>
where
S: FragmentableIpSerializer<Ipv4, Buffer: BufferMut> + netstack3_filter::IpPacket<Ipv4>,
{
let addr = match destination {
IpPacketDestination::Multicast(addr) => addr,
_ => panic!("destination is not multicast: {:?}", destination),
};
(*self)
.send_frame(bindings_ctx, IgmpPacketMetadata::new(device.clone(), addr), body)
.map_err(|err| err.err_into())
}
}
#[test]
fn test_igmp_state_with_igmpv1_router() {
run_with_many_seeds(|seed| {
let mut rng = new_rng(seed);
let cfg = IgmpConfig::default();
let (mut s, _actions) =
gmp::v1::GmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
assert_eq!(
s.query_received(&mut rng, Duration::from_secs(0), FakeInstant::default(), &cfg),
gmp::v1::QueryReceivedActions {
generic: None,
protocol_specific: Some(Igmpv2Actions::ScheduleV1RouterPresentTimer(
DEFAULT_V1_ROUTER_PRESENT_TIMEOUT
))
}
);
assert_eq!(s.report_timer_expired(), gmp::v1::ReportTimerExpiredActions);
});
}
#[test]
fn test_igmp_state_igmpv1_router_present_timer_expires() {
run_with_many_seeds(|seed| {
let mut rng = new_rng(seed);
let cfg = IgmpConfig::default();
let (mut s, _actions) =
gmp::v1::GmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
assert_eq!(
s.query_received(&mut rng, Duration::from_secs(0), FakeInstant::default(), &cfg),
gmp::v1::QueryReceivedActions {
generic: None,
protocol_specific: Some(Igmpv2Actions::ScheduleV1RouterPresentTimer(
DEFAULT_V1_ROUTER_PRESENT_TIMEOUT
))
}
);
assert_eq!(
s.query_received(&mut rng, Duration::from_secs(0), FakeInstant::default(), &cfg),
gmp::v1::QueryReceivedActions {
generic: None,
protocol_specific: Some(Igmpv2Actions::ScheduleV1RouterPresentTimer(
DEFAULT_V1_ROUTER_PRESENT_TIMEOUT
))
}
);
assert_eq!(s.report_received(), gmp::v1::ReportReceivedActions { stop_timer: true });
});
}
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: MulticastAddr<Ipv4Addr> = <Ipv4 as gmp::testutil::TestIpExt>::GROUP_ADDR1;
const GROUP_ADDR_2: MulticastAddr<Ipv4Addr> = <Ipv4 as gmp::testutil::TestIpExt>::GROUP_ADDR2;
const GMP_TIMER_ID: IgmpTimerId<FakeWeakDeviceId<FakeDeviceId>> =
IgmpTimerId::Gmp(GmpTimerId {
device: FakeWeakDeviceId(FakeDeviceId),
_marker: IpVersionMarker::new(),
});
const V1_ROUTER_PRESENT_TIMER_ID: IgmpTimerId<FakeWeakDeviceId<FakeDeviceId>> =
IgmpTimerId::V1RouterPresent { device: FakeWeakDeviceId(FakeDeviceId) };
fn receive_igmp_query(
core_ctx: &mut FakeCoreCtx,
bindings_ctx: &mut FakeBindingsCtx,
resp_time: Duration,
) {
let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipQueryV2>::new_with_resp_time(
GROUP_ADDR.get(),
resp_time.try_into().unwrap(),
);
let buff = ser.into_serializer().serialize_vec_outer().unwrap();
core_ctx.receive_igmp_packet(bindings_ctx, &FakeDeviceId, ROUTER_ADDR, MY_ADDR, buff);
}
fn receive_igmp_general_query(
core_ctx: &mut FakeCoreCtx,
bindings_ctx: &mut FakeBindingsCtx,
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 buff = ser.into_serializer().serialize_vec_outer().unwrap();
core_ctx.receive_igmp_packet(bindings_ctx, &FakeDeviceId, ROUTER_ADDR, MY_ADDR, buff);
}
fn receive_igmp_report(core_ctx: &mut FakeCoreCtx, bindings_ctx: &mut FakeBindingsCtx) {
let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipReportV2>::new(GROUP_ADDR.get());
let buff = ser.into_serializer().serialize_vec_outer().unwrap();
core_ctx.receive_igmp_packet(bindings_ctx, &FakeDeviceId, OTHER_HOST_ADDR, MY_ADDR, buff);
}
fn setup_simple_test_environment_with_addr_subnet(
seed: u128,
a: Option<AddrSubnet<Ipv4Addr, Ipv4DeviceAddr>>,
) -> FakeCtx {
let mut ctx = FakeCtx::with_default_bindings_ctx(|bindings_ctx| {
let igmp_enabled = true;
FakeCoreCtx::with_state(FakeIgmpCtx {
shared: Rc::new(RefCell::new(Shared {
groups: MulticastGroupSet::default(),
gmp_state: GmpState::new_with_enabled::<_, IntoCoreTimerCtx>(
bindings_ctx,
FakeWeakDeviceId(FakeDeviceId),
igmp_enabled,
),
igmp_state: IgmpState::new::<_, IntoCoreTimerCtx>(
bindings_ctx,
FakeWeakDeviceId(FakeDeviceId),
),
config: Default::default(),
})),
igmp_enabled,
addr_subnet: None,
})
});
ctx.bindings_ctx.seed_rng(seed);
ctx.core_ctx.state.addr_subnet = a;
ctx
}
fn setup_simple_test_environment(seed: u128) -> FakeCtx {
setup_simple_test_environment_with_addr_subnet(
seed,
Some(AddrSubnet::new(MY_ADDR.get(), 24).unwrap()),
)
}
fn ensure_ttl_ihl_rtr(core_ctx: &FakeCoreCtx) {
for (_, frame) in core_ctx.frames() {
assert_eq!(frame[8], IGMP_IP_TTL); assert_eq!(&frame[20..24], &[148, 4, 0, 0]); assert_eq!(frame[0], 0x46); }
}
#[test_case(Some(MY_ADDR); "specified_src")]
#[test_case(None; "unspecified_src")]
fn test_igmp_simple_integration(src_ip: Option<SpecifiedAddr<Ipv4Addr>>) {
let check_report = |core_ctx: &mut FakeCoreCtx| {
let expected_src_ip = src_ip.map_or(Ipv4::UNSPECIFIED_ADDRESS, |a| a.get());
let frames = core_ctx.take_frames();
let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) = assert_matches!(
&frames[..], [x] => x);
assert_eq!(dst_ip, &GROUP_ADDR);
let (body, src_ip, dst_ip, proto, ttl) = parse_ip_packet::<Ipv4>(frame).unwrap();
assert_eq!(src_ip, expected_src_ip);
assert_eq!(dst_ip, GROUP_ADDR.get());
assert_eq!(proto, Ipv4Proto::Igmp);
assert_eq!(ttl, IGMP_IP_TTL);
let mut bv = &body[..];
assert_matches!(
IgmpPacket::parse(&mut bv, ()).unwrap(),
IgmpPacket::MembershipReportV2(msg) => {
assert_eq!(msg.group_addr(), GROUP_ADDR.get());
}
);
};
let addr_subnet = src_ip.map(|a| AddrSubnet::new(a.get(), 16).unwrap());
run_with_many_seeds(|seed| {
let FakeCtx { mut core_ctx, mut bindings_ctx } =
setup_simple_test_environment_with_addr_subnet(seed, addr_subnet);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
check_report(&mut core_ctx);
receive_igmp_query(&mut core_ctx, &mut bindings_ctx, Duration::from_secs(10));
core_ctx
.state
.gmp_state()
.timers
.assert_top(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), &());
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
check_report(&mut core_ctx);
});
}
#[test]
fn test_igmp_integration_fallback_from_idle() {
run_with_many_seeds(|seed| {
let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_simple_test_environment(seed);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
assert_eq!(core_ctx.frames().len(), 1);
core_ctx
.state
.gmp_state()
.timers
.assert_top(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), &());
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
assert_eq!(core_ctx.frames().len(), 2);
receive_igmp_query(&mut core_ctx, &mut bindings_ctx, Duration::from_secs(10));
let group_state = core_ctx.state.groups().get(&GROUP_ADDR).unwrap().v1();
match group_state.get_inner() {
gmp::v1::MemberState::Delaying(_) => {}
_ => panic!("Wrong State!"),
}
core_ctx
.state
.gmp_state()
.timers
.assert_top(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), &());
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
assert_eq!(core_ctx.frames().len(), 3);
ensure_ttl_ihl_rtr(&core_ctx);
});
}
#[test]
fn test_igmp_integration_igmpv1_router_present() {
run_with_many_seeds(|seed| {
let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_simple_test_environment(seed);
assert_eq!(core_ctx.state.igmp_state().v1_router_present, false);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
let now = bindings_ctx.now();
core_ctx.state.gmp_state().timers.assert_range([(
&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(),
now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
)]);
let instant1 = bindings_ctx.timers.timers()[0].0.clone();
receive_igmp_query(&mut core_ctx, &mut bindings_ctx, Duration::from_secs(0));
assert_eq!(core_ctx.frames().len(), 1);
assert_eq!(core_ctx.state.igmp_state().v1_router_present, true);
assert_eq!(core_ctx.frames().len(), 1);
let now = bindings_ctx.now();
core_ctx.state.gmp_state().timers.assert_range([(
&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(),
now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
)]);
bindings_ctx.timers.assert_timers_installed_range([
(GMP_TIMER_ID, now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL)),
(V1_ROUTER_PRESENT_TIMER_ID, now..=(now + DEFAULT_V1_ROUTER_PRESENT_TIMEOUT)),
]);
let instant2 = bindings_ctx.timers.timers()[1].0.clone();
assert_eq!(instant1, instant2);
core_ctx
.state
.gmp_state()
.timers
.assert_top(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), &());
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
assert_eq!(core_ctx.frames().len(), 2);
let (_, frame) = core_ctx.frames().last().unwrap();
assert_eq!(frame[24], 0x12);
assert_eq!(
bindings_ctx.trigger_next_timer(&mut core_ctx),
Some(V1_ROUTER_PRESENT_TIMER_ID)
);
assert_eq!(core_ctx.state.igmp_state().v1_router_present, false);
receive_igmp_query(&mut core_ctx, &mut bindings_ctx, Duration::from_secs(10));
core_ctx
.state
.gmp_state()
.timers
.assert_top(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), &());
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
assert_eq!(core_ctx.frames().len(), 3);
assert_eq!(core_ctx.frames().last().unwrap().1[24], 0x16);
ensure_ttl_ihl_rtr(&core_ctx);
});
}
#[test]
fn test_igmp_integration_delay_reset_timer() {
let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_simple_test_environment(123456);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
let now = bindings_ctx.now();
core_ctx.state.gmp_state().timers.assert_range([(
&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(),
now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
)]);
let instant1 = bindings_ctx.timers.timers()[0].0.clone();
let start = bindings_ctx.now();
let duration = Duration::from_micros(((instant1 - start).as_micros() / 2) as u64);
assert!(duration.as_millis() > 100);
receive_igmp_query(&mut core_ctx, &mut bindings_ctx, duration);
assert_eq!(core_ctx.frames().len(), 1);
let now = bindings_ctx.now();
core_ctx.state.gmp_state().timers.assert_range([(
&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(),
now..=(now + duration),
)]);
let instant2 = bindings_ctx.timers.timers()[0].0.clone();
assert!(instant2 <= instant1);
core_ctx
.state
.gmp_state()
.timers
.assert_top(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), &());
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
assert!(bindings_ctx.now() - start <= duration);
assert_eq!(core_ctx.frames().len(), 2);
assert_eq!(core_ctx.frames().last().unwrap().1[24], 0x16);
ensure_ttl_ihl_rtr(&core_ctx);
}
#[test]
fn test_igmp_integration_last_send_leave() {
run_with_many_seeds(|seed| {
let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_simple_test_environment(seed);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
let now = bindings_ctx.now();
core_ctx.state.gmp_state().timers.assert_range([(
&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(),
now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
)]);
assert_eq!(core_ctx.frames().len(), 1);
core_ctx
.state
.gmp_state()
.timers
.assert_top(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), &());
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
assert_eq!(core_ctx.frames().len(), 2);
assert_eq!(
core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupLeaveResult::Left(())
);
assert_eq!(core_ctx.frames().len(), 3);
let leave_frame = &core_ctx.frames().last().unwrap().1;
assert_eq!(leave_frame[24], 0x17);
assert_eq!(leave_frame[16], 224);
assert_eq!(leave_frame[17], 0);
assert_eq!(leave_frame[18], 0);
assert_eq!(leave_frame[19], 2);
ensure_ttl_ihl_rtr(&core_ctx);
});
}
#[test]
fn test_igmp_integration_always_idle_member() {
run_with_many_seeds(|seed| {
let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_simple_test_environment(seed);
assert_eq!(
core_ctx.gmp_join_group(
&mut bindings_ctx,
&FakeDeviceId,
Ipv4::ALL_SYSTEMS_MULTICAST_ADDRESS
),
GroupJoinResult::Joined(())
);
assert_eq!(core_ctx.frames().len(), 0);
bindings_ctx.timers.assert_no_timers_installed();
});
}
#[test]
fn test_igmp_integration_not_last_does_not_send_leave() {
run_with_many_seeds(|seed| {
let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_simple_test_environment(seed);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
let now = bindings_ctx.now();
core_ctx.state.gmp_state().timers.assert_range([(
&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(),
now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
)]);
assert_eq!(core_ctx.frames().len(), 1);
receive_igmp_report(&mut core_ctx, &mut bindings_ctx);
bindings_ctx.timers.assert_no_timers_installed();
assert_eq!(core_ctx.frames().len(), 1);
assert_eq!(
core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupLeaveResult::Left(())
);
assert_eq!(core_ctx.frames().len(), 1);
ensure_ttl_ihl_rtr(&core_ctx);
});
}
#[test]
fn test_receive_general_query() {
run_with_many_seeds(|seed| {
let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_simple_test_environment(seed);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR_2),
GroupJoinResult::Joined(())
);
let now = bindings_ctx.now();
let range = now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL);
core_ctx.state.gmp_state().timers.assert_range([
(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), range.clone()),
(&gmp::v1::DelayedReportTimerId(GROUP_ADDR_2).into(), range),
]);
assert_eq!(core_ctx.frames().len(), 2);
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
assert_eq!(core_ctx.frames().len(), 4);
const RESP_TIME: Duration = Duration::from_secs(10);
receive_igmp_general_query(&mut core_ctx, &mut bindings_ctx, RESP_TIME);
let now = bindings_ctx.now();
let range = now..=(now + RESP_TIME);
core_ctx.state.gmp_state().timers.assert_range([
(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), range.clone()),
(&gmp::v1::DelayedReportTimerId(GROUP_ADDR_2).into(), range),
]);
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
assert_eq!(core_ctx.frames().len(), 6);
ensure_ttl_ihl_rtr(&core_ctx);
});
}
#[test]
fn test_skip_igmp() {
run_with_many_seeds(|seed| {
let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_simple_test_environment(seed);
bindings_ctx.seed_rng(seed);
core_ctx.state.igmp_enabled = false;
core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
let assert_no_effect = |core_ctx: &FakeCoreCtx, bindings_ctx: &FakeBindingsCtx| {
bindings_ctx.timers.assert_no_timers_installed();
assert_empty(core_ctx.frames());
};
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
assert_no_effect(&core_ctx, &bindings_ctx);
receive_igmp_report(&mut core_ctx, &mut bindings_ctx);
assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
assert_no_effect(&core_ctx, &bindings_ctx);
receive_igmp_query(&mut core_ctx, &mut bindings_ctx, Duration::from_secs(10));
assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
assert_no_effect(&core_ctx, &bindings_ctx);
assert_eq!(
core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupLeaveResult::Left(())
);
assert!(core_ctx.state.groups().get(&GROUP_ADDR).is_none());
assert_no_effect(&core_ctx, &bindings_ctx);
});
}
#[test]
fn test_igmp_integration_with_local_join_leave() {
run_with_many_seeds(|seed| {
let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_simple_test_environment(seed);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
assert_eq!(core_ctx.frames().len(), 1);
let now = bindings_ctx.now();
let range = now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL);
core_ctx
.state
.gmp_state()
.timers
.assert_range([(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), range.clone())]);
ensure_ttl_ihl_rtr(&core_ctx);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::AlreadyMember
);
assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
assert_eq!(core_ctx.frames().len(), 1);
core_ctx
.state
.gmp_state()
.timers
.assert_range([(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), range.clone())]);
assert_eq!(
core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupLeaveResult::StillMember
);
assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
assert_eq!(core_ctx.frames().len(), 1);
core_ctx
.state
.gmp_state()
.timers
.assert_range([(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), range)]);
assert_eq!(
core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupLeaveResult::Left(())
);
assert_eq!(core_ctx.frames().len(), 2);
bindings_ctx.timers.assert_no_timers_installed();
ensure_ttl_ihl_rtr(&core_ctx);
});
}
#[test]
fn test_igmp_enable_disable() {
run_with_many_seeds(|seed| {
let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_simple_test_environment(seed);
assert_eq!(core_ctx.take_frames(), []);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
{
let frames = core_ctx.take_frames();
let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) =
assert_matches!(&frames[..], [x] => x);
assert_eq!(dst_ip, &GROUP_ADDR);
let (body, src_ip, dst_ip, proto, ttl) = parse_ip_packet::<Ipv4>(frame).unwrap();
assert_eq!(src_ip, MY_ADDR.get());
assert_eq!(dst_ip, GROUP_ADDR.get());
assert_eq!(proto, Ipv4Proto::Igmp);
assert_eq!(ttl, IGMP_IP_TTL);
let mut bv = &body[..];
assert_matches!(
IgmpPacket::parse(&mut bv, ()).unwrap(),
IgmpPacket::MembershipReportV2(msg) => {
assert_eq!(msg.group_addr(), GROUP_ADDR.get());
}
);
}
core_ctx.gmp_handle_maybe_enabled(&mut bindings_ctx, &FakeDeviceId);
assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
assert_eq!(core_ctx.take_frames(), []);
core_ctx.state.igmp_enabled = false;
core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
{
let frames = core_ctx.take_frames();
let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) =
assert_matches!(&frames[..], [x] => x);
assert_eq!(dst_ip, &Ipv4::ALL_ROUTERS_MULTICAST_ADDRESS);
let (body, src_ip, dst_ip, proto, ttl) = parse_ip_packet::<Ipv4>(frame).unwrap();
assert_eq!(src_ip, MY_ADDR.get());
assert_eq!(dst_ip, Ipv4::ALL_ROUTERS_MULTICAST_ADDRESS.get());
assert_eq!(proto, Ipv4Proto::Igmp);
assert_eq!(ttl, IGMP_IP_TTL);
let mut bv = &body[..];
assert_matches!(
IgmpPacket::parse(&mut bv, ()).unwrap(),
IgmpPacket::LeaveGroup(msg) => {
assert_eq!(msg.group_addr(), GROUP_ADDR.get());
}
);
}
core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
assert_eq!(core_ctx.take_frames(), []);
core_ctx.state.igmp_enabled = true;
core_ctx.gmp_handle_maybe_enabled(&mut bindings_ctx, &FakeDeviceId);
assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
{
let frames = core_ctx.take_frames();
let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) =
assert_matches!(&frames[..], [x] => x);
assert_eq!(dst_ip, &GROUP_ADDR);
let (body, src_ip, dst_ip, proto, ttl) = parse_ip_packet::<Ipv4>(frame).unwrap();
assert_eq!(src_ip, MY_ADDR.get());
assert_eq!(dst_ip, GROUP_ADDR.get());
assert_eq!(proto, Ipv4Proto::Igmp);
assert_eq!(ttl, IGMP_IP_TTL);
let mut bv = &body[..];
assert_matches!(
IgmpPacket::parse(&mut bv, ()).unwrap(),
IgmpPacket::MembershipReportV2(msg) => {
assert_eq!(msg.group_addr(), GROUP_ADDR.get());
}
);
}
});
}
#[test]
fn send_igmpv3_report() {
let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_simple_test_environment(0);
let sent_report_addr = Ipv4::get_multicast_addr(130);
let sent_report_mode = GroupRecordType::ModeIsExclude;
let sent_report_sources = Vec::<Ipv4Addr>::new();
core_ctx.with_gmp_state_mut_and_ctx(&FakeDeviceId, |mut core_ctx, _| {
core_ctx.send_report_v2(
&mut bindings_ctx,
&FakeDeviceId,
[(sent_report_addr, sent_report_mode, sent_report_sources.iter())].into_iter(),
);
});
let frames = core_ctx.take_frames();
let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) =
assert_matches!(&frames[..], [x] => x);
assert_eq!(dst_ip, &ALL_IGMPV3_CAPABLE_ROUTERS);
let mut buff = &frame[..];
let ipv4 = buff.parse::<Ipv4Packet<_>>().expect("parse IPv4");
assert_eq!(ipv4.ttl(), IGMP_IP_TTL);
assert_eq!(ipv4.src_ip(), MY_ADDR.get());
assert_eq!(ipv4.dst_ip(), ALL_IGMPV3_CAPABLE_ROUTERS.get());
assert_eq!(ipv4.proto(), Ipv4Proto::Igmp);
assert_eq!(
ipv4.iter_options()
.map(|o| {
assert_matches!(o, Ipv4Option::RouterAlert { data: 0 });
})
.count(),
1
);
let igmp = buff.parse::<IgmpPacket<_>>().expect("parse IGMP");
let report = assert_matches!(
igmp,
IgmpPacket::MembershipReportV3(report) => report
);
let report = report
.body()
.iter()
.map(|r| {
(
r.header().multicast_addr().clone(),
r.header().record_type().unwrap(),
r.sources().to_vec(),
)
})
.collect::<Vec<_>>();
assert_eq!(report, vec![(sent_report_addr.get(), sent_report_mode, sent_report_sources)]);
}
}