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