use core::convert::Infallible as Never;
use core::time::Duration;
use log::{debug, error};
use net_declare::net_ip_v6;
use net_types::ip::{Ip, Ipv6, Ipv6Addr, Ipv6ReservedScope, Ipv6Scope, Ipv6SourceAddr};
use net_types::{LinkLocalUnicastAddr, MulticastAddr, ScopeableAddress, SpecifiedAddr, Witness};
use netstack3_base::{
AnyDevice, DeviceIdContext, ErrorAndSerializer, HandleableTimer, WeakDeviceIdentifier,
};
use netstack3_filter as filter;
use packet::serialize::{PacketBuilder, Serializer};
use packet::InnerPacketBuilder;
use packet_formats::gmp::GmpReportGroupRecord;
use packet_formats::icmp::mld::{
IcmpMldv1MessageType, MldPacket, Mldv1Body, Mldv1MessageBuilder, Mldv2QueryBody,
Mldv2ReportMessageBuilder, MulticastListenerDone, MulticastListenerReport,
MulticastListenerReportV2,
};
use packet_formats::icmp::{IcmpMessage, IcmpPacketBuilder, IcmpUnusedCode};
use packet_formats::ip::Ipv6Proto;
use packet_formats::ipv6::ext_hdrs::{
ExtensionHeaderOptionAction, HopByHopOption, HopByHopOptionData,
};
use packet_formats::ipv6::{Ipv6PacketBuilder, Ipv6PacketBuilderWithHbhOptions};
use packet_formats::utils::NonZeroDuration;
use thiserror::Error;
use zerocopy::SplitByteSlice;
use crate::internal::base::{IpDeviceMtuContext, IpLayerHandler, IpPacketDestination};
use crate::internal::gmp::{
self, GmpBindingsContext, GmpBindingsTypes, GmpContext, GmpContextInner, GmpGroupState,
GmpStateContext, GmpStateRef, GmpTimerId, GmpTypeLayout, IpExt, MulticastGroupSet,
NotAMemberErr,
};
const ALL_MLDV2_CAPABLE_ROUTERS: MulticastAddr<Ipv6Addr> =
unsafe { MulticastAddr::new_unchecked(net_ip_v6!("FF02::16")) };
pub trait MldBindingsTypes: GmpBindingsTypes {}
impl<BT> MldBindingsTypes for BT where BT: GmpBindingsTypes {}
pub(crate) trait MldBindingsContext: GmpBindingsContext {}
impl<BC> MldBindingsContext for BC where BC: GmpBindingsContext {}
pub trait MldStateContext<BT: MldBindingsTypes>:
DeviceIdContext<AnyDevice> + MldContextMarker
{
fn with_mld_state<O, F: FnOnce(&MulticastGroupSet<Ipv6Addr, GmpGroupState<Ipv6, BT>>) -> O>(
&mut self,
device: &Self::DeviceId,
cb: F,
) -> O;
}
pub trait MldSendContext<BT: MldBindingsTypes>:
DeviceIdContext<AnyDevice> + IpLayerHandler<Ipv6, BT> + IpDeviceMtuContext<Ipv6> + MldContextMarker
{
fn get_ipv6_link_local_addr(
&mut self,
device: &Self::DeviceId,
) -> Option<LinkLocalUnicastAddr<Ipv6Addr>>;
}
pub trait MldContextMarker {}
pub trait MldContext<BT: MldBindingsTypes>: DeviceIdContext<AnyDevice> + MldContextMarker {
type SendContext<'a>: MldSendContext<BT, DeviceId = Self::DeviceId> + 'a;
fn with_mld_state_mut<
O,
F: FnOnce(Self::SendContext<'_>, GmpStateRef<'_, Ipv6, Self, BT>) -> O,
>(
&mut self,
device: &Self::DeviceId,
cb: F,
) -> O;
}
pub trait MldPacketHandler<BC, DeviceId> {
fn receive_mld_packet<B: SplitByteSlice>(
&mut self,
bindings_ctx: &mut BC,
device: &DeviceId,
src_ip: Ipv6SourceAddr,
dst_ip: SpecifiedAddr<Ipv6Addr>,
packet: MldPacket<B>,
);
}
impl<BC: MldBindingsContext, CC: MldContext<BC>> MldPacketHandler<BC, CC::DeviceId> for CC {
fn receive_mld_packet<B: SplitByteSlice>(
&mut self,
bindings_ctx: &mut BC,
device: &CC::DeviceId,
_src_ip: Ipv6SourceAddr,
_dst_ip: SpecifiedAddr<Ipv6Addr>,
packet: MldPacket<B>,
) {
let result = match packet {
MldPacket::MulticastListenerQuery(msg) => {
gmp::v1::handle_query_message(self, bindings_ctx, device, msg.body())
.map_err(Into::into)
}
MldPacket::MulticastListenerQueryV2(msg) => {
gmp::v2::handle_query_message(self, bindings_ctx, device, msg.body())
.map_err(Into::into)
}
MldPacket::MulticastListenerReport(msg) => {
let addr = msg.body().group_addr;
MulticastAddr::new(msg.body().group_addr).map_or(
Err(MldError::NotAMember { addr }),
|group_addr| {
gmp::v1::handle_report_message(self, bindings_ctx, device, group_addr)
.map_err(Into::into)
},
)
}
MldPacket::MulticastListenerReportV2(_) => {
debug!("Hosts are not interested in MLDv2 report messages");
return;
}
MldPacket::MulticastListenerDone(_) => {
debug!("Hosts are not interested in Done messages");
return;
}
};
result.unwrap_or_else(|e| debug!("Error occurred when handling MLD message: {}", e));
}
}
impl<B: SplitByteSlice> gmp::v1::QueryMessage<Ipv6> for Mldv1Body<B> {
fn group_addr(&self) -> Ipv6Addr {
self.group_addr
}
fn max_response_time(&self) -> Duration {
self.max_response_delay()
}
}
impl<B: SplitByteSlice> gmp::v2::QueryMessage<Ipv6> for Mldv2QueryBody<B> {
fn as_v1(&self) -> impl gmp::v1::QueryMessage<Ipv6> + '_ {
self.as_v1_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) -> Ipv6Addr {
self.header().group_address()
}
fn max_response_time(&self) -> Duration {
self.header().max_response_delay().into()
}
fn sources(&self) -> impl Iterator<Item = Ipv6Addr> + '_ {
self.sources().iter().copied()
}
}
impl IpExt for Ipv6 {
fn should_perform_gmp(group_addr: MulticastAddr<Ipv6Addr>) -> bool {
group_addr != Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS
&& ![Ipv6Scope::Reserved(Ipv6ReservedScope::Scope0), Ipv6Scope::InterfaceLocal]
.contains(&group_addr.scope())
}
}
impl<BT: MldBindingsTypes, CC: DeviceIdContext<AnyDevice> + MldContextMarker>
GmpTypeLayout<Ipv6, BT> for CC
{
type Actions = Never;
type Config = MldConfig;
}
impl<BT: MldBindingsTypes, CC: MldStateContext<BT>> GmpStateContext<Ipv6, BT> for CC {
fn with_gmp_state<O, F: FnOnce(&MulticastGroupSet<Ipv6Addr, GmpGroupState<Ipv6, BT>>) -> O>(
&mut self,
device: &Self::DeviceId,
cb: F,
) -> O {
self.with_mld_state(device, cb)
}
}
impl<BC: MldBindingsContext, CC: MldContext<BC>> GmpContext<Ipv6, BC> for CC {
type Inner<'a> = CC::SendContext<'a>;
fn with_gmp_state_mut_and_ctx<
O,
F: FnOnce(Self::Inner<'_>, GmpStateRef<'_, Ipv6, Self, BC>) -> O,
>(
&mut self,
device: &Self::DeviceId,
cb: F,
) -> O {
self.with_mld_state_mut(device, cb)
}
}
impl<BC: MldBindingsContext, CC: MldSendContext<BC>> GmpContextInner<Ipv6, BC> for CC {
fn send_message_v1(
&mut self,
bindings_ctx: &mut BC,
device: &Self::DeviceId,
group_addr: MulticastAddr<Ipv6Addr>,
msg_type: gmp::v1::GmpMessageType,
) {
let result = match msg_type {
gmp::v1::GmpMessageType::Report => send_mld_v1_packet::<_, _, _>(
self,
bindings_ctx,
device,
group_addr,
MulticastListenerReport,
group_addr,
(),
),
gmp::v1::GmpMessageType::Leave => send_mld_v1_packet::<_, _, _>(
self,
bindings_ctx,
device,
Ipv6::ALL_ROUTERS_LINK_LOCAL_MULTICAST_ADDRESS,
MulticastListenerDone,
group_addr,
(),
),
};
match result {
Ok(()) => {}
Err(err) => debug!(
"error sending MLD 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<Ipv6Addr> + Clone> + Clone,
) {
let dst_ip = ALL_MLDV2_CAPABLE_ROUTERS;
let (ipv6, icmp) =
new_ip_and_icmp_builders(self, device, dst_ip, MulticastListenerReportV2);
let header = ipv6.constraints().header_len() + icmp.constraints().header_len();
let avail_len = usize::from(self.get_mtu(device)).saturating_sub(header);
let reports = match Mldv2ReportMessageBuilder::new(groups).with_len_limits(avail_len) {
Ok(msg) => msg,
Err(e) => {
error!("MTU too small to send MLD reports: {e:?}");
return;
}
};
for report in reports {
let destination = IpPacketDestination::Multicast(dst_ip);
let ip_frame =
report.into_serializer().encapsulate(icmp.clone()).encapsulate(ipv6.clone());
IpLayerHandler::send_ip_frame(self, bindings_ctx, device, destination, ip_frame)
.unwrap_or_else(|ErrorAndSerializer { error, .. }| {
debug!("failed to send MLDv2 report over {device:?}: {error:?}")
});
}
}
fn run_actions(&mut self, _bindings_ctx: &mut BC, _device: &CC::DeviceId, actions: Never) {
match actions {}
}
fn handle_mode_change(
&mut self,
_bindings_ctx: &mut BC,
_device: &Self::DeviceId,
_new_mode: gmp::GmpMode,
) {
}
}
#[derive(Debug, Error)]
pub(crate) enum MldError {
#[error("the host has not already been a member of the address: {}", addr)]
NotAMember { addr: Ipv6Addr },
#[error("failed to send out an MLD packet to address: {}", addr)]
SendFailure { addr: Ipv6Addr },
#[error("MLD is disabled on interface")]
Disabled,
}
impl From<NotAMemberErr<Ipv6>> for MldError {
fn from(NotAMemberErr(addr): NotAMemberErr<Ipv6>) -> Self {
Self::NotAMember { addr }
}
}
impl From<gmp::v2::QueryError<Ipv6>> for MldError {
fn from(err: gmp::v2::QueryError<Ipv6>) -> Self {
match err {
gmp::v2::QueryError::NotAMember(addr) => Self::NotAMember { addr },
gmp::v2::QueryError::Disabled => Self::Disabled,
}
}
}
pub(crate) type MldResult<T> = Result<T, MldError>;
#[derive(Debug)]
pub struct MldConfig {
unsolicited_report_interval: Duration,
send_leave_anyway: bool,
}
pub const MLD_DEFAULT_UNSOLICITED_REPORT_INTERVAL: Duration = Duration::from_secs(10);
impl Default for MldConfig {
fn default() -> Self {
MldConfig {
unsolicited_report_interval: MLD_DEFAULT_UNSOLICITED_REPORT_INTERVAL,
send_leave_anyway: false,
}
}
}
impl gmp::v1::ProtocolConfig for MldConfig {
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> {
NonZeroDuration::new(resp_time)
}
type QuerySpecificActions = Never;
fn do_query_received_specific(&self, _max_resp_time: Duration) -> Option<Never> {
None
}
}
impl gmp::v2::ProtocolConfig for MldConfig {
fn query_response_interval(&self) -> NonZeroDuration {
gmp::v2::DEFAULT_QUERY_RESPONSE_INTERVAL
}
fn unsolicited_report_interval(&self) -> NonZeroDuration {
gmp::v2::DEFAULT_UNSOLICITED_REPORT_INTERVAL
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)]
pub struct MldTimerId<D: WeakDeviceIdentifier>(GmpTimerId<Ipv6, D>);
impl<D: WeakDeviceIdentifier> MldTimerId<D> {
pub(crate) fn device_id(&self) -> &D {
let Self(this) = self;
this.device_id()
}
#[cfg(any(test, feature = "testutils"))]
pub fn new_delayed_report(device: D) -> Self {
Self(GmpTimerId { device, _marker: Default::default() })
}
}
impl<D: WeakDeviceIdentifier> From<GmpTimerId<Ipv6, D>> for MldTimerId<D> {
fn from(id: GmpTimerId<Ipv6, D>) -> MldTimerId<D> {
MldTimerId(id)
}
}
impl<BC: MldBindingsContext, CC: MldContext<BC>> HandleableTimer<CC, BC>
for MldTimerId<CC::WeakDeviceId>
{
fn handle(self, core_ctx: &mut CC, bindings_ctx: &mut BC, _: BC::UniqueTimerId) {
let Self(id) = self;
gmp::handle_timer(core_ctx, bindings_ctx, id);
}
}
#[derive(Debug, Clone, Default)]
struct MldIpOptions(bool);
impl Iterator for MldIpOptions {
type Item = HopByHopOption<'static>;
fn next(&mut self) -> Option<Self::Item> {
let Self(yielded) = self;
if core::mem::replace(yielded, true) {
None
} else {
Some(HopByHopOption {
action: ExtensionHeaderOptionAction::SkipAndContinue,
mutable: false,
data: HopByHopOptionData::RouterAlert { data: 0 },
})
}
}
}
const MLD_IP_HOP_LIMIT: u8 = 1;
fn new_ip_and_icmp_builders<
BC: MldBindingsContext,
CC: MldSendContext<BC>,
M: IcmpMessage<Ipv6, Code = IcmpUnusedCode> + filter::IcmpMessage<Ipv6>,
>(
core_ctx: &mut CC,
device: &CC::DeviceId,
dst_ip: MulticastAddr<Ipv6Addr>,
msg: M,
) -> (Ipv6PacketBuilderWithHbhOptions<'static, MldIpOptions>, IcmpPacketBuilder<Ipv6, M>) {
let src_ip =
core_ctx.get_ipv6_link_local_addr(device).map_or(Ipv6::UNSPECIFIED_ADDRESS, |x| x.get());
let ipv6 = Ipv6PacketBuilderWithHbhOptions::new(
Ipv6PacketBuilder::new(src_ip, dst_ip.get(), MLD_IP_HOP_LIMIT, Ipv6Proto::Icmpv6),
MldIpOptions::default(),
)
.unwrap();
let icmp = IcmpPacketBuilder::new(src_ip, dst_ip.get(), IcmpUnusedCode, msg);
(ipv6, icmp)
}
fn send_mld_v1_packet<
BC: MldBindingsContext,
CC: MldSendContext<BC>,
M: IcmpMldv1MessageType + filter::IcmpMessage<Ipv6>,
>(
core_ctx: &mut CC,
bindings_ctx: &mut BC,
device: &CC::DeviceId,
dst_ip: MulticastAddr<Ipv6Addr>,
msg: M,
group_addr: M::GroupAddr,
max_resp_delay: M::MaxRespDelay,
) -> MldResult<()> {
let (ipv6, icmp) = new_ip_and_icmp_builders(core_ctx, device, dst_ip, msg);
let body = Mldv1MessageBuilder::<M>::new_with_max_resp_delay(group_addr, max_resp_delay)
.into_serializer()
.encapsulate(icmp)
.encapsulate(ipv6);
let destination = IpPacketDestination::Multicast(dst_ip);
IpLayerHandler::send_ip_frame(core_ctx, bindings_ctx, &device, destination, body)
.map_err(|_| MldError::SendFailure { addr: group_addr.into() })
}
#[cfg(test)]
mod tests {
use alloc::rc::Rc;
use alloc::vec;
use alloc::vec::Vec;
use core::cell::RefCell;
use assert_matches::assert_matches;
use net_types::ethernet::Mac;
use net_types::ip::{Ip as _, IpVersionMarker, Mtu};
use netstack3_base::testutil::{
assert_empty, new_rng, run_with_many_seeds, FakeDeviceId, FakeInstant, FakeTimerCtxExt,
FakeWeakDeviceId, TestIpExt,
};
use netstack3_base::{CtxPair, InstantContext as _, IntoCoreTimerCtx, SendFrameContext};
use packet::{BufferMut, ParseBuffer};
use packet_formats::gmp::GroupRecordType;
use packet_formats::icmp::mld::MulticastListenerQuery;
use packet_formats::icmp::{IcmpParseArgs, Icmpv6MessageType, Icmpv6Packet};
use packet_formats::ip::IpPacket;
use packet_formats::ipv6::ext_hdrs::Ipv6ExtensionHeaderData;
use packet_formats::ipv6::Ipv6Packet;
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 MldFrameMetadata<D> {
pub(crate) device: D,
pub(crate) dst_ip: MulticastAddr<Ipv6Addr>,
}
impl<D> MldFrameMetadata<D> {
fn new(device: D, dst_ip: MulticastAddr<Ipv6Addr>) -> MldFrameMetadata<D> {
MldFrameMetadata { device, dst_ip }
}
}
struct FakeMldCtx {
shared: Rc<RefCell<Shared>>,
mld_enabled: bool,
ipv6_link_local: Option<LinkLocalUnicastAddr<Ipv6Addr>>,
}
impl FakeMldCtx {
fn gmp_state(&mut self) -> &mut GmpState<Ipv6, FakeBindingsCtxImpl> {
&mut Rc::get_mut(&mut self.shared).unwrap().get_mut().gmp_state
}
fn groups(
&mut self,
) -> &mut MulticastGroupSet<Ipv6Addr, GmpGroupState<Ipv6, FakeBindingsCtxImpl>> {
&mut Rc::get_mut(&mut self.shared).unwrap().get_mut().groups
}
}
struct Shared {
groups: MulticastGroupSet<Ipv6Addr, GmpGroupState<Ipv6, FakeBindingsCtxImpl>>,
gmp_state: GmpState<Ipv6, FakeBindingsCtxImpl>,
config: MldConfig,
}
fn new_context() -> FakeCtxImpl {
FakeCtxImpl::with_default_bindings_ctx(|bindings_ctx| {
let mld_enabled = true;
FakeCoreCtxImpl::with_state(FakeMldCtx {
shared: Rc::new(RefCell::new(Shared {
groups: MulticastGroupSet::default(),
gmp_state: GmpState::new_with_enabled::<_, IntoCoreTimerCtx>(
bindings_ctx,
FakeWeakDeviceId(FakeDeviceId),
mld_enabled,
),
config: Default::default(),
})),
mld_enabled,
ipv6_link_local: None,
})
})
}
type FakeCtxImpl = CtxPair<FakeCoreCtxImpl, FakeBindingsCtxImpl>;
type FakeCoreCtxImpl = netstack3_base::testutil::FakeCoreCtx<
FakeMldCtx,
MldFrameMetadata<FakeDeviceId>,
FakeDeviceId,
>;
type FakeBindingsCtxImpl = netstack3_base::testutil::FakeBindingsCtx<
MldTimerId<FakeWeakDeviceId<FakeDeviceId>>,
(),
(),
(),
>;
impl MldContextMarker for FakeCoreCtxImpl {}
impl MldContextMarker for &'_ mut FakeCoreCtxImpl {}
impl MldStateContext<FakeBindingsCtxImpl> for FakeCoreCtxImpl {
fn with_mld_state<
O,
F: FnOnce(&MulticastGroupSet<Ipv6Addr, GmpGroupState<Ipv6, FakeBindingsCtxImpl>>) -> O,
>(
&mut self,
&FakeDeviceId: &FakeDeviceId,
cb: F,
) -> O {
cb(&self.state.shared.borrow().groups)
}
}
impl MldContext<FakeBindingsCtxImpl> for FakeCoreCtxImpl {
type SendContext<'a> = &'a mut Self;
fn with_mld_state_mut<
O,
F: FnOnce(Self::SendContext<'_>, GmpStateRef<'_, Ipv6, Self, FakeBindingsCtxImpl>) -> O,
>(
&mut self,
&FakeDeviceId: &FakeDeviceId,
cb: F,
) -> O {
let FakeMldCtx { mld_enabled, shared, .. } = &mut self.state;
let enabled = *mld_enabled;
let shared = Rc::clone(shared);
let mut shared = shared.borrow_mut();
let Shared { gmp_state, groups, config } = &mut *shared;
cb(self, GmpStateRef { enabled, groups, gmp: gmp_state, config })
}
}
impl IpDeviceMtuContext<Ipv6> for &mut FakeCoreCtxImpl {
fn get_mtu(&mut self, _device: &FakeDeviceId) -> Mtu {
Ipv6::MINIMUM_LINK_MTU
}
}
impl MldSendContext<FakeBindingsCtxImpl> for &mut FakeCoreCtxImpl {
fn get_ipv6_link_local_addr(
&mut self,
_device: &FakeDeviceId,
) -> Option<LinkLocalUnicastAddr<Ipv6Addr>> {
self.state.ipv6_link_local
}
}
impl IpLayerHandler<Ipv6, FakeBindingsCtxImpl> for &mut FakeCoreCtxImpl {
fn send_ip_packet_from_device<S>(
&mut self,
_bindings_ctx: &mut FakeBindingsCtxImpl,
_meta: SendIpPacketMeta<
Ipv6,
&Self::DeviceId,
Option<SpecifiedAddr<<Ipv6 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 FakeBindingsCtxImpl,
device: &Self::DeviceId,
destination: IpPacketDestination<Ipv6, &Self::DeviceId>,
body: S,
) -> Result<(), IpSendFrameError<S>>
where
S: FragmentableIpSerializer<Ipv6, Buffer: BufferMut> + netstack3_filter::IpPacket<Ipv6>,
{
let addr = match destination {
IpPacketDestination::Multicast(addr) => addr,
_ => panic!("destination is not multicast: {:?}", destination),
};
(*self)
.send_frame(bindings_ctx, MldFrameMetadata::new(device.clone(), addr), body)
.map_err(|e| e.err_into())
}
}
#[test]
fn test_mld_immediate_report() {
run_with_many_seeds(|seed| {
let mut rng = new_rng(seed);
let cfg = MldConfig::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: Some(gmp::v1::QueryReceivedGenericAction::StopTimerAndSendReport),
protocol_specific: None
}
);
});
}
const MY_IP: SpecifiedAddr<Ipv6Addr> = unsafe {
SpecifiedAddr::new_unchecked(Ipv6Addr::from_bytes([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 0, 3,
]))
};
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: MulticastAddr<Ipv6Addr> = <Ipv6 as gmp::testutil::TestIpExt>::GROUP_ADDR1;
const TIMER_ID: MldTimerId<FakeWeakDeviceId<FakeDeviceId>> = MldTimerId(GmpTimerId {
device: FakeWeakDeviceId(FakeDeviceId),
_marker: IpVersionMarker::new(),
});
fn receive_mld_query(
core_ctx: &mut FakeCoreCtxImpl,
bindings_ctx: &mut FakeBindingsCtxImpl,
resp_time: Duration,
group_addr: MulticastAddr<Ipv6Addr>,
) {
let router_addr: Ipv6Addr = ROUTER_MAC.to_ipv6_link_local().addr().get();
let mut buffer = Mldv1MessageBuilder::<MulticastListenerQuery>::new_with_max_resp_delay(
group_addr.get(),
resp_time.try_into().unwrap(),
)
.into_serializer()
.encapsulate(IcmpPacketBuilder::<_, _>::new(
router_addr,
MY_IP,
IcmpUnusedCode,
MulticastListenerQuery,
))
.serialize_vec_outer()
.unwrap();
match buffer
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(router_addr, MY_IP))
.unwrap()
{
Icmpv6Packet::Mld(packet) => core_ctx.receive_mld_packet(
bindings_ctx,
&FakeDeviceId,
router_addr.try_into().unwrap(),
MY_IP,
packet,
),
_ => panic!("serialized icmpv6 message is not an mld message"),
}
}
fn receive_mld_report(
core_ctx: &mut FakeCoreCtxImpl,
bindings_ctx: &mut FakeBindingsCtxImpl,
group_addr: MulticastAddr<Ipv6Addr>,
) {
let router_addr: Ipv6Addr = ROUTER_MAC.to_ipv6_link_local().addr().get();
let mut buffer = Mldv1MessageBuilder::<MulticastListenerReport>::new(group_addr)
.into_serializer()
.encapsulate(IcmpPacketBuilder::<_, _>::new(
router_addr,
MY_IP,
IcmpUnusedCode,
MulticastListenerReport,
))
.serialize_vec_outer()
.unwrap()
.unwrap_b();
match buffer
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(router_addr, MY_IP))
.unwrap()
{
Icmpv6Packet::Mld(packet) => core_ctx.receive_mld_packet(
bindings_ctx,
&FakeDeviceId,
router_addr.try_into().unwrap(),
MY_IP,
packet,
),
_ => panic!("serialized icmpv6 message is not an mld message"),
}
}
fn ensure_ttl(frame: &[u8]) {
assert_eq!(frame[7], MLD_IP_HOP_LIMIT);
}
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::from_bytes(bytes), ip);
}
fn ensure_dst_addr(frame: &[u8], ip: Ipv6Addr) {
ensure_slice_addr(frame, 24, 40, ip);
}
fn ensure_multicast_addr(frame: &[u8], ip: Ipv6Addr) {
ensure_slice_addr(frame, 56, 72, ip);
}
fn ensure_frame(
frame: &[u8],
op: u8,
dst: MulticastAddr<Ipv6Addr>,
multicast: MulticastAddr<Ipv6Addr>,
) {
ensure_ttl(frame);
assert_eq!(frame[48], op);
assert_eq!(frame[5], 32);
assert_eq!(frame[6], 0);
assert_eq!(&frame[40..48], &[58, 0, 5, 2, 0, 0, 1, 0]);
ensure_ttl(&frame[..]);
ensure_dst_addr(&frame[..], dst.get());
ensure_multicast_addr(&frame[..], multicast.get());
}
#[test]
fn test_mld_simple_integration() {
run_with_many_seeds(|seed| {
let FakeCtxImpl { mut core_ctx, mut bindings_ctx } = new_context();
bindings_ctx.seed_rng(seed);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
receive_mld_query(
&mut core_ctx,
&mut bindings_ctx,
Duration::from_secs(10),
GROUP_ADDR,
);
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(TIMER_ID));
assert_eq!(core_ctx.frames().len(), 2);
for (_, frame) in core_ctx.frames() {
ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR);
ensure_slice_addr(&frame, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
}
});
}
#[test]
fn test_mld_immediate_query() {
run_with_many_seeds(|seed| {
let FakeCtxImpl { mut core_ctx, mut bindings_ctx } = new_context();
bindings_ctx.seed_rng(seed);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
assert_eq!(core_ctx.frames().len(), 1);
receive_mld_query(&mut core_ctx, &mut bindings_ctx, Duration::from_secs(0), GROUP_ADDR);
assert_eq!(core_ctx.frames().len(), 2);
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), None);
for (_, frame) in core_ctx.frames() {
ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR);
ensure_slice_addr(&frame, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
}
});
}
#[test]
fn test_mld_integration_fallback_from_idle() {
run_with_many_seeds(|seed| {
let FakeCtxImpl { mut core_ctx, mut bindings_ctx } = new_context();
bindings_ctx.seed_rng(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(TIMER_ID));
assert_eq!(core_ctx.frames().len(), 2);
receive_mld_query(
&mut core_ctx,
&mut bindings_ctx,
Duration::from_secs(10),
GROUP_ADDR,
);
let group_state = core_ctx.state.groups().get(&GROUP_ADDR).unwrap();
match group_state.v1().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(TIMER_ID));
assert_eq!(core_ctx.frames().len(), 3);
for (_, frame) in core_ctx.frames() {
ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR);
ensure_slice_addr(&frame, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
}
});
}
#[test]
fn test_mld_integration_immediate_query_wont_fallback() {
run_with_many_seeds(|seed| {
let FakeCtxImpl { mut core_ctx, mut bindings_ctx } = new_context();
bindings_ctx.seed_rng(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(TIMER_ID));
assert_eq!(core_ctx.frames().len(), 2);
receive_mld_query(&mut core_ctx, &mut bindings_ctx, Duration::from_secs(0), GROUP_ADDR);
let group_state = core_ctx.state.groups().get(&GROUP_ADDR).unwrap();
match group_state.v1().get_inner() {
gmp::v1::MemberState::Idle(_) => {}
_ => panic!("Wrong State!"),
}
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), None);
assert_eq!(core_ctx.frames().len(), 3);
for (_, frame) in core_ctx.frames() {
ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR);
ensure_slice_addr(&frame, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
}
});
}
#[test]
fn test_mld_integration_delay_reset_timer() {
let FakeCtxImpl { mut core_ctx, mut bindings_ctx } = new_context();
bindings_ctx.seed_rng(123456);
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
core_ctx.state.gmp_state().timers.assert_timers([(
gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(),
(),
FakeInstant::from(Duration::from_micros(590_354)),
)]);
let instant1 = bindings_ctx.timers.timers()[0].0.clone();
let start = bindings_ctx.now();
let duration = instant1 - start;
receive_mld_query(&mut core_ctx, &mut bindings_ctx, duration, GROUP_ADDR);
assert_eq!(core_ctx.frames().len(), 1);
core_ctx.state.gmp_state().timers.assert_timers([(
gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(),
(),
FakeInstant::from(Duration::from_micros(34_751)),
)]);
let instant2 = bindings_ctx.timers.timers()[0].0.clone();
assert!(instant2 <= instant1);
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(TIMER_ID));
assert!(bindings_ctx.now() - start <= duration);
assert_eq!(core_ctx.frames().len(), 2);
for (_, frame) in core_ctx.frames() {
ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR);
ensure_slice_addr(&frame, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
}
}
#[test]
fn test_mld_integration_last_send_leave() {
run_with_many_seeds(|seed| {
let FakeCtxImpl { mut core_ctx, mut bindings_ctx } = new_context();
bindings_ctx.seed_rng(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 + MLD_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
)]);
assert_eq!(core_ctx.frames().len(), 1);
assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(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);
ensure_frame(&core_ctx.frames()[0].1, 131, GROUP_ADDR, GROUP_ADDR);
ensure_slice_addr(&core_ctx.frames()[0].1, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
ensure_frame(&core_ctx.frames()[1].1, 131, GROUP_ADDR, GROUP_ADDR);
ensure_slice_addr(&core_ctx.frames()[1].1, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
ensure_frame(
&core_ctx.frames()[2].1,
132,
Ipv6::ALL_ROUTERS_LINK_LOCAL_MULTICAST_ADDRESS,
GROUP_ADDR,
);
ensure_slice_addr(&core_ctx.frames()[2].1, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
});
}
#[test]
fn test_mld_integration_not_last_does_not_send_leave() {
run_with_many_seeds(|seed| {
let FakeCtxImpl { mut core_ctx, mut bindings_ctx } = new_context();
bindings_ctx.seed_rng(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 + MLD_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
)]);
assert_eq!(core_ctx.frames().len(), 1);
receive_mld_report(&mut core_ctx, &mut bindings_ctx, GROUP_ADDR);
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);
for (_, frame) in core_ctx.frames() {
ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR);
ensure_slice_addr(&frame, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
}
});
}
#[test]
fn test_mld_with_link_local() {
run_with_many_seeds(|seed| {
let FakeCtxImpl { mut core_ctx, mut bindings_ctx } = new_context();
bindings_ctx.seed_rng(seed);
core_ctx.state.ipv6_link_local = Some(MY_MAC.to_ipv6_link_local().addr());
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
GroupJoinResult::Joined(())
);
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(TIMER_ID));
for (_, frame) in core_ctx.frames() {
ensure_frame(&frame, 131, GROUP_ADDR, GROUP_ADDR);
ensure_slice_addr(&frame, 8, 24, MY_MAC.to_ipv6_link_local().addr().get());
}
});
}
#[test]
fn test_skip_mld() {
run_with_many_seeds(|seed| {
let test = |FakeCtxImpl { mut core_ctx, mut bindings_ctx }, group| {
core_ctx.state.ipv6_link_local = Some(MY_MAC.to_ipv6_link_local().addr());
let assert_no_effect =
|core_ctx: &FakeCoreCtxImpl, bindings_ctx: &FakeBindingsCtxImpl| {
bindings_ctx.timers.assert_no_timers_installed();
assert_empty(core_ctx.frames());
};
assert_eq!(
core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, group),
GroupJoinResult::Joined(())
);
assert_gmp_state!(core_ctx, &group, NonMember);
assert_no_effect(&core_ctx, &bindings_ctx);
receive_mld_report(&mut core_ctx, &mut bindings_ctx, group);
assert_gmp_state!(core_ctx, &group, NonMember);
assert_no_effect(&core_ctx, &bindings_ctx);
receive_mld_query(&mut core_ctx, &mut bindings_ctx, Duration::from_secs(10), group);
assert_gmp_state!(core_ctx, &group, NonMember);
assert_no_effect(&core_ctx, &bindings_ctx);
assert_eq!(
core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, group),
GroupLeaveResult::Left(())
);
assert!(core_ctx.state.groups().get(&group).is_none());
assert_no_effect(&core_ctx, &bindings_ctx);
};
let new_ctx = || {
let mut ctx = new_context();
ctx.bindings_ctx.seed_rng(seed);
ctx
};
test(new_ctx(), Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS);
let mut bytes = Ipv6::MULTICAST_SUBNET.network().ipv6_bytes();
bytes[1] = bytes[1] & 0xF0;
let reserved0 = MulticastAddr::new(Ipv6Addr::from_bytes(bytes)).unwrap();
bytes[1] = (bytes[1] & 0xF0) | 1;
let iface_local = MulticastAddr::new(Ipv6Addr::from_bytes(bytes)).unwrap();
test(new_ctx(), reserved0);
test(new_ctx(), iface_local);
let mut ctx = new_ctx();
ctx.core_ctx.state.mld_enabled = false;
ctx.core_ctx.gmp_handle_disabled(&mut ctx.bindings_ctx, &FakeDeviceId);
test(ctx, GROUP_ADDR);
});
}
#[test]
fn test_mld_integration_with_local_join_leave() {
run_with_many_seeds(|seed| {
let FakeCtxImpl { mut core_ctx, mut bindings_ctx } = new_context();
bindings_ctx.seed_rng(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 + MLD_DEFAULT_UNSOLICITED_REPORT_INTERVAL);
core_ctx
.state
.gmp_state()
.timers
.assert_range([(&gmp::v1::DelayedReportTimerId(GROUP_ADDR).into(), range.clone())]);
let frame = &core_ctx.frames().last().unwrap().1;
ensure_frame(frame, 131, GROUP_ADDR, GROUP_ADDR);
ensure_slice_addr(frame, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
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();
let frame = &core_ctx.frames().last().unwrap().1;
ensure_frame(frame, 132, Ipv6::ALL_ROUTERS_LINK_LOCAL_MULTICAST_ADDRESS, GROUP_ADDR);
ensure_slice_addr(frame, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
});
}
#[test]
fn test_mld_enable_disable() {
run_with_many_seeds(|seed| {
let FakeCtxImpl { mut core_ctx, mut bindings_ctx } = new_context();
bindings_ctx.seed_rng(seed);
assert_eq!(core_ctx.take_frames(), []);
assert_eq!(
core_ctx.gmp_join_group(
&mut bindings_ctx,
&FakeDeviceId,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS
),
GroupJoinResult::Joined(())
);
assert_gmp_state!(core_ctx, &Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS, NonMember);
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 (MldFrameMetadata { device: FakeDeviceId, dst_ip }, frame) =
assert_matches!(&frames[..], [x] => x);
assert_eq!(dst_ip, &GROUP_ADDR);
ensure_frame(
frame,
Icmpv6MessageType::MulticastListenerReport.into(),
GROUP_ADDR,
GROUP_ADDR,
);
ensure_slice_addr(frame, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
}
core_ctx.gmp_handle_maybe_enabled(&mut bindings_ctx, &FakeDeviceId);
assert_gmp_state!(core_ctx, &Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS, NonMember);
assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
assert_eq!(core_ctx.take_frames(), []);
core_ctx.state.mld_enabled = false;
core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
assert_gmp_state!(core_ctx, &Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS, NonMember);
assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
{
let frames = core_ctx.take_frames();
let (MldFrameMetadata { device: FakeDeviceId, dst_ip }, frame) =
assert_matches!(&frames[..], [x] => x);
assert_eq!(dst_ip, &Ipv6::ALL_ROUTERS_LINK_LOCAL_MULTICAST_ADDRESS);
ensure_frame(
frame,
Icmpv6MessageType::MulticastListenerDone.into(),
Ipv6::ALL_ROUTERS_LINK_LOCAL_MULTICAST_ADDRESS,
GROUP_ADDR,
);
ensure_slice_addr(frame, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
}
core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
assert_gmp_state!(core_ctx, &Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS, NonMember);
assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
assert_eq!(core_ctx.take_frames(), []);
core_ctx.state.mld_enabled = true;
core_ctx.gmp_handle_maybe_enabled(&mut bindings_ctx, &FakeDeviceId);
assert_gmp_state!(core_ctx, &Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS, NonMember);
assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
let frames = core_ctx.take_frames();
let (MldFrameMetadata { device: FakeDeviceId, dst_ip }, frame) =
assert_matches!(&frames[..], [x] => x);
assert_eq!(dst_ip, &GROUP_ADDR);
ensure_frame(
frame,
Icmpv6MessageType::MulticastListenerReport.into(),
GROUP_ADDR,
GROUP_ADDR,
);
ensure_slice_addr(frame, 8, 24, Ipv6::UNSPECIFIED_ADDRESS);
});
}
#[test]
fn send_gmpv2_report() {
let FakeCtxImpl { mut core_ctx, mut bindings_ctx } = new_context();
let sent_report_addr = Ipv6::get_multicast_addr(130);
let sent_report_mode = GroupRecordType::ModeIsExclude;
let sent_report_sources = Vec::<Ipv6Addr>::new();
(&mut 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 (MldFrameMetadata { device: FakeDeviceId, dst_ip }, frame) =
assert_matches!(&frames[..], [x] => x);
assert_eq!(dst_ip, &ALL_MLDV2_CAPABLE_ROUTERS);
let mut buff = &frame[..];
let ipv6 = buff.parse::<Ipv6Packet<_>>().expect("parse IPv6");
assert_eq!(ipv6.ttl(), MLD_IP_HOP_LIMIT);
assert_eq!(ipv6.src_ip(), Ipv6::UNSPECIFIED_ADDRESS);
assert_eq!(ipv6.dst_ip(), ALL_MLDV2_CAPABLE_ROUTERS.get());
assert_eq!(ipv6.proto(), Ipv6Proto::Icmpv6);
assert_eq!(
ipv6.iter_extension_hdrs()
.map(|h| {
let options = assert_matches!(
h.data(),
Ipv6ExtensionHeaderData::HopByHopOptions { options } => options
);
assert_eq!(
options
.iter()
.map(|o| {
assert_matches!(
o.data,
HopByHopOptionData::RouterAlert { data: 0 }
);
})
.count(),
1
);
})
.count(),
1
);
let args = IcmpParseArgs::new(ipv6.src_ip(), ipv6.dst_ip());
let icmp = buff.parse_with::<_, Icmpv6Packet<_>>(args).expect("parse ICMPv6");
let report = assert_matches!(
icmp,
Icmpv6Packet::Mld(MldPacket::MulticastListenerReportV2(report)) => report
);
let report = report
.body()
.iter_multicast_records()
.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)]);
}
}