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::collections::hash_map::Entry;
6use alloc::collections::HashMap;
7use alloc::sync::Arc;
8use alloc::vec::Vec;
9use core::fmt::Debug;
10
11use assert_matches::assert_matches;
12use derivative::Derivative;
13use net_types::ip::{GenericOverIp, Ip};
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, DeviceClass>(Routines<I, DeviceClass, ()>);
52
53impl<I: IpExt, DeviceClass> ValidRoutines<I, DeviceClass> {
54    /// Accesses the inner state.
55    pub fn get(&self) -> &Routines<I, DeviceClass, ()> {
56        let Self(state) = self;
57        &state
58    }
59}
60
61impl<I: IpExt, DeviceClass: Clone + Debug> ValidRoutines<I, DeviceClass> {
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, DeviceClass, RuleInfo>,
75    ) -> Result<(Self, Vec<UninstalledRoutine<I, DeviceClass, ()>>), ValidationError<RuleInfo>>
76    {
77        let Routines { ip: ip_routines, nat: nat_routines } = &routines;
78
79        // Ensure that no rule has a matcher that is unavailable in the context in which
80        // the rule will be evaluated.
81        let IpRoutines { ingress, local_ingress, egress, local_egress, forwarding } = ip_routines;
82        validate_hook(
83            &ingress,
84            &[UnavailableMatcher::OutInterface],
85            &[UnavailableAction::Redirect, UnavailableAction::Masquerade],
86        )?;
87        validate_hook(
88            &local_ingress,
89            &[UnavailableMatcher::OutInterface],
90            &[
91                UnavailableAction::TransparentProxy,
92                UnavailableAction::Redirect,
93                UnavailableAction::Masquerade,
94            ],
95        )?;
96        validate_hook(
97            &forwarding,
98            &[],
99            &[
100                UnavailableAction::TransparentProxy,
101                UnavailableAction::Redirect,
102                UnavailableAction::Masquerade,
103            ],
104        )?;
105        validate_hook(
106            &egress,
107            &[UnavailableMatcher::InInterface],
108            &[
109                UnavailableAction::TransparentProxy,
110                UnavailableAction::Redirect,
111                UnavailableAction::Masquerade,
112            ],
113        )?;
114        validate_hook(
115            &local_egress,
116            &[UnavailableMatcher::InInterface],
117            &[
118                UnavailableAction::TransparentProxy,
119                UnavailableAction::Redirect,
120                UnavailableAction::Masquerade,
121            ],
122        )?;
123
124        let NatRoutines { ingress, local_ingress, egress, local_egress } = nat_routines;
125        validate_hook(
126            &ingress,
127            &[UnavailableMatcher::OutInterface],
128            &[UnavailableAction::Masquerade, UnavailableAction::Mark],
129        )?;
130        validate_hook(
131            &local_ingress,
132            &[UnavailableMatcher::OutInterface],
133            &[
134                UnavailableAction::TransparentProxy,
135                UnavailableAction::Redirect,
136                UnavailableAction::Masquerade,
137                UnavailableAction::Mark,
138            ],
139        )?;
140        validate_hook(
141            &egress,
142            &[UnavailableMatcher::InInterface],
143            &[
144                UnavailableAction::TransparentProxy,
145                UnavailableAction::Redirect,
146                UnavailableAction::Mark,
147            ],
148        )?;
149        validate_hook(
150            &local_egress,
151            &[UnavailableMatcher::InInterface],
152            &[
153                UnavailableAction::TransparentProxy,
154                UnavailableAction::Masquerade,
155                UnavailableAction::Mark,
156            ],
157        )?;
158
159        let mut index = UninstalledRoutineIndex::default();
160        let routines = routines.strip_debug_info(&mut index);
161        Ok((Self(routines), index.into_values()))
162    }
163}
164
165#[derive(Clone, Copy)]
166enum UnavailableMatcher {
167    InInterface,
168    OutInterface,
169}
170
171impl UnavailableMatcher {
172    fn validate<I: IpExt, DeviceClass, RuleInfo: Clone>(
173        &self,
174        matcher: &PacketMatcher<I, DeviceClass>,
175        rule: &RuleInfo,
176    ) -> Result<(), ValidationError<RuleInfo>> {
177        let unavailable_matcher = match self {
178            UnavailableMatcher::InInterface => matcher.in_interface.as_ref(),
179            UnavailableMatcher::OutInterface => matcher.out_interface.as_ref(),
180        };
181        if unavailable_matcher.is_some() {
182            Err(ValidationError::RuleWithInvalidMatcher(rule.clone()))
183        } else {
184            Ok(())
185        }
186    }
187}
188
189#[derive(Clone, Copy)]
190enum UnavailableAction {
191    TransparentProxy,
192    Redirect,
193    Masquerade,
194    Mark,
195}
196
197impl UnavailableAction {
198    fn validate<I: IpExt, DeviceClass, RuleInfo: Clone>(
199        &self,
200        action: &Action<I, DeviceClass, RuleInfo>,
201        rule: &RuleInfo,
202    ) -> Result<(), ValidationError<RuleInfo>> {
203        match (self, action) {
204            (UnavailableAction::TransparentProxy, Action::TransparentProxy(_))
205            | (UnavailableAction::Redirect, Action::Redirect { .. })
206            | (UnavailableAction::Masquerade, Action::Masquerade { .. })
207            | (UnavailableAction::Mark, Action::Mark { .. }) => {
208                Err(ValidationError::RuleWithInvalidAction(rule.clone()))
209            }
210            _ => Ok(()),
211        }
212    }
213}
214
215/// Ensures that no rules reachable from this hook match on
216/// `unavailable_matcher`.
217fn validate_hook<I: IpExt, DeviceClass, RuleInfo: Clone>(
218    Hook { routines }: &Hook<I, DeviceClass, RuleInfo>,
219    unavailable_matchers: &[UnavailableMatcher],
220    unavailable_actions: &[UnavailableAction],
221) -> Result<(), ValidationError<RuleInfo>> {
222    for routine in routines {
223        validate_routine(routine, unavailable_matchers, unavailable_actions)?;
224    }
225
226    Ok(())
227}
228
229/// Ensures that:
230///  * no rules reachable from this routine match on any of the
231///    `unavailable_matchers`.
232///  * no rules reachable from this routine include one of the
233///    `unavailable_actions`.
234///  * all rules reachable from this routine have matchers that are compatible
235///    with their actions (for example, specifying a port rewrite requires that
236///    a transport protocol matcher be present).
237fn validate_routine<I: IpExt, DeviceClass, RuleInfo: Clone>(
238    Routine { rules }: &Routine<I, DeviceClass, RuleInfo>,
239    unavailable_matchers: &[UnavailableMatcher],
240    unavailable_actions: &[UnavailableAction],
241) -> Result<(), ValidationError<RuleInfo>> {
242    for Rule { matcher, action, validation_info } in rules {
243        for unavailable in unavailable_matchers {
244            unavailable.validate(matcher, validation_info)?;
245        }
246        for unavailable in unavailable_actions {
247            unavailable.validate(action, validation_info)?;
248        }
249
250        let has_tcp_or_udp_matcher = |matcher: &PacketMatcher<_, _>| {
251            let Some(TransportProtocolMatcher { proto, .. }) = matcher.transport_protocol else {
252                return false;
253            };
254            I::map_ip(
255                proto,
256                |proto| match proto {
257                    Ipv4Proto::Proto(IpProto::Tcp | IpProto::Udp) => true,
258                    _ => false,
259                },
260                |proto| match proto {
261                    Ipv6Proto::Proto(IpProto::Tcp | IpProto::Udp) => true,
262                    _ => false,
263                },
264            )
265        };
266
267        match action {
268            Action::Accept | Action::Drop | Action::Return | Action::Mark { .. } => {}
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, Debug)]
311#[derivative(PartialEq(bound = ""))]
312enum ConvertedRoutine<I: IpExt, DeviceClass> {
313    InProgress,
314    Done(UninstalledRoutine<I, DeviceClass, ()>),
315}
316
317#[derive(Derivative)]
318#[derivative(Default(bound = ""))]
319struct UninstalledRoutineIndex<I: IpExt, DeviceClass, RuleInfo> {
320    index: HashMap<UninstalledRoutine<I, DeviceClass, RuleInfo>, ConvertedRoutine<I, DeviceClass>>,
321}
322
323impl<I: IpExt, DeviceClass: Clone + Debug + Debug, RuleInfo: Clone>
324    UninstalledRoutineIndex<I, DeviceClass, RuleInfo>
325{
326    fn get_or_insert_with(
327        &mut self,
328        target: UninstalledRoutine<I, DeviceClass, RuleInfo>,
329        convert: impl FnOnce(
330            &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
331        ) -> UninstalledRoutine<I, DeviceClass, ()>,
332    ) -> UninstalledRoutine<I, DeviceClass, ()> {
333        match self.index.entry(target.clone()) {
334            Entry::Occupied(entry) => match entry.get() {
335                ConvertedRoutine::InProgress => panic!("cycle in routine graph"),
336                ConvertedRoutine::Done(routine) => return routine.clone(),
337            },
338            Entry::Vacant(entry) => {
339                let _ = entry.insert(ConvertedRoutine::InProgress);
340            }
341        }
342        // Convert the target routine and store it in the index, so that the next time
343        // we attempt to convert it, we just reuse the already-converted routine.
344        let converted = convert(self);
345        let previous = self.index.insert(target, ConvertedRoutine::Done(converted.clone()));
346        assert_eq!(previous, Some(ConvertedRoutine::InProgress));
347        converted
348    }
349
350    fn into_values(self) -> Vec<UninstalledRoutine<I, DeviceClass, ()>> {
351        self.index
352            .into_values()
353            .map(|routine| assert_matches!(routine, ConvertedRoutine::Done(routine) => routine))
354            .collect()
355    }
356}
357
358impl<I: IpExt, DeviceClass: Clone + Debug, RuleInfo: Clone> Routines<I, DeviceClass, RuleInfo> {
359    fn strip_debug_info(
360        self,
361        index: &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
362    ) -> Routines<I, DeviceClass, ()> {
363        let Self { ip: ip_routines, nat: nat_routines } = self;
364        Routines {
365            ip: ip_routines.strip_debug_info(index),
366            nat: nat_routines.strip_debug_info(index),
367        }
368    }
369}
370
371impl<I: IpExt, DeviceClass: Clone + Debug, RuleInfo: Clone> IpRoutines<I, DeviceClass, RuleInfo> {
372    fn strip_debug_info(
373        self,
374        index: &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
375    ) -> IpRoutines<I, DeviceClass, ()> {
376        let Self { ingress, local_ingress, egress, local_egress, forwarding } = self;
377        IpRoutines {
378            ingress: ingress.strip_debug_info(index),
379            local_ingress: local_ingress.strip_debug_info(index),
380            forwarding: forwarding.strip_debug_info(index),
381            egress: egress.strip_debug_info(index),
382            local_egress: local_egress.strip_debug_info(index),
383        }
384    }
385}
386
387impl<I: IpExt, DeviceClass: Clone + Debug, RuleInfo: Clone> NatRoutines<I, DeviceClass, RuleInfo> {
388    fn strip_debug_info(
389        self,
390        index: &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
391    ) -> NatRoutines<I, DeviceClass, ()> {
392        let Self { ingress, local_ingress, egress, local_egress } = self;
393        NatRoutines {
394            ingress: ingress.strip_debug_info(index),
395            local_ingress: local_ingress.strip_debug_info(index),
396            egress: egress.strip_debug_info(index),
397            local_egress: local_egress.strip_debug_info(index),
398        }
399    }
400}
401
402impl<I: IpExt, DeviceClass: Clone + Debug, RuleInfo: Clone> Hook<I, DeviceClass, RuleInfo> {
403    fn strip_debug_info(
404        self,
405        index: &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
406    ) -> Hook<I, DeviceClass, ()> {
407        let Self { routines } = self;
408        Hook {
409            routines: routines.into_iter().map(|routine| routine.strip_debug_info(index)).collect(),
410        }
411    }
412}
413
414impl<I: IpExt, DeviceClass: Clone + Debug, RuleInfo: Clone> Routine<I, DeviceClass, RuleInfo> {
415    fn strip_debug_info(
416        self,
417        index: &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
418    ) -> Routine<I, DeviceClass, ()> {
419        let Self { rules } = self;
420        Routine {
421            rules: rules
422                .into_iter()
423                .map(|Rule { matcher, action, validation_info: _ }| Rule {
424                    matcher,
425                    action: action.strip_debug_info(index),
426                    validation_info: (),
427                })
428                .collect(),
429        }
430    }
431}
432
433impl<I: IpExt, DeviceClass: Clone + Debug, RuleInfo: Clone> Action<I, DeviceClass, RuleInfo> {
434    fn strip_debug_info(
435        self,
436        index: &mut UninstalledRoutineIndex<I, DeviceClass, RuleInfo>,
437    ) -> Action<I, DeviceClass, ()> {
438        match self {
439            Self::Accept => Action::Accept,
440            Self::Drop => Action::Drop,
441            Self::Return => Action::Return,
442            Self::TransparentProxy(proxy) => Action::TransparentProxy(proxy),
443            Self::Redirect { dst_port } => Action::Redirect { dst_port },
444            Self::Masquerade { src_port } => Action::Masquerade { src_port },
445            Self::Mark { domain, action } => Action::Mark { domain, action },
446            Self::Jump(target) => {
447                let converted = index.get_or_insert_with(target.clone(), |index| {
448                    // Recursively strip debug info from the target routine.
449                    let UninstalledRoutine { ref routine, id } = target;
450                    UninstalledRoutine {
451                        routine: Arc::new(Routine::clone(&*routine).strip_debug_info(index)),
452                        id,
453                    }
454                });
455                Action::Jump(converted)
456            }
457        }
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use alloc::vec;
464    use core::num::NonZeroU16;
465
466    use assert_matches::assert_matches;
467    use ip_test_macro::ip_test;
468    use net_types::ip::Ipv4;
469    use test_case::test_case;
470
471    use super::*;
472    use crate::context::testutil::FakeDeviceClass;
473    use crate::{InterfaceMatcher, 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}