1use std::str::FromStr;
33
34use ipnetwork::IpNetwork;
35
36use super::{
37 Action, Destination, DestinationGroup, Direction, DomainName, DomainNameError, NetworkPolicy,
38 PortRange, Protocol, Rule,
39};
40
41#[derive(Debug, thiserror::Error)]
55pub enum BuildError {
56 #[error(
58 "rule #{rule_index}: direction not set; call .egress(), .ingress(), or .any() before the rule-adder"
59 )]
60 DirectionNotSet { rule_index: usize },
61
62 #[error(
65 "rule #{rule_index}: destination not set; call .ip(), .cidr(), .domain(), .domain_suffix(), .group(), or .any() on the rule-destination builder"
66 )]
67 MissingDestination { rule_index: usize },
68
69 #[error("rule #{rule_index}: invalid IP address `{raw}`")]
72 InvalidIp { rule_index: usize, raw: String },
73
74 #[error("rule #{rule_index}: invalid CIDR `{raw}`")]
76 InvalidCidr { rule_index: usize, raw: String },
77
78 #[error("rule #{rule_index}: invalid domain `{raw}`: {source}")]
81 InvalidDomain {
82 rule_index: usize,
83 raw: String,
84 #[source]
85 source: DomainNameError,
86 },
87
88 #[error("rule #{rule_index}: invalid port range {lo}..{hi}; lo must be <= hi")]
90 InvalidPortRange { rule_index: usize, lo: u16, hi: u16 },
91
92 #[error(
96 "rule #{rule_index}: ICMP protocols are egress-only; ingress and any-direction rules cannot include icmpv4 or icmpv6"
97 )]
98 IngressDoesNotSupportIcmp { rule_index: usize },
99
100 #[error("invalid blocked domain `{raw}`: {source}")]
103 InvalidBlockedDomain {
104 raw: String,
105 #[source]
106 source: DomainNameError,
107 },
108
109 #[error("invalid blocked domain suffix `{raw}`: {source}")]
112 InvalidBlockedDomainSuffix {
113 raw: String,
114 #[source]
115 source: DomainNameError,
116 },
117}
118
119#[derive(Debug, Default)]
127pub struct NetworkPolicyBuilder {
128 default_egress: Option<Action>,
129 default_ingress: Option<Action>,
130 pending_rules: Vec<PendingRule>,
131 errors: Vec<BuildError>,
132}
133
134impl NetworkPolicyBuilder {
135 pub fn new() -> Self {
137 Self::default()
138 }
139
140 pub fn default_allow(mut self) -> Self {
142 self.default_egress = Some(Action::Allow);
143 self.default_ingress = Some(Action::Allow);
144 self
145 }
146
147 pub fn default_deny(mut self) -> Self {
149 self.default_egress = Some(Action::Deny);
150 self.default_ingress = Some(Action::Deny);
151 self
152 }
153
154 pub fn default_egress(mut self, action: Action) -> Self {
156 self.default_egress = Some(action);
157 self
158 }
159
160 pub fn default_ingress(mut self, action: Action) -> Self {
162 self.default_ingress = Some(action);
163 self
164 }
165
166 pub fn rule<F>(self, f: F) -> Self
169 where
170 F: for<'a> FnOnce(&'a mut RuleBuilder) -> &'a mut RuleBuilder,
171 {
172 self.with_rule_builder(None, f)
173 }
174
175 pub fn egress<F>(self, f: F) -> Self
177 where
178 F: for<'a> FnOnce(&'a mut RuleBuilder) -> &'a mut RuleBuilder,
179 {
180 self.with_rule_builder(Some(Direction::Egress), f)
181 }
182
183 pub fn ingress<F>(self, f: F) -> Self
185 where
186 F: for<'a> FnOnce(&'a mut RuleBuilder) -> &'a mut RuleBuilder,
187 {
188 self.with_rule_builder(Some(Direction::Ingress), f)
189 }
190
191 pub fn any<F>(self, f: F) -> Self
194 where
195 F: for<'a> FnOnce(&'a mut RuleBuilder) -> &'a mut RuleBuilder,
196 {
197 self.with_rule_builder(Some(Direction::Any), f)
198 }
199
200 fn with_rule_builder<F>(mut self, initial_direction: Option<Direction>, f: F) -> Self
201 where
202 F: for<'a> FnOnce(&'a mut RuleBuilder) -> &'a mut RuleBuilder,
203 {
204 let mut rb = RuleBuilder {
205 direction: initial_direction,
206 protocols: Vec::new(),
207 ports: Vec::new(),
208 pending_rules: Vec::new(),
209 errors: Vec::new(),
210 };
211 let _ = f(&mut rb);
212 self.pending_rules.append(&mut rb.pending_rules);
213 self.errors.append(&mut rb.errors);
214 self
215 }
216
217 pub fn build(self) -> Result<NetworkPolicy, BuildError> {
226 if let Some(err) = self.errors.into_iter().next() {
227 return Err(err);
228 }
229
230 let mut rules = Vec::with_capacity(self.pending_rules.len());
231 for (idx, pending) in self.pending_rules.into_iter().enumerate() {
232 let direction = pending
233 .direction
234 .ok_or(BuildError::DirectionNotSet { rule_index: idx })?;
235 let destination = pending.destination.parse(idx)?;
236
237 if matches!(direction, Direction::Ingress | Direction::Any)
238 && pending
239 .protocols
240 .iter()
241 .any(|p| matches!(p, Protocol::Icmpv4 | Protocol::Icmpv6))
242 {
243 return Err(BuildError::IngressDoesNotSupportIcmp { rule_index: idx });
244 }
245
246 rules.push(Rule {
247 direction,
248 destination,
249 protocols: pending.protocols,
250 ports: pending.ports,
251 action: pending.action,
252 });
253 }
254
255 warn_about_shadows(&rules);
256
257 Ok(NetworkPolicy {
258 default_egress: self.default_egress.unwrap_or_else(default_egress_default),
259 default_ingress: self.default_ingress.unwrap_or_else(default_ingress_default),
260 rules,
261 })
262 }
263}
264
265fn default_egress_default() -> Action {
269 Action::Deny
270}
271
272fn default_ingress_default() -> Action {
276 Action::Allow
277}
278
279#[derive(Debug)]
289pub struct RuleBuilder {
290 direction: Option<Direction>,
291 protocols: Vec<Protocol>,
292 ports: Vec<PortRange>,
293 pending_rules: Vec<PendingRule>,
294 errors: Vec<BuildError>,
295}
296
297impl RuleBuilder {
298 pub fn egress(&mut self) -> &mut Self {
302 self.direction = Some(Direction::Egress);
303 self
304 }
305
306 pub fn ingress(&mut self) -> &mut Self {
308 self.direction = Some(Direction::Ingress);
309 self
310 }
311
312 pub fn any(&mut self) -> &mut Self {
315 self.direction = Some(Direction::Any);
316 self
317 }
318
319 pub fn tcp(&mut self) -> &mut Self {
323 self.add_protocol(Protocol::Tcp)
324 }
325
326 pub fn udp(&mut self) -> &mut Self {
328 self.add_protocol(Protocol::Udp)
329 }
330
331 pub fn icmpv4(&mut self) -> &mut Self {
335 self.add_protocol(Protocol::Icmpv4)
336 }
337
338 pub fn icmpv6(&mut self) -> &mut Self {
340 self.add_protocol(Protocol::Icmpv6)
341 }
342
343 fn add_protocol(&mut self, p: Protocol) -> &mut Self {
344 if !self.protocols.contains(&p) {
345 self.protocols.push(p);
346 }
347 self
348 }
349
350 pub fn port(&mut self, port: u16) -> &mut Self {
354 let pr = PortRange::single(port);
355 if !self.ports.contains(&pr) {
356 self.ports.push(pr);
357 }
358 self
359 }
360
361 pub fn port_range(&mut self, lo: u16, hi: u16) -> &mut Self {
364 if lo > hi {
365 self.errors.push(BuildError::InvalidPortRange {
366 rule_index: self.pending_rules.len(),
367 lo,
368 hi,
369 });
370 return self;
371 }
372 let pr = PortRange::range(lo, hi);
373 if !self.ports.contains(&pr) {
374 self.ports.push(pr);
375 }
376 self
377 }
378
379 pub fn ports<I: IntoIterator<Item = u16>>(&mut self, ports: I) -> &mut Self {
382 for p in ports {
383 self.port(p);
384 }
385 self
386 }
387
388 pub fn allow_public(&mut self) -> &mut Self {
392 self.commit_group(Action::Allow, DestinationGroup::Public)
393 }
394
395 pub fn deny_public(&mut self) -> &mut Self {
397 self.commit_group(Action::Deny, DestinationGroup::Public)
398 }
399
400 pub fn allow_private(&mut self) -> &mut Self {
402 self.commit_group(Action::Allow, DestinationGroup::Private)
403 }
404
405 pub fn deny_private(&mut self) -> &mut Self {
407 self.commit_group(Action::Deny, DestinationGroup::Private)
408 }
409
410 pub fn allow_loopback(&mut self) -> &mut Self {
419 self.commit_group(Action::Allow, DestinationGroup::Loopback)
420 }
421
422 pub fn deny_loopback(&mut self) -> &mut Self {
430 self.commit_group(Action::Deny, DestinationGroup::Loopback)
431 }
432
433 pub fn allow_link_local(&mut self) -> &mut Self {
437 self.commit_group(Action::Allow, DestinationGroup::LinkLocal)
438 }
439
440 pub fn deny_link_local(&mut self) -> &mut Self {
442 self.commit_group(Action::Deny, DestinationGroup::LinkLocal)
443 }
444
445 pub fn allow_meta(&mut self) -> &mut Self {
448 self.commit_group(Action::Allow, DestinationGroup::Metadata)
449 }
450
451 pub fn deny_meta(&mut self) -> &mut Self {
453 self.commit_group(Action::Deny, DestinationGroup::Metadata)
454 }
455
456 pub fn allow_multicast(&mut self) -> &mut Self {
458 self.commit_group(Action::Allow, DestinationGroup::Multicast)
459 }
460
461 pub fn deny_multicast(&mut self) -> &mut Self {
463 self.commit_group(Action::Deny, DestinationGroup::Multicast)
464 }
465
466 pub fn allow_host(&mut self) -> &mut Self {
471 self.commit_group(Action::Allow, DestinationGroup::Host)
472 }
473
474 pub fn deny_host(&mut self) -> &mut Self {
476 self.commit_group(Action::Deny, DestinationGroup::Host)
477 }
478
479 pub fn allow_local(&mut self) -> &mut Self {
492 self.allow_loopback();
493 self.allow_link_local();
494 self.allow_host();
495 self
496 }
497
498 pub fn deny_local(&mut self) -> &mut Self {
501 self.deny_loopback();
502 self.deny_link_local();
503 self.deny_host();
504 self
505 }
506
507 pub fn allow(&mut self) -> RuleDestinationBuilder<'_> {
514 RuleDestinationBuilder {
515 rule_builder: self,
516 action: Action::Allow,
517 }
518 }
519
520 pub fn deny(&mut self) -> RuleDestinationBuilder<'_> {
522 RuleDestinationBuilder {
523 rule_builder: self,
524 action: Action::Deny,
525 }
526 }
527
528 fn commit_group(&mut self, action: Action, group: DestinationGroup) -> &mut Self {
531 self.commit_rule(
532 action,
533 PendingDestination::Resolved(Destination::Group(group)),
534 );
535 self
536 }
537
538 fn commit_rule(&mut self, action: Action, destination: PendingDestination) {
539 self.pending_rules.push(PendingRule {
540 direction: self.direction,
541 destination,
542 protocols: self.protocols.clone(),
543 ports: self.ports.clone(),
544 action,
545 });
546 }
547}
548
549#[must_use = "RuleDestinationBuilder requires a destination method (.ip, .cidr, .domain, .domain_suffix, .group, .any) to commit the rule"]
559pub struct RuleDestinationBuilder<'a> {
560 rule_builder: &'a mut RuleBuilder,
561 action: Action,
562}
563
564impl<'a> RuleDestinationBuilder<'a> {
565 pub fn ip(self, ip: impl Into<String>) -> &'a mut RuleBuilder {
569 self.rule_builder
570 .commit_rule(self.action, PendingDestination::Ip(ip.into()));
571 self.rule_builder
572 }
573
574 pub fn cidr(self, cidr: impl Into<String>) -> &'a mut RuleBuilder {
576 self.rule_builder
577 .commit_rule(self.action, PendingDestination::Cidr(cidr.into()));
578 self.rule_builder
579 }
580
581 pub fn domain(self, domain: impl Into<String>) -> &'a mut RuleBuilder {
585 self.rule_builder
586 .commit_rule(self.action, PendingDestination::Domain(domain.into()));
587 self.rule_builder
588 }
589
590 pub fn domain_suffix(self, suffix: impl Into<String>) -> &'a mut RuleBuilder {
593 self.rule_builder
594 .commit_rule(self.action, PendingDestination::DomainSuffix(suffix.into()));
595 self.rule_builder
596 }
597
598 pub fn group(self, group: DestinationGroup) -> &'a mut RuleBuilder {
600 self.rule_builder.commit_rule(
601 self.action,
602 PendingDestination::Resolved(Destination::Group(group)),
603 );
604 self.rule_builder
605 }
606
607 pub fn any(self) -> &'a mut RuleBuilder {
609 self.rule_builder
610 .commit_rule(self.action, PendingDestination::Resolved(Destination::Any));
611 self.rule_builder
612 }
613}
614
615#[derive(Debug, Clone)]
620struct PendingRule {
621 direction: Option<Direction>,
622 destination: PendingDestination,
623 protocols: Vec<Protocol>,
624 ports: Vec<PortRange>,
625 action: Action,
626}
627
628#[derive(Debug, Clone)]
629enum PendingDestination {
630 Resolved(Destination),
632 Ip(String),
633 Cidr(String),
634 Domain(String),
635 DomainSuffix(String),
636}
637
638impl PendingDestination {
639 fn parse(&self, idx: usize) -> Result<Destination, BuildError> {
640 match self {
641 PendingDestination::Resolved(d) => Ok(d.clone()),
642 PendingDestination::Ip(raw) => {
643 let ip = std::net::IpAddr::from_str(raw).map_err(|_| BuildError::InvalidIp {
644 rule_index: idx,
645 raw: raw.clone(),
646 })?;
647 let prefix = if ip.is_ipv4() { 32 } else { 128 };
650 let net = IpNetwork::new(ip, prefix).map_err(|_| BuildError::InvalidIp {
651 rule_index: idx,
652 raw: raw.clone(),
653 })?;
654 Ok(Destination::Cidr(net))
655 }
656 PendingDestination::Cidr(raw) => {
657 let net = IpNetwork::from_str(raw).map_err(|_| BuildError::InvalidCidr {
658 rule_index: idx,
659 raw: raw.clone(),
660 })?;
661 Ok(Destination::Cidr(net))
662 }
663 PendingDestination::Domain(raw) => {
664 let name =
665 DomainName::from_str(raw).map_err(|source| BuildError::InvalidDomain {
666 rule_index: idx,
667 raw: raw.clone(),
668 source,
669 })?;
670 Ok(Destination::Domain(name))
671 }
672 PendingDestination::DomainSuffix(raw) => {
673 let name =
674 DomainName::from_str(raw).map_err(|source| BuildError::InvalidDomain {
675 rule_index: idx,
676 raw: raw.clone(),
677 source,
678 })?;
679 Ok(Destination::DomainSuffix(name))
680 }
681 }
682 }
683}
684
685fn warn_about_shadows(rules: &[Rule]) {
697 for (i, later) in rules.iter().enumerate() {
698 for (j, earlier) in rules.iter().take(i).enumerate() {
699 if shadows(earlier, later) {
700 tracing::warn!(
701 shadowed_index = i,
702 shadowed_by = j,
703 "rule #{i} ({:?} {:?} {:?}) is shadowed by rule #{j} ({:?} {:?} {:?}); to narrow, place the more specific rule first",
704 later.direction,
705 later.action,
706 later.destination,
707 earlier.direction,
708 earlier.action,
709 earlier.destination,
710 );
711 }
712 }
713 }
714}
715
716fn shadows(earlier: &Rule, later: &Rule) -> bool {
719 direction_covers(earlier.direction, later.direction)
720 && destination_covers(&earlier.destination, &later.destination)
721 && protocol_set_covers(&earlier.protocols, &later.protocols)
722 && port_set_covers(&earlier.ports, &later.ports)
723}
724
725fn direction_covers(earlier: Direction, later: Direction) -> bool {
726 matches!(
727 (earlier, later),
728 (Direction::Any, _)
729 | (Direction::Egress, Direction::Egress)
730 | (Direction::Ingress, Direction::Ingress)
731 )
732}
733
734fn destination_covers(earlier: &Destination, later: &Destination) -> bool {
735 match (earlier, later) {
736 (Destination::Any, _) => true,
737 (Destination::Group(eg), Destination::Group(lg)) => eg == lg,
738 (Destination::Cidr(en), Destination::Cidr(ln)) => cidr_contains(en, ln),
739 _ => false,
741 }
742}
743
744fn cidr_contains(outer: &IpNetwork, inner: &IpNetwork) -> bool {
745 match (outer, inner) {
746 (IpNetwork::V4(o), IpNetwork::V4(i)) => o.prefix() <= i.prefix() && o.contains(i.network()),
747 (IpNetwork::V6(o), IpNetwork::V6(i)) => o.prefix() <= i.prefix() && o.contains(i.network()),
748 _ => false,
749 }
750}
751
752fn protocol_set_covers(earlier: &[Protocol], later: &[Protocol]) -> bool {
753 if earlier.is_empty() {
754 return true; }
756 if later.is_empty() {
757 return false; }
759 later.iter().all(|p| earlier.contains(p))
760}
761
762fn port_set_covers(earlier: &[PortRange], later: &[PortRange]) -> bool {
763 if earlier.is_empty() {
764 return true;
765 }
766 if later.is_empty() {
767 return false;
768 }
769 later.iter().all(|lp| {
770 earlier
771 .iter()
772 .any(|ep| ep.start <= lp.start && lp.end <= ep.end)
773 })
774}
775
776impl NetworkPolicy {
781 pub fn builder() -> NetworkPolicyBuilder {
783 NetworkPolicyBuilder::new()
784 }
785}
786
787#[cfg(test)]
792mod tests {
793 use super::*;
794
795 #[test]
798 fn empty_builder_yields_asymmetric_default() {
799 let p = NetworkPolicy::builder().build().unwrap();
800 assert!(matches!(p.default_egress, Action::Deny));
801 assert!(matches!(p.default_ingress, Action::Allow));
802 assert!(p.rules.is_empty());
803 }
804
805 #[test]
808 fn defaults_set_and_override() {
809 let p = NetworkPolicy::builder()
810 .default_deny()
811 .default_ingress(Action::Allow)
812 .build()
813 .unwrap();
814 assert!(matches!(p.default_egress, Action::Deny));
815 assert!(matches!(p.default_ingress, Action::Allow));
816 }
817
818 #[test]
821 fn egress_closure_commits_one_rule_per_shortcut() {
822 let p = NetworkPolicy::builder()
823 .egress(|e| e.tcp().port(443).allow_public().allow_private())
824 .build()
825 .unwrap();
826 assert_eq!(p.rules.len(), 2);
827 assert!(matches!(p.rules[0].direction, Direction::Egress));
828 assert!(matches!(p.rules[0].action, Action::Allow));
829 assert!(matches!(
830 p.rules[0].destination,
831 Destination::Group(DestinationGroup::Public)
832 ));
833 assert_eq!(p.rules[0].protocols, vec![Protocol::Tcp]);
834 assert_eq!(p.rules[0].ports.len(), 1);
835 assert!(matches!(
836 p.rules[1].destination,
837 Destination::Group(DestinationGroup::Private)
838 ));
839 }
840
841 #[test]
843 fn allow_local_expands_to_three_groups() {
844 let p = NetworkPolicy::builder()
845 .egress(|e| e.allow_local())
846 .build()
847 .unwrap();
848 assert_eq!(p.rules.len(), 3);
849 let groups: Vec<_> = p
850 .rules
851 .iter()
852 .map(|r| match &r.destination {
853 Destination::Group(g) => *g,
854 other => panic!("unexpected destination {other:?}"),
855 })
856 .collect();
857 assert_eq!(
858 groups,
859 vec![
860 DestinationGroup::Loopback,
861 DestinationGroup::LinkLocal,
862 DestinationGroup::Host,
863 ]
864 );
865 }
866
867 #[test]
870 fn explicit_ip_parses_at_build() {
871 let p = NetworkPolicy::builder()
872 .any(|a| a.deny().ip("198.51.100.5"))
873 .build()
874 .unwrap();
875 assert_eq!(p.rules.len(), 1);
876 assert!(matches!(p.rules[0].direction, Direction::Any));
877 assert!(matches!(p.rules[0].action, Action::Deny));
878 match &p.rules[0].destination {
879 Destination::Cidr(net) => {
880 assert_eq!(net.to_string(), "198.51.100.5/32");
881 }
882 other => panic!("expected Cidr, got {other:?}"),
883 }
884 }
885
886 #[test]
889 fn invalid_ip_surfaces_at_build() {
890 let result = NetworkPolicy::builder()
891 .egress(|e| e.allow().ip("not-an-ip"))
892 .build();
893 match result {
894 Err(BuildError::InvalidIp { raw, rule_index: 0 }) => {
895 assert_eq!(raw, "not-an-ip");
896 }
897 other => panic!("expected InvalidIp, got {other:?}"),
898 }
899 }
900
901 #[test]
903 fn domain_parses_to_canonical_form() {
904 let p = NetworkPolicy::builder()
905 .egress(|e| e.tcp().port(443).allow().domain("PyPI.Org."))
906 .build()
907 .unwrap();
908 match &p.rules[0].destination {
909 Destination::Domain(name) => assert_eq!(name.as_str(), "pypi.org"),
910 other => panic!("expected Domain, got {other:?}"),
911 }
912 }
913
914 #[test]
916 fn invalid_port_range_surfaces_at_build() {
917 let result = NetworkPolicy::builder()
918 .egress(|e| e.tcp().port_range(443, 80).allow_public())
919 .build();
920 match result {
921 Err(BuildError::InvalidPortRange {
922 lo: 443, hi: 80, ..
923 }) => {}
924 other => panic!("expected InvalidPortRange, got {other:?}"),
925 }
926 }
927
928 #[test]
930 fn missing_direction_surfaces_at_build() {
931 let result = NetworkPolicy::builder()
932 .rule(|r| r.tcp().port(443).allow_public())
933 .build();
934 match result {
935 Err(BuildError::DirectionNotSet { rule_index: 0 }) => {}
936 other => panic!("expected DirectionNotSet, got {other:?}"),
937 }
938 }
939
940 #[test]
942 fn icmp_in_ingress_rejected_at_build() {
943 let result = NetworkPolicy::builder()
944 .ingress(|i| i.icmpv4().allow_public())
945 .build();
946 match result {
947 Err(BuildError::IngressDoesNotSupportIcmp { rule_index: 0 }) => {}
948 other => panic!("expected IngressDoesNotSupportIcmp, got {other:?}"),
949 }
950 }
951
952 #[test]
954 fn icmp_in_any_direction_rejected_at_build() {
955 let result = NetworkPolicy::builder()
956 .any(|a| a.icmpv6().allow_public())
957 .build();
958 match result {
959 Err(BuildError::IngressDoesNotSupportIcmp { rule_index: 0 }) => {}
960 other => panic!("expected IngressDoesNotSupportIcmp, got {other:?}"),
961 }
962 }
963
964 #[test]
966 fn duplicate_protocols_dedupe() {
967 let p = NetworkPolicy::builder()
968 .egress(|e| e.tcp().tcp().udp().tcp().allow_public())
969 .build()
970 .unwrap();
971 assert_eq!(p.rules[0].protocols, vec![Protocol::Tcp, Protocol::Udp]);
972 }
973
974 #[test]
977 fn explicit_group_uses_typed_argument() {
978 let p = NetworkPolicy::builder()
979 .egress(|e| e.allow().group(DestinationGroup::Multicast))
980 .build()
981 .unwrap();
982 assert!(matches!(
983 p.rules[0].destination,
984 Destination::Group(DestinationGroup::Multicast)
985 ));
986 }
987
988 #[test]
992 fn chain_form_compiles_without_explicit_return() {
993 let _ = NetworkPolicy::builder()
994 .rule(|r| r.egress().tcp().allow_public())
995 .build()
996 .unwrap();
997 }
998
999 #[test]
1004 fn shadowed_rule_builds_and_is_detected() {
1005 let broader = Rule {
1006 direction: Direction::Egress,
1007 destination: Destination::Cidr("10.0.0.0/8".parse().unwrap()),
1008 protocols: vec![],
1009 ports: vec![],
1010 action: Action::Allow,
1011 };
1012 let narrower = Rule {
1013 direction: Direction::Egress,
1014 destination: Destination::Cidr("10.0.0.5/32".parse().unwrap()),
1015 protocols: vec![],
1016 ports: vec![],
1017 action: Action::Allow,
1018 };
1019 assert!(
1020 shadows(&broader, &narrower),
1021 "10.0.0.0/8 should shadow 10.0.0.5/32 in same direction"
1022 );
1023 assert!(
1024 !shadows(&narrower, &broader),
1025 "10.0.0.5/32 should NOT shadow 10.0.0.0/8"
1026 );
1027
1028 let _ = NetworkPolicy::builder()
1031 .egress(|e| e.allow().cidr("10.0.0.0/8"))
1032 .egress(|e| e.allow().cidr("10.0.0.5/32"))
1033 .build()
1034 .unwrap();
1035 }
1036
1037 #[test]
1040 fn direction_cover_relations() {
1041 use Direction::*;
1042 assert!(direction_covers(Any, Egress));
1043 assert!(direction_covers(Any, Ingress));
1044 assert!(direction_covers(Any, Any));
1045 assert!(direction_covers(Egress, Egress));
1046 assert!(!direction_covers(Egress, Ingress));
1047 assert!(!direction_covers(Egress, Any)); assert!(direction_covers(Ingress, Ingress));
1049 assert!(!direction_covers(Ingress, Egress));
1050 assert!(!direction_covers(Ingress, Any));
1051 }
1052}