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_hashmap::hash_map::{Entry, HashMap};
13use packet_formats::ip::{IpExt, IpProto, Ipv4Proto, Ipv6Proto};
14
15use crate::{
16    Action, Hook, IpRoutines, NatRoutines, PacketMatcher, Routine, Routines, Rule,
17    TransportProtocolMatcher, UninstalledRoutine,
18};
19
20/// Provided filtering state was invalid.
21#[derive(Derivative, Debug, GenericOverIp)]
22#[generic_over_ip()]
23#[cfg_attr(test, derivative(PartialEq(bound = "RuleInfo: PartialEq")))]
24pub enum ValidationError<RuleInfo> {
25    /// A rule matches on a property that is unavailable in the context in which it
26    /// will be evaluated. For example, matching on the input interface in the
27    /// EGRESS hook.
28    RuleWithInvalidMatcher(RuleInfo),
29    /// A rule has an action that is unavailable in the context in which it will be
30    /// evaluated. For example, the TransparentProxy action is only valid in the
31    /// INGRESS hook.
32    RuleWithInvalidAction(RuleInfo),
33    /// A rule has a TransparentProxy action without a corresponding valid matcher:
34    /// the rule must match on transport protocol to ensure that the packet has
35    /// either a TCP or UDP header.
36    TransparentProxyWithInvalidMatcher(RuleInfo),
37    /// A rule has a Redirect action without a corresponding valid matcher: if the
38    /// action specifies a destination port range, the rule must match on transport
39    /// protocol to ensure that the packet has either a TCP or UDP header.
40    RedirectWithInvalidMatcher(RuleInfo),
41    /// A rule has a Masquerade action without a corresponding valid matcher: if the
42    /// action specifies a source port range, the rule must match on transport
43    /// protocol to ensure that the packet has either a TCP or UDP header.
44    MasqueradeWithInvalidMatcher(RuleInfo),
45}
46
47/// Witness type ensuring that the contained filtering state has been validated.
48#[derive(Derivative)]
49#[derivative(Default(bound = ""))]
50pub struct ValidRoutines<I: IpExt, DeviceClass>(Routines<I, DeviceClass, ()>);
51
52impl<I: IpExt, DeviceClass> ValidRoutines<I, DeviceClass> {
53    /// Accesses the inner state.
54    pub fn get(&self) -> &Routines<I, DeviceClass, ()> {
55        let Self(state) = self;
56        &state
57    }
58}
59
60impl<I: IpExt, DeviceClass: Clone + Debug> ValidRoutines<I, DeviceClass> {
61    /// Validates the provide state and creates a new `ValidRoutines` along with a
62    /// list of all uninstalled routines that are referred to from an installed
63    /// routine. Returns a `ValidationError` if the state is invalid.
64    ///
65    /// The provided state must not contain any cyclical routine graphs (formed by
66    /// rules with jump actions). The behavior in this case is unspecified but could
67    /// be a deadlock or a panic, for example.
68    ///
69    /// # Panics
70    ///
71    /// Panics if the provided state includes cyclic routine graphs.
72    pub fn new<RuleInfo: Clone>(
73        routines: Routines<I, DeviceClass, RuleInfo>,
74    ) -> Result<(Self, Vec<UninstalledRoutine<I, DeviceClass, ()>>), ValidationError<RuleInfo>>
75    {
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, DeviceClass, RuleInfo: Clone>(
172        &self,
173        matcher: &PacketMatcher<I, DeviceClass>,
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, DeviceClass, RuleInfo: Clone>(
198        &self,
199        action: &Action<I, DeviceClass, 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, DeviceClass, RuleInfo: Clone>(
217    Hook { routines }: &Hook<I, DeviceClass, 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, DeviceClass, RuleInfo: Clone>(
237    Routine { rules }: &Routine<I, DeviceClass, 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 { .. } => {}
268            Action::TransparentProxy(_) => {
269                // TransparentProxy is only valid in a rule that matches on
270                // either TCP or UDP.
271                if !has_tcp_or_udp_matcher(matcher) {
272                    return Err(ValidationError::TransparentProxyWithInvalidMatcher(
273                        validation_info.clone(),
274                    ));
275                }
276            }
277            Action::Redirect { dst_port } => {
278                if dst_port.is_some() {
279                    // Redirect can only specify a destination port in a rule
280                    // that matches on either TCP or UDP.
281                    if !has_tcp_or_udp_matcher(matcher) {
282                        return Err(ValidationError::RedirectWithInvalidMatcher(
283                            validation_info.clone(),
284                        ));
285                    };
286                }
287            }
288            Action::Masquerade { src_port } => {
289                if src_port.is_some() {
290                    // Masquerde can only specify a source port in a rule that
291                    // matches on either TCP or UDP.
292                    if !has_tcp_or_udp_matcher(matcher) {
293                        return Err(ValidationError::MasqueradeWithInvalidMatcher(
294                            validation_info.clone(),
295                        ));
296                    };
297                }
298            }
299            Action::Jump(target) => {
300                let UninstalledRoutine { routine, id: _ } = target;
301                validate_routine(&*routine, unavailable_matchers, unavailable_actions)?;
302            }
303        }
304    }
305
306    Ok(())
307}
308
309#[derive(Derivative, Debug)]
310#[derivative(PartialEq(bound = ""))]
311enum ConvertedRoutine<I: IpExt, DeviceClass> {
312    InProgress,
313    Done(UninstalledRoutine<I, DeviceClass, ()>),
314}
315
316#[derive(Derivative)]
317#[derivative(Default(bound = ""))]
318struct UninstalledRoutineIndex<I: IpExt, DeviceClass, RuleInfo> {
319    index: HashMap<UninstalledRoutine<I, DeviceClass, RuleInfo>, ConvertedRoutine<I, DeviceClass>>,
320}
321
322impl<I: IpExt, DeviceClass: Clone + Debug + Debug, RuleInfo: Clone>
323    UninstalledRoutineIndex<I, DeviceClass, RuleInfo>
324{
325    fn get_or_insert_with(
326        &mut self,
327        target: UninstalledRoutine<I, DeviceClass, RuleInfo>,
328        convert: impl FnOnce(
329            &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
330        ) -> UninstalledRoutine<I, DeviceClass, ()>,
331    ) -> UninstalledRoutine<I, DeviceClass, ()> {
332        match self.index.entry(target.clone()) {
333            Entry::Occupied(entry) => match entry.get() {
334                ConvertedRoutine::InProgress => panic!("cycle in routine graph"),
335                ConvertedRoutine::Done(routine) => return routine.clone(),
336            },
337            Entry::Vacant(entry) => {
338                let _ = entry.insert(ConvertedRoutine::InProgress);
339            }
340        }
341        // Convert the target routine and store it in the index, so that the next time
342        // we attempt to convert it, we just reuse the already-converted routine.
343        let converted = convert(self);
344        let previous = self.index.insert(target, ConvertedRoutine::Done(converted.clone()));
345        assert_eq!(previous, Some(ConvertedRoutine::InProgress));
346        converted
347    }
348
349    fn into_values(self) -> Vec<UninstalledRoutine<I, DeviceClass, ()>> {
350        self.index
351            .into_values()
352            .map(|routine| assert_matches!(routine, ConvertedRoutine::Done(routine) => routine))
353            .collect()
354    }
355}
356
357impl<I: IpExt, DeviceClass: Clone + Debug, RuleInfo: Clone> Routines<I, DeviceClass, RuleInfo> {
358    fn strip_debug_info(
359        self,
360        index: &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
361    ) -> Routines<I, DeviceClass, ()> {
362        let Self { ip: ip_routines, nat: nat_routines } = self;
363        Routines {
364            ip: ip_routines.strip_debug_info(index),
365            nat: nat_routines.strip_debug_info(index),
366        }
367    }
368}
369
370impl<I: IpExt, DeviceClass: Clone + Debug, RuleInfo: Clone> IpRoutines<I, DeviceClass, RuleInfo> {
371    fn strip_debug_info(
372        self,
373        index: &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
374    ) -> IpRoutines<I, DeviceClass, ()> {
375        let Self { ingress, local_ingress, egress, local_egress, forwarding } = self;
376        IpRoutines {
377            ingress: ingress.strip_debug_info(index),
378            local_ingress: local_ingress.strip_debug_info(index),
379            forwarding: forwarding.strip_debug_info(index),
380            egress: egress.strip_debug_info(index),
381            local_egress: local_egress.strip_debug_info(index),
382        }
383    }
384}
385
386impl<I: IpExt, DeviceClass: Clone + Debug, RuleInfo: Clone> NatRoutines<I, DeviceClass, RuleInfo> {
387    fn strip_debug_info(
388        self,
389        index: &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
390    ) -> NatRoutines<I, DeviceClass, ()> {
391        let Self { ingress, local_ingress, egress, local_egress } = self;
392        NatRoutines {
393            ingress: ingress.strip_debug_info(index),
394            local_ingress: local_ingress.strip_debug_info(index),
395            egress: egress.strip_debug_info(index),
396            local_egress: local_egress.strip_debug_info(index),
397        }
398    }
399}
400
401impl<I: IpExt, DeviceClass: Clone + Debug, RuleInfo: Clone> Hook<I, DeviceClass, RuleInfo> {
402    fn strip_debug_info(
403        self,
404        index: &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
405    ) -> Hook<I, DeviceClass, ()> {
406        let Self { routines } = self;
407        Hook {
408            routines: routines.into_iter().map(|routine| routine.strip_debug_info(index)).collect(),
409        }
410    }
411}
412
413impl<I: IpExt, DeviceClass: Clone + Debug, RuleInfo: Clone> Routine<I, DeviceClass, RuleInfo> {
414    fn strip_debug_info(
415        self,
416        index: &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
417    ) -> Routine<I, DeviceClass, ()> {
418        let Self { rules } = self;
419        Routine {
420            rules: rules
421                .into_iter()
422                .map(|Rule { matcher, action, validation_info: _ }| Rule {
423                    matcher,
424                    action: action.strip_debug_info(index),
425                    validation_info: (),
426                })
427                .collect(),
428        }
429    }
430}
431
432impl<I: IpExt, DeviceClass: Clone + Debug, RuleInfo: Clone> Action<I, DeviceClass, RuleInfo> {
433    fn strip_debug_info(
434        self,
435        index: &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
436    ) -> Action<I, DeviceClass, ()> {
437        match self {
438            Self::Accept => Action::Accept,
439            Self::Drop => Action::Drop,
440            Self::Return => Action::Return,
441            Self::TransparentProxy(proxy) => Action::TransparentProxy(proxy),
442            Self::Redirect { dst_port } => Action::Redirect { dst_port },
443            Self::Masquerade { src_port } => Action::Masquerade { src_port },
444            Self::Mark { domain, action } => Action::Mark { domain, action },
445            Self::Jump(target) => {
446                let converted = index.get_or_insert_with(target.clone(), |index| {
447                    // Recursively strip debug info from the target routine.
448                    let UninstalledRoutine { ref routine, id } = target;
449                    UninstalledRoutine {
450                        routine: Arc::new(Routine::clone(&*routine).strip_debug_info(index)),
451                        id,
452                    }
453                });
454                Action::Jump(converted)
455            }
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::{PacketMatcher, TransparentProxy};
474
475    #[derive(Debug, Clone, PartialEq)]
476    enum RuleId {
477        Valid,
478        Invalid,
479    }
480
481    fn rule<I: IpExt>(
482        matcher: PacketMatcher<I, FakeDeviceClass>,
483        validation_info: RuleId,
484    ) -> Rule<I, FakeDeviceClass, RuleId> {
485        Rule { matcher, action: Action::Drop, validation_info }
486    }
487
488    fn hook_with_rules<I: IpExt>(
489        rules: Vec<Rule<I, FakeDeviceClass, RuleId>>,
490    ) -> Hook<I, FakeDeviceClass, RuleId> {
491        Hook { routines: vec![Routine { rules }] }
492    }
493
494    #[ip_test(I)]
495    #[test_case(
496        hook_with_rules(vec![rule(
497            PacketMatcher {
498                in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
499                ..Default::default()
500            },
501            RuleId::Valid,
502        )]),
503        UnavailableMatcher::OutInterface =>
504        Ok(());
505        "match on input interface in root routine when available"
506    )]
507    #[test_case(
508        hook_with_rules(vec![rule(
509            PacketMatcher {
510                out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
511                ..Default::default()
512            },
513            RuleId::Valid,
514        )]),
515        UnavailableMatcher::InInterface =>
516        Ok(());
517        "match on output interface in root routine when available"
518    )]
519    #[test_case(
520        hook_with_rules(vec![
521            rule(PacketMatcher::default(), RuleId::Valid),
522            rule(
523                PacketMatcher {
524                    in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
525                    ..Default::default()
526                },
527                RuleId::Invalid,
528            ),
529        ]),
530        UnavailableMatcher::InInterface =>
531        Err(ValidationError::RuleWithInvalidMatcher(RuleId::Invalid));
532        "match on input interface in root routine when unavailable"
533    )]
534    #[test_case(
535        hook_with_rules(vec![
536            rule(PacketMatcher::default(), RuleId::Valid),
537            rule(
538                PacketMatcher {
539                    out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
540                    ..Default::default()
541                },
542                RuleId::Invalid,
543            ),
544        ]),
545        UnavailableMatcher::OutInterface =>
546        Err(ValidationError::RuleWithInvalidMatcher(RuleId::Invalid));
547        "match on output interface in root routine when unavailable"
548    )]
549    #[test_case(
550        Hook {
551            routines: vec![Routine {
552                rules: vec![Rule {
553                    matcher: PacketMatcher::default(),
554                    action: Action::Jump(UninstalledRoutine::new(
555                        vec![rule(
556                            PacketMatcher {
557                                in_interface: Some(InterfaceMatcher::DeviceClass(
558                                    FakeDeviceClass::Ethernet,
559                                )),
560                                ..Default::default()
561                            },
562                            RuleId::Invalid,
563                        )],
564                        0,
565                    )),
566                    validation_info: RuleId::Valid,
567                }],
568            }],
569        },
570        UnavailableMatcher::InInterface =>
571        Err(ValidationError::RuleWithInvalidMatcher(RuleId::Invalid));
572        "match on input interface in target routine when unavailable"
573    )]
574    #[test_case(
575        Hook {
576            routines: vec![Routine {
577                rules: vec![Rule {
578                    matcher: PacketMatcher::default(),
579                    action: Action::Jump(UninstalledRoutine::new(
580                        vec![rule(
581                            PacketMatcher {
582                                out_interface: Some(InterfaceMatcher::DeviceClass(
583                                    FakeDeviceClass::Ethernet,
584                                )),
585                                ..Default::default()
586                            },
587                            RuleId::Invalid,
588                        )],
589                        0,
590                    )),
591                    validation_info: RuleId::Valid,
592                }],
593            }],
594        },
595        UnavailableMatcher::OutInterface =>
596        Err(ValidationError::RuleWithInvalidMatcher(RuleId::Invalid));
597        "match on output interface in target routine when unavailable"
598    )]
599    fn validate_interface_matcher_available<I: IpExt>(
600        hook: Hook<I, FakeDeviceClass, RuleId>,
601        unavailable_matcher: UnavailableMatcher,
602    ) -> Result<(), ValidationError<RuleId>> {
603        validate_hook(&hook, &[unavailable_matcher], &[])
604    }
605
606    fn hook_with_rule<I: IpExt>(
607        rule: Rule<I, FakeDeviceClass, RuleId>,
608    ) -> Hook<I, FakeDeviceClass, RuleId> {
609        Hook { routines: vec![Routine { rules: vec![rule] }] }
610    }
611
612    fn transport_matcher<I: IpExt>(proto: I::Proto) -> PacketMatcher<I, FakeDeviceClass> {
613        PacketMatcher {
614            transport_protocol: Some(TransportProtocolMatcher {
615                proto,
616                src_port: None,
617                dst_port: None,
618            }),
619            ..Default::default()
620        }
621    }
622
623    fn udp_matcher<I: IpExt>() -> PacketMatcher<I, FakeDeviceClass> {
624        transport_matcher(I::map_ip(
625            (),
626            |()| Ipv4Proto::Proto(IpProto::Udp),
627            |()| Ipv6Proto::Proto(IpProto::Udp),
628        ))
629    }
630
631    fn tcp_matcher<I: IpExt>() -> PacketMatcher<I, FakeDeviceClass> {
632        transport_matcher(I::map_ip(
633            (),
634            |()| Ipv4Proto::Proto(IpProto::Tcp),
635            |()| Ipv6Proto::Proto(IpProto::Tcp),
636        ))
637    }
638
639    fn icmp_matcher<I: IpExt>() -> PacketMatcher<I, FakeDeviceClass> {
640        transport_matcher(I::map_ip((), |()| Ipv4Proto::Icmp, |()| Ipv6Proto::Icmpv6))
641    }
642
643    const LOCAL_PORT: NonZeroU16 = NonZeroU16::new(8080).unwrap();
644
645    #[ip_test(I)]
646    #[test_case(
647        Routines {
648            ip: IpRoutines {
649                ingress: hook_with_rule(Rule {
650                    matcher: udp_matcher(),
651                    action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
652                    validation_info: RuleId::Valid,
653                }),
654                ..Default::default()
655            },
656            nat: NatRoutines {
657                ingress: hook_with_rule(Rule {
658                    matcher: tcp_matcher(),
659                    action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
660                    validation_info: RuleId::Valid,
661                }),
662                ..Default::default()
663            },
664        } =>
665        Ok(());
666        "transparent proxy available in IP and NAT INGRESS routines"
667    )]
668    #[test_case(
669        Routines {
670            ip: IpRoutines {
671                ingress: hook_with_rule(Rule {
672                    matcher: PacketMatcher::default(),
673                    action: Action::Jump(UninstalledRoutine::new(
674                        vec![Rule {
675                            matcher: udp_matcher(),
676                            action: Action::TransparentProxy(
677                                TransparentProxy::LocalPort(LOCAL_PORT)
678                            ),
679                            validation_info: RuleId::Valid,
680                        }],
681                        0,
682                    )),
683                    validation_info: RuleId::Valid,
684                }),
685                ..Default::default()
686            },
687            ..Default::default()
688        } =>
689        Ok(());
690        "transparent proxy available in target routine reachable from INGRESS"
691    )]
692    #[test_case(
693        Routines {
694            ip: IpRoutines {
695                egress: hook_with_rule(Rule {
696                    matcher: udp_matcher(),
697                    action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
698                    validation_info: RuleId::Invalid,
699                }),
700                ..Default::default()
701            },
702            ..Default::default()
703        } =>
704        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
705        "transparent proxy unavailable in IP EGRESS routine"
706    )]
707    #[test_case(
708        Routines {
709            ip: IpRoutines {
710                egress: hook_with_rule(Rule {
711                    matcher: PacketMatcher::default(),
712                    action: Action::Jump(UninstalledRoutine::new(
713                        vec![Rule {
714                            matcher: udp_matcher(),
715                            action: Action::TransparentProxy(
716                                TransparentProxy::LocalPort(LOCAL_PORT)
717                            ),
718                            validation_info: RuleId::Invalid,
719                        }],
720                        0,
721                    )),
722                    validation_info: RuleId::Valid,
723                }),
724                ..Default::default()
725            },
726            ..Default::default()
727        } =>
728        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
729        "transparent proxy unavailable in target routine reachable from EGRESS"
730    )]
731    #[test_case(
732        Routines {
733            nat: NatRoutines {
734                ingress: hook_with_rule(Rule {
735                    matcher: PacketMatcher::default(),
736                    action: Action::Redirect { dst_port: None },
737                    validation_info: RuleId::Valid,
738                }),
739                local_egress: hook_with_rule(Rule {
740                    matcher: PacketMatcher::default(),
741                    action: Action::Redirect { dst_port: None },
742                    validation_info: RuleId::Valid,
743                }),
744                ..Default::default()
745            },
746            ..Default::default()
747        } =>
748        Ok(());
749        "redirect available in NAT INGRESS and LOCAL_EGRESS routines"
750    )]
751    #[test_case(
752        Routines {
753            nat: NatRoutines {
754                egress: hook_with_rule(Rule {
755                    matcher: PacketMatcher::default(),
756                    action: Action::Redirect { dst_port: None },
757                    validation_info: RuleId::Invalid,
758                }),
759                ..Default::default()
760            },
761            ..Default::default()
762        } =>
763        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
764        "redirect unavailable in NAT EGRESS"
765    )]
766    #[test_case(
767        Routines {
768            ip: IpRoutines {
769                ingress: hook_with_rule(Rule {
770                    matcher: PacketMatcher::default(),
771                    action: Action::Redirect { dst_port: None },
772                    validation_info: RuleId::Invalid,
773                }),
774                ..Default::default()
775            },
776            ..Default::default()
777        } =>
778        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
779        "redirect unavailable in IP routines"
780    )]
781    #[test_case(
782        Routines {
783            nat: NatRoutines {
784                egress: hook_with_rule(Rule {
785                    matcher: PacketMatcher::default(),
786                    action: Action::Masquerade { src_port: None },
787                    validation_info: RuleId::Valid,
788                }),
789                ..Default::default()
790            },
791            ..Default::default()
792        } =>
793        Ok(());
794        "masquerade available in NAT EGRESS"
795    )]
796    #[test_case(
797        Routines {
798            nat: NatRoutines {
799                local_ingress: hook_with_rule(Rule {
800                    matcher: PacketMatcher::default(),
801                    action: Action::Masquerade { src_port: None },
802                    validation_info: RuleId::Invalid,
803                }),
804                ..Default::default()
805            },
806            ..Default::default()
807        } =>
808        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
809        "masquerade unavailable in NAT LOCAL_INGRESS"
810    )]
811    #[test_case(
812        Routines {
813            ip: IpRoutines {
814                egress: hook_with_rule(Rule {
815                    matcher: PacketMatcher::default(),
816                    action: Action::Masquerade { src_port: None },
817                    validation_info: RuleId::Invalid,
818                }),
819                ..Default::default()
820            },
821            ..Default::default()
822        } =>
823        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
824        "masquerade unavailable in IP routines"
825    )]
826    fn validate_action_available<I: IpExt>(
827        routines: Routines<I, FakeDeviceClass, RuleId>,
828    ) -> Result<(), ValidationError<RuleId>> {
829        ValidRoutines::new(routines).map(|_| ())
830    }
831
832    #[ip_test(I)]
833    #[test_case(
834        Routine {
835            rules: vec![Rule {
836                matcher: tcp_matcher(),
837                action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
838                validation_info: RuleId::Valid,
839            }],
840        } =>
841        Ok(());
842        "transparent proxy valid with TCP matcher"
843    )]
844    #[test_case(
845        Routine {
846            rules: vec![Rule {
847                matcher: udp_matcher(),
848                action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
849                validation_info: RuleId::Valid,
850            }],
851        } =>
852        Ok(());
853        "transparent proxy valid with UDP matcher"
854    )]
855    #[test_case(
856        Routine {
857            rules: vec![Rule {
858                matcher: icmp_matcher(),
859                action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
860                validation_info: RuleId::Invalid,
861            }],
862        } =>
863        Err(ValidationError::TransparentProxyWithInvalidMatcher(RuleId::Invalid));
864        "transparent proxy invalid with ICMP matcher"
865    )]
866    #[test_case(
867        Routine {
868            rules: vec![Rule {
869                matcher: PacketMatcher::default(),
870                action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
871                validation_info: RuleId::Invalid,
872            }],
873        } =>
874        Err(ValidationError::TransparentProxyWithInvalidMatcher(RuleId::Invalid));
875        "transparent proxy invalid with no transport protocol matcher"
876    )]
877    fn validate_transparent_proxy_matcher<I: IpExt>(
878        routine: Routine<I, FakeDeviceClass, RuleId>,
879    ) -> Result<(), ValidationError<RuleId>> {
880        validate_routine(&routine, &[], &[])
881    }
882
883    #[ip_test(I)]
884    #[test_case(
885        Routine {
886            rules: vec![Rule {
887                matcher: PacketMatcher::default(),
888                action: Action::Redirect { dst_port: None },
889                validation_info: RuleId::Valid,
890            }],
891        } =>
892        Ok(());
893        "redirect valid with no matcher if dst port unspecified"
894    )]
895    #[test_case(
896        Routine {
897            rules: vec![Rule {
898                matcher: tcp_matcher(),
899                action: Action::Redirect { dst_port: Some(LOCAL_PORT..=LOCAL_PORT) },
900                validation_info: RuleId::Valid,
901            }],
902        } =>
903        Ok(());
904        "redirect valid with TCP matcher when dst port specified"
905    )]
906    #[test_case(
907        Routine {
908            rules: vec![Rule {
909                matcher: udp_matcher(),
910                action: Action::Redirect { dst_port: Some(LOCAL_PORT..=LOCAL_PORT) },
911                validation_info: RuleId::Valid,
912            }],
913        } =>
914        Ok(());
915        "redirect valid with UDP matcher when dst port specified"
916    )]
917    #[test_case(
918        Routine {
919            rules: vec![Rule {
920                matcher: icmp_matcher(),
921                action: Action::Redirect { dst_port: Some(LOCAL_PORT..=LOCAL_PORT) },
922                validation_info: RuleId::Invalid,
923            }],
924        } =>
925        Err(ValidationError::RedirectWithInvalidMatcher(RuleId::Invalid));
926        "redirect invalid with ICMP matcher when dst port specified"
927    )]
928    #[test_case(
929        Routine {
930            rules: vec![Rule {
931                matcher: PacketMatcher::default(),
932                action: Action::Redirect { dst_port: Some(LOCAL_PORT..=LOCAL_PORT) },
933                validation_info: RuleId::Invalid,
934            }],
935        } =>
936        Err(ValidationError::RedirectWithInvalidMatcher(RuleId::Invalid));
937        "redirect invalid with no transport protocol matcher when dst port specified"
938    )]
939    fn validate_redirect_matcher<I: IpExt>(
940        routine: Routine<I, FakeDeviceClass, RuleId>,
941    ) -> Result<(), ValidationError<RuleId>> {
942        validate_routine(&routine, &[], &[])
943    }
944
945    #[ip_test(I)]
946    #[test_case(
947        Routine {
948            rules: vec![Rule {
949                matcher: PacketMatcher::default(),
950                action: Action::Masquerade { src_port: None },
951                validation_info: RuleId::Valid,
952            }],
953        } =>
954        Ok(());
955        "masquerade valid with no matcher if src port unspecified"
956    )]
957    #[test_case(
958        Routine {
959            rules: vec![Rule {
960                matcher: tcp_matcher(),
961                action: Action::Masquerade { src_port: Some(LOCAL_PORT..=LOCAL_PORT) },
962                validation_info: RuleId::Valid,
963            }],
964        } =>
965        Ok(());
966        "masquerade valid with TCP matcher when src port specified"
967    )]
968    #[test_case(
969        Routine {
970            rules: vec![Rule {
971                matcher: udp_matcher(),
972                action: Action::Masquerade { src_port: Some(LOCAL_PORT..=LOCAL_PORT) },
973                validation_info: RuleId::Valid,
974            }],
975        } =>
976        Ok(());
977        "masquerade valid with UDP matcher when src port specified"
978    )]
979    #[test_case(
980        Routine {
981            rules: vec![Rule {
982                matcher: icmp_matcher(),
983                action: Action::Masquerade { src_port: Some(LOCAL_PORT..=LOCAL_PORT) },
984                validation_info: RuleId::Invalid,
985            }],
986        } =>
987        Err(ValidationError::MasqueradeWithInvalidMatcher(RuleId::Invalid));
988        "masquerade invalid with ICMP matcher when src port specified"
989    )]
990    #[test_case(
991        Routine {
992            rules: vec![Rule {
993                matcher: PacketMatcher::default(),
994                action: Action::Masquerade { src_port: Some(LOCAL_PORT..=LOCAL_PORT) },
995                validation_info: RuleId::Invalid,
996            }],
997        } =>
998        Err(ValidationError::MasqueradeWithInvalidMatcher(RuleId::Invalid));
999        "masquerade invalid with no transport protocol matcher when src port specified"
1000    )]
1001    fn validate_masquerade_matcher<I: IpExt>(
1002        routine: Routine<I, FakeDeviceClass, RuleId>,
1003    ) -> Result<(), ValidationError<RuleId>> {
1004        validate_routine(&routine, &[], &[])
1005    }
1006
1007    #[test]
1008    fn strip_debug_info_reuses_uninstalled_routines() {
1009        // Two routines in the hook jump to the same uninstalled routine.
1010        let uninstalled_routine =
1011            UninstalledRoutine::<Ipv4, FakeDeviceClass, _>::new(Vec::new(), 0);
1012        let hook = Hook {
1013            routines: vec![
1014                Routine {
1015                    rules: vec![Rule {
1016                        matcher: PacketMatcher::default(),
1017                        action: Action::Jump(uninstalled_routine.clone()),
1018                        validation_info: "rule-1",
1019                    }],
1020                },
1021                Routine {
1022                    rules: vec![Rule {
1023                        matcher: PacketMatcher::default(),
1024                        action: Action::Jump(uninstalled_routine),
1025                        validation_info: "rule-2",
1026                    }],
1027                },
1028            ],
1029        };
1030
1031        // When we strip the debug info from the routines in the hook, all
1032        // jump targets should be converted 1:1. In this case, there are two
1033        // jump actions that refer to the same uninstalled routine, so that
1034        // uninstalled routine should be converted once, and the resulting jump
1035        // actions should both point to the same new uninstalled routine.
1036        let Hook { routines } = hook.strip_debug_info(&mut UninstalledRoutineIndex::default());
1037        let (first, second) = assert_matches!(
1038            &routines[..],
1039            [Routine { rules: first }, Routine { rules: second }] => (first, second)
1040        );
1041        let first = assert_matches!(
1042            &first[..],
1043            [Rule { action: Action::Jump(target), .. }] => target
1044        );
1045        let second = assert_matches!(
1046            &second[..],
1047            [Rule { action: Action::Jump(target), .. }] => target
1048        );
1049        assert_eq!(first, second);
1050    }
1051}