Skip to main content

netstack3_ip/
multicast_forwarding.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! An implementation of multicast forwarding.
6//!
7//! Multicast forwarding is the ability for netstack to forward multicast
8//! packets that arrive on an interface out multiple interfaces (while also
9//! optionally delivering the packet to the host itself if the arrival host has
10//! an interest in the packet).
11//!
12//! Note that multicast forwarding decisions are made by consulting the
13//! multicast routing table, a routing table entirely separate from the unicast
14//! routing table(s).
15
16pub(crate) mod api;
17pub(crate) mod counters;
18pub(crate) mod packet_queue;
19pub(crate) mod route;
20pub(crate) mod state;
21
22use core::sync::atomic::Ordering;
23
24use net_types::ip::{GenericOverIp, Ip, IpVersionMarker};
25use netstack3_base::{
26    AnyDevice, AtomicInstant, CounterContext, DeviceIdContext, EventContext, HandleableTimer,
27    InstantBindingsTypes, InstantContext, LocalFrameDestination, Marks, TimerBindingsTypes,
28    TimerContext, WeakDeviceIdentifier,
29};
30use packet_formats::ip::IpPacket;
31use zerocopy::SplitByteSlice;
32
33use crate::internal::multicast_forwarding::counters::MulticastForwardingCounters;
34use crate::internal::multicast_forwarding::packet_queue::QueuePacketOutcome;
35use crate::internal::multicast_forwarding::route::{
36    Action, MulticastRouteEntry, MulticastRouteTargets,
37};
38use crate::multicast_forwarding::{
39    MulticastForwardingPendingPacketsContext, MulticastForwardingState,
40    MulticastForwardingStateContext, MulticastRoute, MulticastRouteKey,
41    MulticastRouteTableContext as _,
42};
43use crate::{IpLayerEvent, IpLayerIpExt};
44
45/// Required types for multicast forwarding provided by Bindings.
46pub trait MulticastForwardingBindingsTypes: InstantBindingsTypes + TimerBindingsTypes {}
47impl<BT: InstantBindingsTypes + TimerBindingsTypes> MulticastForwardingBindingsTypes for BT {}
48
49/// Required functionality for multicast forwarding provided by Bindings.
50pub trait MulticastForwardingBindingsContext<I: IpLayerIpExt, D>:
51    MulticastForwardingBindingsTypes + InstantContext + TimerContext + EventContext<IpLayerEvent<D, I>>
52{
53}
54impl<
55    I: IpLayerIpExt,
56    D,
57    BC: MulticastForwardingBindingsTypes
58        + InstantContext
59        + TimerContext
60        + EventContext<IpLayerEvent<D, I>>,
61> MulticastForwardingBindingsContext<I, D> for BC
62{
63}
64
65/// Device related functionality required by multicast forwarding.
66pub trait MulticastForwardingDeviceContext<I: IpLayerIpExt>: DeviceIdContext<AnyDevice> {
67    /// True if the given device has multicast forwarding enabled.
68    fn is_device_multicast_forwarding_enabled(&mut self, dev: &Self::DeviceId) -> bool;
69}
70
71/// A timer event for multicast forwarding.
72#[derive(Clone, Debug, Eq, GenericOverIp, Hash, PartialEq)]
73#[generic_over_ip(I, Ip)]
74pub enum MulticastForwardingTimerId<I: Ip> {
75    /// A trigger to perform garbage collection on the pending packets table.
76    PendingPacketsGc(IpVersionMarker<I>),
77}
78
79impl<
80    I: IpLayerIpExt,
81    BC: MulticastForwardingBindingsContext<I, CC::DeviceId>,
82    CC: MulticastForwardingStateContext<I, BC> + CounterContext<MulticastForwardingCounters<I>>,
83> HandleableTimer<CC, BC> for MulticastForwardingTimerId<I>
84{
85    fn handle(self, core_ctx: &mut CC, bindings_ctx: &mut BC, _: BC::UniqueTimerId) {
86        match self {
87            MulticastForwardingTimerId::PendingPacketsGc(_) => {
88                core_ctx.with_state(|state, ctx| match state {
89                    // Multicast forwarding was disabled after GC was scheduled;
90                    // there are no resources to GC now.
91                    MulticastForwardingState::Disabled => {}
92                    MulticastForwardingState::Enabled(state) => {
93                        CounterContext::<MulticastForwardingCounters<I>>::counters(ctx)
94                            .pending_table_gc
95                            .increment();
96                        let removed_count = ctx.with_pending_table_mut(state, |pending_table| {
97                            pending_table.run_garbage_collection(bindings_ctx)
98                        });
99                        CounterContext::<MulticastForwardingCounters<I>>::counters(ctx)
100                            .pending_packet_drops_gc
101                            .add(removed_count);
102                    }
103                })
104            }
105        }
106    }
107}
108
109/// Events that may be published by the multicast forwarding engine.
110#[derive(Debug, Eq, Hash, PartialEq, GenericOverIp)]
111#[generic_over_ip(I, Ip)]
112pub enum MulticastForwardingEvent<I: IpLayerIpExt, D> {
113    /// A multicast packet was received for which there was no applicable route.
114    MissingRoute {
115        /// The key of the route that's missing.
116        key: MulticastRouteKey<I>,
117        /// The interface on which the packet was received.
118        input_interface: D,
119    },
120    /// A multicast packet was received on an unexpected input interface.
121    WrongInputInterface {
122        /// The key of the route with the unexpected input interface.
123        key: MulticastRouteKey<I>,
124        /// The interface on which the packet was received.
125        actual_input_interface: D,
126        /// The interface on which the packet was expected (as specified in the
127        /// multicast route).
128        expected_input_interface: D,
129    },
130}
131
132impl<I: IpLayerIpExt, D> MulticastForwardingEvent<I, D> {
133    pub(crate) fn map_device<O, F: Fn(D) -> O>(self, map: F) -> MulticastForwardingEvent<I, O> {
134        match self {
135            MulticastForwardingEvent::MissingRoute { key, input_interface } => {
136                MulticastForwardingEvent::MissingRoute {
137                    key,
138                    input_interface: map(input_interface),
139                }
140            }
141            MulticastForwardingEvent::WrongInputInterface {
142                key,
143                actual_input_interface,
144                expected_input_interface,
145            } => MulticastForwardingEvent::WrongInputInterface {
146                key,
147                actual_input_interface: map(actual_input_interface),
148                expected_input_interface: map(expected_input_interface),
149            },
150        }
151    }
152}
153
154impl<I: IpLayerIpExt, D: WeakDeviceIdentifier> MulticastForwardingEvent<I, D> {
155    /// Upgrades the device IDs held by this event.
156    pub fn upgrade_device_id(self) -> Option<MulticastForwardingEvent<I, D::Strong>> {
157        match self {
158            MulticastForwardingEvent::MissingRoute { key, input_interface } => {
159                Some(MulticastForwardingEvent::MissingRoute {
160                    key,
161                    input_interface: input_interface.upgrade()?,
162                })
163            }
164            MulticastForwardingEvent::WrongInputInterface {
165                key,
166                actual_input_interface,
167                expected_input_interface,
168            } => Some(MulticastForwardingEvent::WrongInputInterface {
169                key,
170                actual_input_interface: actual_input_interface.upgrade()?,
171                expected_input_interface: expected_input_interface.upgrade()?,
172            }),
173        }
174    }
175}
176
177/// Query the multicast route table and return the forwarding targets.
178///
179/// `None` may be returned in several situations:
180///   * if multicast forwarding is disabled (either stack-wide or for the
181///     provided `dev`),
182///   * if the packets src/dst addrs are not viable for multicast forwarding
183///     (see the requirements on [`MulticastRouteKey`]), or
184///   * if the route table does not have an entry suitable for this packet.
185///
186/// In the latter case, the packet is stashed in the
187/// [`MulticastForwardingPendingPackets`] table, and a relevant event is
188/// dispatched to bindings.
189///
190/// Note that the returned targets are not synchronized with the multicast route
191/// table and may grow stale if the table is updated.
192pub(crate) fn lookup_multicast_route_or_stash_packet<I, B, CC, BC>(
193    core_ctx: &mut CC,
194    bindings_ctx: &mut BC,
195    packet: &I::Packet<B>,
196    dev: &CC::DeviceId,
197    frame_dst: Option<LocalFrameDestination>,
198    max_fragment_len: Option<usize>,
199    marks: Marks,
200) -> Option<MulticastRouteTargets<CC::DeviceId>>
201where
202    I: IpLayerIpExt,
203    B: SplitByteSlice,
204    CC: MulticastForwardingStateContext<I, BC>
205        + MulticastForwardingDeviceContext<I>
206        + CounterContext<MulticastForwardingCounters<I>>,
207    BC: MulticastForwardingBindingsContext<I, CC::DeviceId>,
208{
209    CounterContext::<MulticastForwardingCounters<I>>::counters(core_ctx).rx.increment();
210    // Short circuit if the packet's addresses don't constitute a valid
211    // multicast route key (e.g. src is not unicast, or dst is not multicast).
212    let Some(key) = MulticastRouteKey::new(packet.src_ip(), packet.dst_ip()) else {
213        CounterContext::<MulticastForwardingCounters<I>>::counters(core_ctx)
214            .no_tx_invalid_key
215            .increment();
216        return None;
217    };
218
219    // Short circuit if the device has forwarding disabled.
220    if !core_ctx.is_device_multicast_forwarding_enabled(dev) {
221        CounterContext::<MulticastForwardingCounters<I>>::counters(core_ctx)
222            .no_tx_disabled_dev
223            .increment();
224        return None;
225    }
226
227    core_ctx.with_state(|state, ctx| {
228        // Short circuit if forwarding is disabled stack-wide.
229        let Some(state) = state.enabled() else {
230            CounterContext::<MulticastForwardingCounters<I>>::counters(ctx)
231                .no_tx_disabled_stack_wide
232                .increment();
233            return None;
234        };
235        ctx.with_route_table(state, |route_table, ctx| {
236            if let Some(MulticastRouteEntry {
237                route: MulticastRoute { input_interface, action },
238                stats,
239            }) = route_table.get(&key)
240            {
241                if dev != input_interface {
242                    CounterContext::<MulticastForwardingCounters<I>>::counters(ctx)
243                        .no_tx_wrong_dev
244                        .increment();
245                    bindings_ctx.on_event(
246                        MulticastForwardingEvent::WrongInputInterface {
247                            key,
248                            actual_input_interface: dev.clone(),
249                            expected_input_interface: input_interface.clone(),
250                        }
251                        .into(),
252                    );
253                    return None;
254                }
255
256                stats.last_used.store_max(bindings_ctx.now(), Ordering::Relaxed);
257
258                match action {
259                    Action::Forward(targets) => {
260                        CounterContext::<MulticastForwardingCounters<I>>::counters(ctx)
261                            .tx
262                            .increment();
263                        return Some(targets.clone());
264                    }
265                }
266            }
267            CounterContext::<MulticastForwardingCounters<I>>::counters(ctx)
268                .pending_packets
269                .increment();
270            match ctx.with_pending_table_mut(state, |pending_table| {
271                pending_table.try_queue_packet(
272                    bindings_ctx,
273                    key.clone(),
274                    packet,
275                    dev,
276                    frame_dst,
277                    max_fragment_len,
278                    marks,
279                )
280            }) {
281                QueuePacketOutcome::QueuedInNewQueue => {
282                    bindings_ctx.on_event(
283                        MulticastForwardingEvent::MissingRoute {
284                            key,
285                            input_interface: dev.clone(),
286                        }
287                        .into(),
288                    );
289                }
290                QueuePacketOutcome::QueuedInExistingQueue => {}
291                QueuePacketOutcome::ExistingQueueFull => {
292                    CounterContext::<MulticastForwardingCounters<I>>::counters(ctx)
293                        .pending_packet_drops_queue_full
294                        .increment();
295                }
296            }
297            return None;
298        })
299    })
300}
301
302#[cfg(test)]
303mod testutil {
304    use super::*;
305
306    use alloc::rc::Rc;
307    use alloc::vec::Vec;
308    use core::cell::RefCell;
309    use derivative::Derivative;
310    use net_declare::{net_ip_v4, net_ip_v6};
311    use net_types::MulticastAddr;
312    use net_types::ip::{Ipv4, Ipv4Addr, Ipv6, Ipv6Addr, Mtu};
313    use netstack3_base::socket::SocketIpAddr;
314    use netstack3_base::testutil::{FakeStrongDeviceId, MultipleDevicesId};
315    use netstack3_base::{
316        CoreTimerContext, CounterContext, CtxPair, Marks, NetworkSerializationContext,
317        NetworkSerializer, ResourceCounterContext,
318    };
319    use netstack3_filter::ProofOfEgressCheck;
320    use netstack3_hashmap::HashSet;
321    use packet::{BufferMut, InnerPacketBuilder, NestablePacketBuilder as _, Serializer};
322    use packet_formats::ip::{IpPacketBuilder, IpProto};
323
324    use crate::device::IpDeviceSendContext;
325    use crate::internal::base::DeviceIpLayerMetadata;
326    use crate::internal::icmp::IcmpErrorHandler;
327    use crate::multicast_forwarding::{
328        MulticastForwardingApi, MulticastForwardingEnabledState, MulticastForwardingPendingPackets,
329        MulticastForwardingPendingPacketsContext, MulticastForwardingState, MulticastRouteTable,
330        MulticastRouteTableContext,
331    };
332    use crate::{IpCounters, IpDeviceMtuContext, IpLayerEvent, IpPacketDestination};
333
334    /// An IP extension trait providing constants for various IP addresses.
335    pub(crate) trait TestIpExt: IpLayerIpExt {
336        const SRC1: Self::Addr;
337        const SRC2: Self::Addr;
338        const DST1: Self::Addr;
339        const DST2: Self::Addr;
340    }
341
342    impl TestIpExt for Ipv4 {
343        const SRC1: Ipv4Addr = net_ip_v4!("192.0.2.1");
344        const SRC2: Ipv4Addr = net_ip_v4!("192.0.2.2");
345        const DST1: Ipv4Addr = net_ip_v4!("224.0.1.1");
346        const DST2: Ipv4Addr = net_ip_v4!("224.0.1.2");
347    }
348
349    impl TestIpExt for Ipv6 {
350        const SRC1: Ipv6Addr = net_ip_v6!("2001:0DB8::1");
351        const SRC2: Ipv6Addr = net_ip_v6!("2001:0DB8::2");
352        const DST1: Ipv6Addr = net_ip_v6!("ff0e::1");
353        const DST2: Ipv6Addr = net_ip_v6!("ff0e::2");
354    }
355
356    /// Constructs a buffer containing an IP packet with sensible defaults.
357    pub(crate) fn new_ip_packet_buf<I: IpLayerIpExt>(
358        src_addr: I::Addr,
359        dst_addr: I::Addr,
360    ) -> impl AsRef<[u8]> {
361        const TTL: u8 = 255;
362        /// Arbitrary data to put inside of an IP packet.
363        const IP_BODY: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
364        I::PacketBuilder::new(src_addr, dst_addr, TTL, IpProto::Udp.into())
365            .wrap_body(IP_BODY.into_serializer())
366            .serialize_vec_outer(&mut NetworkSerializationContext::default())
367            .unwrap()
368    }
369
370    #[derive(Debug, PartialEq)]
371    pub(crate) struct SentPacket<I: IpLayerIpExt, D> {
372        pub(crate) dst: MulticastAddr<I::Addr>,
373        pub(crate) device: D,
374    }
375
376    #[derive(Derivative)]
377    #[derivative(Default(bound = ""))]
378    pub(crate) struct FakeCoreCtxState<I: IpLayerIpExt, D: FakeStrongDeviceId> {
379        // NB: Hold in an `Rc<RefCell<...>>` to switch to runtime borrow
380        // checking. This allows us to borrow the multicast forwarding state at
381        // the same time as the outer `FakeCoreCtx` is mutably borrowed.
382        pub(crate) multicast_forwarding:
383            Rc<RefCell<MulticastForwardingState<I, D, FakeBindingsCtx<I, D>>>>,
384        // The list of devices that have multicast forwarding enabled.
385        pub(crate) forwarding_enabled_devices: HashSet<D>,
386        // The list of packets sent by the netstack.
387        pub(crate) sent_packets: Vec<SentPacket<I, D>>,
388        stack_wide_counters: IpCounters<I>,
389        per_device_counters: IpCounters<I>,
390        multicast_forwarding_counters: MulticastForwardingCounters<I>,
391    }
392
393    impl<I: IpLayerIpExt, D: FakeStrongDeviceId> FakeCoreCtxState<I, D> {
394        pub(crate) fn set_multicast_forwarding_enabled_for_dev(&mut self, dev: D, enabled: bool) {
395            if enabled {
396                let _: bool = self.forwarding_enabled_devices.insert(dev);
397            } else {
398                let _: bool = self.forwarding_enabled_devices.remove(&dev);
399            }
400        }
401
402        pub(crate) fn take_sent_packets(&mut self) -> Vec<SentPacket<I, D>> {
403            core::mem::take(&mut self.sent_packets)
404        }
405    }
406
407    impl<I: IpLayerIpExt, D: FakeStrongDeviceId> CounterContext<IpCounters<I>>
408        for FakeCoreCtxState<I, D>
409    {
410        fn counters(&self) -> &IpCounters<I> {
411            &self.stack_wide_counters
412        }
413    }
414
415    impl<I: IpLayerIpExt, D: FakeStrongDeviceId> ResourceCounterContext<D, IpCounters<I>>
416        for FakeCoreCtxState<I, D>
417    {
418        fn per_resource_counters(&self, _resource: &D) -> &IpCounters<I> {
419            &self.per_device_counters
420        }
421    }
422
423    impl<I: IpLayerIpExt, D: FakeStrongDeviceId> CounterContext<MulticastForwardingCounters<I>>
424        for FakeCoreCtxState<I, D>
425    {
426        fn counters(&self) -> &MulticastForwardingCounters<I> {
427            &self.multicast_forwarding_counters
428        }
429    }
430
431    pub(crate) type FakeBindingsCtx<I, D> = netstack3_base::testutil::FakeBindingsCtx<
432        MulticastForwardingTimerId<I>,
433        IpLayerEvent<D, I>,
434        (),
435        (),
436    >;
437    pub(crate) type FakeCoreCtx<I, D> =
438        netstack3_base::testutil::FakeCoreCtx<FakeCoreCtxState<I, D>, (), D>;
439
440    impl<I: IpLayerIpExt, D: FakeStrongDeviceId>
441        MulticastForwardingStateContext<I, FakeBindingsCtx<I, D>> for FakeCoreCtx<I, D>
442    {
443        type Ctx<'a> = FakeCoreCtx<I, D>;
444        fn with_state<
445            O,
446            F: FnOnce(
447                &MulticastForwardingState<I, Self::DeviceId, FakeBindingsCtx<I, D>>,
448                &mut Self::Ctx<'_>,
449            ) -> O,
450        >(
451            &mut self,
452            cb: F,
453        ) -> O {
454            let state = self.state.multicast_forwarding.clone();
455            let borrow = state.borrow();
456            cb(&borrow, self)
457        }
458        fn with_state_mut<
459            O,
460            F: FnOnce(
461                &mut MulticastForwardingState<I, Self::DeviceId, FakeBindingsCtx<I, D>>,
462                &mut Self::Ctx<'_>,
463            ) -> O,
464        >(
465            &mut self,
466            cb: F,
467        ) -> O {
468            let state = self.state.multicast_forwarding.clone();
469            let mut borrow = state.borrow_mut();
470            cb(&mut borrow, self)
471        }
472    }
473
474    impl<I: IpLayerIpExt, D: FakeStrongDeviceId>
475        MulticastRouteTableContext<I, FakeBindingsCtx<I, D>> for FakeCoreCtx<I, D>
476    {
477        type Ctx<'a> = FakeCoreCtx<I, D>;
478        fn with_route_table<
479            O,
480            F: FnOnce(
481                &MulticastRouteTable<I, Self::DeviceId, FakeBindingsCtx<I, D>>,
482                &mut Self::Ctx<'_>,
483            ) -> O,
484        >(
485            &mut self,
486            state: &MulticastForwardingEnabledState<I, Self::DeviceId, FakeBindingsCtx<I, D>>,
487            cb: F,
488        ) -> O {
489            let route_table = state.route_table().read();
490            cb(&route_table, self)
491        }
492        fn with_route_table_mut<
493            O,
494            F: FnOnce(
495                &mut MulticastRouteTable<I, Self::DeviceId, FakeBindingsCtx<I, D>>,
496                &mut Self::Ctx<'_>,
497            ) -> O,
498        >(
499            &mut self,
500            state: &MulticastForwardingEnabledState<I, Self::DeviceId, FakeBindingsCtx<I, D>>,
501            cb: F,
502        ) -> O {
503            let mut route_table = state.route_table().write();
504            cb(&mut route_table, self)
505        }
506    }
507
508    impl<I: IpLayerIpExt, D: FakeStrongDeviceId>
509        MulticastForwardingPendingPacketsContext<I, FakeBindingsCtx<I, D>> for FakeCoreCtx<I, D>
510    {
511        fn with_pending_table_mut<
512            O,
513            F: FnOnce(
514                &mut MulticastForwardingPendingPackets<I, Self::WeakDeviceId, FakeBindingsCtx<I, D>>,
515            ) -> O,
516        >(
517            &mut self,
518            state: &MulticastForwardingEnabledState<I, Self::DeviceId, FakeBindingsCtx<I, D>>,
519            cb: F,
520        ) -> O {
521            let mut pending_table = state.pending_table().lock();
522            cb(&mut pending_table)
523        }
524    }
525
526    impl<I: IpLayerIpExt, D: FakeStrongDeviceId> MulticastForwardingDeviceContext<I>
527        for FakeCoreCtx<I, D>
528    {
529        fn is_device_multicast_forwarding_enabled(&mut self, device_id: &Self::DeviceId) -> bool {
530            self.state.forwarding_enabled_devices.contains(device_id)
531        }
532    }
533
534    impl<I: IpLayerIpExt, D: FakeStrongDeviceId>
535        CoreTimerContext<MulticastForwardingTimerId<I>, FakeBindingsCtx<I, D>>
536        for FakeCoreCtx<I, D>
537    {
538        fn convert_timer(
539            dispatch_id: MulticastForwardingTimerId<I>,
540        ) -> MulticastForwardingTimerId<I> {
541            dispatch_id
542        }
543    }
544
545    impl<I: IpLayerIpExt, D: FakeStrongDeviceId> IpDeviceSendContext<I, FakeBindingsCtx<I, D>>
546        for FakeCoreCtx<I, D>
547    {
548        fn send_ip_frame<S>(
549            &mut self,
550            _bindings_ctx: &mut FakeBindingsCtx<I, D>,
551            device_id: &D,
552            destination: IpPacketDestination<I, &D>,
553            _ip_layer_metadata: DeviceIpLayerMetadata<FakeBindingsCtx<I, D>>,
554            _body: S,
555            _egress_proof: ProofOfEgressCheck,
556        ) -> Result<(), netstack3_base::SendFrameError<S>>
557        where
558            S: NetworkSerializer,
559            S::Buffer: BufferMut,
560        {
561            let dst = match destination {
562                IpPacketDestination::Multicast(dst) => dst,
563                dst => panic!("unexpected sent packet: destination={dst:?}"),
564            };
565            self.state.sent_packets.push(SentPacket { dst, device: device_id.clone() });
566            Ok(())
567        }
568    }
569
570    impl<I: IpLayerIpExt, D: FakeStrongDeviceId> IpDeviceMtuContext<I> for FakeCoreCtx<I, D> {
571        fn get_mtu(&mut self, _device_id: &Self::DeviceId) -> Mtu {
572            Mtu::max()
573        }
574    }
575
576    impl<I: IpLayerIpExt, D: FakeStrongDeviceId> IcmpErrorHandler<I, FakeBindingsCtx<I, D>>
577        for FakeCoreCtx<I, D>
578    {
579        fn send_icmp_error_message<B: BufferMut>(
580            &mut self,
581            _bindings_ctx: &mut FakeBindingsCtx<I, D>,
582            _device: Option<&D>,
583            _frame_dst: Option<LocalFrameDestination>,
584            _src_ip: SocketIpAddr<I::Addr>,
585            _dst_ip: SocketIpAddr<I::Addr>,
586            _original_packet: B,
587            _error: I::IcmpError,
588            _header_len: usize,
589            _proto: I::Proto,
590            _marks: &Marks,
591        ) {
592            unimplemented!()
593        }
594    }
595
596    pub(crate) fn new_api<I: IpLayerIpExt>() -> MulticastForwardingApi<
597        I,
598        CtxPair<FakeCoreCtx<I, MultipleDevicesId>, FakeBindingsCtx<I, MultipleDevicesId>>,
599    > {
600        MulticastForwardingApi::new(CtxPair::with_core_ctx(FakeCoreCtx::with_state(
601            Default::default(),
602        )))
603    }
604
605    /// A test helper to access the [`MulticastForwardingPendingPackets`] table.
606    ///
607    /// # Panics
608    ///
609    /// Panics if multicast forwarding is disabled.
610    pub(crate) fn with_pending_table<I, O, F, CC, BT>(core_ctx: &mut CC, cb: F) -> O
611    where
612        I: IpLayerIpExt,
613        CC: MulticastForwardingStateContext<I, BT>,
614        BT: MulticastForwardingBindingsTypes,
615        F: FnOnce(&mut MulticastForwardingPendingPackets<I, CC::WeakDeviceId, BT>) -> O,
616    {
617        core_ctx.with_state(|state, ctx| {
618            let state = state.enabled().unwrap();
619            ctx.with_route_table(state, |_routing_table, ctx| {
620                ctx.with_pending_table_mut(state, |pending_table| cb(pending_table))
621            })
622        })
623    }
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629
630    use alloc::vec;
631    use core::num::NonZeroU8;
632    use core::time::Duration;
633
634    use ip_test_macro::ip_test;
635    use netstack3_base::testutil::MultipleDevicesId;
636    use packet::ParseBuffer;
637    use test_case::test_case;
638    use testutil::TestIpExt;
639
640    use crate::internal::multicast_forwarding::route::MulticastRouteStats;
641    use crate::multicast_forwarding::MulticastRouteTarget;
642
643    struct LookupTestCase {
644        // Whether multicast forwarding is enabled for the netstack.
645        enabled: bool,
646        // Whether multicast forwarding is enabled for the device.
647        dev_enabled: bool,
648        // Whether the packet has the correct src/dst addrs.
649        right_key: bool,
650        // Whether the packet arrived on the correct device.
651        right_dev: bool,
652    }
653    const LOOKUP_SUCCESS_CASE: LookupTestCase =
654        LookupTestCase { enabled: true, dev_enabled: true, right_key: true, right_dev: true };
655
656    #[ip_test(I)]
657    #[test_case(LOOKUP_SUCCESS_CASE => true; "success")]
658    #[test_case(LookupTestCase{enabled: false, ..LOOKUP_SUCCESS_CASE} => false; "disabled")]
659    #[test_case(LookupTestCase{dev_enabled: false, ..LOOKUP_SUCCESS_CASE} => false; "dev_disabled")]
660    #[test_case(LookupTestCase{right_key: false, ..LOOKUP_SUCCESS_CASE} => false; "wrong_key")]
661    #[test_case(LookupTestCase{right_dev: false, ..LOOKUP_SUCCESS_CASE} => false; "wrong_dev")]
662    fn lookup_route<I: TestIpExt>(test_case: LookupTestCase) -> bool {
663        let LookupTestCase { enabled, dev_enabled, right_key, right_dev } = test_case;
664        const FRAME_DST: Option<LocalFrameDestination> = None;
665        const MAX_FRAGMENT_LEN: Option<usize> = None;
666        let mut api = testutil::new_api::<I>();
667
668        let expected_key = MulticastRouteKey::new(I::SRC1, I::DST1).unwrap();
669        let actual_key = if right_key {
670            expected_key.clone()
671        } else {
672            MulticastRouteKey::new(I::SRC2, I::DST2).unwrap()
673        };
674
675        let expected_dev = MultipleDevicesId::A;
676        let actual_dev = if right_dev { expected_dev } else { MultipleDevicesId::B };
677
678        if enabled {
679            assert!(api.enable());
680            // NB: Only attempt to install the route when enabled; Otherwise
681            // installation fails.
682            assert_eq!(
683                api.add_multicast_route(
684                    expected_key.clone(),
685                    MulticastRoute::new_forward(
686                        expected_dev,
687                        [MulticastRouteTarget {
688                            output_interface: MultipleDevicesId::C,
689                            min_ttl: NonZeroU8::new(1).unwrap(),
690                        }]
691                        .into()
692                    )
693                    .unwrap()
694                ),
695                Ok(None)
696            );
697        }
698
699        api.core_ctx().state.set_multicast_forwarding_enabled_for_dev(actual_dev, dev_enabled);
700
701        let (core_ctx, bindings_ctx) = api.contexts();
702        let creation_time = bindings_ctx.now();
703        bindings_ctx.timers.instant.sleep(Duration::from_secs(5));
704        let lookup_time = bindings_ctx.now();
705        assert!(lookup_time > creation_time);
706
707        let buf = testutil::new_ip_packet_buf::<I>(actual_key.src_addr(), actual_key.dst_addr());
708        let mut buf_ref = buf.as_ref();
709        let packet = buf_ref.parse::<I::Packet<_>>().expect("parse should succeed");
710
711        let route = lookup_multicast_route_or_stash_packet(
712            core_ctx,
713            bindings_ctx,
714            &packet,
715            &actual_dev,
716            FRAME_DST,
717            MAX_FRAGMENT_LEN,
718            Default::default(),
719        );
720
721        // Verify that multicast routing events are generated.
722        let mut expected_events = vec![];
723        if !right_key {
724            expected_events.push(IpLayerEvent::MulticastForwarding(
725                MulticastForwardingEvent::MissingRoute {
726                    key: actual_key.clone(),
727                    input_interface: actual_dev,
728                },
729            ));
730        }
731        if !right_dev {
732            expected_events.push(IpLayerEvent::MulticastForwarding(
733                MulticastForwardingEvent::WrongInputInterface {
734                    key: actual_key,
735                    actual_input_interface: actual_dev,
736                    expected_input_interface: expected_dev,
737                },
738            ));
739        }
740        assert_eq!(bindings_ctx.take_events(), expected_events);
741
742        let lookup_succeeded = route.is_some();
743
744        if enabled {
745            // Verify that on success, the last_used field in stats is updated.
746            let expected_stats = if lookup_succeeded {
747                MulticastRouteStats { last_used: lookup_time }
748            } else {
749                MulticastRouteStats { last_used: creation_time }
750            };
751            assert_eq!(api.get_route_stats(&expected_key), Ok(Some(expected_stats)));
752        }
753
754        // Verify that counters are updated.
755        let counters: &MulticastForwardingCounters<I> = api.core_ctx().counters();
756        assert_eq!(counters.rx.get(), 1);
757        assert_eq!(counters.tx.get(), if lookup_succeeded { 1 } else { 0 });
758        assert_eq!(counters.no_tx_disabled_dev.get(), if dev_enabled { 0 } else { 1 });
759        assert_eq!(counters.no_tx_disabled_stack_wide.get(), if enabled { 0 } else { 1 });
760        assert_eq!(counters.no_tx_wrong_dev.get(), if right_dev { 0 } else { 1 });
761        assert_eq!(counters.pending_packets.get(), if right_key { 0 } else { 1 });
762
763        lookup_succeeded
764    }
765}