netstack3_filter/
logic.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
5pub(crate) mod nat;
6
7use core::num::NonZeroU16;
8use core::ops::RangeInclusive;
9
10use log::error;
11use net_types::ip::{GenericOverIp, Ip, IpVersionMarker};
12use netstack3_base::{
13    AnyDevice, DeviceIdContext, HandleableTimer, InterfaceProperties, IpDeviceAddressIdContext,
14};
15use packet_formats::ip::IpExt;
16
17use crate::conntrack::{Connection, FinalizeConnectionError, GetConnectionError};
18use crate::context::{FilterBindingsContext, FilterBindingsTypes, FilterIpContext};
19use crate::packets::{FilterIpExt, IpPacket, MaybeTransportPacket};
20use crate::state::{
21    Action, FilterIpMetadata, FilterMarkMetadata, Hook, Routine, Rule, TransparentProxy,
22};
23
24/// The final result of packet processing at a given filtering hook.
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum Verdict<R = ()> {
27    /// The packet should continue traversing the stack.
28    Accept(R),
29    /// The packet should be dropped immediately.
30    Drop,
31}
32
33/// The final result of packet processing at the INGRESS hook.
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub enum IngressVerdict<I: IpExt, R = ()> {
36    /// A verdict that is valid at any hook.
37    Verdict(Verdict<R>),
38    /// The packet should be immediately redirected to a local socket without its
39    /// header being changed in any way.
40    TransparentLocalDelivery {
41        /// The bound address of the local socket to redirect the packet to.
42        addr: I::Addr,
43        /// The bound port of the local socket to redirect the packet to.
44        port: NonZeroU16,
45    },
46}
47
48impl<I: IpExt, R> From<Verdict<R>> for IngressVerdict<I, R> {
49    fn from(verdict: Verdict<R>) -> Self {
50        IngressVerdict::Verdict(verdict)
51    }
52}
53
54/// A witness type to indicate that the egress filtering hook has been run.
55#[derive(Debug)]
56pub struct ProofOfEgressCheck {
57    _private_field_to_prevent_construction_outside_of_module: (),
58}
59
60impl ProofOfEgressCheck {
61    /// Clones this proof of egress check.
62    ///
63    /// May only be used in case of fragmentation after going through the egress
64    /// hook.
65    pub fn clone_for_fragmentation(&self) -> Self {
66        Self { _private_field_to_prevent_construction_outside_of_module: () }
67    }
68}
69
70#[derive(Clone)]
71pub(crate) struct Interfaces<'a, D> {
72    pub ingress: Option<&'a D>,
73    pub egress: Option<&'a D>,
74}
75
76/// The result of packet processing for a given routine.
77#[derive(Debug)]
78#[cfg_attr(test, derive(PartialEq, Eq))]
79pub(crate) enum RoutineResult<I: IpExt> {
80    /// The packet should stop traversing the rest of the current installed
81    /// routine, but continue travsering other routines installed in the hook.
82    Accept,
83    /// The packet should continue at the next rule in the calling chain.
84    Return,
85    /// The packet should be dropped immediately.
86    Drop,
87    /// The packet should be immediately redirected to a local socket without its
88    /// header being changed in any way.
89    TransparentLocalDelivery {
90        /// The bound address of the local socket to redirect the packet to.
91        addr: I::Addr,
92        /// The bound port of the local socket to redirect the packet to.
93        port: NonZeroU16,
94    },
95    /// Destination NAT (DNAT) should be performed to redirect the packet to the
96    /// local host.
97    Redirect {
98        /// The optional range of destination ports used to rewrite the packet.
99        ///
100        /// If absent, the destination port of the packet is not rewritten.
101        dst_port: Option<RangeInclusive<NonZeroU16>>,
102    },
103    /// Source NAT (SNAT) should be performed to rewrite the source address of the
104    /// packet to one owned by the outgoing interface.
105    Masquerade {
106        /// The optional range of source ports used to rewrite the packet.
107        ///
108        /// If absent, the source port of the packet is not rewritten.
109        src_port: Option<RangeInclusive<NonZeroU16>>,
110    },
111}
112
113impl<I: IpExt> RoutineResult<I> {
114    fn is_terminal(&self) -> bool {
115        match self {
116            RoutineResult::Accept
117            | RoutineResult::Drop
118            | RoutineResult::TransparentLocalDelivery { .. }
119            | RoutineResult::Redirect { .. }
120            | RoutineResult::Masquerade { .. } => true,
121            RoutineResult::Return => false,
122        }
123    }
124}
125
126fn apply_transparent_proxy<I: IpExt, P: MaybeTransportPacket>(
127    proxy: &TransparentProxy<I>,
128    dst_addr: I::Addr,
129    maybe_transport_packet: P,
130) -> RoutineResult<I> {
131    let (addr, port) = match proxy {
132        TransparentProxy::LocalPort(port) => (dst_addr, *port),
133        TransparentProxy::LocalAddr(addr) => {
134            let Some(transport_packet_data) = maybe_transport_packet.transport_packet_data() else {
135                // We ensure that TransparentProxy rules are always accompanied by a
136                // TCP or UDP matcher when filtering state is provided to Core, but
137                // given this invariant is enforced far from here, we log an error
138                // and drop the packet, which would likely happen at the transport
139                // layer anyway.
140                error!(
141                    "transparent proxy action is only valid on a rule that matches \
142                    on transport protocol, but this packet has no transport header",
143                );
144                return RoutineResult::Drop;
145            };
146            let port = NonZeroU16::new(transport_packet_data.dst_port())
147                .expect("TCP and UDP destination port is always non-zero");
148            (*addr, port)
149        }
150        TransparentProxy::LocalAddrAndPort(addr, port) => (*addr, *port),
151    };
152    RoutineResult::TransparentLocalDelivery { addr, port }
153}
154
155fn check_routine<I, P, D, DeviceClass, M>(
156    Routine { rules }: &Routine<I, DeviceClass, ()>,
157    packet: &P,
158    interfaces: &Interfaces<'_, D>,
159    metadata: &mut M,
160) -> RoutineResult<I>
161where
162    I: FilterIpExt,
163    P: IpPacket<I>,
164    D: InterfaceProperties<DeviceClass>,
165    M: FilterMarkMetadata,
166{
167    for Rule { matcher, action, validation_info: () } in rules {
168        if matcher.matches(packet, &interfaces) {
169            match action {
170                Action::Accept => return RoutineResult::Accept,
171                Action::Return => return RoutineResult::Return,
172                Action::Drop => return RoutineResult::Drop,
173                // TODO(https://fxbug.dev/332739892): enforce some kind of maximum depth on the
174                // routine graph to prevent a stack overflow here.
175                Action::Jump(target) => {
176                    let result = check_routine(target.get(), packet, interfaces, metadata);
177                    if result.is_terminal() {
178                        return result;
179                    }
180                    continue;
181                }
182                Action::TransparentProxy(proxy) => {
183                    return apply_transparent_proxy(
184                        proxy,
185                        packet.dst_addr(),
186                        packet.maybe_transport_packet(),
187                    );
188                }
189                Action::Redirect { dst_port } => {
190                    return RoutineResult::Redirect { dst_port: dst_port.clone() };
191                }
192                Action::Masquerade { src_port } => {
193                    return RoutineResult::Masquerade { src_port: src_port.clone() };
194                }
195                Action::Mark { domain, action } => {
196                    // Mark is a non-terminating action, it will not yield a `RoutineResult` but
197                    // it will continue on processing the next rule in the routine.
198                    metadata.apply_mark_action(*domain, *action);
199                }
200            }
201        }
202    }
203    RoutineResult::Return
204}
205
206fn check_routines_for_hook<I, P, D, DeviceClass, M>(
207    hook: &Hook<I, DeviceClass, ()>,
208    packet: &P,
209    interfaces: Interfaces<'_, D>,
210    metadata: &mut M,
211) -> Verdict
212where
213    I: FilterIpExt,
214    P: IpPacket<I>,
215    D: InterfaceProperties<DeviceClass>,
216    M: FilterMarkMetadata,
217{
218    let Hook { routines } = hook;
219    for routine in routines {
220        match check_routine(&routine, packet, &interfaces, metadata) {
221            RoutineResult::Accept | RoutineResult::Return => {}
222            RoutineResult::Drop => return Verdict::Drop,
223            result @ RoutineResult::TransparentLocalDelivery { .. } => {
224                unreachable!(
225                    "transparent local delivery is only valid in INGRESS hook; got {result:?}"
226                )
227            }
228            result @ (RoutineResult::Redirect { .. } | RoutineResult::Masquerade { .. }) => {
229                unreachable!("NAT actions are only valid in NAT routines; got {result:?}")
230            }
231        }
232    }
233    Verdict::Accept(())
234}
235
236fn check_routines_for_ingress<I, P, D, DeviceClass, M>(
237    hook: &Hook<I, DeviceClass, ()>,
238    packet: &P,
239    interfaces: Interfaces<'_, D>,
240    metadata: &mut M,
241) -> IngressVerdict<I>
242where
243    I: FilterIpExt,
244    P: IpPacket<I>,
245    D: InterfaceProperties<DeviceClass>,
246    M: FilterMarkMetadata,
247{
248    let Hook { routines } = hook;
249    for routine in routines {
250        match check_routine(&routine, packet, &interfaces, metadata) {
251            RoutineResult::Accept | RoutineResult::Return => {}
252            RoutineResult::Drop => return Verdict::Drop.into(),
253            RoutineResult::TransparentLocalDelivery { addr, port } => {
254                return IngressVerdict::TransparentLocalDelivery { addr, port };
255            }
256            result @ (RoutineResult::Redirect { .. } | RoutineResult::Masquerade { .. }) => {
257                unreachable!("NAT actions are only valid in NAT routines; got {result:?}")
258            }
259        }
260    }
261    Verdict::Accept(()).into()
262}
263
264/// An implementation of packet filtering logic, providing entry points at
265/// various stages of packet processing.
266pub trait FilterHandler<I: FilterIpExt, BC: FilterBindingsTypes>:
267    IpDeviceAddressIdContext<I, DeviceId: InterfaceProperties<BC::DeviceClass>>
268{
269    /// The ingress hook intercepts incoming traffic before a routing decision
270    /// has been made.
271    fn ingress_hook<P, M>(
272        &mut self,
273        bindings_ctx: &mut BC,
274        packet: &mut P,
275        interface: &Self::DeviceId,
276        metadata: &mut M,
277    ) -> IngressVerdict<I>
278    where
279        P: IpPacket<I>,
280        M: FilterIpMetadata<I, Self::WeakAddressId, BC>;
281
282    /// The local ingress hook intercepts incoming traffic that is destined for
283    /// the local host.
284    fn local_ingress_hook<P, M>(
285        &mut self,
286        bindings_ctx: &mut BC,
287        packet: &mut P,
288        interface: &Self::DeviceId,
289        metadata: &mut M,
290    ) -> Verdict
291    where
292        P: IpPacket<I>,
293        M: FilterIpMetadata<I, Self::WeakAddressId, BC>;
294
295    /// The forwarding hook intercepts incoming traffic that is destined for
296    /// another host.
297    fn forwarding_hook<P, M>(
298        &mut self,
299        packet: &mut P,
300        in_interface: &Self::DeviceId,
301        out_interface: &Self::DeviceId,
302        metadata: &mut M,
303    ) -> Verdict
304    where
305        P: IpPacket<I>,
306        M: FilterIpMetadata<I, Self::WeakAddressId, BC>;
307
308    /// The local egress hook intercepts locally-generated traffic before a
309    /// routing decision has been made.
310    fn local_egress_hook<P, M>(
311        &mut self,
312        bindings_ctx: &mut BC,
313        packet: &mut P,
314        interface: &Self::DeviceId,
315        metadata: &mut M,
316    ) -> Verdict
317    where
318        P: IpPacket<I>,
319        M: FilterIpMetadata<I, Self::WeakAddressId, BC>;
320
321    /// The egress hook intercepts all outgoing traffic after a routing decision
322    /// has been made.
323    fn egress_hook<P, M>(
324        &mut self,
325        bindings_ctx: &mut BC,
326        packet: &mut P,
327        interface: &Self::DeviceId,
328        metadata: &mut M,
329    ) -> (Verdict, ProofOfEgressCheck)
330    where
331        P: IpPacket<I>,
332        M: FilterIpMetadata<I, Self::WeakAddressId, BC>;
333}
334
335/// The "production" implementation of packet filtering.
336///
337/// Provides an implementation of [`FilterHandler`] for any `CC` that implements
338/// [`FilterIpContext`].
339pub struct FilterImpl<'a, CC>(pub &'a mut CC);
340
341impl<CC: DeviceIdContext<AnyDevice>> DeviceIdContext<AnyDevice> for FilterImpl<'_, CC> {
342    type DeviceId = CC::DeviceId;
343    type WeakDeviceId = CC::WeakDeviceId;
344}
345
346impl<I, CC> IpDeviceAddressIdContext<I> for FilterImpl<'_, CC>
347where
348    I: FilterIpExt,
349    CC: IpDeviceAddressIdContext<I>,
350{
351    type AddressId = CC::AddressId;
352    type WeakAddressId = CC::WeakAddressId;
353}
354
355impl<I, BC, CC> FilterHandler<I, BC> for FilterImpl<'_, CC>
356where
357    I: FilterIpExt,
358    BC: FilterBindingsContext,
359    CC: FilterIpContext<I, BC>,
360{
361    fn ingress_hook<P, M>(
362        &mut self,
363        bindings_ctx: &mut BC,
364        packet: &mut P,
365        interface: &Self::DeviceId,
366        metadata: &mut M,
367    ) -> IngressVerdict<I>
368    where
369        P: IpPacket<I>,
370        M: FilterIpMetadata<I, Self::WeakAddressId, BC>,
371    {
372        let Self(this) = self;
373        this.with_filter_state_and_nat_ctx(|state, core_ctx| {
374            // There usually isn't going to be an existing connection in the metadata before
375            // this hook, but it's possible in the case of looped-back packets, so check for
376            // one first before looking in the conntrack table.
377            let conn = match metadata.take_connection_and_direction() {
378                Some((c, d)) => Some((c, d)),
379                None => {
380                    packet.conntrack_packet().and_then(|packet| {
381                        match state
382                            .conntrack
383                            .get_connection_for_packet_and_update(bindings_ctx, packet)
384                        {
385                            Ok(result) => result,
386                            // TODO(https://fxbug.dev/328064909): Support configurable dropping of
387                            // invalid packets.
388                            Err(GetConnectionError::InvalidPacket(c, d)) => Some((c, d)),
389                        }
390                    })
391                }
392            };
393
394            let mut verdict = match check_routines_for_ingress(
395                &state.installed_routines.get().ip.ingress,
396                packet,
397                Interfaces { ingress: Some(interface), egress: None },
398                metadata,
399            ) {
400                v @ IngressVerdict::Verdict(Verdict::Drop) => return v,
401                v @ IngressVerdict::Verdict(Verdict::Accept(()))
402                | v @ IngressVerdict::TransparentLocalDelivery { .. } => v,
403            };
404
405            if let Some((mut conn, direction)) = conn {
406                // TODO(https://fxbug.dev/343683914): provide a way to run filter routines
407                // post-NAT, but in the same hook. Currently all filter routines are run before
408                // all NAT routines in the same hook.
409                match nat::perform_nat::<nat::IngressHook, _, _, _, _>(
410                    core_ctx,
411                    bindings_ctx,
412                    state.nat_installed.get(),
413                    &state.conntrack,
414                    &mut conn,
415                    direction,
416                    &state.installed_routines.get().nat.ingress,
417                    packet,
418                    Interfaces { ingress: Some(interface), egress: None },
419                ) {
420                    // NB: we only overwrite the verdict returned from the IP routines if it is
421                    // `TransparentLocalDelivery`; in case of an `Accept` verdict from the NAT
422                    // routines, we do not change the existing verdict.
423                    v @ IngressVerdict::Verdict(Verdict::Drop) => return v,
424                    IngressVerdict::Verdict(Verdict::Accept(())) => {}
425                    v @ IngressVerdict::TransparentLocalDelivery { .. } => {
426                        verdict = v;
427                    }
428                }
429
430                let res = metadata.replace_connection_and_direction(conn, direction);
431                debug_assert!(res.is_none());
432            }
433
434            verdict
435        })
436    }
437
438    fn local_ingress_hook<P, M>(
439        &mut self,
440        bindings_ctx: &mut BC,
441        packet: &mut P,
442        interface: &Self::DeviceId,
443        metadata: &mut M,
444    ) -> Verdict
445    where
446        P: IpPacket<I>,
447        M: FilterIpMetadata<I, Self::WeakAddressId, BC>,
448    {
449        let Self(this) = self;
450        this.with_filter_state_and_nat_ctx(|state, core_ctx| {
451            let conn = match metadata.take_connection_and_direction() {
452                Some((c, d)) => Some((c, d)),
453                // It's possible that there won't be a connection in the metadata by this point;
454                // this could be, for example, because the packet is for a protocol not tracked
455                // by conntrack.
456                None => packet.conntrack_packet().and_then(|packet| {
457                    match state.conntrack.get_connection_for_packet_and_update(bindings_ctx, packet)
458                    {
459                        Ok(result) => result,
460                        // TODO(https://fxbug.dev/328064909): Support configurable dropping of
461                        // invalid packets.
462                        Err(GetConnectionError::InvalidPacket(c, d)) => Some((c, d)),
463                    }
464                }),
465            };
466
467            let verdict = match check_routines_for_hook(
468                &state.installed_routines.get().ip.local_ingress,
469                packet,
470                Interfaces { ingress: Some(interface), egress: None },
471                metadata,
472            ) {
473                Verdict::Drop => return Verdict::Drop,
474                Verdict::Accept(()) => Verdict::Accept(()),
475            };
476
477            if let Some((mut conn, direction)) = conn {
478                // TODO(https://fxbug.dev/343683914): provide a way to run filter routines
479                // post-NAT, but in the same hook. Currently all filter routines are run before
480                // all NAT routines in the same hook.
481                match nat::perform_nat::<nat::LocalIngressHook, _, _, _, _>(
482                    core_ctx,
483                    bindings_ctx,
484                    state.nat_installed.get(),
485                    &state.conntrack,
486                    &mut conn,
487                    direction,
488                    &state.installed_routines.get().nat.local_ingress,
489                    packet,
490                    Interfaces { ingress: Some(interface), egress: None },
491                ) {
492                    Verdict::Drop => return Verdict::Drop,
493                    Verdict::Accept(()) => {}
494                }
495
496                match state.conntrack.finalize_connection(bindings_ctx, conn) {
497                    Ok((_inserted, _weak_conn)) => {}
498                    // If finalizing the connection would result in a conflict in the connection
499                    // tracking table, or if the table is at capacity, drop the packet.
500                    Err(FinalizeConnectionError::Conflict | FinalizeConnectionError::TableFull) => {
501                        return Verdict::Drop;
502                    }
503                }
504            }
505
506            verdict
507        })
508    }
509
510    fn forwarding_hook<P, M>(
511        &mut self,
512        packet: &mut P,
513        in_interface: &Self::DeviceId,
514        out_interface: &Self::DeviceId,
515        metadata: &mut M,
516    ) -> Verdict
517    where
518        P: IpPacket<I>,
519        M: FilterIpMetadata<I, Self::WeakAddressId, BC>,
520    {
521        let Self(this) = self;
522        this.with_filter_state(|state| {
523            check_routines_for_hook(
524                &state.installed_routines.get().ip.forwarding,
525                packet,
526                Interfaces { ingress: Some(in_interface), egress: Some(out_interface) },
527                metadata,
528            )
529        })
530    }
531
532    fn local_egress_hook<P, M>(
533        &mut self,
534        bindings_ctx: &mut BC,
535        packet: &mut P,
536        interface: &Self::DeviceId,
537        metadata: &mut M,
538    ) -> Verdict
539    where
540        P: IpPacket<I>,
541        M: FilterIpMetadata<I, Self::WeakAddressId, BC>,
542    {
543        let Self(this) = self;
544        this.with_filter_state_and_nat_ctx(|state, core_ctx| {
545            // There isn't going to be an existing connection in the metadata
546            // before this hook, so we don't have to look.
547            let conn = packet.conntrack_packet().and_then(|packet| {
548                match state.conntrack.get_connection_for_packet_and_update(bindings_ctx, packet) {
549                    Ok(result) => result,
550                    // TODO(https://fxbug.dev/328064909): Support configurable dropping of invalid
551                    // packets.
552                    Err(GetConnectionError::InvalidPacket(c, d)) => Some((c, d)),
553                }
554            });
555
556            let verdict = match check_routines_for_hook(
557                &state.installed_routines.get().ip.local_egress,
558                packet,
559                Interfaces { ingress: None, egress: Some(interface) },
560                metadata,
561            ) {
562                Verdict::Drop => return Verdict::Drop,
563                Verdict::Accept(()) => Verdict::Accept(()),
564            };
565
566            if let Some((mut conn, direction)) = conn {
567                // TODO(https://fxbug.dev/343683914): provide a way to run filter routines
568                // post-NAT, but in the same hook. Currently all filter routines are run before
569                // all NAT routines in the same hook.
570                match nat::perform_nat::<nat::LocalEgressHook, _, _, _, _>(
571                    core_ctx,
572                    bindings_ctx,
573                    state.nat_installed.get(),
574                    &state.conntrack,
575                    &mut conn,
576                    direction,
577                    &state.installed_routines.get().nat.local_egress,
578                    packet,
579                    Interfaces { ingress: None, egress: Some(interface) },
580                ) {
581                    Verdict::Drop => return Verdict::Drop,
582                    Verdict::Accept(()) => {}
583                }
584
585                let res = metadata.replace_connection_and_direction(conn, direction);
586                debug_assert!(res.is_none());
587            }
588
589            verdict
590        })
591    }
592
593    fn egress_hook<P, M>(
594        &mut self,
595        bindings_ctx: &mut BC,
596        packet: &mut P,
597        interface: &Self::DeviceId,
598        metadata: &mut M,
599    ) -> (Verdict, ProofOfEgressCheck)
600    where
601        P: IpPacket<I>,
602        M: FilterIpMetadata<I, Self::WeakAddressId, BC>,
603    {
604        let Self(this) = self;
605        let verdict = this.with_filter_state_and_nat_ctx(|state, core_ctx| {
606            let conn = match metadata.take_connection_and_direction() {
607                Some((c, d)) => Some((c, d)),
608                // It's possible that there won't be a connection in the metadata by this point;
609                // this could be, for example, because the packet is for a protocol not tracked
610                // by conntrack.
611                None => packet.conntrack_packet().and_then(|packet| {
612                    match state.conntrack.get_connection_for_packet_and_update(bindings_ctx, packet)
613                    {
614                        Ok(result) => result,
615                        // TODO(https://fxbug.dev/328064909): Support configurable dropping of
616                        // invalid packets.
617                        Err(GetConnectionError::InvalidPacket(c, d)) => Some((c, d)),
618                    }
619                }),
620            };
621
622            let verdict = match check_routines_for_hook(
623                &state.installed_routines.get().ip.egress,
624                packet,
625                Interfaces { ingress: None, egress: Some(interface) },
626                metadata,
627            ) {
628                Verdict::Drop => return Verdict::Drop,
629                Verdict::Accept(()) => Verdict::Accept(()),
630            };
631
632            if let Some((mut conn, direction)) = conn {
633                // TODO(https://fxbug.dev/343683914): provide a way to run filter routines
634                // post-NAT, but in the same hook. Currently all filter routines are run before
635                // all NAT routines in the same hook.
636                match nat::perform_nat::<nat::EgressHook, _, _, _, _>(
637                    core_ctx,
638                    bindings_ctx,
639                    state.nat_installed.get(),
640                    &state.conntrack,
641                    &mut conn,
642                    direction,
643                    &state.installed_routines.get().nat.egress,
644                    packet,
645                    Interfaces { ingress: None, egress: Some(interface) },
646                ) {
647                    Verdict::Drop => return Verdict::Drop,
648                    Verdict::Accept(()) => {}
649                }
650
651                match state.conntrack.finalize_connection(bindings_ctx, conn) {
652                    Ok((_inserted, conn)) => {
653                        if let Some(conn) = conn {
654                            let res = metadata.replace_connection_and_direction(
655                                Connection::Shared(conn),
656                                direction,
657                            );
658                            debug_assert!(res.is_none());
659                        }
660                    }
661                    // If finalizing the connection would result in a conflict in the connection
662                    // tracking table, or if the table is at capacity, drop the packet.
663                    Err(FinalizeConnectionError::Conflict | FinalizeConnectionError::TableFull) => {
664                        return Verdict::Drop;
665                    }
666                }
667            }
668
669            verdict
670        });
671        (
672            verdict,
673            ProofOfEgressCheck { _private_field_to_prevent_construction_outside_of_module: () },
674        )
675    }
676}
677
678/// A timer ID for the filtering crate.
679#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, GenericOverIp, Hash)]
680#[generic_over_ip(I, Ip)]
681pub enum FilterTimerId<I: Ip> {
682    /// A trigger for the conntrack module to perform garbage collection.
683    ConntrackGc(IpVersionMarker<I>),
684}
685
686impl<I: FilterIpExt, BC: FilterBindingsContext, CC: FilterIpContext<I, BC>> HandleableTimer<CC, BC>
687    for FilterTimerId<I>
688{
689    fn handle(self, core_ctx: &mut CC, bindings_ctx: &mut BC, _: BC::UniqueTimerId) {
690        match self {
691            FilterTimerId::ConntrackGc(_) => core_ctx.with_filter_state(|state| {
692                state.conntrack.perform_gc(bindings_ctx);
693            }),
694        }
695    }
696}
697
698#[cfg(any(test, feature = "testutils"))]
699pub mod testutil {
700    use core::marker::PhantomData;
701
702    use net_types::ip::AddrSubnet;
703    use netstack3_base::AssignedAddrIpExt;
704    use netstack3_base::testutil::{FakeStrongDeviceId, FakeWeakAddressId, FakeWeakDeviceId};
705
706    use super::*;
707
708    /// A no-op implementation of packet filtering that accepts any packet that
709    /// passes through it, useful for unit tests of other modules where trait bounds
710    /// require that a `FilterHandler` is available but no filtering logic is under
711    /// test.
712    ///
713    /// Provides an implementation of [`FilterHandler`].
714    pub struct NoopImpl<DeviceId>(PhantomData<DeviceId>);
715
716    impl<DeviceId> Default for NoopImpl<DeviceId> {
717        fn default() -> Self {
718            Self(PhantomData)
719        }
720    }
721
722    impl<DeviceId: FakeStrongDeviceId> DeviceIdContext<AnyDevice> for NoopImpl<DeviceId> {
723        type DeviceId = DeviceId;
724        type WeakDeviceId = FakeWeakDeviceId<DeviceId>;
725    }
726
727    impl<I: AssignedAddrIpExt, DeviceId: FakeStrongDeviceId> IpDeviceAddressIdContext<I>
728        for NoopImpl<DeviceId>
729    {
730        type AddressId = AddrSubnet<I::Addr, I::AssignedWitness>;
731        type WeakAddressId = FakeWeakAddressId<Self::AddressId>;
732    }
733
734    impl<I, BC, DeviceId> FilterHandler<I, BC> for NoopImpl<DeviceId>
735    where
736        I: FilterIpExt + AssignedAddrIpExt,
737        BC: FilterBindingsContext,
738        DeviceId: FakeStrongDeviceId + InterfaceProperties<BC::DeviceClass>,
739    {
740        fn ingress_hook<P, M>(
741            &mut self,
742            _: &mut BC,
743            _: &mut P,
744            _: &Self::DeviceId,
745            _: &mut M,
746        ) -> IngressVerdict<I>
747        where
748            P: IpPacket<I>,
749            M: FilterIpMetadata<I, Self::WeakAddressId, BC>,
750        {
751            Verdict::Accept(()).into()
752        }
753
754        fn local_ingress_hook<P, M>(
755            &mut self,
756            _: &mut BC,
757            _: &mut P,
758            _: &Self::DeviceId,
759            _: &mut M,
760        ) -> Verdict
761        where
762            P: IpPacket<I>,
763            M: FilterIpMetadata<I, Self::WeakAddressId, BC>,
764        {
765            Verdict::Accept(())
766        }
767
768        fn forwarding_hook<P, M>(
769            &mut self,
770            _: &mut P,
771            _: &Self::DeviceId,
772            _: &Self::DeviceId,
773            _: &mut M,
774        ) -> Verdict
775        where
776            P: IpPacket<I>,
777            M: FilterIpMetadata<I, Self::WeakAddressId, BC>,
778        {
779            Verdict::Accept(())
780        }
781
782        fn local_egress_hook<P, M>(
783            &mut self,
784            _: &mut BC,
785            _: &mut P,
786            _: &Self::DeviceId,
787            _: &mut M,
788        ) -> Verdict
789        where
790            P: IpPacket<I>,
791            M: FilterIpMetadata<I, Self::WeakAddressId, BC>,
792        {
793            Verdict::Accept(())
794        }
795
796        fn egress_hook<P, M>(
797            &mut self,
798            _: &mut BC,
799            _: &mut P,
800            _: &Self::DeviceId,
801            _: &mut M,
802        ) -> (Verdict, ProofOfEgressCheck)
803        where
804            P: IpPacket<I>,
805            M: FilterIpMetadata<I, Self::WeakAddressId, BC>,
806        {
807            (Verdict::Accept(()), ProofOfEgressCheck::forge_proof_for_test())
808        }
809    }
810
811    impl ProofOfEgressCheck {
812        /// For tests where it's not feasible to run the egress hook.
813        pub(crate) fn forge_proof_for_test() -> Self {
814            ProofOfEgressCheck { _private_field_to_prevent_construction_outside_of_module: () }
815        }
816    }
817}
818
819#[cfg(test)]
820mod tests {
821    use alloc::sync::Arc;
822    use alloc::vec;
823    use alloc::vec::Vec;
824
825    use assert_matches::assert_matches;
826    use derivative::Derivative;
827    use ip_test_macro::ip_test;
828    use net_types::ip::{AddrSubnet, Ipv4};
829    use netstack3_base::testutil::{FakeDeviceClass, FakeMatcherDeviceId};
830    use netstack3_base::{AssignedAddrIpExt, InterfaceMatcher, MarkDomain, Marks, SegmentHeader};
831    use netstack3_hashmap::HashMap;
832    use test_case::test_case;
833
834    use super::*;
835    use crate::actions::MarkAction;
836    use crate::conntrack::{self, ConnectionDirection};
837    use crate::context::testutil::{FakeBindingsCtx, FakeCtx, FakeWeakAddressId};
838    use crate::logic::nat::NatConfig;
839    use crate::matchers::{
840        AddressMatcher, AddressMatcherType, PacketMatcher, PortMatcher, TransportProtocolMatcher,
841    };
842    use crate::packets::testutil::internal::{
843        ArbitraryValue, FakeIpPacket, FakeTcpSegment, FakeUdpPacket, TransportPacketExt,
844    };
845    use crate::state::{IpRoutines, NatRoutines, UninstalledRoutine};
846    use crate::testutil::TestIpExt;
847
848    impl<I: IpExt> Rule<I, FakeDeviceClass, ()> {
849        pub(crate) fn new(
850            matcher: PacketMatcher<I, FakeDeviceClass>,
851            action: Action<I, FakeDeviceClass, ()>,
852        ) -> Self {
853            Rule { matcher, action, validation_info: () }
854        }
855    }
856
857    #[test]
858    fn return_by_default_if_no_matching_rules_in_routine() {
859        assert_eq!(
860            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
861                &Routine { rules: Vec::new() },
862                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
863                &Interfaces { ingress: None, egress: None },
864                &mut NullMetadata {},
865            ),
866            RoutineResult::Return
867        );
868
869        // A subroutine should also yield `Return` if no rules match, allowing
870        // the calling routine to continue execution after the `Jump`.
871        let routine = Routine {
872            rules: vec![
873                Rule::new(
874                    PacketMatcher::default(),
875                    Action::Jump(UninstalledRoutine::new(Vec::new(), 0)),
876                ),
877                Rule::new(PacketMatcher::default(), Action::Drop),
878            ],
879        };
880        assert_eq!(
881            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
882                &routine,
883                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
884                &Interfaces { ingress: None, egress: None },
885                &mut NullMetadata {},
886            ),
887            RoutineResult::Drop
888        );
889    }
890
891    struct NullMetadata {}
892
893    impl<I: IpExt, A, BT: FilterBindingsTypes> FilterIpMetadata<I, A, BT> for NullMetadata {
894        fn take_connection_and_direction(
895            &mut self,
896        ) -> Option<(Connection<I, NatConfig<I, A>, BT>, ConnectionDirection)> {
897            None
898        }
899
900        fn replace_connection_and_direction(
901            &mut self,
902            _conn: Connection<I, NatConfig<I, A>, BT>,
903            _direction: ConnectionDirection,
904        ) -> Option<Connection<I, NatConfig<I, A>, BT>> {
905            None
906        }
907    }
908
909    impl FilterMarkMetadata for NullMetadata {
910        fn apply_mark_action(&mut self, _domain: MarkDomain, _action: MarkAction) {}
911    }
912
913    #[derive(Derivative)]
914    #[derivative(Default(bound = ""))]
915    struct PacketMetadata<I: IpExt + AssignedAddrIpExt, A, BT: FilterBindingsTypes> {
916        conn: Option<(Connection<I, NatConfig<I, A>, BT>, ConnectionDirection)>,
917        marks: Marks,
918    }
919
920    impl<I: TestIpExt, A, BT: FilterBindingsTypes> FilterIpMetadata<I, A, BT>
921        for PacketMetadata<I, A, BT>
922    {
923        fn take_connection_and_direction(
924            &mut self,
925        ) -> Option<(Connection<I, NatConfig<I, A>, BT>, ConnectionDirection)> {
926            let Self { conn, marks: _ } = self;
927            conn.take()
928        }
929
930        fn replace_connection_and_direction(
931            &mut self,
932            new_conn: Connection<I, NatConfig<I, A>, BT>,
933            direction: ConnectionDirection,
934        ) -> Option<Connection<I, NatConfig<I, A>, BT>> {
935            let Self { conn, marks: _ } = self;
936            conn.replace((new_conn, direction)).map(|(conn, _dir)| conn)
937        }
938    }
939
940    impl<I: TestIpExt, A, BT: FilterBindingsTypes> FilterMarkMetadata for PacketMetadata<I, A, BT> {
941        fn apply_mark_action(&mut self, domain: MarkDomain, action: MarkAction) {
942            action.apply(self.marks.get_mut(domain))
943        }
944    }
945
946    #[test]
947    fn accept_by_default_if_no_matching_rules_in_hook() {
948        assert_eq!(
949            check_routines_for_hook::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
950                &Hook::default(),
951                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
952                Interfaces { ingress: None, egress: None },
953                &mut NullMetadata {},
954            ),
955            Verdict::Accept(())
956        );
957    }
958
959    #[test]
960    fn accept_by_default_if_return_from_routine() {
961        let hook = Hook {
962            routines: vec![Routine {
963                rules: vec![Rule::new(PacketMatcher::default(), Action::Return)],
964            }],
965        };
966
967        assert_eq!(
968            check_routines_for_hook::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
969                &hook,
970                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
971                Interfaces { ingress: None, egress: None },
972                &mut NullMetadata {},
973            ),
974            Verdict::Accept(())
975        );
976    }
977
978    #[test]
979    fn accept_terminal_for_installed_routine() {
980        let routine = Routine {
981            rules: vec![
982                // Accept all traffic.
983                Rule::new(PacketMatcher::default(), Action::Accept),
984                // Drop all traffic.
985                Rule::new(PacketMatcher::default(), Action::Drop),
986            ],
987        };
988        assert_eq!(
989            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
990                &routine,
991                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
992                &Interfaces { ingress: None, egress: None },
993                &mut NullMetadata {},
994            ),
995            RoutineResult::Accept
996        );
997
998        // `Accept` should also be propagated from subroutines.
999        let routine = Routine {
1000            rules: vec![
1001                // Jump to a routine that accepts all traffic.
1002                Rule::new(
1003                    PacketMatcher::default(),
1004                    Action::Jump(UninstalledRoutine::new(
1005                        vec![Rule::new(PacketMatcher::default(), Action::Accept)],
1006                        0,
1007                    )),
1008                ),
1009                // Drop all traffic.
1010                Rule::new(PacketMatcher::default(), Action::Drop),
1011            ],
1012        };
1013        assert_eq!(
1014            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1015                &routine,
1016                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1017                &Interfaces { ingress: None, egress: None },
1018                &mut NullMetadata {},
1019            ),
1020            RoutineResult::Accept
1021        );
1022
1023        // Now put that routine in a hook that also includes *another* installed
1024        // routine which drops all traffic. The first installed routine should
1025        // terminate at its `Accept` result, but the hook should terminate at
1026        // the `Drop` result in the second routine.
1027        let hook = Hook {
1028            routines: vec![
1029                routine,
1030                Routine {
1031                    rules: vec![
1032                        // Drop all traffic.
1033                        Rule::new(PacketMatcher::default(), Action::Drop),
1034                    ],
1035                },
1036            ],
1037        };
1038
1039        assert_eq!(
1040            check_routines_for_hook::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1041                &hook,
1042                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1043                Interfaces { ingress: None, egress: None },
1044                &mut NullMetadata {},
1045            ),
1046            Verdict::Drop
1047        );
1048    }
1049
1050    #[test]
1051    fn drop_terminal_for_entire_hook() {
1052        let hook = Hook {
1053            routines: vec![
1054                Routine {
1055                    rules: vec![
1056                        // Drop all traffic.
1057                        Rule::new(PacketMatcher::default(), Action::Drop),
1058                    ],
1059                },
1060                Routine {
1061                    rules: vec![
1062                        // Accept all traffic.
1063                        Rule::new(PacketMatcher::default(), Action::Accept),
1064                    ],
1065                },
1066            ],
1067        };
1068
1069        assert_eq!(
1070            check_routines_for_hook::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1071                &hook,
1072                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1073                Interfaces { ingress: None, egress: None },
1074                &mut NullMetadata {},
1075            ),
1076            Verdict::Drop
1077        );
1078    }
1079
1080    #[test]
1081    fn transparent_proxy_terminal_for_entire_hook() {
1082        const TPROXY_PORT: NonZeroU16 = NonZeroU16::new(8080).unwrap();
1083
1084        let ingress = Hook {
1085            routines: vec![
1086                Routine {
1087                    rules: vec![Rule::new(
1088                        PacketMatcher::default(),
1089                        Action::TransparentProxy(TransparentProxy::LocalPort(TPROXY_PORT)),
1090                    )],
1091                },
1092                Routine {
1093                    rules: vec![
1094                        // Accept all traffic.
1095                        Rule::new(PacketMatcher::default(), Action::Accept),
1096                    ],
1097                },
1098            ],
1099        };
1100
1101        assert_eq!(
1102            check_routines_for_ingress::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1103                &ingress,
1104                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1105                Interfaces { ingress: None, egress: None },
1106                &mut NullMetadata {},
1107            ),
1108            IngressVerdict::TransparentLocalDelivery {
1109                addr: <Ipv4 as crate::packets::testutil::internal::TestIpExt>::DST_IP,
1110                port: TPROXY_PORT
1111            }
1112        );
1113    }
1114
1115    #[test]
1116    fn jump_recursively_evaluates_target_routine() {
1117        // Drop result from a target routine is propagated to the calling
1118        // routine.
1119        let routine = Routine {
1120            rules: vec![Rule::new(
1121                PacketMatcher::default(),
1122                Action::Jump(UninstalledRoutine::new(
1123                    vec![Rule::new(PacketMatcher::default(), Action::Drop)],
1124                    0,
1125                )),
1126            )],
1127        };
1128        assert_eq!(
1129            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1130                &routine,
1131                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1132                &Interfaces { ingress: None, egress: None },
1133                &mut NullMetadata {},
1134            ),
1135            RoutineResult::Drop
1136        );
1137
1138        // Accept result from a target routine is also propagated to the calling
1139        // routine.
1140        let routine = Routine {
1141            rules: vec![
1142                Rule::new(
1143                    PacketMatcher::default(),
1144                    Action::Jump(UninstalledRoutine::new(
1145                        vec![Rule::new(PacketMatcher::default(), Action::Accept)],
1146                        0,
1147                    )),
1148                ),
1149                Rule::new(PacketMatcher::default(), Action::Drop),
1150            ],
1151        };
1152        assert_eq!(
1153            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1154                &routine,
1155                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1156                &Interfaces { ingress: None, egress: None },
1157                &mut NullMetadata {},
1158            ),
1159            RoutineResult::Accept
1160        );
1161
1162        // Return from a target routine results in continued evaluation of the
1163        // calling routine.
1164        let routine = Routine {
1165            rules: vec![
1166                Rule::new(
1167                    PacketMatcher::default(),
1168                    Action::Jump(UninstalledRoutine::new(
1169                        vec![Rule::new(PacketMatcher::default(), Action::Return)],
1170                        0,
1171                    )),
1172                ),
1173                Rule::new(PacketMatcher::default(), Action::Drop),
1174            ],
1175        };
1176        assert_eq!(
1177            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1178                &routine,
1179                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1180                &Interfaces { ingress: None, egress: None },
1181                &mut NullMetadata {},
1182            ),
1183            RoutineResult::Drop
1184        );
1185    }
1186
1187    #[test]
1188    fn return_terminal_for_single_routine() {
1189        let routine = Routine {
1190            rules: vec![
1191                Rule::new(PacketMatcher::default(), Action::Return),
1192                // Drop all traffic.
1193                Rule::new(PacketMatcher::default(), Action::Drop),
1194            ],
1195        };
1196
1197        assert_eq!(
1198            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1199                &routine,
1200                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1201                &Interfaces { ingress: None, egress: None },
1202                &mut NullMetadata {},
1203            ),
1204            RoutineResult::Return
1205        );
1206    }
1207
1208    #[ip_test(I)]
1209    fn filter_handler_implements_ip_hooks_correctly<I: TestIpExt>() {
1210        fn drop_all_traffic<I: TestIpExt>(
1211            matcher: PacketMatcher<I, FakeDeviceClass>,
1212        ) -> Hook<I, FakeDeviceClass, ()> {
1213            Hook { routines: vec![Routine { rules: vec![Rule::new(matcher, Action::Drop)] }] }
1214        }
1215
1216        let mut bindings_ctx = FakeBindingsCtx::new();
1217
1218        // Ingress hook should use ingress routines and check the input
1219        // interface.
1220        let mut ctx = FakeCtx::with_ip_routines(
1221            &mut bindings_ctx,
1222            IpRoutines {
1223                ingress: drop_all_traffic(PacketMatcher {
1224                    in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1225                    ..Default::default()
1226                }),
1227                ..Default::default()
1228            },
1229        );
1230        assert_eq!(
1231            FilterImpl(&mut ctx).ingress_hook(
1232                &mut bindings_ctx,
1233                &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1234                &FakeMatcherDeviceId::wlan_interface(),
1235                &mut NullMetadata {},
1236            ),
1237            Verdict::Drop.into()
1238        );
1239
1240        // Local ingress hook should use local ingress routines and check the
1241        // input interface.
1242        let mut ctx = FakeCtx::with_ip_routines(
1243            &mut bindings_ctx,
1244            IpRoutines {
1245                local_ingress: drop_all_traffic(PacketMatcher {
1246                    in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1247                    ..Default::default()
1248                }),
1249                ..Default::default()
1250            },
1251        );
1252        assert_eq!(
1253            FilterImpl(&mut ctx).local_ingress_hook(
1254                &mut bindings_ctx,
1255                &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1256                &FakeMatcherDeviceId::wlan_interface(),
1257                &mut NullMetadata {},
1258            ),
1259            Verdict::Drop
1260        );
1261
1262        // Forwarding hook should use forwarding routines and check both the
1263        // input and output interfaces.
1264        let mut ctx = FakeCtx::with_ip_routines(
1265            &mut bindings_ctx,
1266            IpRoutines {
1267                forwarding: drop_all_traffic(PacketMatcher {
1268                    in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1269                    out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
1270                    ..Default::default()
1271                }),
1272                ..Default::default()
1273            },
1274        );
1275        assert_eq!(
1276            FilterImpl(&mut ctx).forwarding_hook(
1277                &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1278                &FakeMatcherDeviceId::wlan_interface(),
1279                &FakeMatcherDeviceId::ethernet_interface(),
1280                &mut NullMetadata {},
1281            ),
1282            Verdict::Drop
1283        );
1284
1285        // Local egress hook should use local egress routines and check the
1286        // output interface.
1287        let mut ctx = FakeCtx::with_ip_routines(
1288            &mut bindings_ctx,
1289            IpRoutines {
1290                local_egress: drop_all_traffic(PacketMatcher {
1291                    out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1292                    ..Default::default()
1293                }),
1294                ..Default::default()
1295            },
1296        );
1297        assert_eq!(
1298            FilterImpl(&mut ctx).local_egress_hook(
1299                &mut bindings_ctx,
1300                &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1301                &FakeMatcherDeviceId::wlan_interface(),
1302                &mut NullMetadata {},
1303            ),
1304            Verdict::Drop
1305        );
1306
1307        // Egress hook should use egress routines and check the output
1308        // interface.
1309        let mut ctx = FakeCtx::with_ip_routines(
1310            &mut bindings_ctx,
1311            IpRoutines {
1312                egress: drop_all_traffic(PacketMatcher {
1313                    out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1314                    ..Default::default()
1315                }),
1316                ..Default::default()
1317            },
1318        );
1319        assert_eq!(
1320            FilterImpl(&mut ctx)
1321                .egress_hook(
1322                    &mut bindings_ctx,
1323                    &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1324                    &FakeMatcherDeviceId::wlan_interface(),
1325                    &mut NullMetadata {},
1326                )
1327                .0,
1328            Verdict::Drop
1329        );
1330    }
1331
1332    #[ip_test(I)]
1333    #[test_case(22 => Verdict::Accept(()); "port 22 allowed for SSH")]
1334    #[test_case(80 => Verdict::Accept(()); "port 80 allowed for HTTP")]
1335    #[test_case(1024 => Verdict::Accept(()); "ephemeral port 1024 allowed")]
1336    #[test_case(65535 => Verdict::Accept(()); "ephemeral port 65535 allowed")]
1337    #[test_case(1023 => Verdict::Drop; "privileged port 1023 blocked")]
1338    #[test_case(53 => Verdict::Drop; "privileged port 53 blocked")]
1339    fn block_privileged_ports_except_ssh_http<I: TestIpExt>(port: u16) -> Verdict {
1340        fn tcp_port_rule<I: FilterIpExt>(
1341            src_port: Option<PortMatcher>,
1342            dst_port: Option<PortMatcher>,
1343            action: Action<I, FakeDeviceClass, ()>,
1344        ) -> Rule<I, FakeDeviceClass, ()> {
1345            Rule::new(
1346                PacketMatcher {
1347                    transport_protocol: Some(TransportProtocolMatcher {
1348                        proto: <&FakeTcpSegment as TransportPacketExt<I>>::proto().unwrap(),
1349                        src_port,
1350                        dst_port,
1351                    }),
1352                    ..Default::default()
1353                },
1354                action,
1355            )
1356        }
1357
1358        fn default_filter_rules<I: FilterIpExt>() -> Routine<I, FakeDeviceClass, ()> {
1359            Routine {
1360                rules: vec![
1361                    // pass in proto tcp to port 22;
1362                    tcp_port_rule(
1363                        /* src_port */ None,
1364                        Some(PortMatcher { range: 22..=22, invert: false }),
1365                        Action::Accept,
1366                    ),
1367                    // pass in proto tcp to port 80;
1368                    tcp_port_rule(
1369                        /* src_port */ None,
1370                        Some(PortMatcher { range: 80..=80, invert: false }),
1371                        Action::Accept,
1372                    ),
1373                    // pass in proto tcp to range 1024:65535;
1374                    tcp_port_rule(
1375                        /* src_port */ None,
1376                        Some(PortMatcher { range: 1024..=65535, invert: false }),
1377                        Action::Accept,
1378                    ),
1379                    // drop in proto tcp to range 1:6553;
1380                    tcp_port_rule(
1381                        /* src_port */ None,
1382                        Some(PortMatcher { range: 1..=65535, invert: false }),
1383                        Action::Drop,
1384                    ),
1385                ],
1386            }
1387        }
1388
1389        let mut bindings_ctx = FakeBindingsCtx::new();
1390
1391        let mut ctx = FakeCtx::with_ip_routines(
1392            &mut bindings_ctx,
1393            IpRoutines {
1394                local_ingress: Hook { routines: vec![default_filter_rules()] },
1395                ..Default::default()
1396            },
1397        );
1398
1399        FilterImpl(&mut ctx).local_ingress_hook(
1400            &mut bindings_ctx,
1401            &mut FakeIpPacket::<I, _> {
1402                body: FakeTcpSegment {
1403                    dst_port: port,
1404                    src_port: 11111,
1405                    segment: SegmentHeader::arbitrary_value(),
1406                    payload_len: 8888,
1407                },
1408                ..ArbitraryValue::arbitrary_value()
1409            },
1410            &FakeMatcherDeviceId::wlan_interface(),
1411            &mut NullMetadata {},
1412        )
1413    }
1414
1415    #[ip_test(I)]
1416    #[test_case(
1417        FakeMatcherDeviceId::ethernet_interface() => Verdict::Accept(());
1418        "allow incoming traffic on ethernet interface"
1419    )]
1420    #[test_case(
1421        FakeMatcherDeviceId::wlan_interface() => Verdict::Drop;
1422        "drop incoming traffic on wlan interface"
1423    )]
1424    fn filter_on_wlan_only<I: TestIpExt>(interface: FakeMatcherDeviceId) -> Verdict {
1425        fn drop_wlan_traffic<I: IpExt>() -> Routine<I, FakeDeviceClass, ()> {
1426            Routine {
1427                rules: vec![Rule::new(
1428                    PacketMatcher {
1429                        in_interface: Some(InterfaceMatcher::Id(
1430                            FakeMatcherDeviceId::wlan_interface().id,
1431                        )),
1432                        ..Default::default()
1433                    },
1434                    Action::Drop,
1435                )],
1436            }
1437        }
1438
1439        let mut bindings_ctx = FakeBindingsCtx::new();
1440
1441        let mut ctx = FakeCtx::with_ip_routines(
1442            &mut bindings_ctx,
1443            IpRoutines {
1444                local_ingress: Hook { routines: vec![drop_wlan_traffic()] },
1445                ..Default::default()
1446            },
1447        );
1448
1449        FilterImpl(&mut ctx).local_ingress_hook(
1450            &mut bindings_ctx,
1451            &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1452            &interface,
1453            &mut NullMetadata {},
1454        )
1455    }
1456
1457    #[test]
1458    fn ingress_reuses_cached_connection_when_available() {
1459        let mut bindings_ctx = FakeBindingsCtx::new();
1460        let mut core_ctx = FakeCtx::new(&mut bindings_ctx);
1461
1462        // When a connection is finalized in the EGRESS hook, it should stash a shared
1463        // reference to the connection in the packet metadata.
1464        let mut packet = FakeIpPacket::<Ipv4, FakeUdpPacket>::arbitrary_value();
1465        let mut metadata = PacketMetadata::default();
1466        let (verdict, _proof) = FilterImpl(&mut core_ctx).egress_hook(
1467            &mut bindings_ctx,
1468            &mut packet,
1469            &FakeMatcherDeviceId::ethernet_interface(),
1470            &mut metadata,
1471        );
1472        assert_eq!(verdict, Verdict::Accept(()));
1473
1474        // The stashed reference should point to the connection that is in the table.
1475        let (stashed, _dir) =
1476            metadata.take_connection_and_direction().expect("metadata should include connection");
1477        let tuple = packet.conntrack_packet().expect("packet should be trackable").tuple();
1478        let table = core_ctx
1479            .conntrack()
1480            .get_connection(&tuple)
1481            .expect("packet should be inserted in table");
1482        assert_matches!(
1483            (table, stashed),
1484            (Connection::Shared(table), Connection::Shared(stashed)) => {
1485                assert!(Arc::ptr_eq(&table, &stashed));
1486            }
1487        );
1488
1489        // Provided with the connection, the INGRESS hook should reuse it rather than
1490        // creating a new one.
1491        let verdict = FilterImpl(&mut core_ctx).ingress_hook(
1492            &mut bindings_ctx,
1493            &mut packet,
1494            &FakeMatcherDeviceId::ethernet_interface(),
1495            &mut metadata,
1496        );
1497        assert_eq!(verdict, Verdict::Accept(()).into());
1498
1499        // As a result, rather than there being a new connection in the packet metadata,
1500        // it should contain the same connection that is still in the table.
1501        let (after_ingress, _dir) =
1502            metadata.take_connection_and_direction().expect("metadata should include connection");
1503        let table = core_ctx
1504            .conntrack()
1505            .get_connection(&tuple)
1506            .expect("packet should be inserted in table");
1507        assert_matches!(
1508            (table, after_ingress),
1509            (Connection::Shared(before), Connection::Shared(after)) => {
1510                assert!(Arc::ptr_eq(&before, &after));
1511            }
1512        );
1513    }
1514
1515    #[ip_test(I)]
1516    fn drop_packet_on_finalize_connection_failure<I: TestIpExt>() {
1517        let mut bindings_ctx = FakeBindingsCtx::new();
1518        let mut ctx = FakeCtx::new(&mut bindings_ctx);
1519
1520        for i in 0..u32::try_from(conntrack::MAXIMUM_ENTRIES / 2).unwrap() {
1521            let (mut packet, mut reply_packet) = conntrack::testutils::make_test_udp_packets(i);
1522            let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1523                &mut bindings_ctx,
1524                &mut packet,
1525                &FakeMatcherDeviceId::ethernet_interface(),
1526                &mut NullMetadata {},
1527            );
1528            assert_eq!(verdict, Verdict::Accept(()));
1529
1530            let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1531                &mut bindings_ctx,
1532                &mut reply_packet,
1533                &FakeMatcherDeviceId::ethernet_interface(),
1534                &mut NullMetadata {},
1535            );
1536            assert_eq!(verdict, Verdict::Accept(()));
1537
1538            let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1539                &mut bindings_ctx,
1540                &mut packet,
1541                &FakeMatcherDeviceId::ethernet_interface(),
1542                &mut NullMetadata {},
1543            );
1544            assert_eq!(verdict, Verdict::Accept(()));
1545        }
1546
1547        // Finalizing the connection should fail when the conntrack table is at maximum
1548        // capacity and there are no connections to remove, because all existing
1549        // connections are considered established.
1550        let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1551            &mut bindings_ctx,
1552            &mut FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value(),
1553            &FakeMatcherDeviceId::ethernet_interface(),
1554            &mut NullMetadata {},
1555        );
1556        assert_eq!(verdict, Verdict::Drop);
1557    }
1558
1559    #[ip_test(I)]
1560    fn implicit_snat_to_prevent_tuple_clash<I: TestIpExt>() {
1561        let mut bindings_ctx = FakeBindingsCtx::new();
1562        let mut ctx = FakeCtx::with_nat_routines_and_device_addrs(
1563            &mut bindings_ctx,
1564            NatRoutines {
1565                egress: Hook {
1566                    routines: vec![Routine {
1567                        rules: vec![Rule::new(
1568                            PacketMatcher {
1569                                src_address: Some(AddressMatcher {
1570                                    matcher: AddressMatcherType::Range(I::SRC_IP_2..=I::SRC_IP_2),
1571                                    invert: false,
1572                                }),
1573                                ..Default::default()
1574                            },
1575                            Action::Masquerade { src_port: None },
1576                        )],
1577                    }],
1578                },
1579                ..Default::default()
1580            },
1581            HashMap::from([(
1582                FakeMatcherDeviceId::ethernet_interface(),
1583                AddrSubnet::new(I::SRC_IP, I::SUBNET.prefix()).unwrap(),
1584            )]),
1585        );
1586
1587        // Simulate a forwarded packet, originally from I::SRC_IP_2, that is masqueraded
1588        // to be from I::SRC_IP. The packet should have had SNAT performed.
1589        let mut packet = FakeIpPacket {
1590            src_ip: I::SRC_IP_2,
1591            dst_ip: I::DST_IP,
1592            body: FakeUdpPacket::arbitrary_value(),
1593        };
1594        let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1595            &mut bindings_ctx,
1596            &mut packet,
1597            &FakeMatcherDeviceId::ethernet_interface(),
1598            &mut NullMetadata {},
1599        );
1600        assert_eq!(verdict, Verdict::Accept(()));
1601        assert_eq!(packet.src_ip, I::SRC_IP);
1602
1603        // Now simulate a locally-generated packet that conflicts with this flow; it is
1604        // from I::SRC_IP to I::DST_IP and has the same source and destination ports.
1605        // Finalizing the connection would typically fail, causing the packet to be
1606        // dropped, because the reply tuple conflicts with the reply tuple of the
1607        // masqueraded flow. So instead this new flow is implicitly SNATed to a free
1608        // port and the connection should be successfully finalized.
1609        let mut packet = FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value();
1610        let src_port = packet.body.src_port;
1611        let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1612            &mut bindings_ctx,
1613            &mut packet,
1614            &FakeMatcherDeviceId::ethernet_interface(),
1615            &mut NullMetadata {},
1616        );
1617        assert_eq!(verdict, Verdict::Accept(()));
1618        assert_ne!(packet.body.src_port, src_port);
1619    }
1620
1621    #[ip_test(I)]
1622    fn packet_adopts_tracked_connection_in_table_if_identical<I: TestIpExt>() {
1623        let mut bindings_ctx = FakeBindingsCtx::new();
1624        let mut core_ctx = FakeCtx::new(&mut bindings_ctx);
1625
1626        // Simulate a race where two packets in the same flow both end up
1627        // creating identical exclusive connections.
1628        let mut first_packet = FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value();
1629        let mut first_metadata = PacketMetadata::default();
1630        let verdict = FilterImpl(&mut core_ctx).local_egress_hook(
1631            &mut bindings_ctx,
1632            &mut first_packet,
1633            &FakeMatcherDeviceId::ethernet_interface(),
1634            &mut first_metadata,
1635        );
1636        assert_eq!(verdict, Verdict::Accept(()));
1637
1638        let mut second_packet = FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value();
1639        let mut second_metadata = PacketMetadata::default();
1640        let verdict = FilterImpl(&mut core_ctx).local_egress_hook(
1641            &mut bindings_ctx,
1642            &mut second_packet,
1643            &FakeMatcherDeviceId::ethernet_interface(),
1644            &mut second_metadata,
1645        );
1646        assert_eq!(verdict, Verdict::Accept(()));
1647
1648        // Finalize the first connection; it should get inserted in the table.
1649        let (verdict, _proof) = FilterImpl(&mut core_ctx).egress_hook(
1650            &mut bindings_ctx,
1651            &mut first_packet,
1652            &FakeMatcherDeviceId::ethernet_interface(),
1653            &mut first_metadata,
1654        );
1655        assert_eq!(verdict, Verdict::Accept(()));
1656
1657        // The second packet conflicts with the connection that's in the table, but it's
1658        // identical to the first one, so it should adopt the finalized connection.
1659        let (verdict, _proof) = FilterImpl(&mut core_ctx).egress_hook(
1660            &mut bindings_ctx,
1661            &mut second_packet,
1662            &FakeMatcherDeviceId::ethernet_interface(),
1663            &mut second_metadata,
1664        );
1665        assert_eq!(second_packet.body.src_port, first_packet.body.src_port);
1666        assert_eq!(verdict, Verdict::Accept(()));
1667
1668        let (first_conn, _dir) = first_metadata.take_connection_and_direction().unwrap();
1669        let (second_conn, _dir) = second_metadata.take_connection_and_direction().unwrap();
1670        assert_matches!(
1671            (first_conn, second_conn),
1672            (Connection::Shared(first), Connection::Shared(second)) => {
1673                assert!(Arc::ptr_eq(&first, &second));
1674            }
1675        );
1676    }
1677
1678    #[ip_test(I)]
1679    fn both_source_and_destination_nat_configured<I: TestIpExt>() {
1680        let mut bindings_ctx = FakeBindingsCtx::new();
1681        // Install NAT rules to perform both DNAT (in LOCAL_EGRESS) and SNAT (in
1682        // EGRESS).
1683        let mut core_ctx = FakeCtx::with_nat_routines_and_device_addrs(
1684            &mut bindings_ctx,
1685            NatRoutines {
1686                local_egress: Hook {
1687                    routines: vec![Routine {
1688                        rules: vec![Rule::new(
1689                            PacketMatcher::default(),
1690                            Action::Redirect { dst_port: None },
1691                        )],
1692                    }],
1693                },
1694                egress: Hook {
1695                    routines: vec![Routine {
1696                        rules: vec![Rule::new(
1697                            PacketMatcher::default(),
1698                            Action::Masquerade { src_port: None },
1699                        )],
1700                    }],
1701                },
1702                ..Default::default()
1703            },
1704            HashMap::from([(
1705                FakeMatcherDeviceId::ethernet_interface(),
1706                AddrSubnet::new(I::SRC_IP_2, I::SUBNET.prefix()).unwrap(),
1707            )]),
1708        );
1709
1710        // Even though the packet is modified after the first hook, where DNAT is
1711        // configured...
1712        let mut packet = FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value();
1713        let mut metadata = PacketMetadata::default();
1714        let verdict = FilterImpl(&mut core_ctx).local_egress_hook(
1715            &mut bindings_ctx,
1716            &mut packet,
1717            &FakeMatcherDeviceId::ethernet_interface(),
1718            &mut metadata,
1719        );
1720        assert_eq!(verdict, Verdict::Accept(()));
1721        assert_eq!(packet.dst_ip, *I::LOOPBACK_ADDRESS);
1722
1723        // ...SNAT is also successfully configured for the packet, because the packet's
1724        // [`ConnectionDirection`] is cached in the metadata.
1725        let (verdict, _proof) = FilterImpl(&mut core_ctx).egress_hook(
1726            &mut bindings_ctx,
1727            &mut packet,
1728            &FakeMatcherDeviceId::ethernet_interface(),
1729            &mut metadata,
1730        );
1731        assert_eq!(verdict, Verdict::Accept(()));
1732        assert_eq!(packet.src_ip, I::SRC_IP_2);
1733    }
1734
1735    #[ip_test(I)]
1736    #[test_case(
1737        Hook {
1738            routines: vec![
1739                Routine {
1740                    rules: vec![
1741                        Rule::new(
1742                            PacketMatcher::default(),
1743                            Action::Mark {
1744                                domain: MarkDomain::Mark1,
1745                                action: MarkAction::SetMark { clearing_mask: 0, mark: 1 },
1746                            },
1747                        ),
1748                        Rule::new(PacketMatcher::default(), Action::Drop),
1749                    ],
1750                },
1751            ],
1752        }; "non terminal for routine"
1753    )]
1754    #[test_case(
1755        Hook {
1756            routines: vec![
1757                Routine {
1758                    rules: vec![Rule::new(
1759                        PacketMatcher::default(),
1760                        Action::Mark {
1761                            domain: MarkDomain::Mark1,
1762                            action: MarkAction::SetMark { clearing_mask: 0, mark: 1 },
1763                        },
1764                    )],
1765                },
1766                Routine {
1767                    rules: vec![
1768                        Rule::new(PacketMatcher::default(), Action::Drop),
1769                    ],
1770                },
1771            ],
1772        }; "non terminal for hook"
1773    )]
1774    fn mark_action<I: TestIpExt>(ingress: Hook<I, FakeDeviceClass, ()>) {
1775        let mut metadata = PacketMetadata::<I, FakeWeakAddressId<I>, FakeBindingsCtx<I>>::default();
1776        assert_eq!(
1777            check_routines_for_ingress::<I, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1778                &ingress,
1779                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1780                Interfaces { ingress: None, egress: None },
1781                &mut metadata,
1782            ),
1783            IngressVerdict::Verdict(Verdict::Drop),
1784        );
1785        assert_eq!(metadata.marks, Marks::new([(MarkDomain::Mark1, 1)]));
1786    }
1787
1788    #[ip_test(I)]
1789    fn mark_action_applied_in_succession<I: TestIpExt>() {
1790        fn hook_with_single_mark_action<I: TestIpExt>(
1791            domain: MarkDomain,
1792            action: MarkAction,
1793        ) -> Hook<I, FakeDeviceClass, ()> {
1794            Hook {
1795                routines: vec![Routine {
1796                    rules: vec![Rule::new(
1797                        PacketMatcher::default(),
1798                        Action::Mark { domain, action },
1799                    )],
1800                }],
1801            }
1802        }
1803        let mut metadata = PacketMetadata::<I, FakeWeakAddressId<I>, FakeBindingsCtx<I>>::default();
1804        assert_eq!(
1805            check_routines_for_ingress::<I, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1806                &hook_with_single_mark_action(
1807                    MarkDomain::Mark1,
1808                    MarkAction::SetMark { clearing_mask: 0, mark: 1 }
1809                ),
1810                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1811                Interfaces { ingress: None, egress: None },
1812                &mut metadata,
1813            ),
1814            IngressVerdict::Verdict(Verdict::Accept(())),
1815        );
1816        assert_eq!(metadata.marks, Marks::new([(MarkDomain::Mark1, 1)]));
1817
1818        assert_eq!(
1819            check_routines_for_hook(
1820                &hook_with_single_mark_action::<I>(
1821                    MarkDomain::Mark2,
1822                    MarkAction::SetMark { clearing_mask: 0, mark: 1 }
1823                ),
1824                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1825                Interfaces::<FakeMatcherDeviceId> { ingress: None, egress: None },
1826                &mut metadata,
1827            ),
1828            Verdict::Accept(())
1829        );
1830        assert_eq!(metadata.marks, Marks::new([(MarkDomain::Mark1, 1), (MarkDomain::Mark2, 1)]));
1831
1832        assert_eq!(
1833            check_routines_for_hook(
1834                &hook_with_single_mark_action::<I>(
1835                    MarkDomain::Mark1,
1836                    MarkAction::SetMark { clearing_mask: 1, mark: 2 }
1837                ),
1838                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1839                Interfaces::<FakeMatcherDeviceId> { ingress: None, egress: None },
1840                &mut metadata,
1841            ),
1842            Verdict::Accept(())
1843        );
1844        assert_eq!(metadata.marks, Marks::new([(MarkDomain::Mark1, 2), (MarkDomain::Mark2, 1)]));
1845    }
1846}