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::{
831        AddressMatcher, AddressMatcherType, AssignedAddrIpExt, InterfaceMatcher, MarkDomain, Marks,
832        PortMatcher, SegmentHeader,
833    };
834    use netstack3_hashmap::HashMap;
835    use test_case::test_case;
836
837    use super::*;
838    use crate::actions::MarkAction;
839    use crate::conntrack::{self, ConnectionDirection};
840    use crate::context::testutil::{FakeBindingsCtx, FakeCtx, FakeWeakAddressId};
841    use crate::logic::nat::NatConfig;
842    use crate::matchers::{PacketMatcher, TransportProtocolMatcher};
843    use crate::packets::testutil::internal::{
844        ArbitraryValue, FakeIpPacket, FakeTcpSegment, FakeUdpPacket, TransportPacketExt,
845    };
846    use crate::state::{IpRoutines, NatRoutines, UninstalledRoutine};
847    use crate::testutil::TestIpExt;
848
849    impl<I: IpExt> Rule<I, FakeDeviceClass, ()> {
850        pub(crate) fn new(
851            matcher: PacketMatcher<I, FakeDeviceClass>,
852            action: Action<I, FakeDeviceClass, ()>,
853        ) -> Self {
854            Rule { matcher, action, validation_info: () }
855        }
856    }
857
858    #[test]
859    fn return_by_default_if_no_matching_rules_in_routine() {
860        assert_eq!(
861            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
862                &Routine { rules: Vec::new() },
863                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
864                &Interfaces { ingress: None, egress: None },
865                &mut NullMetadata {},
866            ),
867            RoutineResult::Return
868        );
869
870        // A subroutine should also yield `Return` if no rules match, allowing
871        // the calling routine to continue execution after the `Jump`.
872        let routine = Routine {
873            rules: vec![
874                Rule::new(
875                    PacketMatcher::default(),
876                    Action::Jump(UninstalledRoutine::new(Vec::new(), 0)),
877                ),
878                Rule::new(PacketMatcher::default(), Action::Drop),
879            ],
880        };
881        assert_eq!(
882            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
883                &routine,
884                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
885                &Interfaces { ingress: None, egress: None },
886                &mut NullMetadata {},
887            ),
888            RoutineResult::Drop
889        );
890    }
891
892    struct NullMetadata {}
893
894    impl<I: IpExt, A, BT: FilterBindingsTypes> FilterIpMetadata<I, A, BT> for NullMetadata {
895        fn take_connection_and_direction(
896            &mut self,
897        ) -> Option<(Connection<I, NatConfig<I, A>, BT>, ConnectionDirection)> {
898            None
899        }
900
901        fn replace_connection_and_direction(
902            &mut self,
903            _conn: Connection<I, NatConfig<I, A>, BT>,
904            _direction: ConnectionDirection,
905        ) -> Option<Connection<I, NatConfig<I, A>, BT>> {
906            None
907        }
908    }
909
910    impl FilterMarkMetadata for NullMetadata {
911        fn apply_mark_action(&mut self, _domain: MarkDomain, _action: MarkAction) {}
912    }
913
914    #[derive(Derivative)]
915    #[derivative(Default(bound = ""))]
916    struct PacketMetadata<I: IpExt + AssignedAddrIpExt, A, BT: FilterBindingsTypes> {
917        conn: Option<(Connection<I, NatConfig<I, A>, BT>, ConnectionDirection)>,
918        marks: Marks,
919    }
920
921    impl<I: TestIpExt, A, BT: FilterBindingsTypes> FilterIpMetadata<I, A, BT>
922        for PacketMetadata<I, A, BT>
923    {
924        fn take_connection_and_direction(
925            &mut self,
926        ) -> Option<(Connection<I, NatConfig<I, A>, BT>, ConnectionDirection)> {
927            let Self { conn, marks: _ } = self;
928            conn.take()
929        }
930
931        fn replace_connection_and_direction(
932            &mut self,
933            new_conn: Connection<I, NatConfig<I, A>, BT>,
934            direction: ConnectionDirection,
935        ) -> Option<Connection<I, NatConfig<I, A>, BT>> {
936            let Self { conn, marks: _ } = self;
937            conn.replace((new_conn, direction)).map(|(conn, _dir)| conn)
938        }
939    }
940
941    impl<I: TestIpExt, A, BT: FilterBindingsTypes> FilterMarkMetadata for PacketMetadata<I, A, BT> {
942        fn apply_mark_action(&mut self, domain: MarkDomain, action: MarkAction) {
943            action.apply(self.marks.get_mut(domain))
944        }
945    }
946
947    #[test]
948    fn accept_by_default_if_no_matching_rules_in_hook() {
949        assert_eq!(
950            check_routines_for_hook::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
951                &Hook::default(),
952                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
953                Interfaces { ingress: None, egress: None },
954                &mut NullMetadata {},
955            ),
956            Verdict::Accept(())
957        );
958    }
959
960    #[test]
961    fn accept_by_default_if_return_from_routine() {
962        let hook = Hook {
963            routines: vec![Routine {
964                rules: vec![Rule::new(PacketMatcher::default(), Action::Return)],
965            }],
966        };
967
968        assert_eq!(
969            check_routines_for_hook::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
970                &hook,
971                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
972                Interfaces { ingress: None, egress: None },
973                &mut NullMetadata {},
974            ),
975            Verdict::Accept(())
976        );
977    }
978
979    #[test]
980    fn accept_terminal_for_installed_routine() {
981        let routine = Routine {
982            rules: vec![
983                // Accept all traffic.
984                Rule::new(PacketMatcher::default(), Action::Accept),
985                // Drop all traffic.
986                Rule::new(PacketMatcher::default(), Action::Drop),
987            ],
988        };
989        assert_eq!(
990            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
991                &routine,
992                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
993                &Interfaces { ingress: None, egress: None },
994                &mut NullMetadata {},
995            ),
996            RoutineResult::Accept
997        );
998
999        // `Accept` should also be propagated from subroutines.
1000        let routine = Routine {
1001            rules: vec![
1002                // Jump to a routine that accepts all traffic.
1003                Rule::new(
1004                    PacketMatcher::default(),
1005                    Action::Jump(UninstalledRoutine::new(
1006                        vec![Rule::new(PacketMatcher::default(), Action::Accept)],
1007                        0,
1008                    )),
1009                ),
1010                // Drop all traffic.
1011                Rule::new(PacketMatcher::default(), Action::Drop),
1012            ],
1013        };
1014        assert_eq!(
1015            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1016                &routine,
1017                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1018                &Interfaces { ingress: None, egress: None },
1019                &mut NullMetadata {},
1020            ),
1021            RoutineResult::Accept
1022        );
1023
1024        // Now put that routine in a hook that also includes *another* installed
1025        // routine which drops all traffic. The first installed routine should
1026        // terminate at its `Accept` result, but the hook should terminate at
1027        // the `Drop` result in the second routine.
1028        let hook = Hook {
1029            routines: vec![
1030                routine,
1031                Routine {
1032                    rules: vec![
1033                        // Drop all traffic.
1034                        Rule::new(PacketMatcher::default(), Action::Drop),
1035                    ],
1036                },
1037            ],
1038        };
1039
1040        assert_eq!(
1041            check_routines_for_hook::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1042                &hook,
1043                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1044                Interfaces { ingress: None, egress: None },
1045                &mut NullMetadata {},
1046            ),
1047            Verdict::Drop
1048        );
1049    }
1050
1051    #[test]
1052    fn drop_terminal_for_entire_hook() {
1053        let hook = Hook {
1054            routines: vec![
1055                Routine {
1056                    rules: vec![
1057                        // Drop all traffic.
1058                        Rule::new(PacketMatcher::default(), Action::Drop),
1059                    ],
1060                },
1061                Routine {
1062                    rules: vec![
1063                        // Accept all traffic.
1064                        Rule::new(PacketMatcher::default(), Action::Accept),
1065                    ],
1066                },
1067            ],
1068        };
1069
1070        assert_eq!(
1071            check_routines_for_hook::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1072                &hook,
1073                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1074                Interfaces { ingress: None, egress: None },
1075                &mut NullMetadata {},
1076            ),
1077            Verdict::Drop
1078        );
1079    }
1080
1081    #[test]
1082    fn transparent_proxy_terminal_for_entire_hook() {
1083        const TPROXY_PORT: NonZeroU16 = NonZeroU16::new(8080).unwrap();
1084
1085        let ingress = Hook {
1086            routines: vec![
1087                Routine {
1088                    rules: vec![Rule::new(
1089                        PacketMatcher::default(),
1090                        Action::TransparentProxy(TransparentProxy::LocalPort(TPROXY_PORT)),
1091                    )],
1092                },
1093                Routine {
1094                    rules: vec![
1095                        // Accept all traffic.
1096                        Rule::new(PacketMatcher::default(), Action::Accept),
1097                    ],
1098                },
1099            ],
1100        };
1101
1102        assert_eq!(
1103            check_routines_for_ingress::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1104                &ingress,
1105                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1106                Interfaces { ingress: None, egress: None },
1107                &mut NullMetadata {},
1108            ),
1109            IngressVerdict::TransparentLocalDelivery {
1110                addr: <Ipv4 as crate::packets::testutil::internal::TestIpExt>::DST_IP,
1111                port: TPROXY_PORT
1112            }
1113        );
1114    }
1115
1116    #[test]
1117    fn jump_recursively_evaluates_target_routine() {
1118        // Drop result from a target routine is propagated to the calling
1119        // routine.
1120        let routine = Routine {
1121            rules: vec![Rule::new(
1122                PacketMatcher::default(),
1123                Action::Jump(UninstalledRoutine::new(
1124                    vec![Rule::new(PacketMatcher::default(), Action::Drop)],
1125                    0,
1126                )),
1127            )],
1128        };
1129        assert_eq!(
1130            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1131                &routine,
1132                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1133                &Interfaces { ingress: None, egress: None },
1134                &mut NullMetadata {},
1135            ),
1136            RoutineResult::Drop
1137        );
1138
1139        // Accept result from a target routine is also propagated to the calling
1140        // routine.
1141        let routine = Routine {
1142            rules: vec![
1143                Rule::new(
1144                    PacketMatcher::default(),
1145                    Action::Jump(UninstalledRoutine::new(
1146                        vec![Rule::new(PacketMatcher::default(), Action::Accept)],
1147                        0,
1148                    )),
1149                ),
1150                Rule::new(PacketMatcher::default(), Action::Drop),
1151            ],
1152        };
1153        assert_eq!(
1154            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1155                &routine,
1156                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1157                &Interfaces { ingress: None, egress: None },
1158                &mut NullMetadata {},
1159            ),
1160            RoutineResult::Accept
1161        );
1162
1163        // Return from a target routine results in continued evaluation of the
1164        // calling routine.
1165        let routine = Routine {
1166            rules: vec![
1167                Rule::new(
1168                    PacketMatcher::default(),
1169                    Action::Jump(UninstalledRoutine::new(
1170                        vec![Rule::new(PacketMatcher::default(), Action::Return)],
1171                        0,
1172                    )),
1173                ),
1174                Rule::new(PacketMatcher::default(), Action::Drop),
1175            ],
1176        };
1177        assert_eq!(
1178            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1179                &routine,
1180                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1181                &Interfaces { ingress: None, egress: None },
1182                &mut NullMetadata {},
1183            ),
1184            RoutineResult::Drop
1185        );
1186    }
1187
1188    #[test]
1189    fn return_terminal_for_single_routine() {
1190        let routine = Routine {
1191            rules: vec![
1192                Rule::new(PacketMatcher::default(), Action::Return),
1193                // Drop all traffic.
1194                Rule::new(PacketMatcher::default(), Action::Drop),
1195            ],
1196        };
1197
1198        assert_eq!(
1199            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1200                &routine,
1201                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1202                &Interfaces { ingress: None, egress: None },
1203                &mut NullMetadata {},
1204            ),
1205            RoutineResult::Return
1206        );
1207    }
1208
1209    #[ip_test(I)]
1210    fn filter_handler_implements_ip_hooks_correctly<I: TestIpExt>() {
1211        fn drop_all_traffic<I: TestIpExt>(
1212            matcher: PacketMatcher<I, FakeDeviceClass>,
1213        ) -> Hook<I, FakeDeviceClass, ()> {
1214            Hook { routines: vec![Routine { rules: vec![Rule::new(matcher, Action::Drop)] }] }
1215        }
1216
1217        let mut bindings_ctx = FakeBindingsCtx::new();
1218
1219        // Ingress hook should use ingress routines and check the input
1220        // interface.
1221        let mut ctx = FakeCtx::with_ip_routines(
1222            &mut bindings_ctx,
1223            IpRoutines {
1224                ingress: drop_all_traffic(PacketMatcher {
1225                    in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1226                    ..Default::default()
1227                }),
1228                ..Default::default()
1229            },
1230        );
1231        assert_eq!(
1232            FilterImpl(&mut ctx).ingress_hook(
1233                &mut bindings_ctx,
1234                &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1235                &FakeMatcherDeviceId::wlan_interface(),
1236                &mut NullMetadata {},
1237            ),
1238            Verdict::Drop.into()
1239        );
1240
1241        // Local ingress hook should use local ingress routines and check the
1242        // input interface.
1243        let mut ctx = FakeCtx::with_ip_routines(
1244            &mut bindings_ctx,
1245            IpRoutines {
1246                local_ingress: drop_all_traffic(PacketMatcher {
1247                    in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1248                    ..Default::default()
1249                }),
1250                ..Default::default()
1251            },
1252        );
1253        assert_eq!(
1254            FilterImpl(&mut ctx).local_ingress_hook(
1255                &mut bindings_ctx,
1256                &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1257                &FakeMatcherDeviceId::wlan_interface(),
1258                &mut NullMetadata {},
1259            ),
1260            Verdict::Drop
1261        );
1262
1263        // Forwarding hook should use forwarding routines and check both the
1264        // input and output interfaces.
1265        let mut ctx = FakeCtx::with_ip_routines(
1266            &mut bindings_ctx,
1267            IpRoutines {
1268                forwarding: drop_all_traffic(PacketMatcher {
1269                    in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1270                    out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
1271                    ..Default::default()
1272                }),
1273                ..Default::default()
1274            },
1275        );
1276        assert_eq!(
1277            FilterImpl(&mut ctx).forwarding_hook(
1278                &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1279                &FakeMatcherDeviceId::wlan_interface(),
1280                &FakeMatcherDeviceId::ethernet_interface(),
1281                &mut NullMetadata {},
1282            ),
1283            Verdict::Drop
1284        );
1285
1286        // Local egress hook should use local egress routines and check the
1287        // output interface.
1288        let mut ctx = FakeCtx::with_ip_routines(
1289            &mut bindings_ctx,
1290            IpRoutines {
1291                local_egress: drop_all_traffic(PacketMatcher {
1292                    out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1293                    ..Default::default()
1294                }),
1295                ..Default::default()
1296            },
1297        );
1298        assert_eq!(
1299            FilterImpl(&mut ctx).local_egress_hook(
1300                &mut bindings_ctx,
1301                &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1302                &FakeMatcherDeviceId::wlan_interface(),
1303                &mut NullMetadata {},
1304            ),
1305            Verdict::Drop
1306        );
1307
1308        // Egress hook should use egress routines and check the output
1309        // interface.
1310        let mut ctx = FakeCtx::with_ip_routines(
1311            &mut bindings_ctx,
1312            IpRoutines {
1313                egress: drop_all_traffic(PacketMatcher {
1314                    out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1315                    ..Default::default()
1316                }),
1317                ..Default::default()
1318            },
1319        );
1320        assert_eq!(
1321            FilterImpl(&mut ctx)
1322                .egress_hook(
1323                    &mut bindings_ctx,
1324                    &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1325                    &FakeMatcherDeviceId::wlan_interface(),
1326                    &mut NullMetadata {},
1327                )
1328                .0,
1329            Verdict::Drop
1330        );
1331    }
1332
1333    #[ip_test(I)]
1334    #[test_case(22 => Verdict::Accept(()); "port 22 allowed for SSH")]
1335    #[test_case(80 => Verdict::Accept(()); "port 80 allowed for HTTP")]
1336    #[test_case(1024 => Verdict::Accept(()); "ephemeral port 1024 allowed")]
1337    #[test_case(65535 => Verdict::Accept(()); "ephemeral port 65535 allowed")]
1338    #[test_case(1023 => Verdict::Drop; "privileged port 1023 blocked")]
1339    #[test_case(53 => Verdict::Drop; "privileged port 53 blocked")]
1340    fn block_privileged_ports_except_ssh_http<I: TestIpExt>(port: u16) -> Verdict {
1341        fn tcp_port_rule<I: FilterIpExt>(
1342            src_port: Option<PortMatcher>,
1343            dst_port: Option<PortMatcher>,
1344            action: Action<I, FakeDeviceClass, ()>,
1345        ) -> Rule<I, FakeDeviceClass, ()> {
1346            Rule::new(
1347                PacketMatcher {
1348                    transport_protocol: Some(TransportProtocolMatcher {
1349                        proto: <&FakeTcpSegment as TransportPacketExt<I>>::proto().unwrap(),
1350                        src_port,
1351                        dst_port,
1352                    }),
1353                    ..Default::default()
1354                },
1355                action,
1356            )
1357        }
1358
1359        fn default_filter_rules<I: FilterIpExt>() -> Routine<I, FakeDeviceClass, ()> {
1360            Routine {
1361                rules: vec![
1362                    // pass in proto tcp to port 22;
1363                    tcp_port_rule(
1364                        /* src_port */ None,
1365                        Some(PortMatcher { range: 22..=22, invert: false }),
1366                        Action::Accept,
1367                    ),
1368                    // pass in proto tcp to port 80;
1369                    tcp_port_rule(
1370                        /* src_port */ None,
1371                        Some(PortMatcher { range: 80..=80, invert: false }),
1372                        Action::Accept,
1373                    ),
1374                    // pass in proto tcp to range 1024:65535;
1375                    tcp_port_rule(
1376                        /* src_port */ None,
1377                        Some(PortMatcher { range: 1024..=65535, invert: false }),
1378                        Action::Accept,
1379                    ),
1380                    // drop in proto tcp to range 1:6553;
1381                    tcp_port_rule(
1382                        /* src_port */ None,
1383                        Some(PortMatcher { range: 1..=65535, invert: false }),
1384                        Action::Drop,
1385                    ),
1386                ],
1387            }
1388        }
1389
1390        let mut bindings_ctx = FakeBindingsCtx::new();
1391
1392        let mut ctx = FakeCtx::with_ip_routines(
1393            &mut bindings_ctx,
1394            IpRoutines {
1395                local_ingress: Hook { routines: vec![default_filter_rules()] },
1396                ..Default::default()
1397            },
1398        );
1399
1400        FilterImpl(&mut ctx).local_ingress_hook(
1401            &mut bindings_ctx,
1402            &mut FakeIpPacket::<I, _> {
1403                body: FakeTcpSegment {
1404                    dst_port: port,
1405                    src_port: 11111,
1406                    segment: SegmentHeader::arbitrary_value(),
1407                    payload_len: 8888,
1408                },
1409                ..ArbitraryValue::arbitrary_value()
1410            },
1411            &FakeMatcherDeviceId::wlan_interface(),
1412            &mut NullMetadata {},
1413        )
1414    }
1415
1416    #[ip_test(I)]
1417    #[test_case(
1418        FakeMatcherDeviceId::ethernet_interface() => Verdict::Accept(());
1419        "allow incoming traffic on ethernet interface"
1420    )]
1421    #[test_case(
1422        FakeMatcherDeviceId::wlan_interface() => Verdict::Drop;
1423        "drop incoming traffic on wlan interface"
1424    )]
1425    fn filter_on_wlan_only<I: TestIpExt>(interface: FakeMatcherDeviceId) -> Verdict {
1426        fn drop_wlan_traffic<I: IpExt>() -> Routine<I, FakeDeviceClass, ()> {
1427            Routine {
1428                rules: vec![Rule::new(
1429                    PacketMatcher {
1430                        in_interface: Some(InterfaceMatcher::Id(
1431                            FakeMatcherDeviceId::wlan_interface().id,
1432                        )),
1433                        ..Default::default()
1434                    },
1435                    Action::Drop,
1436                )],
1437            }
1438        }
1439
1440        let mut bindings_ctx = FakeBindingsCtx::new();
1441
1442        let mut ctx = FakeCtx::with_ip_routines(
1443            &mut bindings_ctx,
1444            IpRoutines {
1445                local_ingress: Hook { routines: vec![drop_wlan_traffic()] },
1446                ..Default::default()
1447            },
1448        );
1449
1450        FilterImpl(&mut ctx).local_ingress_hook(
1451            &mut bindings_ctx,
1452            &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1453            &interface,
1454            &mut NullMetadata {},
1455        )
1456    }
1457
1458    #[test]
1459    fn ingress_reuses_cached_connection_when_available() {
1460        let mut bindings_ctx = FakeBindingsCtx::new();
1461        let mut core_ctx = FakeCtx::new(&mut bindings_ctx);
1462
1463        // When a connection is finalized in the EGRESS hook, it should stash a shared
1464        // reference to the connection in the packet metadata.
1465        let mut packet = FakeIpPacket::<Ipv4, FakeUdpPacket>::arbitrary_value();
1466        let mut metadata = PacketMetadata::default();
1467        let (verdict, _proof) = FilterImpl(&mut core_ctx).egress_hook(
1468            &mut bindings_ctx,
1469            &mut packet,
1470            &FakeMatcherDeviceId::ethernet_interface(),
1471            &mut metadata,
1472        );
1473        assert_eq!(verdict, Verdict::Accept(()));
1474
1475        // The stashed reference should point to the connection that is in the table.
1476        let (stashed, _dir) =
1477            metadata.take_connection_and_direction().expect("metadata should include connection");
1478        let tuple = packet.conntrack_packet().expect("packet should be trackable").tuple();
1479        let table = core_ctx
1480            .conntrack()
1481            .get_connection(&tuple)
1482            .expect("packet should be inserted in table");
1483        assert_matches!(
1484            (table, stashed),
1485            (Connection::Shared(table), Connection::Shared(stashed)) => {
1486                assert!(Arc::ptr_eq(&table, &stashed));
1487            }
1488        );
1489
1490        // Provided with the connection, the INGRESS hook should reuse it rather than
1491        // creating a new one.
1492        let verdict = FilterImpl(&mut core_ctx).ingress_hook(
1493            &mut bindings_ctx,
1494            &mut packet,
1495            &FakeMatcherDeviceId::ethernet_interface(),
1496            &mut metadata,
1497        );
1498        assert_eq!(verdict, Verdict::Accept(()).into());
1499
1500        // As a result, rather than there being a new connection in the packet metadata,
1501        // it should contain the same connection that is still in the table.
1502        let (after_ingress, _dir) =
1503            metadata.take_connection_and_direction().expect("metadata should include connection");
1504        let table = core_ctx
1505            .conntrack()
1506            .get_connection(&tuple)
1507            .expect("packet should be inserted in table");
1508        assert_matches!(
1509            (table, after_ingress),
1510            (Connection::Shared(before), Connection::Shared(after)) => {
1511                assert!(Arc::ptr_eq(&before, &after));
1512            }
1513        );
1514    }
1515
1516    #[ip_test(I)]
1517    fn drop_packet_on_finalize_connection_failure<I: TestIpExt>() {
1518        let mut bindings_ctx = FakeBindingsCtx::new();
1519        let mut ctx = FakeCtx::new(&mut bindings_ctx);
1520
1521        for i in 0..u32::try_from(conntrack::MAXIMUM_ENTRIES / 2).unwrap() {
1522            let (mut packet, mut reply_packet) = conntrack::testutils::make_test_udp_packets(i);
1523            let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1524                &mut bindings_ctx,
1525                &mut packet,
1526                &FakeMatcherDeviceId::ethernet_interface(),
1527                &mut NullMetadata {},
1528            );
1529            assert_eq!(verdict, Verdict::Accept(()));
1530
1531            let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1532                &mut bindings_ctx,
1533                &mut reply_packet,
1534                &FakeMatcherDeviceId::ethernet_interface(),
1535                &mut NullMetadata {},
1536            );
1537            assert_eq!(verdict, Verdict::Accept(()));
1538
1539            let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1540                &mut bindings_ctx,
1541                &mut packet,
1542                &FakeMatcherDeviceId::ethernet_interface(),
1543                &mut NullMetadata {},
1544            );
1545            assert_eq!(verdict, Verdict::Accept(()));
1546        }
1547
1548        // Finalizing the connection should fail when the conntrack table is at maximum
1549        // capacity and there are no connections to remove, because all existing
1550        // connections are considered established.
1551        let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1552            &mut bindings_ctx,
1553            &mut FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value(),
1554            &FakeMatcherDeviceId::ethernet_interface(),
1555            &mut NullMetadata {},
1556        );
1557        assert_eq!(verdict, Verdict::Drop);
1558    }
1559
1560    #[ip_test(I)]
1561    fn implicit_snat_to_prevent_tuple_clash<I: TestIpExt>() {
1562        let mut bindings_ctx = FakeBindingsCtx::new();
1563        let mut ctx = FakeCtx::with_nat_routines_and_device_addrs(
1564            &mut bindings_ctx,
1565            NatRoutines {
1566                egress: Hook {
1567                    routines: vec![Routine {
1568                        rules: vec![Rule::new(
1569                            PacketMatcher {
1570                                src_address: Some(AddressMatcher {
1571                                    matcher: AddressMatcherType::Range(I::SRC_IP_2..=I::SRC_IP_2),
1572                                    invert: false,
1573                                }),
1574                                ..Default::default()
1575                            },
1576                            Action::Masquerade { src_port: None },
1577                        )],
1578                    }],
1579                },
1580                ..Default::default()
1581            },
1582            HashMap::from([(
1583                FakeMatcherDeviceId::ethernet_interface(),
1584                AddrSubnet::new(I::SRC_IP, I::SUBNET.prefix()).unwrap(),
1585            )]),
1586        );
1587
1588        // Simulate a forwarded packet, originally from I::SRC_IP_2, that is masqueraded
1589        // to be from I::SRC_IP. The packet should have had SNAT performed.
1590        let mut packet = FakeIpPacket {
1591            src_ip: I::SRC_IP_2,
1592            dst_ip: I::DST_IP,
1593            body: FakeUdpPacket::arbitrary_value(),
1594        };
1595        let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1596            &mut bindings_ctx,
1597            &mut packet,
1598            &FakeMatcherDeviceId::ethernet_interface(),
1599            &mut NullMetadata {},
1600        );
1601        assert_eq!(verdict, Verdict::Accept(()));
1602        assert_eq!(packet.src_ip, I::SRC_IP);
1603
1604        // Now simulate a locally-generated packet that conflicts with this flow; it is
1605        // from I::SRC_IP to I::DST_IP and has the same source and destination ports.
1606        // Finalizing the connection would typically fail, causing the packet to be
1607        // dropped, because the reply tuple conflicts with the reply tuple of the
1608        // masqueraded flow. So instead this new flow is implicitly SNATed to a free
1609        // port and the connection should be successfully finalized.
1610        let mut packet = FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value();
1611        let src_port = packet.body.src_port;
1612        let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1613            &mut bindings_ctx,
1614            &mut packet,
1615            &FakeMatcherDeviceId::ethernet_interface(),
1616            &mut NullMetadata {},
1617        );
1618        assert_eq!(verdict, Verdict::Accept(()));
1619        assert_ne!(packet.body.src_port, src_port);
1620    }
1621
1622    #[ip_test(I)]
1623    fn packet_adopts_tracked_connection_in_table_if_identical<I: TestIpExt>() {
1624        let mut bindings_ctx = FakeBindingsCtx::new();
1625        let mut core_ctx = FakeCtx::new(&mut bindings_ctx);
1626
1627        // Simulate a race where two packets in the same flow both end up
1628        // creating identical exclusive connections.
1629        let mut first_packet = FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value();
1630        let mut first_metadata = PacketMetadata::default();
1631        let verdict = FilterImpl(&mut core_ctx).local_egress_hook(
1632            &mut bindings_ctx,
1633            &mut first_packet,
1634            &FakeMatcherDeviceId::ethernet_interface(),
1635            &mut first_metadata,
1636        );
1637        assert_eq!(verdict, Verdict::Accept(()));
1638
1639        let mut second_packet = FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value();
1640        let mut second_metadata = PacketMetadata::default();
1641        let verdict = FilterImpl(&mut core_ctx).local_egress_hook(
1642            &mut bindings_ctx,
1643            &mut second_packet,
1644            &FakeMatcherDeviceId::ethernet_interface(),
1645            &mut second_metadata,
1646        );
1647        assert_eq!(verdict, Verdict::Accept(()));
1648
1649        // Finalize the first connection; it should get inserted in the table.
1650        let (verdict, _proof) = FilterImpl(&mut core_ctx).egress_hook(
1651            &mut bindings_ctx,
1652            &mut first_packet,
1653            &FakeMatcherDeviceId::ethernet_interface(),
1654            &mut first_metadata,
1655        );
1656        assert_eq!(verdict, Verdict::Accept(()));
1657
1658        // The second packet conflicts with the connection that's in the table, but it's
1659        // identical to the first one, so it should adopt the finalized connection.
1660        let (verdict, _proof) = FilterImpl(&mut core_ctx).egress_hook(
1661            &mut bindings_ctx,
1662            &mut second_packet,
1663            &FakeMatcherDeviceId::ethernet_interface(),
1664            &mut second_metadata,
1665        );
1666        assert_eq!(second_packet.body.src_port, first_packet.body.src_port);
1667        assert_eq!(verdict, Verdict::Accept(()));
1668
1669        let (first_conn, _dir) = first_metadata.take_connection_and_direction().unwrap();
1670        let (second_conn, _dir) = second_metadata.take_connection_and_direction().unwrap();
1671        assert_matches!(
1672            (first_conn, second_conn),
1673            (Connection::Shared(first), Connection::Shared(second)) => {
1674                assert!(Arc::ptr_eq(&first, &second));
1675            }
1676        );
1677    }
1678
1679    #[ip_test(I)]
1680    fn both_source_and_destination_nat_configured<I: TestIpExt>() {
1681        let mut bindings_ctx = FakeBindingsCtx::new();
1682        // Install NAT rules to perform both DNAT (in LOCAL_EGRESS) and SNAT (in
1683        // EGRESS).
1684        let mut core_ctx = FakeCtx::with_nat_routines_and_device_addrs(
1685            &mut bindings_ctx,
1686            NatRoutines {
1687                local_egress: Hook {
1688                    routines: vec![Routine {
1689                        rules: vec![Rule::new(
1690                            PacketMatcher::default(),
1691                            Action::Redirect { dst_port: None },
1692                        )],
1693                    }],
1694                },
1695                egress: Hook {
1696                    routines: vec![Routine {
1697                        rules: vec![Rule::new(
1698                            PacketMatcher::default(),
1699                            Action::Masquerade { src_port: None },
1700                        )],
1701                    }],
1702                },
1703                ..Default::default()
1704            },
1705            HashMap::from([(
1706                FakeMatcherDeviceId::ethernet_interface(),
1707                AddrSubnet::new(I::SRC_IP_2, I::SUBNET.prefix()).unwrap(),
1708            )]),
1709        );
1710
1711        // Even though the packet is modified after the first hook, where DNAT is
1712        // configured...
1713        let mut packet = FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value();
1714        let mut metadata = PacketMetadata::default();
1715        let verdict = FilterImpl(&mut core_ctx).local_egress_hook(
1716            &mut bindings_ctx,
1717            &mut packet,
1718            &FakeMatcherDeviceId::ethernet_interface(),
1719            &mut metadata,
1720        );
1721        assert_eq!(verdict, Verdict::Accept(()));
1722        assert_eq!(packet.dst_ip, *I::LOOPBACK_ADDRESS);
1723
1724        // ...SNAT is also successfully configured for the packet, because the packet's
1725        // [`ConnectionDirection`] is cached in the metadata.
1726        let (verdict, _proof) = FilterImpl(&mut core_ctx).egress_hook(
1727            &mut bindings_ctx,
1728            &mut packet,
1729            &FakeMatcherDeviceId::ethernet_interface(),
1730            &mut metadata,
1731        );
1732        assert_eq!(verdict, Verdict::Accept(()));
1733        assert_eq!(packet.src_ip, I::SRC_IP_2);
1734    }
1735
1736    #[ip_test(I)]
1737    #[test_case(
1738        Hook {
1739            routines: vec![
1740                Routine {
1741                    rules: vec![
1742                        Rule::new(
1743                            PacketMatcher::default(),
1744                            Action::Mark {
1745                                domain: MarkDomain::Mark1,
1746                                action: MarkAction::SetMark { clearing_mask: 0, mark: 1 },
1747                            },
1748                        ),
1749                        Rule::new(PacketMatcher::default(), Action::Drop),
1750                    ],
1751                },
1752            ],
1753        }; "non terminal for routine"
1754    )]
1755    #[test_case(
1756        Hook {
1757            routines: vec![
1758                Routine {
1759                    rules: vec![Rule::new(
1760                        PacketMatcher::default(),
1761                        Action::Mark {
1762                            domain: MarkDomain::Mark1,
1763                            action: MarkAction::SetMark { clearing_mask: 0, mark: 1 },
1764                        },
1765                    )],
1766                },
1767                Routine {
1768                    rules: vec![
1769                        Rule::new(PacketMatcher::default(), Action::Drop),
1770                    ],
1771                },
1772            ],
1773        }; "non terminal for hook"
1774    )]
1775    fn mark_action<I: TestIpExt>(ingress: Hook<I, FakeDeviceClass, ()>) {
1776        let mut metadata = PacketMetadata::<I, FakeWeakAddressId<I>, FakeBindingsCtx<I>>::default();
1777        assert_eq!(
1778            check_routines_for_ingress::<I, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1779                &ingress,
1780                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1781                Interfaces { ingress: None, egress: None },
1782                &mut metadata,
1783            ),
1784            IngressVerdict::Verdict(Verdict::Drop),
1785        );
1786        assert_eq!(metadata.marks, Marks::new([(MarkDomain::Mark1, 1)]));
1787    }
1788
1789    #[ip_test(I)]
1790    fn mark_action_applied_in_succession<I: TestIpExt>() {
1791        fn hook_with_single_mark_action<I: TestIpExt>(
1792            domain: MarkDomain,
1793            action: MarkAction,
1794        ) -> Hook<I, FakeDeviceClass, ()> {
1795            Hook {
1796                routines: vec![Routine {
1797                    rules: vec![Rule::new(
1798                        PacketMatcher::default(),
1799                        Action::Mark { domain, action },
1800                    )],
1801                }],
1802            }
1803        }
1804        let mut metadata = PacketMetadata::<I, FakeWeakAddressId<I>, FakeBindingsCtx<I>>::default();
1805        assert_eq!(
1806            check_routines_for_ingress::<I, _, FakeMatcherDeviceId, FakeDeviceClass, _>(
1807                &hook_with_single_mark_action(
1808                    MarkDomain::Mark1,
1809                    MarkAction::SetMark { clearing_mask: 0, mark: 1 }
1810                ),
1811                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1812                Interfaces { ingress: None, egress: None },
1813                &mut metadata,
1814            ),
1815            IngressVerdict::Verdict(Verdict::Accept(())),
1816        );
1817        assert_eq!(metadata.marks, Marks::new([(MarkDomain::Mark1, 1)]));
1818
1819        assert_eq!(
1820            check_routines_for_hook(
1821                &hook_with_single_mark_action::<I>(
1822                    MarkDomain::Mark2,
1823                    MarkAction::SetMark { clearing_mask: 0, mark: 1 }
1824                ),
1825                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1826                Interfaces::<FakeMatcherDeviceId> { ingress: None, egress: None },
1827                &mut metadata,
1828            ),
1829            Verdict::Accept(())
1830        );
1831        assert_eq!(metadata.marks, Marks::new([(MarkDomain::Mark1, 1), (MarkDomain::Mark2, 1)]));
1832
1833        assert_eq!(
1834            check_routines_for_hook(
1835                &hook_with_single_mark_action::<I>(
1836                    MarkDomain::Mark1,
1837                    MarkAction::SetMark { clearing_mask: 1, mark: 2 }
1838                ),
1839                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1840                Interfaces::<FakeMatcherDeviceId> { ingress: None, egress: None },
1841                &mut metadata,
1842            ),
1843            Verdict::Accept(())
1844        );
1845        assert_eq!(metadata.marks, Marks::new([(MarkDomain::Mark1, 2), (MarkDomain::Mark2, 1)]));
1846    }
1847}