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