Skip to main content

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