1#[cfg(test)]
25macro_rules! assert_gmp_state {
26 ($ctx:expr, $group:expr, NonMember) => {
27 assert_gmp_state!(@inner $ctx, $group, crate::internal::gmp::v1::MemberState::NonMember(_));
28 };
29 ($ctx:expr, $group:expr, Delaying) => {
30 assert_gmp_state!(@inner $ctx, $group, crate::internal::gmp::v1::MemberState::Delaying(_));
31 };
32 (@inner $ctx:expr, $group:expr, $pattern:pat) => {
33 assert!(matches!($ctx.state.groups().get($group).unwrap().v1().inner.as_ref().unwrap(), $pattern))
34 };
35}
36
37pub(crate) mod igmp;
38pub(crate) mod mld;
39#[cfg(test)]
40mod testutil;
41mod v1;
42mod v2;
43
44use core::fmt::Debug;
45use core::num::NonZeroU64;
46use core::time::Duration;
47
48use assert_matches::assert_matches;
49use log::info;
50use net_types::MulticastAddr;
51use net_types::ip::{Ip, IpAddress, IpVersionMarker};
52use netstack3_base::ref_counted_hash_map::{InsertResult, RefCountedHashMap, RemoveResult};
53use netstack3_base::{
54 AnyDevice, CoreTimerContext, DeviceIdContext, InspectableValue, Inspector,
55 InstantBindingsTypes, LocalTimerHeap, RngContext, TimerBindingsTypes, TimerContext,
56 WeakDeviceIdentifier,
57};
58use rand::Rng;
59
60#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
65pub enum GroupJoinResult<O = ()> {
66 Joined(O),
69 AlreadyMember,
72}
73
74impl<O> GroupJoinResult<O> {
75 pub(crate) fn map<P, F: FnOnce(O) -> P>(self, f: F) -> GroupJoinResult<P> {
79 match self {
80 GroupJoinResult::Joined(output) => GroupJoinResult::Joined(f(output)),
81 GroupJoinResult::AlreadyMember => GroupJoinResult::AlreadyMember,
82 }
83 }
84}
85
86impl<O> From<InsertResult<O>> for GroupJoinResult<O> {
87 fn from(result: InsertResult<O>) -> Self {
88 match result {
89 InsertResult::Inserted(output) => GroupJoinResult::Joined(output),
90 InsertResult::AlreadyPresent => GroupJoinResult::AlreadyMember,
91 }
92 }
93}
94
95#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
100pub enum GroupLeaveResult<T = ()> {
101 Left(T),
103 StillMember,
106 NotMember,
108}
109
110impl<T> GroupLeaveResult<T> {
111 pub(crate) fn map<U, F: FnOnce(T) -> U>(self, f: F) -> GroupLeaveResult<U> {
116 match self {
117 GroupLeaveResult::Left(value) => GroupLeaveResult::Left(f(value)),
118 GroupLeaveResult::StillMember => GroupLeaveResult::StillMember,
119 GroupLeaveResult::NotMember => GroupLeaveResult::NotMember,
120 }
121 }
122}
123
124impl<T> From<RemoveResult<T>> for GroupLeaveResult<T> {
125 fn from(result: RemoveResult<T>) -> Self {
126 match result {
127 RemoveResult::Removed(value) => GroupLeaveResult::Left(value),
128 RemoveResult::StillPresent => GroupLeaveResult::StillMember,
129 RemoveResult::NotPresent => GroupLeaveResult::NotMember,
130 }
131 }
132}
133
134#[cfg_attr(test, derive(Debug))]
140pub struct MulticastGroupSet<A: IpAddress, T> {
141 inner: RefCountedHashMap<MulticastAddr<A>, T>,
142}
143
144impl<A: IpAddress, T> Default for MulticastGroupSet<A, T> {
145 fn default() -> MulticastGroupSet<A, T> {
146 MulticastGroupSet { inner: RefCountedHashMap::default() }
147 }
148}
149
150impl<A: IpAddress, T> MulticastGroupSet<A, T> {
151 fn groups_mut(&mut self) -> impl Iterator<Item = (&MulticastAddr<A>, &mut T)> + '_ {
152 self.inner.iter_mut()
153 }
154
155 fn join_group_with<O, F: FnOnce() -> (T, O)>(
156 &mut self,
157 group: MulticastAddr<A>,
158 f: F,
159 ) -> GroupJoinResult<O> {
160 self.inner.insert_with(group, f).into()
161 }
162
163 fn leave_group(&mut self, group: MulticastAddr<A>) -> GroupLeaveResult<T> {
164 self.inner.remove(group).into()
165 }
166
167 pub(crate) fn contains(&self, group: &MulticastAddr<A>) -> bool {
169 self.inner.contains_key(group)
170 }
171
172 #[cfg(test)]
173 fn get(&self, group: &MulticastAddr<A>) -> Option<&T> {
174 self.inner.get(group)
175 }
176
177 fn get_mut(&mut self, group: &MulticastAddr<A>) -> Option<&mut T> {
178 self.inner.get_mut(group)
179 }
180
181 fn iter_mut<'a>(&'a mut self) -> impl 'a + Iterator<Item = (&'a MulticastAddr<A>, &'a mut T)> {
182 self.inner.iter_mut()
183 }
184
185 fn iter<'a>(&'a self) -> impl 'a + Iterator<Item = (&'a MulticastAddr<A>, &'a T)> + Clone {
186 self.inner.iter()
187 }
188
189 fn is_empty(&self) -> bool {
190 self.inner.is_empty()
191 }
192}
193
194impl<A: IpAddress, T> InspectableValue for MulticastGroupSet<A, T> {
195 fn record<I: Inspector>(&self, name: &str, inspector: &mut I) {
196 inspector.record_child(name, |inspector| {
197 for (addr, ref_count) in self.inner.iter_ref_counts() {
198 inspector.record_display_child(addr, |inspector| {
199 inspector.record_usize("Refs", ref_count.get())
200 });
201 }
202 });
203 }
204}
205
206pub trait GmpQueryHandler<I: Ip, BC>: DeviceIdContext<AnyDevice> {
208 fn gmp_is_in_group(
210 &mut self,
211 device: &Self::DeviceId,
212 group_addr: MulticastAddr<I::Addr>,
213 ) -> bool;
214}
215
216pub trait GmpHandler<I: IpExt, BC>: DeviceIdContext<AnyDevice> {
220 fn gmp_handle_maybe_enabled(&mut self, bindings_ctx: &mut BC, device: &Self::DeviceId);
230
231 fn gmp_handle_disabled(&mut self, bindings_ctx: &mut BC, device: &Self::DeviceId);
239
240 fn gmp_join_group(
242 &mut self,
243 bindings_ctx: &mut BC,
244 device: &Self::DeviceId,
245 group_addr: MulticastAddr<I::Addr>,
246 ) -> GroupJoinResult;
247
248 fn gmp_leave_group(
250 &mut self,
251 bindings_ctx: &mut BC,
252 device: &Self::DeviceId,
253 group_addr: MulticastAddr<I::Addr>,
254 ) -> GroupLeaveResult;
255
256 fn gmp_get_mode(&mut self, device: &Self::DeviceId) -> I::GmpProtoConfigMode;
258
259 fn gmp_set_mode(
264 &mut self,
265 bindings_ctx: &mut BC,
266 device: &Self::DeviceId,
267 new_mode: I::GmpProtoConfigMode,
268 ) -> I::GmpProtoConfigMode;
269}
270
271impl<I: IpExt, BT: GmpBindingsTypes, CC: GmpStateContext<I, BT>> GmpQueryHandler<I, BT> for CC {
272 fn gmp_is_in_group(
273 &mut self,
274 device: &Self::DeviceId,
275 group_addr: MulticastAddr<I::Addr>,
276 ) -> bool {
277 self.with_multicast_groups(device, |groups| groups.contains(&group_addr))
278 }
279}
280
281impl<I: IpExt, BC: GmpBindingsContext, CC: GmpContext<I, BC>> GmpHandler<I, BC> for CC {
282 fn gmp_handle_maybe_enabled(&mut self, bindings_ctx: &mut BC, device: &Self::DeviceId) {
283 self.with_gmp_state_mut_and_ctx(device, |mut core_ctx, state| {
284 if !state.enabled {
285 return;
286 }
287 match core::mem::replace(
289 &mut state.gmp.enablement_idempotency_guard,
290 LastState::Enabled,
291 ) {
292 LastState::Disabled => {}
293 LastState::Enabled => {
294 return;
296 }
297 }
298
299 match state.gmp.gmp_mode() {
300 GmpMode::V1 { compat: _ } => {
301 v1::handle_enabled(&mut core_ctx, bindings_ctx, device, state);
302 }
303 GmpMode::V2 => {
304 v2::handle_enabled(bindings_ctx, state);
305 }
306 }
307 })
308 }
309
310 fn gmp_handle_disabled(&mut self, bindings_ctx: &mut BC, device: &Self::DeviceId) {
311 self.with_gmp_state_mut_and_ctx(device, |mut core_ctx, mut state| {
312 assert!(!state.enabled, "handle_disabled called with enabled GMP state");
313 match core::mem::replace(
315 &mut state.gmp.enablement_idempotency_guard,
316 LastState::Disabled,
317 ) {
318 LastState::Enabled => {}
319 LastState::Disabled => {
320 return;
322 }
323 }
324
325 match state.gmp.gmp_mode() {
326 GmpMode::V1 { .. } => {
327 v1::handle_disabled(&mut core_ctx, bindings_ctx, device, state.as_mut());
328 }
329 GmpMode::V2 => {
330 v2::handle_disabled(&mut core_ctx, bindings_ctx, device, state.as_mut());
331 }
332 }
333 let next_mode =
335 <CC::Inner<'_> as GmpContextInner<I, BC>>::mode_on_disable(&state.gmp.mode);
336 enter_mode(bindings_ctx, state.as_mut(), next_mode);
337 state.gmp.v2_proto = Default::default();
340 state.gmp.timers.clear(bindings_ctx);
342 })
343 }
344
345 fn gmp_join_group(
346 &mut self,
347 bindings_ctx: &mut BC,
348 device: &CC::DeviceId,
349 group_addr: MulticastAddr<I::Addr>,
350 ) -> GroupJoinResult {
351 self.with_gmp_state_mut_and_ctx(device, |mut core_ctx, state| match state.gmp.gmp_mode() {
352 GmpMode::V1 { compat: _ } => {
353 v1::join_group(&mut core_ctx, bindings_ctx, device, group_addr, state)
354 }
355 GmpMode::V2 => v2::join_group(bindings_ctx, group_addr, state),
356 })
357 }
358
359 fn gmp_leave_group(
360 &mut self,
361 bindings_ctx: &mut BC,
362 device: &CC::DeviceId,
363 group_addr: MulticastAddr<I::Addr>,
364 ) -> GroupLeaveResult {
365 self.with_gmp_state_mut_and_ctx(device, |mut core_ctx, state| match state.gmp.gmp_mode() {
366 GmpMode::V1 { compat: _ } => {
367 v1::leave_group(&mut core_ctx, bindings_ctx, device, group_addr, state)
368 }
369 GmpMode::V2 => v2::leave_group(bindings_ctx, group_addr, state),
370 })
371 }
372
373 fn gmp_get_mode(&mut self, device: &CC::DeviceId) -> I::GmpProtoConfigMode {
374 self.with_gmp_state_mut(device, |state| {
375 <CC::Inner<'_> as GmpContextInner<I, BC>>::mode_to_config(&state.gmp.mode)
376 })
377 }
378
379 fn gmp_set_mode(
380 &mut self,
381 bindings_ctx: &mut BC,
382 device: &CC::DeviceId,
383 new_mode: I::GmpProtoConfigMode,
384 ) -> I::GmpProtoConfigMode {
385 self.with_gmp_state_mut(device, |state| {
386 let old_mode =
387 <CC::Inner<'_> as GmpContextInner<I, BC>>::mode_to_config(&state.gmp.mode);
388 info!("GMP({}) mode change by user from {:?} to {:?}", I::NAME, old_mode, new_mode);
389 let new_mode = <CC::Inner<'_> as GmpContextInner<I, BC>>::config_to_mode(
390 &state.gmp.mode,
391 new_mode,
392 );
393 enter_mode(bindings_ctx, state, new_mode);
394 old_mode
395 })
396 }
397}
398
399fn random_report_timeout<R: Rng>(rng: &mut R, period: Duration) -> Duration {
401 let micros = if let Some(micros) =
402 NonZeroU64::new(u64::try_from(period.as_micros()).unwrap_or(u64::MAX))
403 {
404 rng.random_range(1..=micros.get())
407 } else {
408 1
409 };
410 Duration::from_micros(micros)
414}
415
416#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
418pub struct GmpTimerId<I: Ip, D: WeakDeviceIdentifier> {
419 pub(crate) device: D,
420 pub(crate) _marker: IpVersionMarker<I>,
421}
422
423impl<I: Ip, D: WeakDeviceIdentifier> GmpTimerId<I, D> {
424 fn device_id(&self) -> &D {
425 let Self { device, _marker: IpVersionMarker { .. } } = self;
426 device
427 }
428
429 const fn new(device: D) -> Self {
430 Self { device, _marker: IpVersionMarker::new() }
431 }
432}
433
434pub trait GmpBindingsTypes: InstantBindingsTypes + TimerBindingsTypes {}
436impl<BT> GmpBindingsTypes for BT where BT: InstantBindingsTypes + TimerBindingsTypes {}
437
438pub trait GmpBindingsContext: RngContext + TimerContext + GmpBindingsTypes {}
440impl<BC> GmpBindingsContext for BC where BC: RngContext + TimerContext + GmpBindingsTypes {}
441
442pub trait IpExt: Ip {
444 type GmpProtoConfigMode: Debug + Copy + Clone + Eq + PartialEq;
446
447 fn should_perform_gmp(addr: MulticastAddr<Self::Addr>) -> bool;
449}
450
451#[derive(Debug, Eq, PartialEq, Hash, Clone)]
453enum TimerIdInner<I: Ip> {
454 V1(v1::DelayedReportTimerId<I>),
456 V1Compat,
458 V2(v2::TimerId<I>),
459}
460
461impl<I: Ip> From<v1::DelayedReportTimerId<I>> for TimerIdInner<I> {
462 fn from(value: v1::DelayedReportTimerId<I>) -> Self {
463 Self::V1(value)
464 }
465}
466
467impl<I: Ip> From<v2::TimerId<I>> for TimerIdInner<I> {
468 fn from(value: v2::TimerId<I>) -> Self {
469 Self::V2(value)
470 }
471}
472
473#[cfg_attr(test, derive(Debug))]
475pub struct GmpState<I: Ip, CC: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> {
476 timers: LocalTimerHeap<TimerIdInner<I>, (), BT>,
477 mode: CC::ProtoMode,
478 v2_proto: v2::ProtocolState<I>,
479 enablement_idempotency_guard: LastState,
495}
496
497#[cfg_attr(test, derive(Debug))]
499enum LastState {
500 Disabled,
501 Enabled,
502}
503
504impl LastState {
505 fn from_enabled(enabled: bool) -> Self {
506 if enabled { Self::Enabled } else { Self::Disabled }
507 }
508}
509
510impl<I: Ip, T: GmpTypeLayout<I, BC>, BC: GmpBindingsTypes + TimerContext> GmpState<I, T, BC> {
513 pub fn new<D: WeakDeviceIdentifier, CC: CoreTimerContext<GmpTimerId<I, D>, BC>>(
515 bindings_ctx: &mut BC,
516 device: D,
517 ) -> Self {
518 Self::new_with_enabled_and_mode::<D, CC>(bindings_ctx, device, false, Default::default())
519 }
520
521 fn new_with_enabled_and_mode<
529 D: WeakDeviceIdentifier,
530 CC: CoreTimerContext<GmpTimerId<I, D>, BC>,
531 >(
532 bindings_ctx: &mut BC,
533 device: D,
534 enabled: bool,
535 mode: T::ProtoMode,
536 ) -> Self {
537 Self {
538 timers: LocalTimerHeap::new_with_context::<_, CC>(
539 bindings_ctx,
540 GmpTimerId::new(device),
541 ),
542 mode,
543 v2_proto: Default::default(),
544 enablement_idempotency_guard: LastState::from_enabled(enabled),
545 }
546 }
547}
548
549impl<I: IpExt, T: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> GmpState<I, T, BT> {
550 fn gmp_mode(&self) -> GmpMode {
551 self.mode.into()
552 }
553
554 pub(crate) fn mode(&self) -> &T::ProtoMode {
555 &self.mode
556 }
557}
558
559pub struct GmpStateRef<'a, I: IpExt, CC: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> {
561 pub enabled: bool,
563 pub groups: &'a mut MulticastGroupSet<I::Addr, GmpGroupState<I, BT>>,
565 pub gmp: &'a mut GmpState<I, CC, BT>,
567 pub config: &'a CC::Config,
569}
570
571impl<'a, I: IpExt, CC: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> GmpStateRef<'a, I, CC, BT> {
572 fn as_mut(&mut self) -> GmpStateRef<'_, I, CC, BT> {
573 let Self { enabled, groups, gmp, config } = self;
574 GmpStateRef { enabled: *enabled, groups, gmp, config }
575 }
576}
577
578pub trait GmpTypeLayout<I: Ip, BT: GmpBindingsTypes>: Sized {
580 type Config: Debug + v1::ProtocolConfig + v2::ProtocolConfig;
582 type ProtoMode: Debug
584 + Copy
585 + Clone
586 + Eq
587 + PartialEq
588 + Into<GmpMode>
589 + Default
590 + InspectableValue;
591}
592
593pub struct GmpGroupState<I: Ip, BT: GmpBindingsTypes> {
595 version_specific: GmpGroupStateByVersion<I, BT>,
596 }
599
600impl<I: Ip, BT: GmpBindingsTypes> GmpGroupState<I, BT> {
601 fn v1_mut(&mut self) -> &mut v1::GmpStateMachine<BT::Instant> {
609 match &mut self.version_specific {
610 GmpGroupStateByVersion::V1(v1) => return v1,
611 GmpGroupStateByVersion::V2(_) => {
612 panic!("expected GMP v1")
613 }
614 }
615 }
616
617 fn v2_mut(&mut self) -> &mut v2::GroupState<I> {
625 match &mut self.version_specific {
626 GmpGroupStateByVersion::V2(v2) => return v2,
627 GmpGroupStateByVersion::V1(_) => {
628 panic!("expected GMP v2")
629 }
630 }
631 }
632
633 #[cfg(test)]
635 fn v1(&self) -> &v1::GmpStateMachine<BT::Instant> {
636 match &self.version_specific {
637 GmpGroupStateByVersion::V1(v1) => v1,
638 GmpGroupStateByVersion::V2(_) => panic!("group not in v1 mode"),
639 }
640 }
641
642 fn v2(&self) -> &v2::GroupState<I> {
644 match &self.version_specific {
645 GmpGroupStateByVersion::V2(v2) => v2,
646 GmpGroupStateByVersion::V1 { .. } => panic!("group not in v2 mode"),
647 }
648 }
649
650 fn into_v1(self) -> v1::GmpStateMachine<BT::Instant> {
656 let Self { version_specific } = self;
657 match version_specific {
658 GmpGroupStateByVersion::V1(v1) => v1,
659 GmpGroupStateByVersion::V2(_) => panic!("expected GMP v1"),
660 }
661 }
662
663 fn into_v2(self) -> v2::GroupState<I> {
669 let Self { version_specific } = self;
670 match version_specific {
671 GmpGroupStateByVersion::V2(v2) => v2,
672 GmpGroupStateByVersion::V1(_) => panic!("expected GMP v2"),
673 }
674 }
675
676 fn new_v1(v1: v1::GmpStateMachine<BT::Instant>) -> Self {
678 Self { version_specific: GmpGroupStateByVersion::V1(v1) }
679 }
680
681 fn new_v2(v2: v2::GroupState<I>) -> Self {
683 Self { version_specific: GmpGroupStateByVersion::V2(v2) }
684 }
685}
686
687#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
689pub enum GmpMode {
690 V1 {
692 compat: bool,
701 },
702 #[default]
704 V2,
705}
706
707impl GmpMode {
708 fn is_v1(&self) -> bool {
709 match self {
710 Self::V1 { .. } => true,
711 Self::V2 => false,
712 }
713 }
714
715 fn is_v2(&self) -> bool {
716 match self {
717 Self::V2 => true,
718 Self::V1 { .. } => false,
719 }
720 }
721
722 fn maybe_enter_v1_compat(&self) -> Self {
723 match self {
724 Self::V2 => Self::V1 { compat: true },
726 m @ Self::V1 { .. } => *m,
728 }
729 }
730
731 fn maybe_exit_v1_compat(&self) -> Self {
732 match self {
733 m @ Self::V2 | m @ Self::V1 { compat: false } => *m,
735 Self::V1 { compat: true } => Self::V2,
737 }
738 }
739}
740
741#[cfg_attr(test, derive(derivative::Derivative))]
742#[cfg_attr(test, derivative(Debug(bound = "")))]
743enum GmpGroupStateByVersion<I: Ip, BT: GmpBindingsTypes> {
744 V1(v1::GmpStateMachine<BT::Instant>),
745 V2(v2::GroupState<I>),
746}
747
748pub trait GmpStateContext<I: IpExt, BT: GmpBindingsTypes>: DeviceIdContext<AnyDevice> {
750 type TypeLayout: GmpTypeLayout<I, BT>;
752
753 fn with_multicast_groups<
755 O,
756 F: FnOnce(&MulticastGroupSet<I::Addr, GmpGroupState<I, BT>>) -> O,
757 >(
758 &mut self,
759 device: &Self::DeviceId,
760 cb: F,
761 ) -> O {
762 self.with_gmp_state(device, |groups, _gmp_state| cb(groups))
763 }
764
765 fn with_gmp_state<
768 O,
769 F: FnOnce(
770 &MulticastGroupSet<I::Addr, GmpGroupState<I, BT>>,
771 &GmpState<I, Self::TypeLayout, BT>,
772 ) -> O,
773 >(
774 &mut self,
775 device: &Self::DeviceId,
776 cb: F,
777 ) -> O;
778}
779
780trait GmpContext<I: IpExt, BC: GmpBindingsContext>: DeviceIdContext<AnyDevice> {
784 type TypeLayout: GmpTypeLayout<I, BC>;
786
787 type Inner<'a>: GmpContextInner<I, BC, TypeLayout = Self::TypeLayout, DeviceId = Self::DeviceId>
789 + 'a;
790
791 fn with_gmp_state_mut_and_ctx<
794 O,
795 F: FnOnce(Self::Inner<'_>, GmpStateRef<'_, I, Self::TypeLayout, BC>) -> O,
796 >(
797 &mut self,
798 device: &Self::DeviceId,
799 cb: F,
800 ) -> O;
801
802 fn with_gmp_state_mut<O, F: FnOnce(GmpStateRef<'_, I, Self::TypeLayout, BC>) -> O>(
804 &mut self,
805 device: &Self::DeviceId,
806 cb: F,
807 ) -> O {
808 self.with_gmp_state_mut_and_ctx(device, |_core_ctx, state| cb(state))
809 }
810}
811
812trait GmpContextInner<I: IpExt, BC: GmpBindingsContext>: DeviceIdContext<AnyDevice> {
816 type TypeLayout: GmpTypeLayout<I, BC>;
818
819 fn send_message_v1(
821 &mut self,
822 bindings_ctx: &mut BC,
823 device: &Self::DeviceId,
824 cur_mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
825 group_addr: GmpEnabledGroup<I::Addr>,
826 msg_type: v1::GmpMessageType,
827 );
828
829 fn send_report_v2(
831 &mut self,
832 bindings_ctx: &mut BC,
833 device: &Self::DeviceId,
834 groups: impl Iterator<Item: v2::VerifiedReportGroupRecord<I::Addr> + Clone> + Clone,
835 );
836
837 fn mode_update_from_v1_query<Q: v1::QueryMessage<I>>(
840 &mut self,
841 bindings_ctx: &mut BC,
842 query: &Q,
843 gmp_state: &GmpState<I, Self::TypeLayout, BC>,
844 config: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::Config,
845 ) -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
846
847 fn mode_to_config(
850 mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
851 ) -> I::GmpProtoConfigMode;
852
853 fn config_to_mode(
856 cur_mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
857 config: I::GmpProtoConfigMode,
858 ) -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
859
860 fn mode_on_disable(
863 cur_mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
864 ) -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
865
866 fn mode_on_exit_compat() -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
870}
871
872fn handle_timer<I, BC, CC>(
873 core_ctx: &mut CC,
874 bindings_ctx: &mut BC,
875 timer: GmpTimerId<I, CC::WeakDeviceId>,
876) where
877 BC: GmpBindingsContext,
878 CC: GmpContext<I, BC>,
879 I: IpExt,
880{
881 let GmpTimerId { device, _marker: IpVersionMarker { .. } } = timer;
882 let Some(device) = device.upgrade() else {
883 return;
884 };
885 core_ctx.with_gmp_state_mut_and_ctx(&device, |mut core_ctx, state| {
886 let Some((timer_id, ())) = state.gmp.timers.pop(bindings_ctx) else {
887 return;
888 };
889 assert!(state.enabled, "{timer_id:?} fired in GMP disabled state");
891
892 match (timer_id, state.gmp.gmp_mode()) {
893 (TimerIdInner::V1(v1), GmpMode::V1 { .. }) => {
894 v1::handle_timer(&mut core_ctx, bindings_ctx, &device, state, v1);
895 }
896 (TimerIdInner::V1Compat, GmpMode::V1 { compat: true }) => {
897 let mode = <CC::Inner<'_> as GmpContextInner<I, BC>>::mode_on_exit_compat();
898 debug_assert_eq!(mode.into(), GmpMode::V2);
899 enter_mode(bindings_ctx, state, mode);
900 }
901 (TimerIdInner::V2(timer), GmpMode::V2) => {
902 v2::handle_timer(&mut core_ctx, bindings_ctx, &device, timer, state);
903 }
904 (TimerIdInner::V1Compat, bad) => {
905 panic!("v1 compat timer fired in non v1 compat mode: {bad:?}")
906 }
907 bad @ (TimerIdInner::V1(_), GmpMode::V2)
908 | bad @ (TimerIdInner::V2(_), GmpMode::V1 { .. }) => {
909 panic!("incompatible timer fired {bad:?}")
910 }
911 }
912 });
913}
914
915fn enter_mode<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
922 bindings_ctx: &mut BC,
923 state: GmpStateRef<'_, I, CC, BC>,
924 new_mode: CC::ProtoMode,
925) {
926 let GmpStateRef { enabled: _, gmp, groups, config: _ } = state;
927 let old_mode = core::mem::replace(&mut gmp.mode, new_mode);
928 match (old_mode.into(), gmp.gmp_mode()) {
929 (GmpMode::V1 { compat }, GmpMode::V1 { compat: new_compat }) => {
930 if new_compat != compat {
931 assert_eq!(new_compat, false, "attempted to enter compatibility mode from forced");
934 assert_matches!(
936 gmp.timers.cancel(bindings_ctx, &TimerIdInner::V1Compat),
937 Some((_, ()))
938 );
939 info!("GMP({}) enter mode {:?}", I::NAME, &gmp.mode);
940 }
941 return;
942 }
943 (GmpMode::V2, GmpMode::V2) => {
944 return;
946 }
947 (GmpMode::V1 { compat: _ }, GmpMode::V2) => {
948 for (_, GmpGroupState { version_specific }) in groups.iter_mut() {
954 *version_specific =
955 GmpGroupStateByVersion::V2(v2::GroupState::new_for_mode_transition())
956 }
957 }
958 (GmpMode::V2, GmpMode::V1 { compat: _ }) => {
959 for (_, GmpGroupState { version_specific }) in groups.iter_mut() {
965 *version_specific =
966 GmpGroupStateByVersion::V1(v1::GmpStateMachine::new_for_mode_transition())
967 }
968 gmp.v2_proto.on_enter_v1();
969 }
970 };
971 info!("GMP({}) enter mode {:?}", I::NAME, new_mode);
972 gmp.timers.clear(bindings_ctx);
973 gmp.mode = new_mode;
974}
975
976fn schedule_v1_compat<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
977 bindings_ctx: &mut BC,
978 state: GmpStateRef<'_, I, CC, BC>,
979) {
980 let GmpStateRef { gmp, config, .. } = state;
981 let timeout = gmp.v2_proto.older_version_querier_present_timeout(config);
982 let _: Option<_> =
983 gmp.timers.schedule_after(bindings_ctx, TimerIdInner::V1Compat, (), timeout.into());
984}
985
986#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
988struct NotAMemberErr<I: Ip>(I::Addr);
989
990enum QueryTarget<A> {
992 Unspecified,
993 Specified(MulticastAddr<A>),
994}
995
996impl<A: IpAddress> QueryTarget<A> {
997 fn new(addr: A) -> Option<Self> {
998 if addr == <A::Version as Ip>::UNSPECIFIED_ADDRESS {
999 Some(Self::Unspecified)
1000 } else {
1001 MulticastAddr::new(addr).map(Self::Specified)
1002 }
1003 }
1004}
1005
1006mod witness {
1007 use super::*;
1008
1009 #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
1012 pub(super) struct GmpEnabledGroup<A>(MulticastAddr<A>);
1013
1014 impl<A: IpAddress<Version: IpExt>> GmpEnabledGroup<A> {
1015 pub fn new(addr: MulticastAddr<A>) -> Option<Self> {
1018 <A::Version as IpExt>::should_perform_gmp(addr).then_some(Self(addr))
1019 }
1020
1021 pub fn try_new(addr: MulticastAddr<A>) -> Result<Self, MulticastAddr<A>> {
1024 Self::new(addr).ok_or(addr)
1025 }
1026
1027 pub fn multicast_addr(&self) -> MulticastAddr<A> {
1029 let Self(addr) = self;
1030 *addr
1031 }
1032
1033 pub fn into_multicast_addr(self) -> MulticastAddr<A> {
1035 let Self(addr) = self;
1036 addr
1037 }
1038 }
1039
1040 impl<A> AsRef<MulticastAddr<A>> for GmpEnabledGroup<A> {
1041 fn as_ref(&self) -> &MulticastAddr<A> {
1042 let Self(addr) = self;
1043 addr
1044 }
1045 }
1046}
1047use witness::GmpEnabledGroup;
1048
1049#[cfg(test)]
1050mod tests {
1051 use alloc::vec::Vec;
1052 use core::num::NonZeroU8;
1053
1054 use assert_matches::assert_matches;
1055 use ip_test_macro::ip_test;
1056 use net_types::Witness as _;
1057 use netstack3_base::InstantContext as _;
1058 use netstack3_base::testutil::{FakeDeviceId, FakeTimerCtxExt, FakeWeakDeviceId};
1059
1060 use testutil::{FakeCtx, FakeGmpContextInner, FakeV1Query, TestIpExt};
1061
1062 use super::*;
1063
1064 #[ip_test(I)]
1065 fn mode_change_state_clearing<I: TestIpExt>() {
1066 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1067 testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1068
1069 assert_eq!(
1070 core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1071 GroupJoinResult::Joined(())
1072 );
1073 core_ctx.inner.v1_messages.clear();
1076
1077 assert!(core_ctx.gmp.timers.iter().next().is_some());
1079 assert_matches!(
1080 core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().version_specific,
1081 GmpGroupStateByVersion::V1(_)
1082 );
1083
1084 core_ctx.with_gmp_state_mut(&FakeDeviceId, |mut state| {
1085 enter_mode(&mut bindings_ctx, state.as_mut(), GmpMode::V2);
1086 assert_eq!(state.gmp.mode, GmpMode::V2);
1087 });
1088 core_ctx.gmp.timers.assert_timers([]);
1090 assert_matches!(
1091 core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().version_specific,
1092 GmpGroupStateByVersion::V2(_)
1093 );
1094
1095 core_ctx.with_gmp_state_mut(&FakeDeviceId, |mut state| {
1097 enter_mode(&mut bindings_ctx, state.as_mut(), GmpMode::V1 { compat: false });
1098 assert_eq!(state.gmp.mode, GmpMode::V1 { compat: false });
1099 });
1100 assert_matches!(
1101 core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().version_specific,
1102 GmpGroupStateByVersion::V1(_)
1103 );
1104
1105 let FakeGmpContextInner { v1_messages, v2_messages } = &core_ctx.inner;
1107 assert_eq!(v1_messages, &Vec::new());
1108 assert_eq!(v2_messages, &Vec::<Vec<_>>::new());
1109 }
1110
1111 #[ip_test(I)]
1112 #[should_panic(expected = "attempted to enter compatibility mode from forced")]
1113 fn cant_enter_v1_compat<I: TestIpExt>() {
1114 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1115 testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1116 core_ctx.with_gmp_state_mut(&FakeDeviceId, |mut state| {
1117 enter_mode(&mut bindings_ctx, state.as_mut(), GmpMode::V1 { compat: true });
1118 });
1119 }
1120
1121 #[ip_test(I)]
1122 fn disable_exits_compat<I: TestIpExt>() {
1123 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1125 testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: true });
1126 core_ctx.enabled = false;
1127 core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1128 assert_eq!(core_ctx.gmp.mode, GmpMode::V2);
1129
1130 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1132 testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1133 core_ctx.enabled = false;
1134 core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1135 assert_eq!(core_ctx.gmp.mode, GmpMode::V1 { compat: false });
1136 }
1137
1138 #[ip_test(I)]
1139 fn disable_clears_v2_state<I: TestIpExt>() {
1140 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1141 testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1142 let v2::ProtocolState { robustness_variable, query_interval, left_groups } =
1143 &mut core_ctx.gmp.v2_proto;
1144 *robustness_variable = robustness_variable.checked_add(1).unwrap();
1145 *query_interval = *query_interval + Duration::from_secs(20);
1146 *left_groups =
1147 [(GmpEnabledGroup::new(I::GROUP_ADDR1).unwrap(), NonZeroU8::new(1).unwrap())]
1148 .into_iter()
1149 .collect();
1150 core_ctx.enabled = false;
1151 core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1152 assert_eq!(core_ctx.gmp.v2_proto, v2::ProtocolState::default());
1153 }
1154
1155 #[ip_test(I)]
1156 fn v1_compat_mode_on_timeout<I: TestIpExt>() {
1157 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1158 testutil::new_context_with_mode::<I>(GmpMode::V2);
1159 assert_eq!(
1160 v1::handle_query_message(
1161 &mut core_ctx,
1162 &mut bindings_ctx,
1163 &FakeDeviceId,
1164 &FakeV1Query {
1165 group_addr: I::GROUP_ADDR1.get(),
1166 max_response_time: Duration::from_secs(1)
1167 }
1168 ),
1169 Err(NotAMemberErr(I::GROUP_ADDR1.get()))
1170 );
1171 assert_eq!(core_ctx.gmp.mode, GmpMode::V1 { compat: true });
1173
1174 let timeout =
1175 core_ctx.gmp.v2_proto.older_version_querier_present_timeout(&core_ctx.config).into();
1176 core_ctx.gmp.timers.assert_timers([(
1177 TimerIdInner::V1Compat,
1178 (),
1179 bindings_ctx.now() + timeout,
1180 )]);
1181
1182 bindings_ctx.timers.instant.sleep(timeout / 2);
1184 assert_eq!(
1185 v1::handle_query_message(
1186 &mut core_ctx,
1187 &mut bindings_ctx,
1188 &FakeDeviceId,
1189 &FakeV1Query {
1190 group_addr: I::GROUP_ADDR1.get(),
1191 max_response_time: Duration::from_secs(1)
1192 }
1193 ),
1194 Err(NotAMemberErr(I::GROUP_ADDR1.get()))
1195 );
1196 assert_eq!(core_ctx.gmp.mode, GmpMode::V1 { compat: true });
1197 core_ctx.gmp.timers.assert_timers([(
1198 TimerIdInner::V1Compat,
1199 (),
1200 bindings_ctx.now() + timeout,
1201 )]);
1202
1203 let timer = bindings_ctx.trigger_next_timer(&mut core_ctx);
1205 assert_eq!(timer, Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId))));
1206 assert_eq!(core_ctx.gmp.mode, GmpMode::V2);
1207 core_ctx.gmp.timers.assert_timers([]);
1209 let testutil::FakeGmpContextInner { v1_messages, v2_messages } = &core_ctx.inner;
1210 assert_eq!(v1_messages, &Vec::new());
1211 assert_eq!(v2_messages, &Vec::<Vec<_>>::new());
1212 }
1213}