1use core::num::NonZeroU8;
14
15use alloc::collections::hash_map::HashMap;
16use alloc::collections::HashSet;
17use core::time::Duration;
18use net_types::ip::{Ip, IpAddress};
19use net_types::{MulticastAddr, Witness as _};
20use netstack3_base::{Instant as _, LocalTimerHeap};
21use packet_formats::gmp::{GmpReportGroupRecord, GroupRecordType};
22use packet_formats::utils::NonZeroDuration;
23
24use crate::internal::gmp::{
25 self, GmpBindingsContext, GmpContext, GmpContextInner, GmpEnabledGroup, GmpGroupState, GmpMode,
26 GmpStateRef, GmpTypeLayout, GroupJoinResult, GroupLeaveResult, IpExt, NotAMemberErr,
27 QueryTarget,
28};
29
30pub(super) const DEFAULT_QUERY_RESPONSE_INTERVAL: NonZeroDuration =
38 NonZeroDuration::from_secs(10).unwrap();
39
40pub(super) const DEFAULT_UNSOLICITED_REPORT_INTERVAL: NonZeroDuration =
48 NonZeroDuration::from_secs(1).unwrap();
49
50pub(super) const DEFAULT_ROBUSTNESS_VARIABLE: NonZeroU8 = NonZeroU8::new(2).unwrap();
58
59pub(super) const DEFAULT_QUERY_INTERVAL: NonZeroDuration = NonZeroDuration::from_secs(125).unwrap();
67
68const STATE_CHANGE_REPORT_DELAY: Duration = Duration::from_millis(5);
91
92#[cfg_attr(test, derive(Debug))]
93pub(super) struct GroupState<I: Ip> {
94 filter_mode_retransmission_counter: u8,
95 recorded_sources: HashSet<I::Addr>,
96 }
99
100impl<I: Ip> GroupState<I> {
101 pub(super) fn new_for_mode_transition() -> Self {
102 Self { recorded_sources: Default::default(), filter_mode_retransmission_counter: 0 }
103 }
104}
105
106#[derive(Debug, Eq, PartialEq, Hash, Clone)]
107pub(super) enum TimerId<I: Ip> {
108 GeneralQuery,
109 MulticastAddress(GmpEnabledGroup<I::Addr>),
110 StateChange,
111}
112
113#[derive(Debug)]
119#[cfg_attr(test, derive(Eq, PartialEq))]
120pub(super) struct ProtocolState<I: Ip> {
121 pub robustness_variable: NonZeroU8,
131 pub query_interval: NonZeroDuration,
141
142 pub left_groups: HashMap<GmpEnabledGroup<I::Addr>, NonZeroU8>,
152}
153
154impl<I: Ip> Default for ProtocolState<I> {
155 fn default() -> Self {
156 Self {
157 robustness_variable: DEFAULT_ROBUSTNESS_VARIABLE,
158 query_interval: DEFAULT_QUERY_INTERVAL,
159 left_groups: Default::default(),
160 }
161 }
162}
163
164impl<I: Ip> ProtocolState<I> {
165 pub(super) fn older_version_querier_present_timeout<C: ProtocolConfig>(
175 &self,
176 config: &C,
177 ) -> NonZeroDuration {
178 self.query_interval
179 .saturating_mul(self.robustness_variable.into())
180 .saturating_add(config.query_response_interval().into())
181 }
182
183 pub(super) fn on_enter_v1(&mut self) {
188 let Self { robustness_variable: _, query_interval: _, left_groups } = self;
189 *left_groups = HashMap::new();
195 }
196}
197
198pub trait ProtocolConfig {
206 fn query_response_interval(&self) -> NonZeroDuration;
218
219 fn unsolicited_report_interval(&self) -> NonZeroDuration;
227}
228
229pub(super) trait QueryMessage<I: Ip> {
239 fn as_v1(&self) -> impl gmp::v1::QueryMessage<I> + '_;
241
242 fn robustness_variable(&self) -> u8;
244
245 fn query_interval(&self) -> Duration;
247
248 fn group_address(&self) -> I::Addr;
250
251 fn max_response_time(&self) -> Duration;
253
254 fn sources(&self) -> impl Iterator<Item = I::Addr> + '_;
256}
257
258#[derive(Eq, PartialEq, Debug)]
259pub(super) enum QueryError<I: Ip> {
260 NotAMember(I::Addr),
261 Disabled,
262}
263
264impl<I: Ip> From<NotAMemberErr<I>> for QueryError<I> {
265 fn from(NotAMemberErr(addr): NotAMemberErr<I>) -> Self {
266 Self::NotAMember(addr)
267 }
268}
269
270pub(super) trait VerifiedReportGroupRecord<A: IpAddress>: GmpReportGroupRecord<A> {
273 #[allow(unused)]
276 fn gmp_enabled_group_addr(&self) -> &GmpEnabledGroup<A>;
277}
278
279#[derive(Clone)]
280pub(super) struct GroupRecord<A, Iter> {
281 group: GmpEnabledGroup<A>,
282 record_type: GroupRecordType,
283 iter: Iter,
284}
285
286impl<A> GroupRecord<A, core::iter::Empty<A>> {
287 pub(super) fn new(group: GmpEnabledGroup<A>, record_type: GroupRecordType) -> Self {
288 Self { group, record_type, iter: core::iter::empty() }
289 }
290}
291
292impl<A, Iter> GroupRecord<A, Iter> {
293 pub(super) fn new_with_sources(
294 group: GmpEnabledGroup<A>,
295 record_type: GroupRecordType,
296 iter: Iter,
297 ) -> Self {
298 Self { group, record_type, iter }
299 }
300}
301
302impl<A: IpAddress<Version: IpExt>, Iter: Iterator<Item: core::borrow::Borrow<A>> + Clone>
303 GmpReportGroupRecord<A> for GroupRecord<A, Iter>
304{
305 fn group(&self) -> MulticastAddr<A> {
306 self.group.multicast_addr()
307 }
308
309 fn record_type(&self) -> GroupRecordType {
310 self.record_type
311 }
312
313 fn sources(&self) -> impl Iterator<Item: core::borrow::Borrow<A>> + '_ {
314 self.iter.clone()
315 }
316}
317
318impl<A: IpAddress<Version: IpExt>, Iter: Iterator<Item: core::borrow::Borrow<A>> + Clone>
319 VerifiedReportGroupRecord<A> for GroupRecord<A, Iter>
320{
321 fn gmp_enabled_group_addr(&self) -> &GmpEnabledGroup<A> {
322 &self.group
323 }
324}
325
326pub(super) fn handle_query_message<
336 I: IpExt,
337 CC: GmpContext<I, BC>,
338 BC: GmpBindingsContext,
339 Q: QueryMessage<I>,
340>(
341 core_ctx: &mut CC,
342 bindings_ctx: &mut BC,
343 device: &CC::DeviceId,
344 query: &Q,
345) -> Result<(), QueryError<I>> {
346 core_ctx.with_gmp_state_mut_and_ctx(device, |mut core_ctx, state| {
347 if !state.enabled {
349 return Err(QueryError::Disabled);
350 }
351 match state.gmp.gmp_mode() {
352 GmpMode::V1 { .. } => {
353 return gmp::v1::handle_query_message_inner(
354 &mut core_ctx,
355 bindings_ctx,
356 device,
357 state,
358 &query.as_v1(),
359 )
360 .map_err(Into::into);
361 }
362 GmpMode::V2 => {}
363 }
364 let GmpStateRef { enabled: _, groups, gmp, config: _ } = state;
365 if let Some(qrv) = NonZeroU8::new(query.robustness_variable()) {
367 gmp.v2_proto.robustness_variable = qrv;
368 }
369 if let Some(qqic) = NonZeroDuration::new(query.query_interval()) {
370 gmp.v2_proto.query_interval = qqic;
371 }
372
373 let target = query.group_address();
374 let target = QueryTarget::new(target).ok_or(QueryError::NotAMember(target))?;
375
376 let target = match target {
378 QueryTarget::Unspecified => {
380 if groups.is_empty() {
384 return Ok(());
385 }
386
387 None
389 }
390 QueryTarget::Specified(multicast_addr) => {
392 let group = groups
402 .get_mut(&multicast_addr)
403 .ok_or_else(|| QueryError::NotAMember(multicast_addr.get()))?;
404
405 Some((group.v2_mut(), multicast_addr))
407 }
408 };
409
410 let now = bindings_ctx.now();
413 let delay = now.saturating_add(gmp::random_report_timeout(
414 &mut bindings_ctx.rng(),
415 query.max_response_time(),
416 ));
417
418 match gmp.timers.get(&TimerId::GeneralQuery.into()) {
422 Some((instant, ())) => {
423 if instant <= delay {
424 return Ok(());
425 }
426 }
427 None => {}
428 }
429
430 let (group, addr) = match target {
431 None => {
436 let _: Option<_> = gmp.timers.schedule_instant(
437 bindings_ctx,
438 TimerId::GeneralQuery.into(),
439 (),
440 delay,
441 );
442 return Ok(());
443 }
444 Some(specific) => specific,
445 };
446
447 let addr = GmpEnabledGroup::try_new(addr)
477 .map_err(|addr| QueryError::NotAMember(addr.into_addr()))?;
478
479 let timer_id = TimerId::MulticastAddress(addr).into();
480 let scheduled = gmp.timers.get(&timer_id);
481 let mut sources = query.sources().peekable();
482
483 let (delay, clear_sources) = match scheduled {
484 Some((t, ())) => {
486 let delay = (delay < t).then_some(delay);
488 let is_address_query = sources.peek().is_none();
493 let clear_sources = group.recorded_sources.is_empty() || is_address_query;
494 (delay, clear_sources)
495 }
496 None => (Some(delay), false),
498 };
499
500 if clear_sources {
501 group.recorded_sources = Default::default();
502 } else {
503 group.recorded_sources.extend(sources);
504 }
505
506 if let Some(delay) = delay {
507 let _: Option<_> = gmp.timers.schedule_instant(bindings_ctx, timer_id, (), delay);
508 }
509
510 Ok(())
511 })
512}
513
514pub(super) fn join_group<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
528 bindings_ctx: &mut BC,
529 group_addr: MulticastAddr<I::Addr>,
530 state: GmpStateRef<'_, I, CC, BC>,
531) -> GroupJoinResult {
532 let GmpStateRef { enabled, groups, gmp, config: _ } = state;
533 debug_assert!(gmp.gmp_mode().is_v2());
534 groups.join_group_with(group_addr, || {
535 let filter_mode_retransmission_counter = match GmpEnabledGroup::new(group_addr) {
536 Some(group_addr) => {
537 let _: Option<_> = gmp.v2_proto.left_groups.remove(&group_addr);
540
541 if enabled {
542 trigger_state_change_report(bindings_ctx, &mut gmp.timers);
543 gmp.v2_proto.robustness_variable.get()
544 } else {
545 0
546 }
547 }
548 None => 0,
549 };
550
551 let state =
552 GroupState { recorded_sources: Default::default(), filter_mode_retransmission_counter };
553
554 (GmpGroupState::new_v2(state), ())
555 })
556}
557
558pub(super) fn leave_group<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
572 bindings_ctx: &mut BC,
573 group_addr: MulticastAddr<I::Addr>,
574 state: GmpStateRef<'_, I, CC, BC>,
575) -> GroupLeaveResult {
576 let GmpStateRef { enabled, groups, gmp, config: _ } = state;
577 debug_assert!(gmp.gmp_mode().is_v2());
578 groups.leave_group(group_addr).map(|state| {
579 let group_addr = if let Some(a) = GmpEnabledGroup::new(group_addr) { a } else { return };
580
581 let _: Option<_> =
583 gmp.timers.cancel(bindings_ctx, &TimerId::MulticastAddress(group_addr).into());
584
585 let GroupState { filter_mode_retransmission_counter: _, recorded_sources: _ } =
588 state.into_v2();
589
590 if !enabled {
591 return;
592 }
593 assert_eq!(
594 gmp.v2_proto.left_groups.insert(group_addr, gmp.v2_proto.robustness_variable),
595 None
596 );
597 trigger_state_change_report(bindings_ctx, &mut gmp.timers);
598 })
599}
600
601fn trigger_state_change_report<I: IpExt, BC: GmpBindingsContext>(
610 bindings_ctx: &mut BC,
611 timers: &mut LocalTimerHeap<gmp::TimerIdInner<I>, (), BC>,
612) {
613 let now = bindings_ctx.now();
614 let timer_id = TimerId::StateChange.into();
615 let schedule_timer = timers.get(&timer_id).is_none_or(|(scheduled, ())| {
616 scheduled.saturating_duration_since(now) > STATE_CHANGE_REPORT_DELAY
617 });
618 if schedule_timer {
619 let _: Option<_> =
620 timers.schedule_after(bindings_ctx, timer_id, (), STATE_CHANGE_REPORT_DELAY);
621 }
622}
623
624pub(super) fn handle_timer<I: IpExt, CC: GmpContextInner<I, BC>, BC: GmpBindingsContext>(
636 core_ctx: &mut CC,
637 bindings_ctx: &mut BC,
638 device: &CC::DeviceId,
639 timer: TimerId<I>,
640 state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
641) {
642 match timer {
643 TimerId::GeneralQuery => handle_general_query_timer(core_ctx, bindings_ctx, device, state),
644 TimerId::MulticastAddress(multicast_addr) => {
645 handle_multicast_address_timer(core_ctx, bindings_ctx, device, multicast_addr, state)
646 }
647 TimerId::StateChange => handle_state_change_timer(core_ctx, bindings_ctx, device, state),
648 }
649}
650
651fn handle_general_query_timer<I: IpExt, CC: GmpContextInner<I, BC>, BC: GmpBindingsContext>(
663 core_ctx: &mut CC,
664 bindings_ctx: &mut BC,
665 device: &CC::DeviceId,
666 state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
667) {
668 let GmpStateRef { enabled: _, groups, gmp: _, config: _ } = state;
669 let report = groups.iter().filter_map(|(addr, state)| {
670 let _ = state;
673
674 let group = GmpEnabledGroup::new(*addr)?;
676
677 Some(GroupRecord::new(group, GroupRecordType::ModeIsExclude))
685 });
686 core_ctx.send_report_v2(bindings_ctx, device, report)
687}
688
689fn handle_multicast_address_timer<I: IpExt, CC: GmpContextInner<I, BC>, BC: GmpBindingsContext>(
716 core_ctx: &mut CC,
717 bindings_ctx: &mut BC,
718 device: &CC::DeviceId,
719 multicast_addr: GmpEnabledGroup<I::Addr>,
720 state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
721) {
722 let GmpStateRef { enabled: _, groups, gmp: _, config: _ } = state;
723 let state = groups
726 .get_mut(multicast_addr.as_ref())
727 .expect("multicast timer fired for removed address")
728 .v2_mut();
729 let recorded_sources = core::mem::take(&mut state.recorded_sources);
730
731 let (mode, sources) = if recorded_sources.is_empty() {
732 (GroupRecordType::ModeIsExclude, either::Either::Left(core::iter::empty::<&I::Addr>()))
739 } else {
740 (GroupRecordType::ModeIsInclude, either::Either::Right(recorded_sources.iter()))
747 };
748 core_ctx.send_report_v2(
749 bindings_ctx,
750 device,
751 core::iter::once(GroupRecord::new_with_sources(multicast_addr, mode, sources)),
752 );
753}
754
755fn handle_state_change_timer<I: IpExt, CC: GmpContextInner<I, BC>, BC: GmpBindingsContext>(
778 core_ctx: &mut CC,
779 bindings_ctx: &mut BC,
780 device: &CC::DeviceId,
781 state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
782) {
783 let GmpStateRef { enabled: _, groups, gmp, config } = state;
784
785 let joined_groups = groups.iter().filter_map(|(multicast_addr, state)| {
786 let GroupState { filter_mode_retransmission_counter, recorded_sources: _ } = state.v2();
787 if *filter_mode_retransmission_counter == 0 {
788 return None;
789 }
790 let multicast_addr = GmpEnabledGroup::new(*multicast_addr)?;
791 Some(GroupRecord::new(
792 multicast_addr,
793 GroupRecordType::ChangeToExcludeMode,
796 ))
797 });
798 let left_groups = gmp.v2_proto.left_groups.keys().map(|multicast_addr| {
799 GroupRecord::new(
800 *multicast_addr,
801 GroupRecordType::ChangeToIncludeMode,
804 )
805 });
806 let state_change_report = joined_groups.chain(left_groups);
807 core_ctx.send_report_v2(bindings_ctx, device, state_change_report);
808
809 let has_more = groups.iter_mut().fold(false, |has_more, (_, g)| {
811 let v2 = g.v2_mut();
812 v2.filter_mode_retransmission_counter =
813 v2.filter_mode_retransmission_counter.saturating_sub(1);
814 has_more || v2.filter_mode_retransmission_counter != 0
815 });
816 gmp.v2_proto.left_groups.retain(|_, counter| match NonZeroU8::new(counter.get() - 1) {
817 None => false,
818 Some(new_value) => {
819 *counter = new_value;
820 true
821 }
822 });
823 let has_more = has_more || !gmp.v2_proto.left_groups.is_empty();
824 if has_more {
825 let delay = gmp::random_report_timeout(
826 &mut bindings_ctx.rng(),
827 config.unsolicited_report_interval().get(),
828 );
829 assert_eq!(
830 gmp.timers.schedule_after(bindings_ctx, TimerId::StateChange.into(), (), delay),
831 None
832 );
833 }
834}
835
836pub(super) fn handle_enabled<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
844 bindings_ctx: &mut BC,
845 state: GmpStateRef<'_, I, CC, BC>,
846) {
847 let GmpStateRef { enabled: _, groups, gmp, config: _ } = state;
848
849 let needs_report = groups.iter_mut().fold(false, |needs_report, (multicast_addr, state)| {
850 if !I::should_perform_gmp(*multicast_addr) {
851 return needs_report;
852 }
853 let GroupState { filter_mode_retransmission_counter, recorded_sources: _ } = state.v2_mut();
854 *filter_mode_retransmission_counter = gmp.v2_proto.robustness_variable.get();
855 true
856 });
857 if needs_report {
858 trigger_state_change_report(bindings_ctx, &mut gmp.timers);
859 }
860}
861
862pub(super) fn handle_disabled<I: IpExt, CC: GmpContextInner<I, BC>, BC: GmpBindingsContext>(
875 core_ctx: &mut CC,
876 bindings_ctx: &mut BC,
877 device: &CC::DeviceId,
878 state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
879) {
880 let GmpStateRef { enabled: _, groups, gmp, config: _ } = state;
881 for (_, state) in groups.iter_mut() {
883 *state.v2_mut() = GroupState {
884 filter_mode_retransmission_counter: 0,
885 recorded_sources: Default::default(),
886 };
887 }
888
889 let member_groups =
890 groups.iter().filter_map(|(multicast_addr, _)| GmpEnabledGroup::new(*multicast_addr));
891 let non_member_groups = gmp.v2_proto.left_groups.keys().copied();
894
895 let mut report = member_groups
896 .chain(non_member_groups)
897 .map(|addr| GroupRecord::new(addr, GroupRecordType::ChangeToIncludeMode))
898 .peekable();
899 if report.peek().is_none() {
900 return;
902 }
903 core_ctx.send_report_v2(bindings_ctx, device, report);
904}
905
906#[cfg(test)]
907mod tests {
908 use alloc::vec;
909 use alloc::vec::Vec;
910
911 use assert_matches::assert_matches;
912 use ip_test_macro::ip_test;
913 use net_types::Witness as _;
914 use netstack3_base::testutil::{FakeDeviceId, FakeTimerCtxExt, FakeWeakDeviceId};
915 use netstack3_base::InstantContext as _;
916 use test_case::{test_case, test_matrix};
917
918 use super::*;
919 use crate::gmp::GmpTimerId;
920 use crate::internal::gmp::testutil::{
921 self, FakeCtx, FakeGmpBindingsContext, FakeGmpContext, FakeV2Query, TestIpExt,
922 };
923 use crate::internal::gmp::{GmpHandler as _, GroupJoinResult};
924
925 #[derive(Debug, Eq, PartialEq)]
926 enum SpecificQuery {
927 Multicast,
928 MulticastAndSource,
929 }
930
931 fn join_and_ignore_unsolicited<I: IpExt>(
932 ctx: &mut FakeCtx<I>,
933 groups: impl IntoIterator<Item = MulticastAddr<I::Addr>>,
934 ) {
935 let FakeCtx { core_ctx, bindings_ctx } = ctx;
936 for group in groups {
937 assert_eq!(
938 core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, group),
939 GroupJoinResult::Joined(())
940 );
941 }
942 while !core_ctx.gmp.timers.is_empty() {
943 assert_eq!(
944 bindings_ctx.trigger_next_timer(core_ctx),
945 Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
946 );
947 }
948 core_ctx.inner.v2_messages.clear();
949 }
950
951 impl<I: IpExt> TimerId<I> {
952 fn multicast(addr: MulticastAddr<I::Addr>) -> Self {
953 Self::MulticastAddress(GmpEnabledGroup::new(addr).unwrap())
954 }
955 }
956
957 #[ip_test(I)]
958 fn v2_query_handoff_in_v1_mode<I: TestIpExt>() {
959 let FakeCtx { mut core_ctx, mut bindings_ctx } =
960 testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: true });
961 assert_eq!(
962 core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
963 GroupJoinResult::Joined(())
964 );
965 assert_eq!(
966 bindings_ctx.trigger_next_timer(&mut core_ctx),
967 Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
968 );
969 assert_matches!(
971 core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().v1().get_inner(),
972 gmp::v1::MemberState::Idle(_)
973 );
974 handle_query_message(
975 &mut core_ctx,
976 &mut bindings_ctx,
977 &FakeDeviceId,
978 &FakeV2Query { group_addr: I::GROUP_ADDR1.get(), ..Default::default() },
979 )
980 .expect("handle query");
981 assert_matches!(
983 core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().v1().get_inner(),
984 gmp::v1::MemberState::Delaying(_)
985 );
986 }
987
988 #[ip_test(I)]
989 fn general_query_ignored_if_no_groups<I: TestIpExt>() {
990 let FakeCtx { mut core_ctx, mut bindings_ctx } =
991 testutil::new_context_with_mode::<I>(GmpMode::V2);
992 handle_query_message(
993 &mut core_ctx,
994 &mut bindings_ctx,
995 &FakeDeviceId,
996 &FakeV2Query { group_addr: I::UNSPECIFIED_ADDRESS, ..Default::default() },
997 )
998 .expect("handle query");
999 assert_eq!(core_ctx.gmp.timers.get(&TimerId::GeneralQuery.into()), None);
1000 }
1001
1002 #[ip_test(I)]
1003 fn query_errors_if_not_multicast<I: TestIpExt>() {
1004 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1005 testutil::new_context_with_mode::<I>(GmpMode::V2);
1006 let query = FakeV2Query { group_addr: I::LOOPBACK_ADDRESS.get(), ..Default::default() };
1007 assert_eq!(
1008 handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query,),
1009 Err(QueryError::NotAMember(query.group_addr))
1010 );
1011 }
1012
1013 #[ip_test(I)]
1014 fn general_query_scheduled<I: TestIpExt>() {
1015 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1016 testutil::new_context_with_mode::<I>(GmpMode::V2);
1017 assert_eq!(
1018 core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1019 GroupJoinResult::Joined(())
1020 );
1021 let query = FakeV2Query { group_addr: I::UNSPECIFIED_ADDRESS, ..Default::default() };
1022
1023 let general_query_timer = TimerId::GeneralQuery.into();
1024
1025 handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query)
1026 .expect("handle query");
1027 let now = bindings_ctx.now();
1028 let (scheduled, ()) = core_ctx.gmp.timers.assert_range_single(
1029 &general_query_timer,
1030 now..=now.panicking_add(query.max_response_time),
1031 );
1032
1033 bindings_ctx.timers.instant.sleep(query.max_response_time);
1038
1039 let query = FakeV2Query { group_addr: I::UNSPECIFIED_ADDRESS, ..Default::default() };
1040 handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query)
1041 .expect("handle query");
1042 assert_eq!(core_ctx.gmp.timers.get(&general_query_timer), Some((scheduled, &())));
1043
1044 let query = FakeV2Query { group_addr: I::GROUP_ADDR1.get(), ..Default::default() };
1045 handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query)
1046 .expect("handle query");
1047 assert_eq!(core_ctx.gmp.timers.get(&TimerId::multicast(I::GROUP_ADDR1).into()), None);
1048 }
1049
1050 #[ip_test(I)]
1051 fn specific_query_ignored_if_not_member<I: TestIpExt>() {
1052 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1053 testutil::new_context_with_mode::<I>(GmpMode::V2);
1054 assert_eq!(
1055 core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR2),
1056 GroupJoinResult::Joined(())
1057 );
1058 let query = FakeV2Query { group_addr: I::GROUP_ADDR1.get(), ..Default::default() };
1059 assert_eq!(
1060 handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query),
1061 Err(QueryError::NotAMember(query.group_addr))
1062 );
1063 }
1064
1065 #[ip_test(I)]
1066 fn leave_group_cancels_multicast_address_timer<I: TestIpExt>() {
1067 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1068 testutil::new_context_with_mode::<I>(GmpMode::V2);
1069 assert_eq!(
1070 core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1071 GroupJoinResult::Joined(())
1072 );
1073 let query = FakeV2Query { group_addr: I::GROUP_ADDR1.get(), ..Default::default() };
1074 handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query)
1075 .expect("handle query");
1076 assert_matches!(
1077 core_ctx.gmp.timers.get(&TimerId::multicast(I::GROUP_ADDR1).into()),
1078 Some(_)
1079 );
1080 assert_eq!(
1081 core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1082 GroupLeaveResult::Left(())
1083 );
1084 assert_matches!(core_ctx.gmp.timers.get(&TimerId::multicast(I::GROUP_ADDR1).into()), None);
1085 }
1086
1087 #[ip_test(I)]
1088 #[test_matrix(
1089 [SpecificQuery::Multicast, SpecificQuery::MulticastAndSource],
1090 [SpecificQuery::Multicast, SpecificQuery::MulticastAndSource]
1091 )]
1092 fn schedule_specific_query<I: TestIpExt>(first: SpecificQuery, second: SpecificQuery) {
1093 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1094 testutil::new_context_with_mode::<I>(GmpMode::V2);
1095 assert_eq!(
1096 core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1097 GroupJoinResult::Joined(())
1098 );
1099
1100 let sources = match first {
1101 SpecificQuery::Multicast => Default::default(),
1102 SpecificQuery::MulticastAndSource => {
1103 (1..3).map(|i| I::get_other_ip_address(i).get()).collect()
1104 }
1105 };
1106
1107 let query1 =
1108 FakeV2Query { group_addr: I::GROUP_ADDR1.get(), sources, ..Default::default() };
1109 handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query1)
1110 .expect("handle query");
1111 assert_eq!(
1113 core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().v2().recorded_sources,
1114 query1.sources.iter().copied().collect()
1115 );
1116 let now = bindings_ctx.now();
1118 let (scheduled, ()) = core_ctx.gmp.timers.assert_range_single(
1119 &TimerId::multicast(I::GROUP_ADDR1).into(),
1120 now..=now.panicking_add(query1.max_response_time),
1121 );
1122
1123 let sources = match second {
1124 SpecificQuery::Multicast => Default::default(),
1125 SpecificQuery::MulticastAndSource => {
1126 (3..5).map(|i| I::get_other_ip_address(i).get()).collect()
1127 }
1128 };
1129 let query2 = FakeV2Query {
1130 group_addr: I::GROUP_ADDR1.get(),
1131 max_response_time: DEFAULT_QUERY_RESPONSE_INTERVAL.get() / 2,
1133 sources,
1134 ..Default::default()
1135 };
1136 handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query2)
1137 .expect("handle query");
1138
1139 let (new_scheduled, ()) = core_ctx.gmp.timers.assert_range_single(
1140 &TimerId::multicast(I::GROUP_ADDR1).into(),
1141 now..=now.panicking_add(query2.max_response_time),
1142 );
1143 assert!(new_scheduled <= scheduled, "{new_scheduled:?} <= {scheduled:?}");
1145 let recorded_sources = &core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().v2().recorded_sources;
1147 match (first, second) {
1148 (SpecificQuery::Multicast, _) | (_, SpecificQuery::Multicast) => {
1149 assert_eq!(recorded_sources, &HashSet::new());
1155 }
1156 (SpecificQuery::MulticastAndSource, SpecificQuery::MulticastAndSource) => {
1157 assert_eq!(
1159 recorded_sources,
1160 &query1.sources.iter().chain(query2.sources.iter()).copied().collect()
1161 );
1162 }
1163 }
1164 }
1165
1166 #[ip_test(I)]
1167 fn send_general_query_response<I: TestIpExt>() {
1168 let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1169 join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1, I::GROUP_ADDR2]);
1170 let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1171 handle_query_message(core_ctx, bindings_ctx, &FakeDeviceId, &FakeV2Query::default())
1172 .expect("handle query");
1173 assert_eq!(
1174 bindings_ctx.trigger_next_timer(core_ctx),
1175 Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
1176 );
1177 assert_eq!(
1178 core_ctx.inner.v2_messages,
1179 vec![vec![
1180 (I::GROUP_ADDR1, GroupRecordType::ModeIsExclude, vec![]),
1181 (I::GROUP_ADDR2, GroupRecordType::ModeIsExclude, vec![]),
1182 ]]
1183 );
1184 }
1185
1186 #[ip_test(I)]
1187 fn send_multicast_address_specific_query_response<I: TestIpExt>() {
1188 let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1189 join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1]);
1190 let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1191 handle_query_message(
1192 core_ctx,
1193 bindings_ctx,
1194 &FakeDeviceId,
1195 &FakeV2Query { group_addr: I::GROUP_ADDR1.get(), ..Default::default() },
1196 )
1197 .expect("handle query");
1198 assert_eq!(
1199 bindings_ctx.trigger_next_timer(core_ctx),
1200 Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
1201 );
1202 assert_eq!(
1203 core_ctx.inner.v2_messages,
1204 vec![vec![(I::GROUP_ADDR1, GroupRecordType::ModeIsExclude, vec![])]]
1205 );
1206 }
1207
1208 #[ip_test(I)]
1209 fn send_multicast_address_and_source_specific_query_response<I: TestIpExt>() {
1210 let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1211 join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1]);
1212 let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1213 let query = FakeV2Query {
1214 group_addr: I::GROUP_ADDR1.get(),
1215 sources: vec![I::get_other_ip_address(1).get(), I::get_other_ip_address(2).get()],
1216 ..Default::default()
1217 };
1218 handle_query_message(core_ctx, bindings_ctx, &FakeDeviceId, &query).expect("handle query");
1219 assert_eq!(
1220 bindings_ctx.trigger_next_timer(core_ctx),
1221 Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
1222 );
1223 assert_eq!(
1224 core_ctx.inner.v2_messages,
1225 vec![vec![(I::GROUP_ADDR1, GroupRecordType::ModeIsInclude, query.sources)]]
1226 );
1227 }
1228
1229 #[ip_test(I)]
1230 #[test_case(2)]
1231 #[test_case(4)]
1232 fn join_group_unsolicited_reports<I: TestIpExt>(robustness_variable: u8) {
1233 let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1234 let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1235 core_ctx.gmp.v2_proto.robustness_variable = NonZeroU8::new(robustness_variable).unwrap();
1236 assert_eq!(
1237 core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1238 GroupJoinResult::Joined(())
1239 );
1240 assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1242 let now = bindings_ctx.now();
1243 assert_eq!(
1244 core_ctx.gmp.timers.get(&TimerId::StateChange.into()),
1245 Some((now.panicking_add(STATE_CHANGE_REPORT_DELAY), &()))
1246 );
1247 let mut count = 0;
1248 while let Some(timer) = bindings_ctx.trigger_next_timer(core_ctx) {
1249 count += 1;
1250 assert_eq!(timer, GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)));
1251 let messages = core::mem::take(&mut core_ctx.inner.v2_messages);
1252 assert_eq!(
1253 messages,
1254 vec![vec![(I::GROUP_ADDR1, GroupRecordType::ChangeToExcludeMode, vec![])]]
1255 );
1256
1257 if count != robustness_variable {
1258 let now = bindings_ctx.now();
1259 core_ctx.gmp.timers.assert_range([(
1260 &TimerId::StateChange.into(),
1261 now..=now.panicking_add(core_ctx.config.unsolicited_report_interval().get()),
1262 )]);
1263 }
1264 }
1265 assert_eq!(count, robustness_variable);
1266 core_ctx.gmp.timers.assert_timers([]);
1267
1268 assert_eq!(
1270 core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1271 GroupJoinResult::AlreadyMember
1272 );
1273 core_ctx.gmp.timers.assert_timers([]);
1275 assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1276 }
1277
1278 #[ip_test(I)]
1279 #[test_case(2)]
1280 #[test_case(4)]
1281 fn leave_group_unsolicited_reports<I: TestIpExt>(robustness_variable: u8) {
1282 let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1283 join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1]);
1284 let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1285 core_ctx.gmp.v2_proto.robustness_variable = NonZeroU8::new(robustness_variable).unwrap();
1286
1287 assert_eq!(
1290 core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1291 GroupJoinResult::AlreadyMember
1292 );
1293
1294 assert_eq!(
1296 core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR2),
1297 GroupLeaveResult::NotMember
1298 );
1299 core_ctx.gmp.timers.assert_timers([]);
1300 assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1301
1302 assert_eq!(
1304 core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1305 GroupLeaveResult::StillMember
1306 );
1307 core_ctx.gmp.timers.assert_timers([]);
1308 assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1309
1310 assert_eq!(
1311 core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1312 GroupLeaveResult::Left(())
1313 );
1314 let mut count = 0;
1315 while let Some(timer) = bindings_ctx.trigger_next_timer(core_ctx) {
1316 count += 1;
1317 assert_eq!(timer, GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)));
1318
1319 let messages = core::mem::take(&mut core_ctx.inner.v2_messages);
1320 assert_eq!(
1321 messages,
1322 vec![vec![(I::GROUP_ADDR1, GroupRecordType::ChangeToIncludeMode, vec![])]]
1323 );
1324
1325 if count != robustness_variable {
1326 let now = bindings_ctx.now();
1327 core_ctx.gmp.timers.assert_range([(
1328 &TimerId::StateChange.into(),
1329 now..=now.panicking_add(core_ctx.config.unsolicited_report_interval().get()),
1330 )]);
1331 }
1332 }
1333 assert_eq!(count, robustness_variable);
1334 core_ctx.gmp.timers.assert_timers([]);
1335 assert_eq!(core_ctx.gmp.v2_proto.left_groups, HashMap::new());
1336
1337 assert_eq!(
1339 core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1340 GroupLeaveResult::NotMember
1341 );
1342 core_ctx.gmp.timers.assert_timers([]);
1343 assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1344 }
1345
1346 #[ip_test(I)]
1347 #[test_matrix(
1348 0..=3,
1349 0..=3
1350 )]
1351 fn join_and_leave<I: TestIpExt>(wait_join: u8, wait_leave: u8) {
1352 let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1353 let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1354 core_ctx.gmp.v2_proto.robustness_variable = NonZeroU8::new(3).unwrap();
1357
1358 let wait_reports = |core_ctx: &mut FakeGmpContext<I>,
1359 bindings_ctx: &mut FakeGmpBindingsContext<I>,
1360 mode,
1361 count: u8| {
1362 for _ in 0..count {
1363 assert_eq!(
1364 bindings_ctx.trigger_next_timer(core_ctx),
1365 Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
1366 );
1367 }
1368 let messages = core::mem::take(&mut core_ctx.inner.v2_messages);
1369 assert_eq!(messages.len(), usize::from(count));
1370 for m in messages {
1371 assert_eq!(m, vec![(I::GROUP_ADDR1, mode, vec![])]);
1372 }
1373 };
1374
1375 for _ in 0..3 {
1376 assert_eq!(
1377 core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1378 GroupJoinResult::Joined(())
1379 );
1380 assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1381 let now = bindings_ctx.now();
1382 core_ctx.gmp.timers.assert_range([(
1383 &TimerId::StateChange.into(),
1384 now..=now.panicking_add(STATE_CHANGE_REPORT_DELAY),
1385 )]);
1386 wait_reports(core_ctx, bindings_ctx, GroupRecordType::ChangeToExcludeMode, wait_join);
1387
1388 assert_eq!(
1389 core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1390 GroupLeaveResult::Left(())
1391 );
1392 assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1393 let now = bindings_ctx.now();
1394 core_ctx.gmp.timers.assert_range([(
1395 &TimerId::StateChange.into(),
1396 now..=now.panicking_add(STATE_CHANGE_REPORT_DELAY),
1397 )]);
1398 wait_reports(core_ctx, bindings_ctx, GroupRecordType::ChangeToIncludeMode, wait_leave);
1399 }
1400 }
1401
1402 #[derive(Debug)]
1403 enum GroupOp {
1404 Join,
1405 Leave,
1406 }
1407 #[ip_test(I)]
1408 #[test_matrix(
1409 0..=3,
1410 [GroupOp::Join, GroupOp::Leave]
1411 )]
1412 fn merge_reports<I: TestIpExt>(wait_reports: u8, which_op: GroupOp) {
1413 let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1414 match which_op {
1415 GroupOp::Join => {}
1416 GroupOp::Leave => {
1417 join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1]);
1419 }
1420 }
1421
1422 let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1423 core_ctx.gmp.v2_proto.robustness_variable = NonZeroU8::new(3).unwrap();
1426
1427 assert_eq!(
1429 core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR2),
1430 GroupJoinResult::Joined(())
1431 );
1432 for _ in 0..wait_reports {
1433 assert_eq!(
1434 bindings_ctx.trigger_next_timer(core_ctx),
1435 Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
1436 );
1437 }
1438 assert_eq!(
1441 core::mem::take(&mut core_ctx.inner.v2_messages).len(),
1442 usize::from(wait_reports)
1443 );
1444 let expect_record_type = match which_op {
1445 GroupOp::Join => {
1446 assert_eq!(
1447 core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1448 GroupJoinResult::Joined(())
1449 );
1450 GroupRecordType::ChangeToExcludeMode
1451 }
1452 GroupOp::Leave => {
1453 assert_eq!(
1454 core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1455 GroupLeaveResult::Left(())
1456 );
1457 GroupRecordType::ChangeToIncludeMode
1458 }
1459 };
1460 assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1462 let now = bindings_ctx.now();
1464 core_ctx.gmp.timers.assert_range([(
1465 &TimerId::StateChange.into(),
1466 now..=now.panicking_add(STATE_CHANGE_REPORT_DELAY),
1467 )]);
1468 let reports = core_ctx.gmp.v2_proto.robustness_variable.get();
1471
1472 let expected_messages = (0..reports)
1474 .map(|count| {
1475 assert_eq!(
1476 bindings_ctx.trigger_next_timer(core_ctx),
1477 Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
1478 );
1479 let mut expect = vec![(I::GROUP_ADDR1, expect_record_type, vec![])];
1480 if count < reports - wait_reports {
1481 expect.push((I::GROUP_ADDR2, GroupRecordType::ChangeToExcludeMode, vec![]));
1482 }
1483 expect
1484 })
1485 .collect::<Vec<_>>();
1486 assert_eq!(core_ctx.inner.v2_messages, expected_messages);
1487 core_ctx.gmp.timers.assert_timers([]);
1488 assert_eq!(core_ctx.gmp.v2_proto.left_groups, HashMap::new());
1489 }
1490
1491 #[ip_test(I)]
1492 fn enable_disable<I: TestIpExt>() {
1493 let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1494 join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1, I::GROUP_ADDR2]);
1495 let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1496
1497 core_ctx.gmp_handle_maybe_enabled(bindings_ctx, &FakeDeviceId);
1500 core_ctx.gmp.timers.assert_timers([]);
1501 assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1502
1503 core_ctx.enabled = false;
1505 core_ctx.gmp_handle_disabled(bindings_ctx, &FakeDeviceId);
1506 core_ctx.gmp.timers.assert_timers([]);
1507 assert_eq!(
1508 core::mem::take(&mut core_ctx.inner.v2_messages),
1509 vec![vec![
1510 (I::GROUP_ADDR1, GroupRecordType::ChangeToIncludeMode, vec![],),
1511 (I::GROUP_ADDR2, GroupRecordType::ChangeToIncludeMode, vec![],),
1512 ]]
1513 );
1514
1515 core_ctx.gmp_handle_disabled(bindings_ctx, &FakeDeviceId);
1517 core_ctx.gmp.timers.assert_timers([]);
1518 assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1519
1520 core_ctx.enabled = true;
1522 core_ctx.gmp_handle_maybe_enabled(bindings_ctx, &FakeDeviceId);
1523 let now = bindings_ctx.now();
1524 core_ctx.gmp.timers.assert_range([(
1525 &TimerId::StateChange.into(),
1526 now..=now.panicking_add(STATE_CHANGE_REPORT_DELAY),
1527 )]);
1528 assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1531
1532 while let Some(timer) = bindings_ctx.trigger_next_timer(core_ctx) {
1533 assert_eq!(timer, GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)));
1534 }
1535 let expect_messages = core::iter::repeat_with(|| {
1536 vec![
1537 (I::GROUP_ADDR1, GroupRecordType::ChangeToExcludeMode, vec![]),
1538 (I::GROUP_ADDR2, GroupRecordType::ChangeToExcludeMode, vec![]),
1539 ]
1540 })
1541 .take(core_ctx.gmp.v2_proto.robustness_variable.get().into())
1542 .collect::<Vec<_>>();
1543 assert_eq!(core::mem::take(&mut core_ctx.inner.v2_messages), expect_messages);
1544
1545 assert_eq!(
1549 core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1550 GroupLeaveResult::Left(())
1551 );
1552 assert_eq!(
1553 core_ctx.gmp.v2_proto.left_groups.get(&GmpEnabledGroup::new(I::GROUP_ADDR1).unwrap()),
1554 Some(&core_ctx.gmp.v2_proto.robustness_variable)
1555 );
1556 core_ctx.enabled = false;
1559 core_ctx.gmp_handle_disabled(bindings_ctx, &FakeDeviceId);
1560 core_ctx.gmp.timers.assert_timers([]);
1561 assert_eq!(
1562 core::mem::take(&mut core_ctx.inner.v2_messages),
1563 vec![vec![
1564 (I::GROUP_ADDR1, GroupRecordType::ChangeToIncludeMode, vec![],),
1565 (I::GROUP_ADDR2, GroupRecordType::ChangeToIncludeMode, vec![],),
1566 ]]
1567 );
1568 }
1569
1570 #[ip_test(I)]
1571 fn ignore_query_if_disabled<I: TestIpExt>() {
1572 let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1573 let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1574 core_ctx.enabled = false;
1575 core_ctx.gmp_handle_disabled(bindings_ctx, &FakeDeviceId);
1576
1577 assert_eq!(
1578 core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1579 GroupJoinResult::Joined(())
1580 );
1581
1582 assert_eq!(
1584 handle_query_message(core_ctx, bindings_ctx, &FakeDeviceId, &FakeV2Query::default()),
1585 Err(QueryError::Disabled)
1586 );
1587 core_ctx.gmp.timers.assert_timers([]);
1589 assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1590 }
1591
1592 #[ip_test(I)]
1593 fn clears_v2_proto_state_on_mode_change<I: TestIpExt>() {
1594 let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1595 join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1]);
1596
1597 let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1598 let query = FakeV2Query {
1599 robustness_variable: DEFAULT_ROBUSTNESS_VARIABLE.get() + 1,
1600 query_interval: DEFAULT_QUERY_INTERVAL.get() + Duration::from_secs(1),
1601 ..Default::default()
1602 };
1603 handle_query_message(core_ctx, bindings_ctx, &FakeDeviceId, &query).expect("handle query");
1604 assert_eq!(
1605 core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1606 GroupLeaveResult::Left(())
1607 );
1608 let robustness_variable = NonZeroU8::new(query.robustness_variable).unwrap();
1609 let query_interval = NonZeroDuration::new(query.query_interval).unwrap();
1610 assert_eq!(
1611 core_ctx.gmp.v2_proto,
1612 ProtocolState {
1613 robustness_variable,
1614 query_interval,
1615 left_groups: [(GmpEnabledGroup::new(I::GROUP_ADDR1).unwrap(), robustness_variable)]
1616 .into_iter()
1617 .collect()
1618 }
1619 );
1620
1621 core_ctx.with_gmp_state_mut(&FakeDeviceId, |state| {
1622 gmp::enter_mode(bindings_ctx, state, GmpMode::V1 { compat: false });
1623 });
1624
1625 assert_eq!(
1626 core_ctx.gmp.v2_proto,
1627 ProtocolState { robustness_variable, query_interval, left_groups: HashMap::new() }
1628 );
1629 }
1630}