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_or_id()) 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        fn packet_type(&self) -> netstack3_base::PacketType {
1026            netstack3_base::PacketType::Host
1027        }
1028
1029        fn gso_info(&self) -> Option<netstack3_base::GsoInfo> {
1030            None
1031        }
1032    }
1033
1034    #[test]
1035    fn accept_by_default_if_no_matching_rules_in_hook() {
1036        assert_eq!(
1037            check_routines_for_hook::<
1038                Ipv4,
1039                _,
1040                FakeMatcherDeviceId,
1041                FakeBindingsCtx<Ipv4>,
1042                _,
1043                DropPacket,
1044            >(
1045                &Hook::default(),
1046                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1047                Interfaces { ingress: None, egress: None },
1048                &mut FakePacketMetadata::default(),
1049            ),
1050            Verdict::Proceed(Accept)
1051        );
1052    }
1053
1054    #[test]
1055    fn accept_by_default_if_return_from_routine() {
1056        let hook = Hook {
1057            routines: vec![Routine {
1058                rules: vec![Rule::new(PacketMatcher::default(), Action::Return)],
1059            }],
1060        };
1061
1062        assert_eq!(
1063            check_routines_for_hook::<
1064                Ipv4,
1065                _,
1066                FakeMatcherDeviceId,
1067                FakeBindingsCtx<Ipv4>,
1068                _,
1069                DropPacket,
1070            >(
1071                &hook,
1072                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1073                Interfaces { ingress: None, egress: None },
1074                &mut FakePacketMetadata::default(),
1075            ),
1076            Verdict::Proceed(Accept)
1077        );
1078    }
1079
1080    #[test]
1081    fn accept_terminal_for_installed_routine() {
1082        let routine = Routine {
1083            rules: vec![
1084                // Accept all traffic.
1085                Rule::new(PacketMatcher::default(), Action::Accept),
1086                // Drop all traffic.
1087                Rule::new(PacketMatcher::default(), Action::Drop),
1088            ],
1089        };
1090        assert_eq!(
1091            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeBindingsCtx<Ipv4>, _>(
1092                &routine,
1093                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1094                Interfaces { ingress: None, egress: None },
1095                &mut FakePacketMetadata::default(),
1096            ),
1097            RoutineResult::Accept
1098        );
1099
1100        // `Accept` should also be propagated from subroutines.
1101        let routine = Routine {
1102            rules: vec![
1103                // Jump to a routine that accepts all traffic.
1104                Rule::new(
1105                    PacketMatcher::default(),
1106                    Action::Jump(UninstalledRoutine::new(
1107                        vec![Rule::new(PacketMatcher::default(), Action::Accept)],
1108                        0,
1109                    )),
1110                ),
1111                // Drop all traffic.
1112                Rule::new(PacketMatcher::default(), Action::Drop),
1113            ],
1114        };
1115        assert_eq!(
1116            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeBindingsCtx<Ipv4>, _>(
1117                &routine,
1118                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1119                Interfaces { ingress: None, egress: None },
1120                &mut FakePacketMetadata::default(),
1121            ),
1122            RoutineResult::Accept
1123        );
1124
1125        // Now put that routine in a hook that also includes *another* installed
1126        // routine which drops all traffic. The first installed routine should
1127        // terminate at its `Accept` result, but the hook should terminate at
1128        // the `Drop` result in the second routine.
1129        let hook = Hook {
1130            routines: vec![
1131                routine,
1132                Routine {
1133                    rules: vec![
1134                        // Drop all traffic.
1135                        Rule::new(PacketMatcher::default(), Action::Drop),
1136                    ],
1137                },
1138            ],
1139        };
1140
1141        assert_eq!(
1142            check_routines_for_hook::<Ipv4, _, FakeMatcherDeviceId, FakeBindingsCtx<Ipv4>, _, _>(
1143                &hook,
1144                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1145                Interfaces { ingress: None, egress: None },
1146                &mut FakePacketMetadata::default(),
1147            ),
1148            Verdict::Stop(DropPacket)
1149        );
1150    }
1151
1152    #[test]
1153    fn drop_terminal_for_entire_hook() {
1154        let hook = Hook {
1155            routines: vec![
1156                Routine {
1157                    rules: vec![
1158                        // Drop all traffic.
1159                        Rule::new(PacketMatcher::default(), Action::Drop),
1160                    ],
1161                },
1162                Routine {
1163                    rules: vec![
1164                        // Accept all traffic.
1165                        Rule::new(PacketMatcher::default(), Action::Accept),
1166                    ],
1167                },
1168            ],
1169        };
1170
1171        assert_eq!(
1172            check_routines_for_hook::<
1173                Ipv4,
1174                _,
1175                FakeMatcherDeviceId,
1176                FakeBindingsCtx<Ipv4>,
1177                _,
1178                DropPacket,
1179            >(
1180                &hook,
1181                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1182                Interfaces { ingress: None, egress: None },
1183                &mut FakePacketMetadata::default(),
1184            ),
1185            Verdict::Stop(DropPacket)
1186        );
1187    }
1188
1189    #[test]
1190    fn transparent_proxy_terminal_for_entire_hook() {
1191        const TPROXY_PORT: NonZeroU16 = NonZeroU16::new(8080).unwrap();
1192
1193        let ingress = Hook {
1194            routines: vec![
1195                Routine {
1196                    rules: vec![Rule::new(
1197                        PacketMatcher::default(),
1198                        Action::TransparentProxy(TransparentProxy::LocalPort(TPROXY_PORT)),
1199                    )],
1200                },
1201                Routine {
1202                    rules: vec![
1203                        // Accept all traffic.
1204                        Rule::new(PacketMatcher::default(), Action::Accept),
1205                    ],
1206                },
1207            ],
1208        };
1209
1210        assert_eq!(
1211            check_routines_for_hook::<Ipv4, _, FakeMatcherDeviceId, FakeBindingsCtx<Ipv4>, _, _>(
1212                &ingress,
1213                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1214                Interfaces { ingress: None, egress: None },
1215                &mut FakePacketMetadata::default(),
1216            ),
1217            IngressVerdict::Stop(IngressStopReason::TransparentLocalDelivery {
1218                addr: <Ipv4 as crate::packets::testutil::internal::TestIpExt>::DST_IP,
1219                port: TPROXY_PORT
1220            })
1221        );
1222    }
1223
1224    #[test]
1225    fn jump_recursively_evaluates_target_routine() {
1226        // Drop result from a target routine is propagated to the calling
1227        // routine.
1228        let routine = Routine {
1229            rules: vec![Rule::new(
1230                PacketMatcher::default(),
1231                Action::Jump(UninstalledRoutine::new(
1232                    vec![Rule::new(PacketMatcher::default(), Action::Drop)],
1233                    0,
1234                )),
1235            )],
1236        };
1237        assert_eq!(
1238            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeBindingsCtx<Ipv4>, _>(
1239                &routine,
1240                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1241                Interfaces { ingress: None, egress: None },
1242                &mut FakePacketMetadata::default(),
1243            ),
1244            RoutineResult::Drop
1245        );
1246
1247        // Accept result from a target routine is also propagated to the calling
1248        // routine.
1249        let routine = Routine {
1250            rules: vec![
1251                Rule::new(
1252                    PacketMatcher::default(),
1253                    Action::Jump(UninstalledRoutine::new(
1254                        vec![Rule::new(PacketMatcher::default(), Action::Accept)],
1255                        0,
1256                    )),
1257                ),
1258                Rule::new(PacketMatcher::default(), Action::Drop),
1259            ],
1260        };
1261        assert_eq!(
1262            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeBindingsCtx<Ipv4>, _>(
1263                &routine,
1264                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1265                Interfaces { ingress: None, egress: None },
1266                &mut FakePacketMetadata::default(),
1267            ),
1268            RoutineResult::Accept
1269        );
1270
1271        // Return from a target routine results in continued evaluation of the
1272        // calling routine.
1273        let routine = Routine {
1274            rules: vec![
1275                Rule::new(
1276                    PacketMatcher::default(),
1277                    Action::Jump(UninstalledRoutine::new(
1278                        vec![Rule::new(PacketMatcher::default(), Action::Return)],
1279                        0,
1280                    )),
1281                ),
1282                Rule::new(PacketMatcher::default(), Action::Drop),
1283            ],
1284        };
1285        assert_eq!(
1286            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeBindingsCtx<Ipv4>, _>(
1287                &routine,
1288                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1289                Interfaces { ingress: None, egress: None },
1290                &mut FakePacketMetadata::default(),
1291            ),
1292            RoutineResult::Drop
1293        );
1294    }
1295
1296    #[test]
1297    fn return_terminal_for_single_routine() {
1298        let routine = Routine {
1299            rules: vec![
1300                Rule::new(PacketMatcher::default(), Action::Return),
1301                // Drop all traffic.
1302                Rule::new(PacketMatcher::default(), Action::Drop),
1303            ],
1304        };
1305
1306        assert_eq!(
1307            check_routine::<Ipv4, _, FakeMatcherDeviceId, FakeBindingsCtx<Ipv4>, _>(
1308                &routine,
1309                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1310                Interfaces { ingress: None, egress: None },
1311                &mut FakePacketMetadata::default(),
1312            ),
1313            RoutineResult::Return
1314        );
1315    }
1316
1317    #[ip_test(I)]
1318    fn filter_handler_implements_ip_hooks_correctly<I: TestIpExt>() {
1319        fn drop_all_traffic<I: TestIpExt>(
1320            matcher: PacketMatcher<I, FakeBindingsCtx<I>>,
1321        ) -> Hook<I, FakeBindingsCtx<I>, ()> {
1322            Hook { routines: vec![Routine { rules: vec![Rule::new(matcher, Action::Drop)] }] }
1323        }
1324
1325        let mut bindings_ctx = FakeBindingsCtx::new();
1326
1327        // Ingress hook should use ingress routines and check the input
1328        // interface.
1329        let mut ctx = FakeCtx::with_ip_routines(
1330            &mut bindings_ctx,
1331            IpRoutines {
1332                ingress: drop_all_traffic(PacketMatcher {
1333                    in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1334                    ..Default::default()
1335                }),
1336                ..Default::default()
1337            },
1338        );
1339        assert_eq!(
1340            FilterImpl(&mut ctx).ingress_hook(
1341                &mut bindings_ctx,
1342                &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1343                &FakeMatcherDeviceId::wlan_interface(),
1344                &mut FakePacketMetadata::default(),
1345            ),
1346            Verdict::Stop(IngressStopReason::Drop)
1347        );
1348
1349        // Local ingress hook should use local ingress routines and check the
1350        // input interface.
1351        let mut ctx = FakeCtx::with_ip_routines(
1352            &mut bindings_ctx,
1353            IpRoutines {
1354                local_ingress: drop_all_traffic(PacketMatcher {
1355                    in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1356                    ..Default::default()
1357                }),
1358                ..Default::default()
1359            },
1360        );
1361        assert_eq!(
1362            FilterImpl(&mut ctx).local_ingress_hook(
1363                &mut bindings_ctx,
1364                &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1365                &FakeMatcherDeviceId::wlan_interface(),
1366                &mut FakePacketMetadata::default(),
1367            ),
1368            Verdict::Stop(DropOrReject::Drop)
1369        );
1370
1371        // Forwarding hook should use forwarding routines and check both the
1372        // input and output interfaces.
1373        let mut ctx = FakeCtx::with_ip_routines(
1374            &mut bindings_ctx,
1375            IpRoutines {
1376                forwarding: drop_all_traffic(PacketMatcher {
1377                    in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1378                    out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
1379                    ..Default::default()
1380                }),
1381                ..Default::default()
1382            },
1383        );
1384        assert_eq!(
1385            FilterImpl(&mut ctx).forwarding_hook(
1386                &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1387                &FakeMatcherDeviceId::wlan_interface(),
1388                &FakeMatcherDeviceId::ethernet_interface(),
1389                &mut FakePacketMetadata::default(),
1390            ),
1391            Verdict::Stop(DropOrReject::Drop)
1392        );
1393
1394        // Local egress hook should use local egress routines and check the
1395        // output interface.
1396        let mut ctx = FakeCtx::with_ip_routines(
1397            &mut bindings_ctx,
1398            IpRoutines {
1399                local_egress: drop_all_traffic(PacketMatcher {
1400                    out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1401                    ..Default::default()
1402                }),
1403                ..Default::default()
1404            },
1405        );
1406        assert_eq!(
1407            FilterImpl(&mut ctx).local_egress_hook(
1408                &mut bindings_ctx,
1409                &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1410                &FakeMatcherDeviceId::wlan_interface(),
1411                &mut FakePacketMetadata::default(),
1412            ),
1413            Verdict::Stop(DropOrReject::Drop)
1414        );
1415
1416        // Egress hook should use egress routines and check the output
1417        // interface.
1418        let mut ctx = FakeCtx::with_ip_routines(
1419            &mut bindings_ctx,
1420            IpRoutines {
1421                egress: drop_all_traffic(PacketMatcher {
1422                    out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Wlan)),
1423                    ..Default::default()
1424                }),
1425                ..Default::default()
1426            },
1427        );
1428        assert_eq!(
1429            FilterImpl(&mut ctx)
1430                .egress_hook(
1431                    &mut bindings_ctx,
1432                    &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1433                    &FakeMatcherDeviceId::wlan_interface(),
1434                    &mut FakePacketMetadata::default(),
1435                )
1436                .0,
1437            Verdict::Stop(DropPacket)
1438        );
1439    }
1440
1441    #[ip_test(I)]
1442    #[test_case(22 => Verdict::Proceed(Accept); "port 22 allowed for SSH")]
1443    #[test_case(80 => Verdict::Proceed(Accept); "port 80 allowed for HTTP")]
1444    #[test_case(1024 => Verdict::Proceed(Accept); "ephemeral port 1024 allowed")]
1445    #[test_case(65535 => Verdict::Proceed(Accept); "ephemeral port 65535 allowed")]
1446    #[test_case(1023 => Verdict::Stop(DropOrReject::Drop); "privileged port 1023 blocked")]
1447    #[test_case(53 => Verdict::Stop(DropOrReject::Drop); "privileged port 53 blocked")]
1448    fn block_privileged_ports_except_ssh_http<I: TestIpExt>(port: u16) -> Verdict<DropOrReject> {
1449        fn tcp_port_rule<I: FilterIpExt>(
1450            src_port: Option<PortMatcher>,
1451            dst_port: Option<PortMatcher>,
1452            action: Action<I, FakeBindingsCtx<I>, ()>,
1453        ) -> Rule<I, FakeBindingsCtx<I>, ()> {
1454            Rule::new(
1455                PacketMatcher {
1456                    transport_protocol: Some(TransportProtocolMatcher {
1457                        proto: <&FakeTcpSegment as TransportPacketExt<I>>::proto().unwrap(),
1458                        src_port,
1459                        dst_port,
1460                    }),
1461                    ..Default::default()
1462                },
1463                action,
1464            )
1465        }
1466
1467        fn default_filter_rules<I: FilterIpExt>() -> Routine<I, FakeBindingsCtx<I>, ()> {
1468            Routine {
1469                rules: vec![
1470                    // pass in proto tcp to port 22;
1471                    tcp_port_rule(
1472                        /* src_port */ None,
1473                        Some(PortMatcher { range: 22..=22, invert: false }),
1474                        Action::Accept,
1475                    ),
1476                    // pass in proto tcp to port 80;
1477                    tcp_port_rule(
1478                        /* src_port */ None,
1479                        Some(PortMatcher { range: 80..=80, invert: false }),
1480                        Action::Accept,
1481                    ),
1482                    // pass in proto tcp to range 1024:65535;
1483                    tcp_port_rule(
1484                        /* src_port */ None,
1485                        Some(PortMatcher { range: 1024..=65535, invert: false }),
1486                        Action::Accept,
1487                    ),
1488                    // drop in proto tcp to range 1:6553;
1489                    tcp_port_rule(
1490                        /* src_port */ None,
1491                        Some(PortMatcher { range: 1..=65535, invert: false }),
1492                        Action::Drop,
1493                    ),
1494                ],
1495            }
1496        }
1497
1498        let mut bindings_ctx = FakeBindingsCtx::new();
1499
1500        let mut ctx = FakeCtx::with_ip_routines(
1501            &mut bindings_ctx,
1502            IpRoutines {
1503                local_ingress: Hook { routines: vec![default_filter_rules()] },
1504                ..Default::default()
1505            },
1506        );
1507
1508        FilterImpl(&mut ctx).local_ingress_hook(
1509            &mut bindings_ctx,
1510            &mut FakeIpPacket::<I, _> {
1511                body: FakeTcpSegment {
1512                    dst_port: port,
1513                    src_port: 11111,
1514                    segment: SegmentHeader::arbitrary_value(),
1515                    payload_len: 8888,
1516                },
1517                ..ArbitraryValue::arbitrary_value()
1518            },
1519            &FakeMatcherDeviceId::wlan_interface(),
1520            &mut FakePacketMetadata::default(),
1521        )
1522    }
1523
1524    #[ip_test(I)]
1525    #[test_case(
1526        FakeMatcherDeviceId::ethernet_interface() => Verdict::Proceed(Accept);
1527        "allow incoming traffic on ethernet interface"
1528    )]
1529    #[test_case(
1530        FakeMatcherDeviceId::wlan_interface() => Verdict::Stop(DropOrReject::Drop);
1531        "drop incoming traffic on wlan interface"
1532    )]
1533    fn filter_on_wlan_only<I: TestIpExt>(interface: FakeMatcherDeviceId) -> Verdict<DropOrReject> {
1534        fn drop_wlan_traffic<I: IpExt>() -> Routine<I, FakeBindingsCtx<I>, ()> {
1535            Routine {
1536                rules: vec![Rule::new(
1537                    PacketMatcher {
1538                        in_interface: Some(InterfaceMatcher::Id(
1539                            FakeMatcherDeviceId::wlan_interface().id,
1540                        )),
1541                        ..Default::default()
1542                    },
1543                    Action::Drop,
1544                )],
1545            }
1546        }
1547
1548        let mut bindings_ctx = FakeBindingsCtx::new();
1549
1550        let mut ctx = FakeCtx::with_ip_routines(
1551            &mut bindings_ctx,
1552            IpRoutines {
1553                local_ingress: Hook { routines: vec![drop_wlan_traffic()] },
1554                ..Default::default()
1555            },
1556        );
1557
1558        FilterImpl(&mut ctx).local_ingress_hook(
1559            &mut bindings_ctx,
1560            &mut FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value(),
1561            &interface,
1562            &mut FakePacketMetadata::default(),
1563        )
1564    }
1565
1566    #[test]
1567    fn ingress_reuses_cached_connection_when_available() {
1568        let mut bindings_ctx = FakeBindingsCtx::new();
1569        let mut core_ctx = FakeCtx::new(&mut bindings_ctx);
1570
1571        // When a connection is finalized in the EGRESS hook, it should stash a shared
1572        // reference to the connection in the packet metadata.
1573        let mut packet = FakeIpPacket::<Ipv4, FakeUdpPacket>::arbitrary_value();
1574        let mut metadata = PacketMetadata::default();
1575        let (verdict, _proof) = FilterImpl(&mut core_ctx).egress_hook(
1576            &mut bindings_ctx,
1577            &mut packet,
1578            &FakeMatcherDeviceId::ethernet_interface(),
1579            &mut metadata,
1580        );
1581        assert_eq!(verdict, Verdict::Proceed(Accept));
1582
1583        // The stashed reference should point to the connection that is in the table.
1584        let (stashed, _dir) =
1585            metadata.take_connection_and_direction().expect("metadata should include connection");
1586        let tuple = packet.conntrack_packet().expect("packet should be trackable").tuple();
1587        let table = core_ctx
1588            .conntrack()
1589            .get_connection(&tuple)
1590            .expect("packet should be inserted in table");
1591        assert_matches!(
1592            (table, stashed),
1593            (Connection::Shared(table), Connection::Shared(stashed)) => {
1594                assert!(Arc::ptr_eq(&table, &stashed));
1595            }
1596        );
1597
1598        // Provided with the connection, the INGRESS hook should reuse it rather than
1599        // creating a new one.
1600        let verdict = FilterImpl(&mut core_ctx).ingress_hook(
1601            &mut bindings_ctx,
1602            &mut packet,
1603            &FakeMatcherDeviceId::ethernet_interface(),
1604            &mut metadata,
1605        );
1606        assert_eq!(verdict, Verdict::Proceed(Accept));
1607
1608        // As a result, rather than there being a new connection in the packet metadata,
1609        // it should contain the same connection that is still in the table.
1610        let (after_ingress, _dir) =
1611            metadata.take_connection_and_direction().expect("metadata should include connection");
1612        let table = core_ctx
1613            .conntrack()
1614            .get_connection(&tuple)
1615            .expect("packet should be inserted in table");
1616        assert_matches!(
1617            (table, after_ingress),
1618            (Connection::Shared(before), Connection::Shared(after)) => {
1619                assert!(Arc::ptr_eq(&before, &after));
1620            }
1621        );
1622    }
1623
1624    #[ip_test(I)]
1625    fn drop_packet_on_finalize_connection_failure<I: TestIpExt>() {
1626        let mut bindings_ctx = FakeBindingsCtx::new();
1627        let mut ctx = FakeCtx::new(&mut bindings_ctx);
1628
1629        for i in 0..u32::try_from(conntrack::MAXIMUM_ENTRIES / 2).unwrap() {
1630            let (mut packet, mut reply_packet) = conntrack::testutils::make_test_udp_packets(i);
1631            let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1632                &mut bindings_ctx,
1633                &mut 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 reply_packet,
1642                &FakeMatcherDeviceId::ethernet_interface(),
1643                &mut FakePacketMetadata::default(),
1644            );
1645            assert_eq!(verdict, Verdict::Proceed(Accept));
1646
1647            let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1648                &mut bindings_ctx,
1649                &mut packet,
1650                &FakeMatcherDeviceId::ethernet_interface(),
1651                &mut FakePacketMetadata::default(),
1652            );
1653            assert_eq!(verdict, Verdict::Proceed(Accept));
1654        }
1655
1656        // Finalizing the connection should fail when the conntrack table is at maximum
1657        // capacity and there are no connections to remove, because all existing
1658        // connections are considered established.
1659        let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1660            &mut bindings_ctx,
1661            &mut FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value(),
1662            &FakeMatcherDeviceId::ethernet_interface(),
1663            &mut FakePacketMetadata::default(),
1664        );
1665        assert_eq!(verdict, Verdict::Stop(DropPacket));
1666    }
1667
1668    #[ip_test(I)]
1669    fn implicit_snat_to_prevent_tuple_clash<I: TestIpExt>() {
1670        let mut bindings_ctx = FakeBindingsCtx::new();
1671        let mut ctx = FakeCtx::with_nat_routines_and_device_addrs(
1672            &mut bindings_ctx,
1673            NatRoutines {
1674                egress: Hook {
1675                    routines: vec![Routine {
1676                        rules: vec![Rule::new(
1677                            PacketMatcher {
1678                                src_address: Some(AddressMatcher {
1679                                    matcher: AddressMatcherType::Range(I::SRC_IP_2..=I::SRC_IP_2),
1680                                    invert: false,
1681                                }),
1682                                ..Default::default()
1683                            },
1684                            Action::Masquerade { src_port: None },
1685                        )],
1686                    }],
1687                },
1688                ..Default::default()
1689            },
1690            [(
1691                FakeMatcherDeviceId::ethernet_interface(),
1692                AddrSubnet::new(I::SRC_IP, I::SUBNET.prefix()).unwrap(),
1693            )],
1694        );
1695
1696        // Simulate a forwarded packet, originally from I::SRC_IP_2, that is masqueraded
1697        // to be from I::SRC_IP. The packet should have had SNAT performed.
1698        let mut packet = FakeIpPacket {
1699            src_ip: I::SRC_IP_2,
1700            dst_ip: I::DST_IP,
1701            body: FakeUdpPacket::arbitrary_value(),
1702        };
1703        let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1704            &mut bindings_ctx,
1705            &mut packet,
1706            &FakeMatcherDeviceId::ethernet_interface(),
1707            &mut FakePacketMetadata::default(),
1708        );
1709        assert_eq!(verdict, Verdict::Proceed(Accept));
1710        assert_eq!(packet.src_ip, I::SRC_IP);
1711
1712        // Now simulate a locally-generated packet that conflicts with this flow; it is
1713        // from I::SRC_IP to I::DST_IP and has the same source and destination ports.
1714        // Finalizing the connection would typically fail, causing the packet to be
1715        // dropped, because the reply tuple conflicts with the reply tuple of the
1716        // masqueraded flow. So instead this new flow is implicitly SNATed to a free
1717        // port and the connection should be successfully finalized.
1718        let mut packet = FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value();
1719        let src_port = packet.body.src_port;
1720        let (verdict, _proof) = FilterImpl(&mut ctx).egress_hook(
1721            &mut bindings_ctx,
1722            &mut packet,
1723            &FakeMatcherDeviceId::ethernet_interface(),
1724            &mut FakePacketMetadata::default(),
1725        );
1726        assert_eq!(verdict, Verdict::Proceed(Accept));
1727        assert_ne!(packet.body.src_port, src_port);
1728    }
1729
1730    #[ip_test(I)]
1731    fn packet_adopts_tracked_connection_in_table_if_identical<I: TestIpExt>() {
1732        let mut bindings_ctx = FakeBindingsCtx::new();
1733        let mut core_ctx = FakeCtx::new(&mut bindings_ctx);
1734
1735        // Simulate a race where two packets in the same flow both end up
1736        // creating identical exclusive connections.
1737        let mut first_packet = FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value();
1738        let mut first_metadata = PacketMetadata::default();
1739        let verdict = FilterImpl(&mut core_ctx).local_egress_hook(
1740            &mut bindings_ctx,
1741            &mut first_packet,
1742            &FakeMatcherDeviceId::ethernet_interface(),
1743            &mut first_metadata,
1744        );
1745        assert_eq!(verdict, Verdict::Proceed(Accept));
1746
1747        let mut second_packet = FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value();
1748        let mut second_metadata = PacketMetadata::default();
1749        let verdict = FilterImpl(&mut core_ctx).local_egress_hook(
1750            &mut bindings_ctx,
1751            &mut second_packet,
1752            &FakeMatcherDeviceId::ethernet_interface(),
1753            &mut second_metadata,
1754        );
1755        assert_eq!(verdict, Verdict::Proceed(Accept));
1756
1757        // Finalize the first connection; it should get inserted in the table.
1758        let (verdict, _proof) = FilterImpl(&mut core_ctx).egress_hook(
1759            &mut bindings_ctx,
1760            &mut first_packet,
1761            &FakeMatcherDeviceId::ethernet_interface(),
1762            &mut first_metadata,
1763        );
1764        assert_eq!(verdict, Verdict::Proceed(Accept));
1765
1766        // The second packet conflicts with the connection that's in the table, but it's
1767        // identical to the first one, so it should adopt the finalized connection.
1768        let (verdict, _proof) = FilterImpl(&mut core_ctx).egress_hook(
1769            &mut bindings_ctx,
1770            &mut second_packet,
1771            &FakeMatcherDeviceId::ethernet_interface(),
1772            &mut second_metadata,
1773        );
1774        assert_eq!(second_packet.body.src_port, first_packet.body.src_port);
1775        assert_eq!(verdict, Verdict::Proceed(Accept));
1776
1777        let (first_conn, _dir) = first_metadata.take_connection_and_direction().unwrap();
1778        let (second_conn, _dir) = second_metadata.take_connection_and_direction().unwrap();
1779        assert_matches!(
1780            (first_conn, second_conn),
1781            (Connection::Shared(first), Connection::Shared(second)) => {
1782                assert!(Arc::ptr_eq(&first, &second));
1783            }
1784        );
1785    }
1786
1787    #[ip_test(I)]
1788    fn both_source_and_destination_nat_configured<I: TestIpExt>() {
1789        let mut bindings_ctx = FakeBindingsCtx::new();
1790        // Install NAT rules to perform both DNAT (in LOCAL_EGRESS) and SNAT (in
1791        // EGRESS).
1792        let mut core_ctx = FakeCtx::with_nat_routines_and_device_addrs(
1793            &mut bindings_ctx,
1794            NatRoutines {
1795                local_egress: Hook {
1796                    routines: vec![Routine {
1797                        rules: vec![Rule::new(
1798                            PacketMatcher::default(),
1799                            Action::Redirect { dst_port: None },
1800                        )],
1801                    }],
1802                },
1803                egress: Hook {
1804                    routines: vec![Routine {
1805                        rules: vec![Rule::new(
1806                            PacketMatcher::default(),
1807                            Action::Masquerade { src_port: None },
1808                        )],
1809                    }],
1810                },
1811                ..Default::default()
1812            },
1813            [(
1814                FakeMatcherDeviceId::ethernet_interface(),
1815                AddrSubnet::new(I::SRC_IP_2, I::SUBNET.prefix()).unwrap(),
1816            )],
1817        );
1818
1819        // Even though the packet is modified after the first hook, where DNAT is
1820        // configured...
1821        let mut packet = FakeIpPacket::<I, FakeUdpPacket>::arbitrary_value();
1822        let mut metadata = PacketMetadata::default();
1823        let verdict = FilterImpl(&mut core_ctx).local_egress_hook(
1824            &mut bindings_ctx,
1825            &mut packet,
1826            &FakeMatcherDeviceId::ethernet_interface(),
1827            &mut metadata,
1828        );
1829        assert_eq!(verdict, Verdict::Proceed(Accept));
1830        assert_eq!(packet.dst_ip, *I::LOOPBACK_ADDRESS);
1831
1832        // ...SNAT is also successfully configured for the packet, because the packet's
1833        // [`ConnectionDirection`] is cached in the metadata.
1834        let (verdict, _proof) = FilterImpl(&mut core_ctx).egress_hook(
1835            &mut bindings_ctx,
1836            &mut packet,
1837            &FakeMatcherDeviceId::ethernet_interface(),
1838            &mut metadata,
1839        );
1840        assert_eq!(verdict, Verdict::Proceed(Accept));
1841        assert_eq!(packet.src_ip, I::SRC_IP_2);
1842    }
1843
1844    #[ip_test(I)]
1845    #[test_case(
1846        Hook {
1847            routines: vec![
1848                Routine {
1849                    rules: vec![
1850                        Rule::new(
1851                            PacketMatcher::default(),
1852                            Action::Mark {
1853                                domain: MarkDomain::Mark1,
1854                                action: MarkAction::SetMark { clearing_mask: 0, mark: 1 },
1855                            },
1856                        ),
1857                        Rule::new(PacketMatcher::default(), Action::Drop),
1858                    ],
1859                },
1860            ],
1861        }; "non terminal for routine"
1862    )]
1863    #[test_case(
1864        Hook {
1865            routines: vec![
1866                Routine {
1867                    rules: vec![Rule::new(
1868                        PacketMatcher::default(),
1869                        Action::Mark {
1870                            domain: MarkDomain::Mark1,
1871                            action: MarkAction::SetMark { clearing_mask: 0, mark: 1 },
1872                        },
1873                    )],
1874                },
1875                Routine {
1876                    rules: vec![
1877                        Rule::new(PacketMatcher::default(), Action::Drop),
1878                    ],
1879                },
1880            ],
1881        }; "non terminal for hook"
1882    )]
1883    fn mark_action<I: TestIpExt>(ingress: Hook<I, FakeBindingsCtx<I>, ()>) {
1884        let mut metadata = PacketMetadata::<I, FakeWeakAddressId<I>, FakeBindingsCtx<I>>::default();
1885        assert_eq!(
1886            check_routines_for_hook::<I, _, FakeMatcherDeviceId, FakeBindingsCtx<I>, _, _>(
1887                &ingress,
1888                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1889                Interfaces { ingress: None, egress: None },
1890                &mut metadata,
1891            ),
1892            IngressVerdict::Stop(IngressStopReason::Drop),
1893        );
1894        assert_eq!(metadata.marks, Marks::new([(MarkDomain::Mark1, 1)]));
1895    }
1896
1897    #[ip_test(I)]
1898    fn mark_action_applied_in_succession<I: TestIpExt>() {
1899        fn hook_with_single_mark_action<I: TestIpExt>(
1900            domain: MarkDomain,
1901            action: MarkAction,
1902        ) -> Hook<I, FakeBindingsCtx<I>, ()> {
1903            Hook {
1904                routines: vec![Routine {
1905                    rules: vec![Rule::new(
1906                        PacketMatcher::default(),
1907                        Action::Mark { domain, action },
1908                    )],
1909                }],
1910            }
1911        }
1912        let mut metadata = PacketMetadata::<I, FakeWeakAddressId<I>, FakeBindingsCtx<I>>::default();
1913        assert_eq!(
1914            check_routines_for_hook::<I, _, FakeMatcherDeviceId, FakeBindingsCtx<I>, _, _>(
1915                &hook_with_single_mark_action(
1916                    MarkDomain::Mark1,
1917                    MarkAction::SetMark { clearing_mask: 0, mark: 1 }
1918                ),
1919                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1920                Interfaces { ingress: None, egress: None },
1921                &mut metadata,
1922            ),
1923            IngressVerdict::Proceed(Accept),
1924        );
1925        assert_eq!(metadata.marks, Marks::new([(MarkDomain::Mark1, 1)]));
1926
1927        assert_eq!(
1928            check_routines_for_hook(
1929                &hook_with_single_mark_action::<I>(
1930                    MarkDomain::Mark2,
1931                    MarkAction::SetMark { clearing_mask: 0, mark: 1 }
1932                ),
1933                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1934                Interfaces::<FakeMatcherDeviceId> { ingress: None, egress: None },
1935                &mut metadata,
1936            ),
1937            IngressVerdict::Proceed(Accept)
1938        );
1939        assert_eq!(metadata.marks, Marks::new([(MarkDomain::Mark1, 1), (MarkDomain::Mark2, 1)]));
1940
1941        assert_eq!(
1942            check_routines_for_hook(
1943                &hook_with_single_mark_action::<I>(
1944                    MarkDomain::Mark1,
1945                    MarkAction::SetMark { clearing_mask: 1, mark: 2 }
1946                ),
1947                &FakeIpPacket::<_, FakeTcpSegment>::arbitrary_value(),
1948                Interfaces::<FakeMatcherDeviceId> { ingress: None, egress: None },
1949                &mut metadata,
1950            ),
1951            IngressVerdict::Proceed(Accept)
1952        );
1953        assert_eq!(metadata.marks, Marks::new([(MarkDomain::Mark1, 2), (MarkDomain::Mark2, 1)]));
1954    }
1955
1956    // Regression test for https://fxbug.dev/517102537.
1957    #[ip_test(I)]
1958    fn transparent_proxy_drop_on_port_0<I: TestIpExt>() {
1959        let ingress = Hook {
1960            routines: vec![Routine {
1961                rules: vec![Rule::new(
1962                    PacketMatcher::default(),
1963                    Action::TransparentProxy(TransparentProxy::LocalAddr(I::DST_IP)),
1964                )],
1965            }],
1966        };
1967
1968        let packet = FakeIpPacket::<I, FakeTcpSegment> {
1969            body: FakeTcpSegment {
1970                dst_port: 0,
1971                src_port: 11111,
1972                segment: SegmentHeader::arbitrary_value(),
1973                payload_len: 0,
1974            },
1975            ..FakeIpPacket::<I, FakeTcpSegment>::arbitrary_value()
1976        };
1977
1978        assert_eq!(
1979            check_routines_for_hook::<
1980                I,
1981                _,
1982                FakeMatcherDeviceId,
1983                FakeBindingsCtx<I>,
1984                _,
1985                IngressStopReason<I>,
1986            >(
1987                &ingress,
1988                &packet,
1989                Interfaces { ingress: None, egress: None },
1990                &mut FakePacketMetadata::default(),
1991            ),
1992            IngressVerdict::Stop(IngressStopReason::Drop),
1993        );
1994    }
1995}