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