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