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