Skip to main content

netstack3_filter/state/
validation.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
5use alloc::sync::Arc;
6use alloc::vec::Vec;
7use core::fmt::Debug;
8
9use assert_matches::assert_matches;
10use derivative::Derivative;
11use net_types::ip::{GenericOverIp, Ip};
12use netstack3_base::MatcherBindingsTypes;
13use netstack3_hashmap::hash_map::{Entry, HashMap};
14use packet_formats::ip::{IpExt, IpProto, Ipv4Proto, Ipv6Proto};
15
16use crate::{
17    Action, Hook, IpRoutines, NatRoutines, PacketMatcher, RejectType, Routine, Routines, Rule,
18    TransportProtocolMatcher, UninstalledRoutine,
19};
20
21/// Provided filtering state was invalid.
22#[derive(Derivative, Debug, GenericOverIp)]
23#[generic_over_ip()]
24#[cfg_attr(test, derivative(PartialEq(bound = "RuleInfo: PartialEq")))]
25pub enum ValidationError<RuleInfo> {
26    /// A rule matches on a property that is unavailable in the context in which it
27    /// will be evaluated. For example, matching on the input interface in the
28    /// EGRESS hook.
29    RuleWithInvalidMatcher(RuleInfo),
30    /// A rule has an action that is unavailable in the context in which it will be
31    /// evaluated. For example, the TransparentProxy action is only valid in the
32    /// INGRESS hook.
33    RuleWithInvalidAction(RuleInfo),
34    /// A rule has a TransparentProxy action without a corresponding valid matcher:
35    /// the rule must match on transport protocol to ensure that the packet has
36    /// either a TCP or UDP header.
37    TransparentProxyWithInvalidMatcher(RuleInfo),
38    /// A rule has a Redirect action without a corresponding valid matcher: if the
39    /// action specifies a destination port range, the rule must match on transport
40    /// protocol to ensure that the packet has either a TCP or UDP header.
41    RedirectWithInvalidMatcher(RuleInfo),
42    /// A rule has a Masquerade action without a corresponding valid matcher: if the
43    /// action specifies a source port range, the rule must match on transport
44    /// protocol to ensure that the packet has either a TCP or UDP header.
45    MasqueradeWithInvalidMatcher(RuleInfo),
46    /// A rule has a TCP-Reset Reject action without a corresponding valid
47    /// matcher. RejectAction with TcpReset reject type is allowed only if the
48    /// rule matches TCP.
49    RejectWithInvalidMatcher(RuleInfo),
50}
51
52/// Witness type ensuring that the contained filtering state has been validated.
53#[derive(Derivative)]
54#[derivative(Default(bound = ""))]
55pub struct ValidRoutines<I: IpExt, BT: MatcherBindingsTypes>(Routines<I, BT, ()>);
56
57impl<I: IpExt, BT: MatcherBindingsTypes> ValidRoutines<I, BT> {
58    /// Accesses the inner state.
59    pub fn get(&self) -> &Routines<I, BT, ()> {
60        let Self(state) = self;
61        &state
62    }
63}
64
65impl<I: IpExt, BT: MatcherBindingsTypes> ValidRoutines<I, BT> {
66    /// Validates the provide state and creates a new `ValidRoutines` along with a
67    /// list of all uninstalled routines that are referred to from an installed
68    /// routine. Returns a `ValidationError` if the state is invalid.
69    ///
70    /// The provided state must not contain any cyclical routine graphs (formed by
71    /// rules with jump actions). The behavior in this case is unspecified but could
72    /// be a deadlock or a panic, for example.
73    ///
74    /// # Panics
75    ///
76    /// Panics if the provided state includes cyclic routine graphs.
77    pub fn new<RuleInfo: Clone>(
78        routines: Routines<I, BT, RuleInfo>,
79    ) -> Result<(Self, Vec<UninstalledRoutine<I, BT, ()>>), ValidationError<RuleInfo>> {
80        let Routines { ip: ip_routines, nat: nat_routines } = &routines;
81
82        // Ensure that no rule has a matcher that is unavailable in the context in which
83        // the rule will be evaluated.
84        let IpRoutines { ingress, local_ingress, egress, local_egress, forwarding } = ip_routines;
85        validate_hook(
86            &ingress,
87            &[UnavailableMatcher::OutInterface],
88            &[
89                UnavailableAction::Redirect,
90                UnavailableAction::Masquerade,
91                UnavailableAction::Reject,
92            ],
93        )?;
94        validate_hook(
95            &local_ingress,
96            &[UnavailableMatcher::OutInterface],
97            &[
98                UnavailableAction::TransparentProxy,
99                UnavailableAction::Redirect,
100                UnavailableAction::Masquerade,
101            ],
102        )?;
103        validate_hook(
104            &forwarding,
105            &[],
106            &[
107                UnavailableAction::TransparentProxy,
108                UnavailableAction::Redirect,
109                UnavailableAction::Masquerade,
110            ],
111        )?;
112        validate_hook(
113            &egress,
114            &[UnavailableMatcher::InInterface],
115            &[
116                UnavailableAction::TransparentProxy,
117                UnavailableAction::Redirect,
118                UnavailableAction::Masquerade,
119                UnavailableAction::Reject,
120            ],
121        )?;
122        validate_hook(
123            &local_egress,
124            &[UnavailableMatcher::InInterface],
125            &[
126                UnavailableAction::TransparentProxy,
127                UnavailableAction::Redirect,
128                UnavailableAction::Masquerade,
129            ],
130        )?;
131
132        let NatRoutines { ingress, local_ingress, egress, local_egress } = nat_routines;
133        validate_hook(
134            &ingress,
135            &[UnavailableMatcher::OutInterface],
136            &[
137                UnavailableAction::TransparentProxy,
138                UnavailableAction::Masquerade,
139                UnavailableAction::Mark,
140                UnavailableAction::Reject,
141            ],
142        )?;
143        validate_hook(
144            &local_ingress,
145            &[UnavailableMatcher::OutInterface],
146            &[
147                UnavailableAction::TransparentProxy,
148                UnavailableAction::Redirect,
149                UnavailableAction::Masquerade,
150                UnavailableAction::Mark,
151                UnavailableAction::Reject,
152            ],
153        )?;
154        validate_hook(
155            &egress,
156            &[UnavailableMatcher::InInterface],
157            &[
158                UnavailableAction::TransparentProxy,
159                UnavailableAction::Redirect,
160                UnavailableAction::Mark,
161                UnavailableAction::Reject,
162            ],
163        )?;
164        validate_hook(
165            &local_egress,
166            &[UnavailableMatcher::InInterface],
167            &[
168                UnavailableAction::TransparentProxy,
169                UnavailableAction::Masquerade,
170                UnavailableAction::Mark,
171                UnavailableAction::Reject,
172            ],
173        )?;
174
175        let mut index = UninstalledRoutineIndex::default();
176        let routines = routines.strip_debug_info(&mut index);
177        Ok((Self(routines), index.into_values()))
178    }
179}
180
181#[derive(Clone, Copy)]
182enum UnavailableMatcher {
183    InInterface,
184    OutInterface,
185}
186
187impl UnavailableMatcher {
188    fn validate<I: IpExt, BT: MatcherBindingsTypes, RuleInfo: Clone>(
189        &self,
190        matcher: &PacketMatcher<I, BT>,
191        rule: &RuleInfo,
192    ) -> Result<(), ValidationError<RuleInfo>> {
193        let unavailable_matcher = match self {
194            UnavailableMatcher::InInterface => matcher.in_interface.as_ref(),
195            UnavailableMatcher::OutInterface => matcher.out_interface.as_ref(),
196        };
197        if unavailable_matcher.is_some() {
198            Err(ValidationError::RuleWithInvalidMatcher(rule.clone()))
199        } else {
200            Ok(())
201        }
202    }
203}
204
205#[derive(Clone, Copy)]
206enum UnavailableAction {
207    TransparentProxy,
208    Redirect,
209    Masquerade,
210    Mark,
211    Reject,
212}
213
214impl UnavailableAction {
215    fn validate<I: IpExt, BT: MatcherBindingsTypes, RuleInfo: Clone>(
216        &self,
217        action: &Action<I, BT, RuleInfo>,
218        rule: &RuleInfo,
219    ) -> Result<(), ValidationError<RuleInfo>> {
220        match (self, action) {
221            (UnavailableAction::TransparentProxy, Action::TransparentProxy(_))
222            | (UnavailableAction::Redirect, Action::Redirect { .. })
223            | (UnavailableAction::Masquerade, Action::Masquerade { .. })
224            | (UnavailableAction::Mark, Action::Mark { .. })
225            | (UnavailableAction::Reject, Action::Reject(_)) => {
226                Err(ValidationError::RuleWithInvalidAction(rule.clone()))
227            }
228            _ => Ok(()),
229        }
230    }
231}
232
233/// Ensures that no rules reachable from this hook match on
234/// `unavailable_matcher`.
235fn validate_hook<I: IpExt, BT: MatcherBindingsTypes, RuleInfo: Clone>(
236    Hook { routines }: &Hook<I, BT, RuleInfo>,
237    unavailable_matchers: &[UnavailableMatcher],
238    unavailable_actions: &[UnavailableAction],
239) -> Result<(), ValidationError<RuleInfo>> {
240    for routine in routines {
241        validate_routine(routine, unavailable_matchers, unavailable_actions)?;
242    }
243
244    Ok(())
245}
246
247/// Ensures that:
248///  * no rules reachable from this routine match on any of the
249///    `unavailable_matchers`.
250///  * no rules reachable from this routine include one of the
251///    `unavailable_actions`.
252///  * all rules reachable from this routine have matchers that are compatible
253///    with their actions (for example, specifying a port rewrite requires that
254///    a transport protocol matcher be present).
255fn validate_routine<I: IpExt, BT: MatcherBindingsTypes, RuleInfo: Clone>(
256    Routine { rules }: &Routine<I, BT, RuleInfo>,
257    unavailable_matchers: &[UnavailableMatcher],
258    unavailable_actions: &[UnavailableAction],
259) -> Result<(), ValidationError<RuleInfo>> {
260    for Rule { matcher, action, validation_info } in rules {
261        for unavailable in unavailable_matchers {
262            unavailable.validate(matcher, validation_info)?;
263        }
264        for unavailable in unavailable_actions {
265            unavailable.validate(action, validation_info)?;
266        }
267
268        let get_proto_matcher = |matcher: &PacketMatcher<_, _>| {
269            let Some(TransportProtocolMatcher { proto, .. }) = matcher.transport_protocol else {
270                return None;
271            };
272            I::map_ip_in(
273                proto,
274                |proto| match proto {
275                    Ipv4Proto::Proto(proto) => Some(proto),
276                    _ => None,
277                },
278                |proto| match proto {
279                    Ipv6Proto::Proto(proto) => Some(proto),
280                    _ => None,
281                },
282            )
283        };
284
285        let has_tcp_or_udp_matcher = |matcher: &PacketMatcher<_, _>| {
286            matches!(get_proto_matcher(matcher), Some(IpProto::Tcp | IpProto::Udp))
287        };
288
289        match action {
290            Action::Accept | Action::Drop | Action::Return | Action::Mark { .. } | Action::None => {
291            }
292            Action::TransparentProxy(_) => {
293                // TransparentProxy is only valid in a rule that matches on
294                // either TCP or UDP.
295                if !has_tcp_or_udp_matcher(matcher) {
296                    return Err(ValidationError::TransparentProxyWithInvalidMatcher(
297                        validation_info.clone(),
298                    ));
299                }
300            }
301            Action::Redirect { dst_port } => {
302                if dst_port.is_some() {
303                    // Redirect can only specify a destination port in a rule
304                    // that matches on either TCP or UDP.
305                    if !has_tcp_or_udp_matcher(matcher) {
306                        return Err(ValidationError::RedirectWithInvalidMatcher(
307                            validation_info.clone(),
308                        ));
309                    };
310                }
311            }
312            Action::Masquerade { src_port } => {
313                if src_port.is_some() {
314                    // Masquerde can only specify a source port in a rule that
315                    // matches on either TCP or UDP.
316                    if !has_tcp_or_udp_matcher(matcher) {
317                        return Err(ValidationError::MasqueradeWithInvalidMatcher(
318                            validation_info.clone(),
319                        ));
320                    };
321                }
322            }
323            Action::Jump(target) => {
324                let UninstalledRoutine { routine, id: _ } = target;
325                validate_routine(&*routine, unavailable_matchers, unavailable_actions)?;
326            }
327            Action::Reject(reject_type) => match reject_type {
328                RejectType::TcpReset => {
329                    if get_proto_matcher(matcher) != Some(IpProto::Tcp) {
330                        return Err(ValidationError::RejectWithInvalidMatcher(
331                            validation_info.clone(),
332                        ));
333                    }
334                }
335                RejectType::NetUnreachable
336                | RejectType::HostUnreachable
337                | RejectType::ProtoUnreachable
338                | RejectType::PortUnreachable
339                | RejectType::RoutePolicyFail
340                | RejectType::RejectRoute
341                | RejectType::AdminProhibited => {}
342            },
343        }
344    }
345
346    Ok(())
347}
348
349#[derive(Derivative)]
350#[derivative(PartialEq(bound = ""), Debug(bound = ""))]
351enum ConvertedRoutine<I: IpExt, BT: MatcherBindingsTypes> {
352    InProgress,
353    Done(UninstalledRoutine<I, BT, ()>),
354}
355
356#[derive(Derivative)]
357#[derivative(Default(bound = ""))]
358struct UninstalledRoutineIndex<I: IpExt, BT: MatcherBindingsTypes, RuleInfo> {
359    index: HashMap<UninstalledRoutine<I, BT, RuleInfo>, ConvertedRoutine<I, BT>>,
360}
361
362impl<I: IpExt, BT: MatcherBindingsTypes, RuleInfo: Clone> UninstalledRoutineIndex<I, BT, RuleInfo> {
363    fn get_or_insert_with(
364        &mut self,
365        target: UninstalledRoutine<I, BT, RuleInfo>,
366        convert: impl FnOnce(
367            &mut UninstalledRoutineIndex<I, BT, RuleInfo>,
368        ) -> UninstalledRoutine<I, BT, ()>,
369    ) -> UninstalledRoutine<I, BT, ()> {
370        match self.index.entry(target.clone()) {
371            Entry::Occupied(entry) => match entry.get() {
372                ConvertedRoutine::InProgress => panic!("cycle in routine graph"),
373                ConvertedRoutine::Done(routine) => return routine.clone(),
374            },
375            Entry::Vacant(entry) => {
376                let _ = entry.insert(ConvertedRoutine::InProgress);
377            }
378        }
379        // Convert the target routine and store it in the index, so that the next time
380        // we attempt to convert it, we just reuse the already-converted routine.
381        let converted = convert(self);
382        let previous = self.index.insert(target, ConvertedRoutine::Done(converted.clone()));
383        assert_eq!(previous, Some(ConvertedRoutine::InProgress));
384        converted
385    }
386
387    fn into_values(self) -> Vec<UninstalledRoutine<I, BT, ()>> {
388        self.index
389            .into_values()
390            .map(|routine| assert_matches!(routine, ConvertedRoutine::Done(routine) => routine))
391            .collect()
392    }
393}
394
395impl<I: IpExt, BT: MatcherBindingsTypes, RuleInfo: Clone> Routines<I, BT, RuleInfo> {
396    fn strip_debug_info(
397        self,
398        index: &mut UninstalledRoutineIndex<I, BT, RuleInfo>,
399    ) -> Routines<I, BT, ()> {
400        let Self { ip: ip_routines, nat: nat_routines } = self;
401        Routines {
402            ip: ip_routines.strip_debug_info(index),
403            nat: nat_routines.strip_debug_info(index),
404        }
405    }
406}
407
408impl<I: IpExt, BT: MatcherBindingsTypes, RuleInfo: Clone> IpRoutines<I, BT, RuleInfo> {
409    fn strip_debug_info(
410        self,
411        index: &mut UninstalledRoutineIndex<I, BT, RuleInfo>,
412    ) -> IpRoutines<I, BT, ()> {
413        let Self { ingress, local_ingress, egress, local_egress, forwarding } = self;
414        IpRoutines {
415            ingress: ingress.strip_debug_info(index),
416            local_ingress: local_ingress.strip_debug_info(index),
417            forwarding: forwarding.strip_debug_info(index),
418            egress: egress.strip_debug_info(index),
419            local_egress: local_egress.strip_debug_info(index),
420        }
421    }
422}
423
424impl<I: IpExt, BT: MatcherBindingsTypes, RuleInfo: Clone> NatRoutines<I, BT, RuleInfo> {
425    fn strip_debug_info(
426        self,
427        index: &mut UninstalledRoutineIndex<I, BT, RuleInfo>,
428    ) -> NatRoutines<I, BT, ()> {
429        let Self { ingress, local_ingress, egress, local_egress } = self;
430        NatRoutines {
431            ingress: ingress.strip_debug_info(index),
432            local_ingress: local_ingress.strip_debug_info(index),
433            egress: egress.strip_debug_info(index),
434            local_egress: local_egress.strip_debug_info(index),
435        }
436    }
437}
438
439impl<I: IpExt, BT: MatcherBindingsTypes, RuleInfo: Clone> Hook<I, BT, RuleInfo> {
440    fn strip_debug_info(
441        self,
442        index: &mut UninstalledRoutineIndex<I, BT, RuleInfo>,
443    ) -> Hook<I, BT, ()> {
444        let Self { routines } = self;
445        Hook {
446            routines: routines.into_iter().map(|routine| routine.strip_debug_info(index)).collect(),
447        }
448    }
449}
450
451impl<I: IpExt, BT: MatcherBindingsTypes, RuleInfo: Clone> Routine<I, BT, RuleInfo> {
452    fn strip_debug_info(
453        self,
454        index: &mut UninstalledRoutineIndex<I, BT, RuleInfo>,
455    ) -> Routine<I, BT, ()> {
456        let Self { rules } = self;
457        Routine {
458            rules: rules
459                .into_iter()
460                .map(|Rule { matcher, action, validation_info: _ }| Rule {
461                    matcher,
462                    action: action.strip_debug_info(index),
463                    validation_info: (),
464                })
465                .collect(),
466        }
467    }
468}
469
470impl<I: IpExt, BT: MatcherBindingsTypes, RuleInfo: Clone> Action<I, BT, RuleInfo> {
471    fn strip_debug_info(
472        self,
473        index: &mut UninstalledRoutineIndex<I, BT, RuleInfo>,
474    ) -> Action<I, BT, ()> {
475        match self {
476            Self::Accept => Action::Accept,
477            Self::Drop => Action::Drop,
478            Self::Return => Action::Return,
479            Self::TransparentProxy(proxy) => Action::TransparentProxy(proxy),
480            Self::Redirect { dst_port } => Action::Redirect { dst_port },
481            Self::Masquerade { src_port } => Action::Masquerade { src_port },
482            Self::Mark { domain, action } => Action::Mark { domain, action },
483            Self::Jump(target) => {
484                let converted = index.get_or_insert_with(target.clone(), |index| {
485                    // Recursively strip debug info from the target routine.
486                    let UninstalledRoutine { ref routine, id } = target;
487                    UninstalledRoutine {
488                        routine: Arc::new(Routine::clone(&*routine).strip_debug_info(index)),
489                        id,
490                    }
491                });
492                Action::Jump(converted)
493            }
494            Self::None => Action::None,
495            Self::Reject(reject_type) => Action::Reject(reject_type),
496        }
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use alloc::vec;
503    use core::num::NonZeroU16;
504
505    use assert_matches::assert_matches;
506    use ip_test_macro::ip_test;
507    use net_types::ip::Ipv4;
508    use netstack3_base::InterfaceMatcher;
509    use netstack3_base::testutil::FakeDeviceClass;
510    use test_case::test_case;
511
512    use super::*;
513    use crate::context::testutil::FakeBindingsCtx;
514    use crate::{PacketMatcher, TransparentProxy};
515
516    #[derive(Debug, Clone, PartialEq)]
517    enum RuleId {
518        Valid,
519        Invalid,
520    }
521
522    fn rule<I: IpExt>(
523        matcher: PacketMatcher<I, FakeBindingsCtx<I>>,
524        validation_info: RuleId,
525    ) -> Rule<I, FakeBindingsCtx<I>, RuleId> {
526        Rule { matcher, action: Action::Drop, validation_info }
527    }
528
529    fn hook_with_rules<I: IpExt>(
530        rules: Vec<Rule<I, FakeBindingsCtx<I>, RuleId>>,
531    ) -> Hook<I, FakeBindingsCtx<I>, RuleId> {
532        Hook { routines: vec![Routine { rules }] }
533    }
534
535    #[ip_test(I)]
536    #[test_case(
537        hook_with_rules(vec![rule(
538            PacketMatcher {
539                in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
540                ..Default::default()
541            },
542            RuleId::Valid,
543        )]),
544        UnavailableMatcher::OutInterface =>
545        Ok(());
546        "match on input interface in root routine when available"
547    )]
548    #[test_case(
549        hook_with_rules(vec![rule(
550            PacketMatcher {
551                out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
552                ..Default::default()
553            },
554            RuleId::Valid,
555        )]),
556        UnavailableMatcher::InInterface =>
557        Ok(());
558        "match on output interface in root routine when available"
559    )]
560    #[test_case(
561        hook_with_rules(vec![
562            rule(PacketMatcher::default(), RuleId::Valid),
563            rule(
564                PacketMatcher {
565                    in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
566                    ..Default::default()
567                },
568                RuleId::Invalid,
569            ),
570        ]),
571        UnavailableMatcher::InInterface =>
572        Err(ValidationError::RuleWithInvalidMatcher(RuleId::Invalid));
573        "match on input interface in root routine when unavailable"
574    )]
575    #[test_case(
576        hook_with_rules(vec![
577            rule(PacketMatcher::default(), RuleId::Valid),
578            rule(
579                PacketMatcher {
580                    out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
581                    ..Default::default()
582                },
583                RuleId::Invalid,
584            ),
585        ]),
586        UnavailableMatcher::OutInterface =>
587        Err(ValidationError::RuleWithInvalidMatcher(RuleId::Invalid));
588        "match on output interface in root routine when unavailable"
589    )]
590    #[test_case(
591        Hook {
592            routines: vec![Routine {
593                rules: vec![Rule {
594                    matcher: PacketMatcher::default(),
595                    action: Action::Jump(UninstalledRoutine::new(
596                        vec![rule(
597                            PacketMatcher {
598                                in_interface: Some(InterfaceMatcher::DeviceClass(
599                                    FakeDeviceClass::Ethernet,
600                                )),
601                                ..Default::default()
602                            },
603                            RuleId::Invalid,
604                        )],
605                        0,
606                    )),
607                    validation_info: RuleId::Valid,
608                }],
609            }],
610        },
611        UnavailableMatcher::InInterface =>
612        Err(ValidationError::RuleWithInvalidMatcher(RuleId::Invalid));
613        "match on input interface in target routine when unavailable"
614    )]
615    #[test_case(
616        Hook {
617            routines: vec![Routine {
618                rules: vec![Rule {
619                    matcher: PacketMatcher::default(),
620                    action: Action::Jump(UninstalledRoutine::new(
621                        vec![rule(
622                            PacketMatcher {
623                                out_interface: Some(InterfaceMatcher::DeviceClass(
624                                    FakeDeviceClass::Ethernet,
625                                )),
626                                ..Default::default()
627                            },
628                            RuleId::Invalid,
629                        )],
630                        0,
631                    )),
632                    validation_info: RuleId::Valid,
633                }],
634            }],
635        },
636        UnavailableMatcher::OutInterface =>
637        Err(ValidationError::RuleWithInvalidMatcher(RuleId::Invalid));
638        "match on output interface in target routine when unavailable"
639    )]
640    fn validate_interface_matcher_available<I: IpExt>(
641        hook: Hook<I, FakeBindingsCtx<I>, RuleId>,
642        unavailable_matcher: UnavailableMatcher,
643    ) -> Result<(), ValidationError<RuleId>> {
644        validate_hook(&hook, &[unavailable_matcher], &[])
645    }
646
647    fn hook_with_rule<I: IpExt>(
648        rule: Rule<I, FakeBindingsCtx<I>, RuleId>,
649    ) -> Hook<I, FakeBindingsCtx<I>, RuleId> {
650        Hook { routines: vec![Routine { rules: vec![rule] }] }
651    }
652
653    fn transport_matcher<I: IpExt>(proto: I::Proto) -> PacketMatcher<I, FakeBindingsCtx<I>> {
654        PacketMatcher {
655            transport_protocol: Some(TransportProtocolMatcher {
656                proto,
657                src_port: None,
658                dst_port: None,
659            }),
660            ..Default::default()
661        }
662    }
663
664    fn udp_matcher<I: IpExt>() -> PacketMatcher<I, FakeBindingsCtx<I>> {
665        transport_matcher(I::map_ip(
666            (),
667            |()| Ipv4Proto::Proto(IpProto::Udp),
668            |()| Ipv6Proto::Proto(IpProto::Udp),
669        ))
670    }
671
672    fn tcp_matcher<I: IpExt>() -> PacketMatcher<I, FakeBindingsCtx<I>> {
673        transport_matcher(I::map_ip(
674            (),
675            |()| Ipv4Proto::Proto(IpProto::Tcp),
676            |()| Ipv6Proto::Proto(IpProto::Tcp),
677        ))
678    }
679
680    fn icmp_matcher<I: IpExt>() -> PacketMatcher<I, FakeBindingsCtx<I>> {
681        transport_matcher(I::map_ip((), |()| Ipv4Proto::Icmp, |()| Ipv6Proto::Icmpv6))
682    }
683
684    const LOCAL_PORT: NonZeroU16 = NonZeroU16::new(8080).unwrap();
685
686    #[ip_test(I)]
687    #[test_case(
688        Routines {
689            ip: IpRoutines {
690                ingress: hook_with_rule(Rule {
691                    matcher: udp_matcher(),
692                    action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
693                    validation_info: RuleId::Valid,
694                }),
695                ..Default::default()
696            },
697            ..Default::default()
698        } =>
699        Ok(());
700        "transparent proxy available in IP INGRESS routines"
701    )]
702    #[test_case(
703        Routines {
704            ip: IpRoutines {
705                ingress: hook_with_rule(Rule {
706                    matcher: PacketMatcher::default(),
707                    action: Action::Jump(UninstalledRoutine::new(
708                        vec![Rule {
709                            matcher: udp_matcher(),
710                            action: Action::TransparentProxy(
711                                TransparentProxy::LocalPort(LOCAL_PORT)
712                            ),
713                            validation_info: RuleId::Valid,
714                        }],
715                        0,
716                    )),
717                    validation_info: RuleId::Valid,
718                }),
719                ..Default::default()
720            },
721            ..Default::default()
722        } =>
723        Ok(());
724        "transparent proxy available in target routine reachable from INGRESS"
725    )]
726    #[test_case(
727        Routines {
728            ip: IpRoutines {
729                egress: hook_with_rule(Rule {
730                    matcher: udp_matcher(),
731                    action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
732                    validation_info: RuleId::Invalid,
733                }),
734                ..Default::default()
735            },
736            ..Default::default()
737        } =>
738        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
739        "transparent proxy unavailable in IP EGRESS routine"
740    )]
741    #[test_case(
742        Routines {
743            ip: IpRoutines {
744                egress: hook_with_rule(Rule {
745                    matcher: PacketMatcher::default(),
746                    action: Action::Jump(UninstalledRoutine::new(
747                        vec![Rule {
748                            matcher: udp_matcher(),
749                            action: Action::TransparentProxy(
750                                TransparentProxy::LocalPort(LOCAL_PORT)
751                            ),
752                            validation_info: RuleId::Invalid,
753                        }],
754                        0,
755                    )),
756                    validation_info: RuleId::Valid,
757                }),
758                ..Default::default()
759            },
760            ..Default::default()
761        } =>
762        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
763        "transparent proxy unavailable in target routine reachable from EGRESS"
764    )]
765    #[test_case(
766        Routines {
767            nat: NatRoutines {
768                ingress: hook_with_rule(Rule {
769                    matcher: PacketMatcher::default(),
770                    action: Action::Redirect { dst_port: None },
771                    validation_info: RuleId::Valid,
772                }),
773                local_egress: hook_with_rule(Rule {
774                    matcher: PacketMatcher::default(),
775                    action: Action::Redirect { dst_port: None },
776                    validation_info: RuleId::Valid,
777                }),
778                ..Default::default()
779            },
780            ..Default::default()
781        } =>
782        Ok(());
783        "redirect available in NAT INGRESS and LOCAL_EGRESS routines"
784    )]
785    #[test_case(
786        Routines {
787            nat: NatRoutines {
788                egress: hook_with_rule(Rule {
789                    matcher: PacketMatcher::default(),
790                    action: Action::Redirect { dst_port: None },
791                    validation_info: RuleId::Invalid,
792                }),
793                ..Default::default()
794            },
795            ..Default::default()
796        } =>
797        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
798        "redirect unavailable in NAT EGRESS"
799    )]
800    #[test_case(
801        Routines {
802            ip: IpRoutines {
803                ingress: hook_with_rule(Rule {
804                    matcher: PacketMatcher::default(),
805                    action: Action::Redirect { dst_port: None },
806                    validation_info: RuleId::Invalid,
807                }),
808                ..Default::default()
809            },
810            ..Default::default()
811        } =>
812        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
813        "redirect unavailable in IP routines"
814    )]
815    #[test_case(
816        Routines {
817            nat: NatRoutines {
818                egress: hook_with_rule(Rule {
819                    matcher: PacketMatcher::default(),
820                    action: Action::Masquerade { src_port: None },
821                    validation_info: RuleId::Valid,
822                }),
823                ..Default::default()
824            },
825            ..Default::default()
826        } =>
827        Ok(());
828        "masquerade available in NAT EGRESS"
829    )]
830    #[test_case(
831        Routines {
832            nat: NatRoutines {
833                local_ingress: hook_with_rule(Rule {
834                    matcher: PacketMatcher::default(),
835                    action: Action::Masquerade { src_port: None },
836                    validation_info: RuleId::Invalid,
837                }),
838                ..Default::default()
839            },
840            ..Default::default()
841        } =>
842        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
843        "masquerade unavailable in NAT LOCAL_INGRESS"
844    )]
845    #[test_case(
846        Routines {
847            ip: IpRoutines {
848                egress: hook_with_rule(Rule {
849                    matcher: PacketMatcher::default(),
850                    action: Action::Masquerade { src_port: None },
851                    validation_info: RuleId::Invalid,
852                }),
853                ..Default::default()
854            },
855            ..Default::default()
856        } =>
857        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
858        "masquerade unavailable in IP routines"
859    )]
860    fn validate_action_available<I: IpExt>(
861        routines: Routines<I, FakeBindingsCtx<I>, RuleId>,
862    ) -> Result<(), ValidationError<RuleId>> {
863        ValidRoutines::new(routines).map(|_| ())
864    }
865
866    #[ip_test(I)]
867    #[test_case(
868        Routine {
869            rules: vec![Rule {
870                matcher: tcp_matcher(),
871                action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
872                validation_info: RuleId::Valid,
873            }],
874        } =>
875        Ok(());
876        "transparent proxy valid with TCP matcher"
877    )]
878    #[test_case(
879        Routine {
880            rules: vec![Rule {
881                matcher: udp_matcher(),
882                action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
883                validation_info: RuleId::Valid,
884            }],
885        } =>
886        Ok(());
887        "transparent proxy valid with UDP matcher"
888    )]
889    #[test_case(
890        Routine {
891            rules: vec![Rule {
892                matcher: icmp_matcher(),
893                action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
894                validation_info: RuleId::Invalid,
895            }],
896        } =>
897        Err(ValidationError::TransparentProxyWithInvalidMatcher(RuleId::Invalid));
898        "transparent proxy invalid with ICMP matcher"
899    )]
900    #[test_case(
901        Routine {
902            rules: vec![Rule {
903                matcher: PacketMatcher::default(),
904                action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
905                validation_info: RuleId::Invalid,
906            }],
907        } =>
908        Err(ValidationError::TransparentProxyWithInvalidMatcher(RuleId::Invalid));
909        "transparent proxy invalid with no transport protocol matcher"
910    )]
911    fn validate_transparent_proxy_matcher<I: IpExt>(
912        routine: Routine<I, FakeBindingsCtx<I>, RuleId>,
913    ) -> Result<(), ValidationError<RuleId>> {
914        validate_routine(&routine, &[], &[])
915    }
916
917    #[ip_test(I)]
918    #[test_case(
919        Routine {
920            rules: vec![Rule {
921                matcher: PacketMatcher::default(),
922                action: Action::Redirect { dst_port: None },
923                validation_info: RuleId::Valid,
924            }],
925        } =>
926        Ok(());
927        "redirect valid with no matcher if dst port unspecified"
928    )]
929    #[test_case(
930        Routine {
931            rules: vec![Rule {
932                matcher: tcp_matcher(),
933                action: Action::Redirect { dst_port: Some(LOCAL_PORT..=LOCAL_PORT) },
934                validation_info: RuleId::Valid,
935            }],
936        } =>
937        Ok(());
938        "redirect valid with TCP matcher when dst port specified"
939    )]
940    #[test_case(
941        Routine {
942            rules: vec![Rule {
943                matcher: udp_matcher(),
944                action: Action::Redirect { dst_port: Some(LOCAL_PORT..=LOCAL_PORT) },
945                validation_info: RuleId::Valid,
946            }],
947        } =>
948        Ok(());
949        "redirect valid with UDP matcher when dst port specified"
950    )]
951    #[test_case(
952        Routine {
953            rules: vec![Rule {
954                matcher: icmp_matcher(),
955                action: Action::Redirect { dst_port: Some(LOCAL_PORT..=LOCAL_PORT) },
956                validation_info: RuleId::Invalid,
957            }],
958        } =>
959        Err(ValidationError::RedirectWithInvalidMatcher(RuleId::Invalid));
960        "redirect invalid with ICMP matcher when dst port specified"
961    )]
962    #[test_case(
963        Routine {
964            rules: vec![Rule {
965                matcher: PacketMatcher::default(),
966                action: Action::Redirect { dst_port: Some(LOCAL_PORT..=LOCAL_PORT) },
967                validation_info: RuleId::Invalid,
968            }],
969        } =>
970        Err(ValidationError::RedirectWithInvalidMatcher(RuleId::Invalid));
971        "redirect invalid with no transport protocol matcher when dst port specified"
972    )]
973    fn validate_redirect_matcher<I: IpExt>(
974        routine: Routine<I, FakeBindingsCtx<I>, RuleId>,
975    ) -> Result<(), ValidationError<RuleId>> {
976        validate_routine(&routine, &[], &[])
977    }
978
979    #[ip_test(I)]
980    #[test_case(
981        Routine {
982            rules: vec![Rule {
983                matcher: PacketMatcher::default(),
984                action: Action::Masquerade { src_port: None },
985                validation_info: RuleId::Valid,
986            }],
987        } =>
988        Ok(());
989        "masquerade valid with no matcher if src port unspecified"
990    )]
991    #[test_case(
992        Routine {
993            rules: vec![Rule {
994                matcher: tcp_matcher(),
995                action: Action::Masquerade { src_port: Some(LOCAL_PORT..=LOCAL_PORT) },
996                validation_info: RuleId::Valid,
997            }],
998        } =>
999        Ok(());
1000        "masquerade valid with TCP matcher when src port specified"
1001    )]
1002    #[test_case(
1003        Routine {
1004            rules: vec![Rule {
1005                matcher: udp_matcher(),
1006                action: Action::Masquerade { src_port: Some(LOCAL_PORT..=LOCAL_PORT) },
1007                validation_info: RuleId::Valid,
1008            }],
1009        } =>
1010        Ok(());
1011        "masquerade valid with UDP matcher when src port specified"
1012    )]
1013    #[test_case(
1014        Routine {
1015            rules: vec![Rule {
1016                matcher: icmp_matcher(),
1017                action: Action::Masquerade { src_port: Some(LOCAL_PORT..=LOCAL_PORT) },
1018                validation_info: RuleId::Invalid,
1019            }],
1020        } =>
1021        Err(ValidationError::MasqueradeWithInvalidMatcher(RuleId::Invalid));
1022        "masquerade invalid with ICMP matcher when src port specified"
1023    )]
1024    #[test_case(
1025        Routine {
1026            rules: vec![Rule {
1027                matcher: PacketMatcher::default(),
1028                action: Action::Masquerade { src_port: Some(LOCAL_PORT..=LOCAL_PORT) },
1029                validation_info: RuleId::Invalid,
1030            }],
1031        } =>
1032        Err(ValidationError::MasqueradeWithInvalidMatcher(RuleId::Invalid));
1033        "masquerade invalid with no transport protocol matcher when src port specified"
1034    )]
1035    fn validate_masquerade_matcher<I: IpExt>(
1036        routine: Routine<I, FakeBindingsCtx<I>, RuleId>,
1037    ) -> Result<(), ValidationError<RuleId>> {
1038        validate_routine(&routine, &[], &[])
1039    }
1040
1041    #[test]
1042    fn strip_debug_info_reuses_uninstalled_routines() {
1043        // Two routines in the hook jump to the same uninstalled routine.
1044        let uninstalled_routine =
1045            UninstalledRoutine::<Ipv4, FakeBindingsCtx<Ipv4>, _>::new(Vec::new(), 0);
1046        let hook = Hook {
1047            routines: vec![
1048                Routine {
1049                    rules: vec![Rule {
1050                        matcher: PacketMatcher::default(),
1051                        action: Action::Jump(uninstalled_routine.clone()),
1052                        validation_info: "rule-1",
1053                    }],
1054                },
1055                Routine {
1056                    rules: vec![Rule {
1057                        matcher: PacketMatcher::default(),
1058                        action: Action::Jump(uninstalled_routine),
1059                        validation_info: "rule-2",
1060                    }],
1061                },
1062            ],
1063        };
1064
1065        // When we strip the debug info from the routines in the hook, all
1066        // jump targets should be converted 1:1. In this case, there are two
1067        // jump actions that refer to the same uninstalled routine, so that
1068        // uninstalled routine should be converted once, and the resulting jump
1069        // actions should both point to the same new uninstalled routine.
1070        let Hook { routines } = hook.strip_debug_info(&mut UninstalledRoutineIndex::default());
1071        let (first, second) = assert_matches!(
1072            &routines[..],
1073            [Routine { rules: first }, Routine { rules: second }] => (first, second)
1074        );
1075        let first = assert_matches!(
1076            &first[..],
1077            [Rule { action: Action::Jump(target), .. }] => target
1078        );
1079        let second = assert_matches!(
1080            &second[..],
1081            [Rule { action: Action::Jump(target), .. }] => target
1082        );
1083        assert_eq!(first, second);
1084    }
1085}