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