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