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::ip::{Ip, IpAddress, IpVersionMarker};
51use net_types::MulticastAddr;
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.gen_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 {
507 Self::Enabled
508 } else {
509 Self::Disabled
510 }
511 }
512}
513
514impl<I: Ip, T: GmpTypeLayout<I, BC>, BC: GmpBindingsTypes + TimerContext> GmpState<I, T, BC> {
517 pub fn new<D: WeakDeviceIdentifier, CC: CoreTimerContext<GmpTimerId<I, D>, BC>>(
519 bindings_ctx: &mut BC,
520 device: D,
521 ) -> Self {
522 Self::new_with_enabled_and_mode::<D, CC>(bindings_ctx, device, false, Default::default())
523 }
524
525 fn new_with_enabled_and_mode<
533 D: WeakDeviceIdentifier,
534 CC: CoreTimerContext<GmpTimerId<I, D>, BC>,
535 >(
536 bindings_ctx: &mut BC,
537 device: D,
538 enabled: bool,
539 mode: T::ProtoMode,
540 ) -> Self {
541 Self {
542 timers: LocalTimerHeap::new_with_context::<_, CC>(
543 bindings_ctx,
544 GmpTimerId::new(device),
545 ),
546 mode,
547 v2_proto: Default::default(),
548 enablement_idempotency_guard: LastState::from_enabled(enabled),
549 }
550 }
551}
552
553impl<I: IpExt, T: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> GmpState<I, T, BT> {
554 fn gmp_mode(&self) -> GmpMode {
555 self.mode.into()
556 }
557
558 pub(crate) fn mode(&self) -> &T::ProtoMode {
559 &self.mode
560 }
561}
562
563pub struct GmpStateRef<'a, I: IpExt, CC: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> {
565 pub enabled: bool,
567 pub groups: &'a mut MulticastGroupSet<I::Addr, GmpGroupState<I, BT>>,
569 pub gmp: &'a mut GmpState<I, CC, BT>,
571 pub config: &'a CC::Config,
573}
574
575impl<'a, I: IpExt, CC: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> GmpStateRef<'a, I, CC, BT> {
576 fn as_mut(&mut self) -> GmpStateRef<'_, I, CC, BT> {
577 let Self { enabled, groups, gmp, config } = self;
578 GmpStateRef { enabled: *enabled, groups, gmp, config }
579 }
580}
581
582pub trait GmpTypeLayout<I: Ip, BT: GmpBindingsTypes>: Sized {
584 type Config: Debug + v1::ProtocolConfig + v2::ProtocolConfig;
586 type ProtoMode: Debug
588 + Copy
589 + Clone
590 + Eq
591 + PartialEq
592 + Into<GmpMode>
593 + Default
594 + InspectableValue;
595}
596
597pub struct GmpGroupState<I: Ip, BT: GmpBindingsTypes> {
599 version_specific: GmpGroupStateByVersion<I, BT>,
600 }
603
604impl<I: Ip, BT: GmpBindingsTypes> GmpGroupState<I, BT> {
605 fn v1_mut(&mut self) -> &mut v1::GmpStateMachine<BT::Instant> {
613 match &mut self.version_specific {
614 GmpGroupStateByVersion::V1(v1) => return v1,
615 GmpGroupStateByVersion::V2(_) => {
616 panic!("expected GMP v1")
617 }
618 }
619 }
620
621 fn v2_mut(&mut self) -> &mut v2::GroupState<I> {
629 match &mut self.version_specific {
630 GmpGroupStateByVersion::V2(v2) => return v2,
631 GmpGroupStateByVersion::V1(_) => {
632 panic!("expected GMP v2")
633 }
634 }
635 }
636
637 #[cfg(test)]
639 fn v1(&self) -> &v1::GmpStateMachine<BT::Instant> {
640 match &self.version_specific {
641 GmpGroupStateByVersion::V1(v1) => v1,
642 GmpGroupStateByVersion::V2(_) => panic!("group not in v1 mode"),
643 }
644 }
645
646 fn v2(&self) -> &v2::GroupState<I> {
648 match &self.version_specific {
649 GmpGroupStateByVersion::V2(v2) => v2,
650 GmpGroupStateByVersion::V1 { .. } => panic!("group not in v2 mode"),
651 }
652 }
653
654 fn into_v1(self) -> v1::GmpStateMachine<BT::Instant> {
660 let Self { version_specific } = self;
661 match version_specific {
662 GmpGroupStateByVersion::V1(v1) => v1,
663 GmpGroupStateByVersion::V2(_) => panic!("expected GMP v1"),
664 }
665 }
666
667 fn into_v2(self) -> v2::GroupState<I> {
673 let Self { version_specific } = self;
674 match version_specific {
675 GmpGroupStateByVersion::V2(v2) => v2,
676 GmpGroupStateByVersion::V1(_) => panic!("expected GMP v2"),
677 }
678 }
679
680 fn new_v1(v1: v1::GmpStateMachine<BT::Instant>) -> Self {
682 Self { version_specific: GmpGroupStateByVersion::V1(v1) }
683 }
684
685 fn new_v2(v2: v2::GroupState<I>) -> Self {
687 Self { version_specific: GmpGroupStateByVersion::V2(v2) }
688 }
689}
690
691#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
693pub enum GmpMode {
694 V1 {
696 compat: bool,
705 },
706 #[default]
708 V2,
709}
710
711impl GmpMode {
712 fn is_v1(&self) -> bool {
713 match self {
714 Self::V1 { .. } => true,
715 Self::V2 => false,
716 }
717 }
718
719 fn is_v2(&self) -> bool {
720 match self {
721 Self::V2 => true,
722 Self::V1 { .. } => false,
723 }
724 }
725
726 fn maybe_enter_v1_compat(&self) -> Self {
727 match self {
728 Self::V2 => Self::V1 { compat: true },
730 m @ Self::V1 { .. } => *m,
732 }
733 }
734
735 fn maybe_exit_v1_compat(&self) -> Self {
736 match self {
737 m @ Self::V2 | m @ Self::V1 { compat: false } => *m,
739 Self::V1 { compat: true } => Self::V2,
741 }
742 }
743}
744
745#[cfg_attr(test, derive(derivative::Derivative))]
746#[cfg_attr(test, derivative(Debug(bound = "")))]
747enum GmpGroupStateByVersion<I: Ip, BT: GmpBindingsTypes> {
748 V1(v1::GmpStateMachine<BT::Instant>),
749 V2(v2::GroupState<I>),
750}
751
752pub trait GmpStateContext<I: IpExt, BT: GmpBindingsTypes>: DeviceIdContext<AnyDevice> {
754 type TypeLayout: GmpTypeLayout<I, BT>;
756
757 fn with_multicast_groups<
759 O,
760 F: FnOnce(&MulticastGroupSet<I::Addr, GmpGroupState<I, BT>>) -> O,
761 >(
762 &mut self,
763 device: &Self::DeviceId,
764 cb: F,
765 ) -> O {
766 self.with_gmp_state(device, |groups, _gmp_state| cb(groups))
767 }
768
769 fn with_gmp_state<
772 O,
773 F: FnOnce(
774 &MulticastGroupSet<I::Addr, GmpGroupState<I, BT>>,
775 &GmpState<I, Self::TypeLayout, BT>,
776 ) -> O,
777 >(
778 &mut self,
779 device: &Self::DeviceId,
780 cb: F,
781 ) -> O;
782}
783
784trait GmpContext<I: IpExt, BC: GmpBindingsContext>: DeviceIdContext<AnyDevice> {
788 type TypeLayout: GmpTypeLayout<I, BC>;
790
791 type Inner<'a>: GmpContextInner<I, BC, TypeLayout = Self::TypeLayout, DeviceId = Self::DeviceId>
793 + 'a;
794
795 fn with_gmp_state_mut_and_ctx<
798 O,
799 F: FnOnce(Self::Inner<'_>, GmpStateRef<'_, I, Self::TypeLayout, BC>) -> O,
800 >(
801 &mut self,
802 device: &Self::DeviceId,
803 cb: F,
804 ) -> O;
805
806 fn with_gmp_state_mut<O, F: FnOnce(GmpStateRef<'_, I, Self::TypeLayout, BC>) -> O>(
808 &mut self,
809 device: &Self::DeviceId,
810 cb: F,
811 ) -> O {
812 self.with_gmp_state_mut_and_ctx(device, |_core_ctx, state| cb(state))
813 }
814}
815
816trait GmpContextInner<I: IpExt, BC: GmpBindingsContext>: DeviceIdContext<AnyDevice> {
820 type TypeLayout: GmpTypeLayout<I, BC>;
822
823 fn send_message_v1(
825 &mut self,
826 bindings_ctx: &mut BC,
827 device: &Self::DeviceId,
828 cur_mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
829 group_addr: GmpEnabledGroup<I::Addr>,
830 msg_type: v1::GmpMessageType,
831 );
832
833 fn send_report_v2(
835 &mut self,
836 bindings_ctx: &mut BC,
837 device: &Self::DeviceId,
838 groups: impl Iterator<Item: v2::VerifiedReportGroupRecord<I::Addr> + Clone> + Clone,
839 );
840
841 fn mode_update_from_v1_query<Q: v1::QueryMessage<I>>(
844 &mut self,
845 bindings_ctx: &mut BC,
846 query: &Q,
847 gmp_state: &GmpState<I, Self::TypeLayout, BC>,
848 config: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::Config,
849 ) -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
850
851 fn mode_to_config(
854 mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
855 ) -> I::GmpProtoConfigMode;
856
857 fn config_to_mode(
860 cur_mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
861 config: I::GmpProtoConfigMode,
862 ) -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
863
864 fn mode_on_disable(
867 cur_mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
868 ) -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
869
870 fn mode_on_exit_compat() -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
874}
875
876fn handle_timer<I, BC, CC>(
877 core_ctx: &mut CC,
878 bindings_ctx: &mut BC,
879 timer: GmpTimerId<I, CC::WeakDeviceId>,
880) where
881 BC: GmpBindingsContext,
882 CC: GmpContext<I, BC>,
883 I: IpExt,
884{
885 let GmpTimerId { device, _marker: IpVersionMarker { .. } } = timer;
886 let Some(device) = device.upgrade() else {
887 return;
888 };
889 core_ctx.with_gmp_state_mut_and_ctx(&device, |mut core_ctx, state| {
890 let Some((timer_id, ())) = state.gmp.timers.pop(bindings_ctx) else {
891 return;
892 };
893 assert!(state.enabled, "{timer_id:?} fired in GMP disabled state");
895
896 match (timer_id, state.gmp.gmp_mode()) {
897 (TimerIdInner::V1(v1), GmpMode::V1 { .. }) => {
898 v1::handle_timer(&mut core_ctx, bindings_ctx, &device, state, v1);
899 }
900 (TimerIdInner::V1Compat, GmpMode::V1 { compat: true }) => {
901 let mode = <CC::Inner<'_> as GmpContextInner<I, BC>>::mode_on_exit_compat();
902 debug_assert_eq!(mode.into(), GmpMode::V2);
903 enter_mode(bindings_ctx, state, mode);
904 }
905 (TimerIdInner::V2(timer), GmpMode::V2) => {
906 v2::handle_timer(&mut core_ctx, bindings_ctx, &device, timer, state);
907 }
908 (TimerIdInner::V1Compat, bad) => {
909 panic!("v1 compat timer fired in non v1 compat mode: {bad:?}")
910 }
911 bad @ (TimerIdInner::V1(_), GmpMode::V2)
912 | bad @ (TimerIdInner::V2(_), GmpMode::V1 { .. }) => {
913 panic!("incompatible timer fired {bad:?}")
914 }
915 }
916 });
917}
918
919fn enter_mode<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
926 bindings_ctx: &mut BC,
927 state: GmpStateRef<'_, I, CC, BC>,
928 new_mode: CC::ProtoMode,
929) {
930 let GmpStateRef { enabled: _, gmp, groups, config: _ } = state;
931 let old_mode = core::mem::replace(&mut gmp.mode, new_mode);
932 match (old_mode.into(), gmp.gmp_mode()) {
933 (GmpMode::V1 { compat }, GmpMode::V1 { compat: new_compat }) => {
934 if new_compat != compat {
935 assert_eq!(new_compat, false, "attempted to enter compatibility mode from forced");
938 assert_matches!(
940 gmp.timers.cancel(bindings_ctx, &TimerIdInner::V1Compat),
941 Some((_, ()))
942 );
943 info!("GMP({}) enter mode {:?}", I::NAME, &gmp.mode);
944 }
945 return;
946 }
947 (GmpMode::V2, GmpMode::V2) => {
948 return;
950 }
951 (GmpMode::V1 { compat: _ }, GmpMode::V2) => {
952 for (_, GmpGroupState { version_specific }) in groups.iter_mut() {
958 *version_specific =
959 GmpGroupStateByVersion::V2(v2::GroupState::new_for_mode_transition())
960 }
961 }
962 (GmpMode::V2, GmpMode::V1 { compat: _ }) => {
963 for (_, GmpGroupState { version_specific }) in groups.iter_mut() {
969 *version_specific =
970 GmpGroupStateByVersion::V1(v1::GmpStateMachine::new_for_mode_transition())
971 }
972 gmp.v2_proto.on_enter_v1();
973 }
974 };
975 info!("GMP({}) enter mode {:?}", I::NAME, new_mode);
976 gmp.timers.clear(bindings_ctx);
977 gmp.mode = new_mode;
978}
979
980fn schedule_v1_compat<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
981 bindings_ctx: &mut BC,
982 state: GmpStateRef<'_, I, CC, BC>,
983) {
984 let GmpStateRef { gmp, config, .. } = state;
985 let timeout = gmp.v2_proto.older_version_querier_present_timeout(config);
986 let _: Option<_> =
987 gmp.timers.schedule_after(bindings_ctx, TimerIdInner::V1Compat, (), timeout.into());
988}
989
990#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
992struct NotAMemberErr<I: Ip>(I::Addr);
993
994enum QueryTarget<A> {
996 Unspecified,
997 Specified(MulticastAddr<A>),
998}
999
1000impl<A: IpAddress> QueryTarget<A> {
1001 fn new(addr: A) -> Option<Self> {
1002 if addr == <A::Version as Ip>::UNSPECIFIED_ADDRESS {
1003 Some(Self::Unspecified)
1004 } else {
1005 MulticastAddr::new(addr).map(Self::Specified)
1006 }
1007 }
1008}
1009
1010mod witness {
1011 use super::*;
1012
1013 #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
1016 pub(super) struct GmpEnabledGroup<A>(MulticastAddr<A>);
1017
1018 impl<A: IpAddress<Version: IpExt>> GmpEnabledGroup<A> {
1019 pub fn new(addr: MulticastAddr<A>) -> Option<Self> {
1022 <A::Version as IpExt>::should_perform_gmp(addr).then_some(Self(addr))
1023 }
1024
1025 pub fn try_new(addr: MulticastAddr<A>) -> Result<Self, MulticastAddr<A>> {
1028 Self::new(addr).ok_or(addr)
1029 }
1030
1031 pub fn multicast_addr(&self) -> MulticastAddr<A> {
1033 let Self(addr) = self;
1034 *addr
1035 }
1036
1037 pub fn into_multicast_addr(self) -> MulticastAddr<A> {
1039 let Self(addr) = self;
1040 addr
1041 }
1042 }
1043
1044 impl<A> AsRef<MulticastAddr<A>> for GmpEnabledGroup<A> {
1045 fn as_ref(&self) -> &MulticastAddr<A> {
1046 let Self(addr) = self;
1047 addr
1048 }
1049 }
1050}
1051use witness::GmpEnabledGroup;
1052
1053#[cfg(test)]
1054mod tests {
1055 use alloc::vec::Vec;
1056 use core::num::NonZeroU8;
1057
1058 use assert_matches::assert_matches;
1059 use ip_test_macro::ip_test;
1060 use net_types::Witness as _;
1061 use netstack3_base::testutil::{FakeDeviceId, FakeTimerCtxExt, FakeWeakDeviceId};
1062 use netstack3_base::InstantContext as _;
1063
1064 use testutil::{FakeCtx, FakeGmpContextInner, FakeV1Query, TestIpExt};
1065
1066 use super::*;
1067
1068 #[ip_test(I)]
1069 fn mode_change_state_clearing<I: TestIpExt>() {
1070 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1071 testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1072
1073 assert_eq!(
1074 core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1075 GroupJoinResult::Joined(())
1076 );
1077 core_ctx.inner.v1_messages.clear();
1080
1081 assert!(core_ctx.gmp.timers.iter().next().is_some());
1083 assert_matches!(
1084 core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().version_specific,
1085 GmpGroupStateByVersion::V1(_)
1086 );
1087
1088 core_ctx.with_gmp_state_mut(&FakeDeviceId, |mut state| {
1089 enter_mode(&mut bindings_ctx, state.as_mut(), GmpMode::V2);
1090 assert_eq!(state.gmp.mode, GmpMode::V2);
1091 });
1092 core_ctx.gmp.timers.assert_timers([]);
1094 assert_matches!(
1095 core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().version_specific,
1096 GmpGroupStateByVersion::V2(_)
1097 );
1098
1099 core_ctx.with_gmp_state_mut(&FakeDeviceId, |mut state| {
1101 enter_mode(&mut bindings_ctx, state.as_mut(), GmpMode::V1 { compat: false });
1102 assert_eq!(state.gmp.mode, GmpMode::V1 { compat: false });
1103 });
1104 assert_matches!(
1105 core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().version_specific,
1106 GmpGroupStateByVersion::V1(_)
1107 );
1108
1109 let FakeGmpContextInner { v1_messages, v2_messages } = &core_ctx.inner;
1111 assert_eq!(v1_messages, &Vec::new());
1112 assert_eq!(v2_messages, &Vec::<Vec<_>>::new());
1113 }
1114
1115 #[ip_test(I)]
1116 #[should_panic(expected = "attempted to enter compatibility mode from forced")]
1117 fn cant_enter_v1_compat<I: TestIpExt>() {
1118 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1119 testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1120 core_ctx.with_gmp_state_mut(&FakeDeviceId, |mut state| {
1121 enter_mode(&mut bindings_ctx, state.as_mut(), GmpMode::V1 { compat: true });
1122 });
1123 }
1124
1125 #[ip_test(I)]
1126 fn disable_exits_compat<I: TestIpExt>() {
1127 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1129 testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: true });
1130 core_ctx.enabled = false;
1131 core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1132 assert_eq!(core_ctx.gmp.mode, GmpMode::V2);
1133
1134 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1136 testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1137 core_ctx.enabled = false;
1138 core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1139 assert_eq!(core_ctx.gmp.mode, GmpMode::V1 { compat: false });
1140 }
1141
1142 #[ip_test(I)]
1143 fn disable_clears_v2_state<I: TestIpExt>() {
1144 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1145 testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1146 let v2::ProtocolState { robustness_variable, query_interval, left_groups } =
1147 &mut core_ctx.gmp.v2_proto;
1148 *robustness_variable = robustness_variable.checked_add(1).unwrap();
1149 *query_interval = *query_interval + Duration::from_secs(20);
1150 *left_groups =
1151 [(GmpEnabledGroup::new(I::GROUP_ADDR1).unwrap(), NonZeroU8::new(1).unwrap())]
1152 .into_iter()
1153 .collect();
1154 core_ctx.enabled = false;
1155 core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1156 assert_eq!(core_ctx.gmp.v2_proto, v2::ProtocolState::default());
1157 }
1158
1159 #[ip_test(I)]
1160 fn v1_compat_mode_on_timeout<I: TestIpExt>() {
1161 let FakeCtx { mut core_ctx, mut bindings_ctx } =
1162 testutil::new_context_with_mode::<I>(GmpMode::V2);
1163 assert_eq!(
1164 v1::handle_query_message(
1165 &mut core_ctx,
1166 &mut bindings_ctx,
1167 &FakeDeviceId,
1168 &FakeV1Query {
1169 group_addr: I::GROUP_ADDR1.get(),
1170 max_response_time: Duration::from_secs(1)
1171 }
1172 ),
1173 Err(NotAMemberErr(I::GROUP_ADDR1.get()))
1174 );
1175 assert_eq!(core_ctx.gmp.mode, GmpMode::V1 { compat: true });
1177
1178 let timeout =
1179 core_ctx.gmp.v2_proto.older_version_querier_present_timeout(&core_ctx.config).into();
1180 core_ctx.gmp.timers.assert_timers([(
1181 TimerIdInner::V1Compat,
1182 (),
1183 bindings_ctx.now() + timeout,
1184 )]);
1185
1186 bindings_ctx.timers.instant.sleep(timeout / 2);
1188 assert_eq!(
1189 v1::handle_query_message(
1190 &mut core_ctx,
1191 &mut bindings_ctx,
1192 &FakeDeviceId,
1193 &FakeV1Query {
1194 group_addr: I::GROUP_ADDR1.get(),
1195 max_response_time: Duration::from_secs(1)
1196 }
1197 ),
1198 Err(NotAMemberErr(I::GROUP_ADDR1.get()))
1199 );
1200 assert_eq!(core_ctx.gmp.mode, GmpMode::V1 { compat: true });
1201 core_ctx.gmp.timers.assert_timers([(
1202 TimerIdInner::V1Compat,
1203 (),
1204 bindings_ctx.now() + timeout,
1205 )]);
1206
1207 let timer = bindings_ctx.trigger_next_timer(&mut core_ctx);
1209 assert_eq!(timer, Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId))));
1210 assert_eq!(core_ctx.gmp.mode, GmpMode::V2);
1211 core_ctx.gmp.timers.assert_timers([]);
1213 let testutil::FakeGmpContextInner { v1_messages, v2_messages } = &core_ctx.inner;
1214 assert_eq!(v1_messages, &Vec::new());
1215 assert_eq!(v2_messages, &Vec::<Vec<_>>::new());
1216 }
1217}