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