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