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