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