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