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