1use crate::PathSelector;
12use crate::PolicyError;
13use cageforge_path::{NativePathKey, paths_equal};
14use globset::GlobBuilder;
15use globset::GlobMatcher;
16use std::collections::{HashMap, HashSet};
17use std::hash::{Hash, Hasher};
18use std::net::{IpAddr, Ipv4Addr, SocketAddr};
19use std::path::Path;
20use std::path::PathBuf;
21
22mod model;
23
24use model::DomainMatcher;
25pub use model::{
26 AuthorizedSocketAddr, ConnectionAuthorization, DomainAccess, DomainMode, DomainRule,
27 LocalNetworkAccess, NetworkDecision, NetworkMode, NetworkPolicy, ResolvedNetworkTarget,
28 UnixSocketMode, UnixSocketRule,
29};
30
31impl NetworkDecision {
32 pub(crate) const fn is_allowed(self) -> bool {
33 matches!(self, Self::Allow)
34 }
35
36 pub const fn is_externally_enforced(self) -> bool {
38 matches!(self, Self::ExternallyEnforced)
39 }
40}
41
42impl AuthorizedSocketAddr {
43 pub const fn into_socket_addr(self) -> SocketAddr {
56 self.0
57 }
58}
59
60impl ResolvedNetworkTarget {
61 pub fn new(
66 domain: impl Into<String>,
67 addresses: impl IntoIterator<Item = SocketAddr>,
68 ) -> Result<Self, PolicyError> {
69 let domain = normalize_domain(&domain.into())?;
70 let literal = parse_ip_literal(&domain);
71 let mut seen = HashSet::new();
72 let mut unique_addresses = Vec::new();
73 for address in addresses {
74 if literal.is_some_and(|literal| literal != address.ip()) {
75 return Err(PolicyError::ResolvedAddressMismatch {
76 literal: domain,
77 address,
78 });
79 }
80 if seen.insert(address) {
81 unique_addresses.push(address);
82 }
83 }
84 Ok(Self {
85 domain,
86 addresses: unique_addresses,
87 })
88 }
89
90 pub fn domain(&self) -> &str {
92 &self.domain
93 }
94
95 pub fn addresses(&self) -> &[SocketAddr] {
97 &self.addresses
98 }
99
100 pub fn contains_address(&self, address: SocketAddr) -> bool {
102 self.addresses.contains(&address)
103 }
104}
105
106impl PartialEq for DomainRule {
107 fn eq(&self, other: &Self) -> bool {
108 self.pattern == other.pattern && self.access == other.access
109 }
110}
111
112impl Eq for DomainRule {}
113
114impl Hash for DomainRule {
115 fn hash<H: Hasher>(&self, state: &mut H) {
116 self.pattern.hash(state);
117 self.access.hash(state);
118 }
119}
120
121impl DomainRule {
122 pub fn new(pattern: impl Into<String>, access: DomainAccess) -> Result<Self, PolicyError> {
124 let pattern = normalize_domain_pattern(&pattern.into())?;
125 let matcher = compile_domain_matcher(&pattern)?;
126 Ok(Self {
127 pattern,
128 access,
129 matcher,
130 })
131 }
132
133 pub fn pattern(&self) -> &str {
135 &self.pattern
136 }
137
138 pub const fn access(&self) -> DomainAccess {
140 self.access
141 }
142
143 fn matches(&self, domain: &str) -> bool {
144 match &self.matcher {
145 DomainMatcher::Any => true,
146 DomainMatcher::Full(matcher) => matcher.is_match(domain),
147 DomainMatcher::Suffix {
148 labels,
149 include_apex,
150 } => {
151 let domain_label_count = domain.split('.').count();
152 if domain_label_count < labels.len()
153 || (!include_apex && domain_label_count == labels.len())
154 {
155 return false;
156 }
157 domain
158 .rsplit('.')
159 .zip(labels.iter().rev())
160 .all(|(domain, matcher)| matcher.is_match(domain))
161 }
162 }
163 }
164}
165
166impl PartialEq for UnixSocketRule {
167 fn eq(&self, other: &Self) -> bool {
168 NativePathKey::new(&self.path) == NativePathKey::new(&other.path)
169 && self.access == other.access
170 }
171}
172
173impl Eq for UnixSocketRule {}
174
175impl Hash for UnixSocketRule {
176 fn hash<H: Hasher>(&self, state: &mut H) {
177 NativePathKey::new(&self.path).hash(state);
178 self.access.hash(state);
179 }
180}
181
182impl UnixSocketRule {
183 pub fn new(path: impl Into<PathBuf>, access: DomainAccess) -> Result<Self, PolicyError> {
185 let path = path.into();
186 PathSelector::absolute(path.clone())?;
187 Ok(Self { path, access })
188 }
189
190 pub fn path(&self) -> &std::path::Path {
192 &self.path
193 }
194
195 pub const fn access(&self) -> DomainAccess {
197 self.access
198 }
199}
200
201impl NetworkPolicy {
202 pub const fn disabled() -> Self {
204 Self {
205 mode: NetworkMode::Disabled,
206 domain_mode: DomainMode::Disabled,
207 unix_socket_mode: UnixSocketMode::Disabled,
208 local_network_access: LocalNetworkAccess::Deny,
209 domains: Vec::new(),
210 unix_sockets: Vec::new(),
211 }
212 }
213
214 pub const fn enabled() -> Self {
220 Self {
221 mode: NetworkMode::Enabled,
222 domain_mode: DomainMode::Enabled,
223 unix_socket_mode: UnixSocketMode::Disabled,
224 local_network_access: LocalNetworkAccess::Deny,
225 domains: Vec::new(),
226 unix_sockets: Vec::new(),
227 }
228 }
229
230 pub const fn unrestricted() -> Self {
232 Self {
233 mode: NetworkMode::Enabled,
234 domain_mode: DomainMode::Enabled,
235 unix_socket_mode: UnixSocketMode::Enabled,
236 local_network_access: LocalNetworkAccess::Allow,
237 domains: Vec::new(),
238 unix_sockets: Vec::new(),
239 }
240 }
241
242 pub const fn external() -> Self {
244 Self {
245 mode: NetworkMode::External,
246 domain_mode: DomainMode::Disabled,
247 unix_socket_mode: UnixSocketMode::Disabled,
248 local_network_access: LocalNetworkAccess::Deny,
249 domains: Vec::new(),
250 unix_sockets: Vec::new(),
251 }
252 }
253
254 pub const fn mode(&self) -> NetworkMode {
256 self.mode
257 }
258
259 pub const fn domain_mode(&self) -> DomainMode {
261 self.domain_mode
262 }
263
264 pub const fn unix_socket_mode(&self) -> UnixSocketMode {
266 self.unix_socket_mode
267 }
268
269 pub const fn local_network_access(&self) -> LocalNetworkAccess {
271 self.local_network_access
272 }
273
274 pub const fn with_domain_mode(mut self, mode: DomainMode) -> Self {
276 self.domain_mode = mode;
277 self
278 }
279
280 pub const fn with_unix_socket_mode(mut self, mode: UnixSocketMode) -> Self {
282 self.unix_socket_mode = mode;
283 self
284 }
285
286 pub const fn with_local_network_access(mut self, access: LocalNetworkAccess) -> Self {
288 self.local_network_access = access;
289 self
290 }
291
292 pub fn domains(&self) -> &[DomainRule] {
294 &self.domains
295 }
296
297 pub fn unix_sockets(&self) -> &[UnixSocketRule] {
299 &self.unix_sockets
300 }
301
302 pub fn with_domain(
304 mut self,
305 pattern: impl Into<String>,
306 access: DomainAccess,
307 ) -> Result<Self, PolicyError> {
308 if self.mode == NetworkMode::External {
309 return Err(PolicyError::InvalidRule {
310 message: "network rules cannot be added to an external policy".to_string(),
311 });
312 }
313 self.domains.push(DomainRule::new(pattern, access)?);
314 Ok(self)
315 }
316
317 pub fn with_unix_socket(
323 mut self,
324 path: impl Into<PathBuf>,
325 access: DomainAccess,
326 ) -> Result<Self, PolicyError> {
327 if self.mode == NetworkMode::External {
328 return Err(PolicyError::InvalidRule {
329 message: "network rules cannot be added to an external policy".to_string(),
330 });
331 }
332 self.unix_sockets.push(UnixSocketRule::new(path, access)?);
333 Ok(self)
334 }
335
336 pub fn validate(&self) -> Result<(), PolicyError> {
338 if self.mode == NetworkMode::External
339 && (self.domain_mode != DomainMode::Disabled
340 || self.unix_socket_mode != UnixSocketMode::Disabled
341 || self.local_network_access != LocalNetworkAccess::Deny
342 || !self.domains.is_empty()
343 || !self.unix_sockets.is_empty())
344 {
345 return Err(PolicyError::InvalidRule {
346 message: "external network policies cannot contain local settings".to_string(),
347 });
348 }
349 Ok(())
350 }
351
352 pub fn normalized(&self) -> Result<Self, PolicyError> {
355 self.validate()?;
356 let mut domains: Vec<DomainRule> = Vec::with_capacity(self.domains.len());
357 let mut domain_positions: HashMap<String, usize> =
358 HashMap::with_capacity(self.domains.len());
359 for rule in &self.domains {
360 if let Some(&index) = domain_positions.get(rule.pattern()) {
361 if rule.access() == DomainAccess::Deny {
362 domains[index].access = DomainAccess::Deny;
363 }
364 } else {
365 domain_positions.insert(rule.pattern().to_owned(), domains.len());
366 domains.push(rule.clone());
367 }
368 }
369
370 let mut unix_sockets: Vec<UnixSocketRule> = Vec::with_capacity(self.unix_sockets.len());
371 let mut socket_positions: HashMap<NativePathKey, usize> =
372 HashMap::with_capacity(self.unix_sockets.len());
373 for rule in &self.unix_sockets {
374 let key = NativePathKey::new(rule.path());
375 if let Some(&index) = socket_positions.get(&key) {
376 if rule.access() == DomainAccess::Deny {
377 unix_sockets[index].access = DomainAccess::Deny;
378 }
379 } else {
380 socket_positions.insert(key, unix_sockets.len());
381 unix_sockets.push(rule.clone());
382 }
383 }
384
385 Ok(Self {
386 mode: self.mode,
387 domain_mode: self.domain_mode,
388 unix_socket_mode: self.unix_socket_mode,
389 local_network_access: self.local_network_access,
390 domains,
391 unix_sockets,
392 })
393 }
394
395 pub fn decision_for_domain(&self, domain: &str) -> Result<NetworkDecision, PolicyError> {
401 let domain = normalize_domain(domain)?;
402 match self.mode {
403 NetworkMode::Disabled => Ok(NetworkDecision::Deny),
404 NetworkMode::External => Ok(NetworkDecision::ExternallyEnforced),
405 NetworkMode::Enabled => match self.domain_mode {
406 DomainMode::Disabled => Ok(NetworkDecision::Deny),
407 DomainMode::Enabled => Ok(
408 if matches!(
409 self.access_for_normalized_domain(&domain),
410 Some(DomainAccess::Deny)
411 ) {
412 NetworkDecision::Deny
413 } else {
414 NetworkDecision::Allow
415 },
416 ),
417 DomainMode::Restricted => Ok(
418 if matches!(
419 self.access_for_normalized_domain(&domain),
420 Some(DomainAccess::Allow)
421 ) {
422 NetworkDecision::Allow
423 } else {
424 NetworkDecision::Deny
425 },
426 ),
427 },
428 }
429 }
430
431 fn decision_for_resolved_target(
437 &self,
438 target: &ResolvedNetworkTarget,
439 ) -> Result<NetworkDecision, PolicyError> {
440 let resolved_ips: Vec<_> = target.addresses().iter().map(SocketAddr::ip).collect();
441 self.decision_for_domain_with_resolved_ips(target.domain(), &resolved_ips)
442 }
443
444 fn decision_for_connected_address(
450 &self,
451 target: &ResolvedNetworkTarget,
452 connected: SocketAddr,
453 ) -> Result<NetworkDecision, PolicyError> {
454 let decision = self.decision_for_resolved_target(target)?;
455 if !decision.is_allowed() {
456 return Ok(decision);
457 }
458 Ok(if target.contains_address(connected) {
459 NetworkDecision::Allow
460 } else {
461 NetworkDecision::Deny
462 })
463 }
464
465 pub fn authorize_connection(
471 &self,
472 target: &ResolvedNetworkTarget,
473 connected: SocketAddr,
474 ) -> Result<ConnectionAuthorization, PolicyError> {
475 Ok(
476 match self.decision_for_connected_address(target, connected)? {
477 NetworkDecision::Allow => {
478 ConnectionAuthorization::Allowed(AuthorizedSocketAddr(connected))
479 }
480 NetworkDecision::Deny => ConnectionAuthorization::Denied,
481 NetworkDecision::ExternallyEnforced => ConnectionAuthorization::ExternallyEnforced,
482 },
483 )
484 }
485
486 pub fn decision_for_domain_with_resolved_ips(
502 &self,
503 domain: &str,
504 resolved_ips: &[IpAddr],
505 ) -> Result<NetworkDecision, PolicyError> {
506 let normalized_domain = normalize_domain(domain)?;
507 let decision = self.decision_for_domain(&normalized_domain)?;
508 if !decision.is_allowed() {
509 return Ok(decision);
510 }
511
512 if let Some(literal) = parse_ip_literal(&normalized_domain) {
513 if resolved_ips.iter().any(|ip| *ip != literal) {
514 return Ok(NetworkDecision::Deny);
515 }
516 return Ok(
517 if self.has_exact_allow(&normalized_domain)
518 || self.local_network_access == LocalNetworkAccess::Allow
519 || !is_non_public_ip(literal)
520 {
521 NetworkDecision::Allow
522 } else {
523 NetworkDecision::Deny
524 },
525 );
526 }
527
528 let explicit_localhost_allow =
529 normalized_domain == "localhost" && self.has_exact_allow(&normalized_domain);
530 if normalized_domain == "localhost"
531 && self.local_network_access == LocalNetworkAccess::Deny
532 && !explicit_localhost_allow
533 {
534 return Ok(NetworkDecision::Deny);
535 }
536 if resolved_ips.is_empty() {
537 return Ok(NetworkDecision::Deny);
538 }
539 if self.local_network_access == LocalNetworkAccess::Deny
540 && resolved_ips
541 .iter()
542 .copied()
543 .any(|ip| is_non_public_ip(ip) && !(explicit_localhost_allow && ip.is_loopback()))
544 {
545 return Ok(NetworkDecision::Deny);
546 }
547 Ok(NetworkDecision::Allow)
548 }
549
550 pub fn decision_for_unix_socket(&self, path: &Path) -> Result<NetworkDecision, PolicyError> {
555 PathSelector::absolute(path.to_path_buf())?;
556 if self.mode == NetworkMode::External {
557 return Ok(NetworkDecision::ExternallyEnforced);
558 }
559 if self.mode == NetworkMode::Disabled || self.unix_socket_mode == UnixSocketMode::Disabled {
560 return Ok(NetworkDecision::Deny);
561 }
562 let mut result = None;
563 for rule in &self.unix_sockets {
564 if paths_equal(path, rule.path()) {
565 result = Some(match (result, rule.access()) {
566 (Some(DomainAccess::Deny), _) | (_, DomainAccess::Deny) => DomainAccess::Deny,
567 _ => DomainAccess::Allow,
568 });
569 }
570 }
571 let decision = match self.unix_socket_mode {
572 UnixSocketMode::Disabled => NetworkDecision::Deny,
573 UnixSocketMode::Enabled => {
574 if matches!(result, Some(DomainAccess::Deny)) {
575 NetworkDecision::Deny
576 } else {
577 NetworkDecision::Allow
578 }
579 }
580 UnixSocketMode::Restricted => {
581 if matches!(result, Some(DomainAccess::Allow)) {
582 NetworkDecision::Allow
583 } else {
584 NetworkDecision::Deny
585 }
586 }
587 };
588 Ok(decision)
589 }
590
591 fn access_for_normalized_domain(&self, domain: &str) -> Option<DomainAccess> {
592 let mut result = None;
593 for rule in &self.domains {
594 if rule.matches(domain) {
595 result = Some(match (result, rule.access()) {
596 (Some(DomainAccess::Deny), _) | (_, DomainAccess::Deny) => DomainAccess::Deny,
597 _ => DomainAccess::Allow,
598 });
599 }
600 }
601 result
602 }
603
604 fn has_exact_allow(&self, domain: &str) -> bool {
605 self.domains.iter().any(|rule| {
606 rule.access() == DomainAccess::Allow
607 && !rule.pattern().contains('*')
608 && !rule.pattern().contains('?')
609 && rule.pattern() == domain
610 })
611 }
612}
613
614fn normalize_domain_pattern(raw: &str) -> Result<String, PolicyError> {
615 let raw = raw.trim();
616 let invalid_syntax = raw.is_empty()
617 || raw.contains("://")
618 || raw.contains('/')
619 || raw.contains('#')
620 || raw.chars().any(char::is_whitespace)
621 || raw.chars().any(char::is_control);
622 if invalid_syntax {
623 return Err(PolicyError::InvalidDomainPattern {
624 pattern: raw.to_string(),
625 });
626 }
627
628 let (prefix, remainder) = if let Some(remainder) = raw.strip_prefix("**.") {
629 ("**.", remainder)
630 } else if let Some(remainder) = raw.strip_prefix("*.") {
631 ("*.", remainder)
632 } else {
633 ("", raw)
634 };
635 let remainder = normalize_host(remainder).ok_or_else(|| PolicyError::InvalidDomainPattern {
636 pattern: raw.to_string(),
637 })?;
638 let pattern = if prefix.is_empty() {
639 remainder
640 } else {
641 format!("{prefix}{remainder}")
642 };
643 if valid_domain_pattern(&pattern) {
644 Ok(pattern)
645 } else {
646 Err(PolicyError::InvalidDomainPattern {
647 pattern: raw.to_string(),
648 })
649 }
650}
651
652fn normalize_domain(raw: &str) -> Result<String, PolicyError> {
653 let normalized = normalize_domain_pattern(raw)?;
654 if !valid_concrete_host(&normalized) {
655 return Err(PolicyError::InvalidDomainPattern {
656 pattern: raw.to_string(),
657 });
658 }
659 Ok(normalized)
660}
661
662fn normalize_host(host: &str) -> Option<String> {
663 let host = host.trim();
664 if host.starts_with('[') {
665 let bracketed = host.strip_prefix('[')?;
666 let end = bracketed.find(']')?;
667 let inner = &bracketed[..end];
668 let suffix = &bracketed[end + 1..];
669 if normalize_ip_literal(inner).is_some() {
670 if !suffix.is_empty() && !valid_port_suffix(suffix) {
671 return None;
672 }
673 return normalize_ip_literal(inner).map(|ip| normalize_dns_host_or_ip_literal(&ip));
674 }
675 }
676 match host.bytes().filter(|byte| *byte == b':').count() {
677 0 => Some(normalize_dns_host_or_ip_literal(host)),
678 1 => {
679 let (host, port) = host.split_once(':')?;
680 if !valid_port(port) {
681 return None;
682 }
683 Some(normalize_dns_host_or_ip_literal(host))
684 }
685 _ => normalize_ip_literal(host).map(|ip| normalize_dns_host_or_ip_literal(&ip)),
686 }
687}
688
689fn valid_port_suffix(suffix: &str) -> bool {
690 let Some(port) = suffix.strip_prefix(':') else {
691 return false;
692 };
693 valid_port(port)
694}
695
696fn valid_port(port: &str) -> bool {
697 !port.is_empty()
698 && port.bytes().all(|byte| byte.is_ascii_digit())
699 && port.parse::<u16>().is_ok()
700}
701
702fn normalize_dns_host_or_ip_literal(host: &str) -> String {
703 let host = host.to_ascii_lowercase();
704 let host = host.trim_end_matches('.');
705 if let Some(ip) = normalize_ip_literal(host) {
706 return ip;
707 }
708 host.to_string()
709}
710
711fn normalize_ip_literal(host: &str) -> Option<String> {
712 if host.parse::<IpAddr>().is_ok() {
713 return Some(host.to_string());
714 }
715 for delimiter in ["%25", "%"] {
716 if let Some((ip, scope)) = host.split_once(delimiter)
717 && ip.parse::<IpAddr>().is_ok()
718 {
719 return Some(format!("{ip}%{scope}"));
720 }
721 }
722 None
723}
724
725fn parse_ip_literal(host: &str) -> Option<IpAddr> {
726 host.split_once('%').map_or_else(
727 || host.parse().ok(),
728 |(address, _scope)| address.parse().ok(),
729 )
730}
731
732fn is_non_public_ip(ip: IpAddr) -> bool {
733 match ip {
734 IpAddr::V4(address) => {
735 let value = u32::from(address);
736 address.is_loopback()
737 || address.is_private()
738 || address.is_link_local()
739 || address.is_unspecified()
740 || address.is_multicast()
741 || address.is_broadcast()
742 || (value & 0xff00_0000) == 0
743 || (value & 0xffc0_0000) == 0x6440_0000
744 || ((value & 0xffff_ff00) == 0xc000_0000
745 && !matches!(value, 0xc000_0009 | 0xc000_000a))
746 || (value & 0xffff_ff00) == 0xc000_0200
747 || (value & 0xffff_ff00) == 0xc058_6300
748 || (value & 0xfffe_0000) == 0xc612_0000
749 || (value & 0xffff_ff00) == 0xc633_6400
750 || (value & 0xffff_ff00) == 0xcb00_7100
751 || (value & 0xf000_0000) == 0xf000_0000
752 }
753 IpAddr::V6(address) => is_non_public_ipv6(address),
754 }
755}
756
757fn is_non_public_ipv6(address: std::net::Ipv6Addr) -> bool {
758 let segments = address.segments();
759 address.is_loopback()
760 || address.is_unspecified()
761 || address.is_multicast()
762 || address.is_unique_local()
763 || address.is_unicast_link_local()
764 || address
765 .to_ipv4()
766 .is_some_and(|address| is_non_public_ip(IpAddr::V4(address)))
767 || well_known_nat64_ipv4(segments)
770 .is_some_and(|address| is_non_public_ip(IpAddr::V4(address)))
771 || matches!(segments, [0x64, 0xff9b, 1, _, _, _, _, _])
773 || matches!(segments, [0x100, 0, 0, 0, _, _, _, _])
775 || matches!(segments, [0x100, 0, 0, 1, _, _, _, _])
777 || is_non_public_ietf_protocol_assignment(segments)
778 || matches!(segments, [0x2002, _, _, _, _, _, _, _])
780 || matches!(segments, [0x2001, 0x0db8, _, _, _, _, _, _])
782 || matches!(segments, [0x3fff, 0x0000..=0x0fff, _, _, _, _, _, _])
783 || matches!(segments, [0x5f00, _, _, _, _, _, _, _])
785 || (segments[0] & 0xffc0) == 0xfec0
788}
789
790fn well_known_nat64_ipv4(segments: [u16; 8]) -> Option<Ipv4Addr> {
791 match segments {
792 [0x64, 0xff9b, 0, 0, 0, 0, high, low] => {
793 let [first, second] = high.to_be_bytes();
794 let [third, fourth] = low.to_be_bytes();
795 Some(Ipv4Addr::new(first, second, third, fourth))
796 }
797 _ => None,
798 }
799}
800
801fn is_non_public_ietf_protocol_assignment(segments: [u16; 8]) -> bool {
802 if segments[0] != 0x2001 || segments[1] >= 0x0200 {
803 return false;
804 }
805 !matches!(
806 segments,
807 [0x2001, 0x0001, 0, 0, 0, 0, 0, 1..=3]
809 | [0x2001, 0x0003, _, _, _, _, _, _]
811 | [0x2001, 0x0004, 0x0112, _, _, _, _, _]
813 | [0x2001, 0x0020..=0x003f, _, _, _, _, _, _]
815 )
816}
817
818fn valid_domain_literal(pattern: &str) -> bool {
819 !pattern.is_empty()
820 && !pattern.contains('*')
821 && !pattern.contains('?')
822 && (!pattern.contains(':') || normalize_ip_literal(pattern).is_some())
823}
824
825fn valid_domain_pattern(pattern: &str) -> bool {
826 if pattern == "*" {
827 return true;
828 }
829 if pattern.contains(':') || pattern.contains('%') {
830 return valid_domain_literal(pattern);
831 }
832 let suffix = pattern
833 .strip_prefix("**.")
834 .or_else(|| pattern.strip_prefix("*."))
835 .unwrap_or(pattern);
836 !suffix.is_empty()
837 && suffix.split('.').all(|label| {
838 !label.is_empty()
839 && label.chars().all(|character| {
840 !character.is_whitespace()
841 && !character.is_control()
842 && !matches!(character, ':' | '%' | '@')
843 })
844 })
845}
846
847fn valid_concrete_host(host: &str) -> bool {
848 if parse_ip_literal(host).is_some() {
849 return true;
850 }
851 host.len() <= 253
852 && host.split('.').all(|label| {
853 !label.is_empty()
854 && label.len() <= 63
855 && !label.starts_with('-')
856 && !label.ends_with('-')
857 && label
858 .chars()
859 .all(|character| character.is_alphanumeric() || matches!(character, '-' | '_'))
860 })
861}
862
863fn compile_domain_matcher(pattern: &str) -> Result<DomainMatcher, PolicyError> {
864 if pattern == "*" {
865 return Ok(DomainMatcher::Any);
866 }
867 if let Some(suffix) = pattern.strip_prefix("**.") {
868 return Ok(DomainMatcher::Suffix {
869 labels: compile_domain_labels(suffix, pattern)?,
870 include_apex: true,
871 });
872 }
873 if let Some(suffix) = pattern.strip_prefix("*.") {
874 return Ok(DomainMatcher::Suffix {
875 labels: compile_domain_labels(suffix, pattern)?,
876 include_apex: false,
877 });
878 }
879 let matcher = GlobBuilder::new(pattern)
880 .case_insensitive(true)
881 .build()
882 .map_err(|_| PolicyError::InvalidDomainPattern {
883 pattern: pattern.to_owned(),
884 })?
885 .compile_matcher();
886 Ok(DomainMatcher::Full(matcher))
887}
888
889fn compile_domain_labels(suffix: &str, pattern: &str) -> Result<Vec<GlobMatcher>, PolicyError> {
890 suffix
891 .split('.')
892 .map(|label| {
893 GlobBuilder::new(label)
894 .case_insensitive(true)
895 .build()
896 .map(|glob| glob.compile_matcher())
897 .map_err(|_| PolicyError::InvalidDomainPattern {
898 pattern: pattern.to_owned(),
899 })
900 })
901 .collect()
902}