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