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 test_case::test_case;
469
470    use super::*;
471    use crate::context::testutil::FakeDeviceClass;
472    use crate::{InterfaceMatcher, PacketMatcher, TransparentProxy};
473
474    #[derive(Debug, Clone, PartialEq)]
475    enum RuleId {
476        Valid,
477        Invalid,
478    }
479
480    fn rule<I: IpExt>(
481        matcher: PacketMatcher<I, FakeDeviceClass>,
482        validation_info: RuleId,
483    ) -> Rule<I, FakeDeviceClass, RuleId> {
484        Rule { matcher, action: Action::Drop, validation_info }
485    }
486
487    fn hook_with_rules<I: IpExt>(
488        rules: Vec<Rule<I, FakeDeviceClass, RuleId>>,
489    ) -> Hook<I, FakeDeviceClass, RuleId> {
490        Hook { routines: vec![Routine { rules }] }
491    }
492
493    #[ip_test(I)]
494    #[test_case(
495        hook_with_rules(vec![rule(
496            PacketMatcher {
497                in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
498                ..Default::default()
499            },
500            RuleId::Valid,
501        )]),
502        UnavailableMatcher::OutInterface =>
503        Ok(());
504        "match on input interface in root routine when available"
505    )]
506    #[test_case(
507        hook_with_rules(vec![rule(
508            PacketMatcher {
509                out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
510                ..Default::default()
511            },
512            RuleId::Valid,
513        )]),
514        UnavailableMatcher::InInterface =>
515        Ok(());
516        "match on output interface in root routine when available"
517    )]
518    #[test_case(
519        hook_with_rules(vec![
520            rule(PacketMatcher::default(), RuleId::Valid),
521            rule(
522                PacketMatcher {
523                    in_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
524                    ..Default::default()
525                },
526                RuleId::Invalid,
527            ),
528        ]),
529        UnavailableMatcher::InInterface =>
530        Err(ValidationError::RuleWithInvalidMatcher(RuleId::Invalid));
531        "match on input interface in root routine when unavailable"
532    )]
533    #[test_case(
534        hook_with_rules(vec![
535            rule(PacketMatcher::default(), RuleId::Valid),
536            rule(
537                PacketMatcher {
538                    out_interface: Some(InterfaceMatcher::DeviceClass(FakeDeviceClass::Ethernet)),
539                    ..Default::default()
540                },
541                RuleId::Invalid,
542            ),
543        ]),
544        UnavailableMatcher::OutInterface =>
545        Err(ValidationError::RuleWithInvalidMatcher(RuleId::Invalid));
546        "match on output interface in root routine when unavailable"
547    )]
548    #[test_case(
549        Hook {
550            routines: vec![Routine {
551                rules: vec![Rule {
552                    matcher: PacketMatcher::default(),
553                    action: Action::Jump(UninstalledRoutine::new(
554                        vec![rule(
555                            PacketMatcher {
556                                in_interface: Some(InterfaceMatcher::DeviceClass(
557                                    FakeDeviceClass::Ethernet,
558                                )),
559                                ..Default::default()
560                            },
561                            RuleId::Invalid,
562                        )],
563                        0,
564                    )),
565                    validation_info: RuleId::Valid,
566                }],
567            }],
568        },
569        UnavailableMatcher::InInterface =>
570        Err(ValidationError::RuleWithInvalidMatcher(RuleId::Invalid));
571        "match on input interface in target routine when unavailable"
572    )]
573    #[test_case(
574        Hook {
575            routines: vec![Routine {
576                rules: vec![Rule {
577                    matcher: PacketMatcher::default(),
578                    action: Action::Jump(UninstalledRoutine::new(
579                        vec![rule(
580                            PacketMatcher {
581                                out_interface: Some(InterfaceMatcher::DeviceClass(
582                                    FakeDeviceClass::Ethernet,
583                                )),
584                                ..Default::default()
585                            },
586                            RuleId::Invalid,
587                        )],
588                        0,
589                    )),
590                    validation_info: RuleId::Valid,
591                }],
592            }],
593        },
594        UnavailableMatcher::OutInterface =>
595        Err(ValidationError::RuleWithInvalidMatcher(RuleId::Invalid));
596        "match on output interface in target routine when unavailable"
597    )]
598    fn validate_interface_matcher_available<I: IpExt>(
599        hook: Hook<I, FakeDeviceClass, RuleId>,
600        unavailable_matcher: UnavailableMatcher,
601    ) -> Result<(), ValidationError<RuleId>> {
602        validate_hook(&hook, &[unavailable_matcher], &[])
603    }
604
605    fn hook_with_rule<I: IpExt>(
606        rule: Rule<I, FakeDeviceClass, RuleId>,
607    ) -> Hook<I, FakeDeviceClass, RuleId> {
608        Hook { routines: vec![Routine { rules: vec![rule] }] }
609    }
610
611    fn transport_matcher<I: IpExt>(proto: I::Proto) -> PacketMatcher<I, FakeDeviceClass> {
612        PacketMatcher {
613            transport_protocol: Some(TransportProtocolMatcher {
614                proto,
615                src_port: None,
616                dst_port: None,
617            }),
618            ..Default::default()
619        }
620    }
621
622    fn udp_matcher<I: IpExt>() -> PacketMatcher<I, FakeDeviceClass> {
623        transport_matcher(I::map_ip(
624            (),
625            |()| Ipv4Proto::Proto(IpProto::Udp),
626            |()| Ipv6Proto::Proto(IpProto::Udp),
627        ))
628    }
629
630    fn tcp_matcher<I: IpExt>() -> PacketMatcher<I, FakeDeviceClass> {
631        transport_matcher(I::map_ip(
632            (),
633            |()| Ipv4Proto::Proto(IpProto::Tcp),
634            |()| Ipv6Proto::Proto(IpProto::Tcp),
635        ))
636    }
637
638    fn icmp_matcher<I: IpExt>() -> PacketMatcher<I, FakeDeviceClass> {
639        transport_matcher(I::map_ip((), |()| Ipv4Proto::Icmp, |()| Ipv6Proto::Icmpv6))
640    }
641
642    const LOCAL_PORT: NonZeroU16 = NonZeroU16::new(8080).unwrap();
643
644    #[ip_test(I)]
645    #[test_case(
646        Routines {
647            ip: IpRoutines {
648                ingress: hook_with_rule(Rule {
649                    matcher: udp_matcher(),
650                    action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
651                    validation_info: RuleId::Valid,
652                }),
653                ..Default::default()
654            },
655            nat: NatRoutines {
656                ingress: hook_with_rule(Rule {
657                    matcher: tcp_matcher(),
658                    action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
659                    validation_info: RuleId::Valid,
660                }),
661                ..Default::default()
662            },
663        } =>
664        Ok(());
665        "transparent proxy available in IP and NAT INGRESS routines"
666    )]
667    #[test_case(
668        Routines {
669            ip: IpRoutines {
670                ingress: hook_with_rule(Rule {
671                    matcher: PacketMatcher::default(),
672                    action: Action::Jump(UninstalledRoutine::new(
673                        vec![Rule {
674                            matcher: udp_matcher(),
675                            action: Action::TransparentProxy(
676                                TransparentProxy::LocalPort(LOCAL_PORT)
677                            ),
678                            validation_info: RuleId::Valid,
679                        }],
680                        0,
681                    )),
682                    validation_info: RuleId::Valid,
683                }),
684                ..Default::default()
685            },
686            ..Default::default()
687        } =>
688        Ok(());
689        "transparent proxy available in target routine reachable from INGRESS"
690    )]
691    #[test_case(
692        Routines {
693            ip: IpRoutines {
694                egress: hook_with_rule(Rule {
695                    matcher: udp_matcher(),
696                    action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
697                    validation_info: RuleId::Invalid,
698                }),
699                ..Default::default()
700            },
701            ..Default::default()
702        } =>
703        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
704        "transparent proxy unavailable in IP EGRESS routine"
705    )]
706    #[test_case(
707        Routines {
708            ip: IpRoutines {
709                egress: hook_with_rule(Rule {
710                    matcher: PacketMatcher::default(),
711                    action: Action::Jump(UninstalledRoutine::new(
712                        vec![Rule {
713                            matcher: udp_matcher(),
714                            action: Action::TransparentProxy(
715                                TransparentProxy::LocalPort(LOCAL_PORT)
716                            ),
717                            validation_info: RuleId::Invalid,
718                        }],
719                        0,
720                    )),
721                    validation_info: RuleId::Valid,
722                }),
723                ..Default::default()
724            },
725            ..Default::default()
726        } =>
727        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
728        "transparent proxy unavailable in target routine reachable from EGRESS"
729    )]
730    #[test_case(
731        Routines {
732            nat: NatRoutines {
733                ingress: hook_with_rule(Rule {
734                    matcher: PacketMatcher::default(),
735                    action: Action::Redirect { dst_port: None },
736                    validation_info: RuleId::Valid,
737                }),
738                local_egress: hook_with_rule(Rule {
739                    matcher: PacketMatcher::default(),
740                    action: Action::Redirect { dst_port: None },
741                    validation_info: RuleId::Valid,
742                }),
743                ..Default::default()
744            },
745            ..Default::default()
746        } =>
747        Ok(());
748        "redirect available in NAT INGRESS and LOCAL_EGRESS routines"
749    )]
750    #[test_case(
751        Routines {
752            nat: NatRoutines {
753                egress: hook_with_rule(Rule {
754                    matcher: PacketMatcher::default(),
755                    action: Action::Redirect { dst_port: None },
756                    validation_info: RuleId::Invalid,
757                }),
758                ..Default::default()
759            },
760            ..Default::default()
761        } =>
762        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
763        "redirect unavailable in NAT EGRESS"
764    )]
765    #[test_case(
766        Routines {
767            ip: IpRoutines {
768                ingress: hook_with_rule(Rule {
769                    matcher: PacketMatcher::default(),
770                    action: Action::Redirect { dst_port: None },
771                    validation_info: RuleId::Invalid,
772                }),
773                ..Default::default()
774            },
775            ..Default::default()
776        } =>
777        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
778        "redirect unavailable in IP routines"
779    )]
780    #[test_case(
781        Routines {
782            nat: NatRoutines {
783                egress: hook_with_rule(Rule {
784                    matcher: PacketMatcher::default(),
785                    action: Action::Masquerade { src_port: None },
786                    validation_info: RuleId::Valid,
787                }),
788                ..Default::default()
789            },
790            ..Default::default()
791        } =>
792        Ok(());
793        "masquerade available in NAT EGRESS"
794    )]
795    #[test_case(
796        Routines {
797            nat: NatRoutines {
798                local_ingress: hook_with_rule(Rule {
799                    matcher: PacketMatcher::default(),
800                    action: Action::Masquerade { src_port: None },
801                    validation_info: RuleId::Invalid,
802                }),
803                ..Default::default()
804            },
805            ..Default::default()
806        } =>
807        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
808        "masquerade unavailable in NAT LOCAL_INGRESS"
809    )]
810    #[test_case(
811        Routines {
812            ip: IpRoutines {
813                egress: hook_with_rule(Rule {
814                    matcher: PacketMatcher::default(),
815                    action: Action::Masquerade { src_port: None },
816                    validation_info: RuleId::Invalid,
817                }),
818                ..Default::default()
819            },
820            ..Default::default()
821        } =>
822        Err(ValidationError::RuleWithInvalidAction(RuleId::Invalid));
823        "masquerade unavailable in IP routines"
824    )]
825    fn validate_action_available<I: IpExt>(
826        routines: Routines<I, FakeDeviceClass, RuleId>,
827    ) -> Result<(), ValidationError<RuleId>> {
828        ValidRoutines::new(routines).map(|_| ())
829    }
830
831    #[ip_test(I)]
832    #[test_case(
833        Routine {
834            rules: vec![Rule {
835                matcher: tcp_matcher(),
836                action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
837                validation_info: RuleId::Valid,
838            }],
839        } =>
840        Ok(());
841        "transparent proxy valid with TCP matcher"
842    )]
843    #[test_case(
844        Routine {
845            rules: vec![Rule {
846                matcher: udp_matcher(),
847                action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
848                validation_info: RuleId::Valid,
849            }],
850        } =>
851        Ok(());
852        "transparent proxy valid with UDP matcher"
853    )]
854    #[test_case(
855        Routine {
856            rules: vec![Rule {
857                matcher: icmp_matcher(),
858                action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
859                validation_info: RuleId::Invalid,
860            }],
861        } =>
862        Err(ValidationError::TransparentProxyWithInvalidMatcher(RuleId::Invalid));
863        "transparent proxy invalid with ICMP matcher"
864    )]
865    #[test_case(
866        Routine {
867            rules: vec![Rule {
868                matcher: PacketMatcher::default(),
869                action: Action::TransparentProxy(TransparentProxy::LocalPort(LOCAL_PORT)),
870                validation_info: RuleId::Invalid,
871            }],
872        } =>
873        Err(ValidationError::TransparentProxyWithInvalidMatcher(RuleId::Invalid));
874        "transparent proxy invalid with no transport protocol matcher"
875    )]
876    fn validate_transparent_proxy_matcher<I: IpExt>(
877        routine: Routine<I, FakeDeviceClass, RuleId>,
878    ) -> Result<(), ValidationError<RuleId>> {
879        validate_routine(&routine, &[], &[])
880    }
881
882    #[ip_test(I)]
883    #[test_case(
884        Routine {
885            rules: vec![Rule {
886                matcher: PacketMatcher::default(),
887                action: Action::Redirect { dst_port: None },
888                validation_info: RuleId::Valid,
889            }],
890        } =>
891        Ok(());
892        "redirect valid with no matcher if dst port unspecified"
893    )]
894    #[test_case(
895        Routine {
896            rules: vec![Rule {
897                matcher: tcp_matcher(),
898                action: Action::Redirect { dst_port: Some(LOCAL_PORT..=LOCAL_PORT) },
899                validation_info: RuleId::Valid,
900            }],
901        } =>
902        Ok(());
903        "redirect valid with TCP matcher when dst port specified"
904    )]
905    #[test_case(
906        Routine {
907            rules: vec![Rule {
908                matcher: udp_matcher(),
909                action: Action::Redirect { dst_port: Some(LOCAL_PORT..=LOCAL_PORT) },
910                validation_info: RuleId::Valid,
911            }],
912        } =>
913        Ok(());
914        "redirect valid with UDP matcher when dst port specified"
915    )]
916    #[test_case(
917        Routine {
918            rules: vec![Rule {
919                matcher: icmp_matcher(),
920                action: Action::Redirect { dst_port: Some(LOCAL_PORT..=LOCAL_PORT) },
921                validation_info: RuleId::Invalid,
922            }],
923        } =>
924        Err(ValidationError::RedirectWithInvalidMatcher(RuleId::Invalid));
925        "redirect invalid with ICMP matcher when dst port specified"
926    )]
927    #[test_case(
928        Routine {
929            rules: vec![Rule {
930                matcher: PacketMatcher::default(),
931                action: Action::Redirect { dst_port: Some(LOCAL_PORT..=LOCAL_PORT) },
932                validation_info: RuleId::Invalid,
933            }],
934        } =>
935        Err(ValidationError::RedirectWithInvalidMatcher(RuleId::Invalid));
936        "redirect invalid with no transport protocol matcher when dst port specified"
937    )]
938    fn validate_redirect_matcher<I: IpExt>(
939        routine: Routine<I, FakeDeviceClass, RuleId>,
940    ) -> Result<(), ValidationError<RuleId>> {
941        validate_routine(&routine, &[], &[])
942    }
943
944    #[ip_test(I)]
945    #[test_case(
946        Routine {
947            rules: vec![Rule {
948                matcher: PacketMatcher::default(),
949                action: Action::Masquerade { src_port: None },
950                validation_info: RuleId::Valid,
951            }],
952        } =>
953        Ok(());
954        "masquerade valid with no matcher if src port unspecified"
955    )]
956    #[test_case(
957        Routine {
958            rules: vec![Rule {
959                matcher: tcp_matcher(),
960                action: Action::Masquerade { src_port: Some(LOCAL_PORT..=LOCAL_PORT) },
961                validation_info: RuleId::Valid,
962            }],
963        } =>
964        Ok(());
965        "masquerade valid with TCP matcher when src port specified"
966    )]
967    #[test_case(
968        Routine {
969            rules: vec![Rule {
970                matcher: udp_matcher(),
971                action: Action::Masquerade { src_port: Some(LOCAL_PORT..=LOCAL_PORT) },
972                validation_info: RuleId::Valid,
973            }],
974        } =>
975        Ok(());
976        "masquerade valid with UDP matcher when src port specified"
977    )]
978    #[test_case(
979        Routine {
980            rules: vec![Rule {
981                matcher: icmp_matcher(),
982                action: Action::Masquerade { src_port: Some(LOCAL_PORT..=LOCAL_PORT) },
983                validation_info: RuleId::Invalid,
984            }],
985        } =>
986        Err(ValidationError::MasqueradeWithInvalidMatcher(RuleId::Invalid));
987        "masquerade invalid with ICMP matcher when src port specified"
988    )]
989    #[test_case(
990        Routine {
991            rules: vec![Rule {
992                matcher: PacketMatcher::default(),
993                action: Action::Masquerade { src_port: Some(LOCAL_PORT..=LOCAL_PORT) },
994                validation_info: RuleId::Invalid,
995            }],
996        } =>
997        Err(ValidationError::MasqueradeWithInvalidMatcher(RuleId::Invalid));
998        "masquerade invalid with no transport protocol matcher when src port specified"
999    )]
1000    fn validate_masquerade_matcher<I: IpExt>(
1001        routine: Routine<I, FakeDeviceClass, RuleId>,
1002    ) -> Result<(), ValidationError<RuleId>> {
1003        validate_routine(&routine, &[], &[])
1004    }
1005
1006    #[test]
1007    fn strip_debug_info_reuses_uninstalled_routines() {
1008        // Two routines in the hook jump to the same uninstalled routine.
1009        let uninstalled_routine =
1010            UninstalledRoutine::<Ipv4, FakeDeviceClass, _>::new(Vec::new(), 0);
1011        let hook = Hook {
1012            routines: vec![
1013                Routine {
1014                    rules: vec![Rule {
1015                        matcher: PacketMatcher::default(),
1016                        action: Action::Jump(uninstalled_routine.clone()),
1017                        validation_info: "rule-1",
1018                    }],
1019                },
1020                Routine {
1021                    rules: vec![Rule {
1022                        matcher: PacketMatcher::default(),
1023                        action: Action::Jump(uninstalled_routine),
1024                        validation_info: "rule-2",
1025                    }],
1026                },
1027            ],
1028        };
1029
1030        // When we strip the debug info from the routines in the hook, all
1031        // jump targets should be converted 1:1. In this case, there are two
1032        // jump actions that refer to the same uninstalled routine, so that
1033        // uninstalled routine should be converted once, and the resulting jump
1034        // actions should both point to the same new uninstalled routine.
1035        let Hook { routines } = hook.strip_debug_info(&mut UninstalledRoutineIndex::default());
1036        let (first, second) = assert_matches!(
1037            &routines[..],
1038            [Routine { rules: first }, Routine { rules: second }] => (first, second)
1039        );
1040        let first = assert_matches!(
1041            &first[..],
1042            [Rule { action: Action::Jump(target), .. }] => target
1043        );
1044        let second = assert_matches!(
1045            &second[..],
1046            [Rule { action: Action::Jump(target), .. }] => target
1047        );
1048        assert_eq!(first, second);
1049    }
1050}