1use crate::PolicyError;
12use crate::{LocalIpcEndpoint, PathSelector};
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 LocalIpcRule, LocalNetworkAccess, NetworkDecision, NetworkMode, NetworkPolicy,
28 ResolvedNetworkTarget, 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 LocalIpcRule {
202 pub fn new(endpoint: LocalIpcEndpoint, access: DomainAccess) -> Result<Self, PolicyError> {
204 endpoint.validate()?;
205 Ok(Self { endpoint, access })
206 }
207
208 pub fn endpoint(&self) -> &LocalIpcEndpoint {
210 &self.endpoint
211 }
212
213 pub const fn access(&self) -> DomainAccess {
215 self.access
216 }
217}
218
219impl NetworkPolicy {
220 pub const fn disabled() -> Self {
222 Self {
223 mode: NetworkMode::Disabled,
224 domain_mode: DomainMode::Disabled,
225 unix_socket_mode: UnixSocketMode::Disabled,
226 local_network_access: LocalNetworkAccess::Deny,
227 domains: Vec::new(),
228 unix_sockets: Vec::new(),
229 local_ipc: Vec::new(),
230 }
231 }
232
233 pub const fn enabled() -> Self {
239 Self {
240 mode: NetworkMode::Enabled,
241 domain_mode: DomainMode::Enabled,
242 unix_socket_mode: UnixSocketMode::Disabled,
243 local_network_access: LocalNetworkAccess::Deny,
244 domains: Vec::new(),
245 unix_sockets: Vec::new(),
246 local_ipc: Vec::new(),
247 }
248 }
249
250 pub const fn unrestricted() -> Self {
252 Self {
253 mode: NetworkMode::Enabled,
254 domain_mode: DomainMode::Enabled,
255 unix_socket_mode: UnixSocketMode::Enabled,
256 local_network_access: LocalNetworkAccess::Allow,
257 domains: Vec::new(),
258 unix_sockets: Vec::new(),
259 local_ipc: Vec::new(),
260 }
261 }
262
263 pub const fn external() -> Self {
265 Self {
266 mode: NetworkMode::External,
267 domain_mode: DomainMode::Disabled,
268 unix_socket_mode: UnixSocketMode::Disabled,
269 local_network_access: LocalNetworkAccess::Deny,
270 domains: Vec::new(),
271 unix_sockets: Vec::new(),
272 local_ipc: Vec::new(),
273 }
274 }
275
276 pub const fn mode(&self) -> NetworkMode {
278 self.mode
279 }
280
281 pub const fn domain_mode(&self) -> DomainMode {
283 self.domain_mode
284 }
285
286 pub const fn unix_socket_mode(&self) -> UnixSocketMode {
288 self.unix_socket_mode
289 }
290
291 pub const fn local_network_access(&self) -> LocalNetworkAccess {
293 self.local_network_access
294 }
295
296 pub const fn with_domain_mode(mut self, mode: DomainMode) -> Self {
298 self.domain_mode = mode;
299 self
300 }
301
302 pub const fn with_unix_socket_mode(mut self, mode: UnixSocketMode) -> Self {
304 self.unix_socket_mode = mode;
305 self
306 }
307
308 pub const fn with_local_network_access(mut self, access: LocalNetworkAccess) -> Self {
310 self.local_network_access = access;
311 self
312 }
313
314 pub fn domains(&self) -> &[DomainRule] {
316 &self.domains
317 }
318
319 pub fn unix_sockets(&self) -> &[UnixSocketRule] {
321 &self.unix_sockets
322 }
323
324 pub fn local_ipc(&self) -> &[LocalIpcRule] {
326 &self.local_ipc
327 }
328
329 pub fn with_domain(
331 mut self,
332 pattern: impl Into<String>,
333 access: DomainAccess,
334 ) -> Result<Self, PolicyError> {
335 if self.mode == NetworkMode::External {
336 return Err(PolicyError::InvalidRule {
337 message: "network rules cannot be added to an external policy".to_string(),
338 });
339 }
340 self.domains.push(DomainRule::new(pattern, access)?);
341 Ok(self)
342 }
343
344 pub fn with_unix_socket(
350 mut self,
351 path: impl Into<PathBuf>,
352 access: DomainAccess,
353 ) -> Result<Self, PolicyError> {
354 if self.mode == NetworkMode::External {
355 return Err(PolicyError::InvalidRule {
356 message: "network rules cannot be added to an external policy".to_string(),
357 });
358 }
359 self.unix_sockets.push(UnixSocketRule::new(path, access)?);
360 Ok(self)
361 }
362
363 pub fn with_local_ipc(
365 mut self,
366 endpoint: LocalIpcEndpoint,
367 access: DomainAccess,
368 ) -> Result<Self, PolicyError> {
369 if self.mode == NetworkMode::External {
370 return Err(PolicyError::InvalidRule {
371 message: "local-IPC rules cannot be added to an external policy".to_owned(),
372 });
373 }
374 if self
375 .local_ipc
376 .iter()
377 .any(|rule| rule.endpoint() == &endpoint)
378 {
379 let endpoint_value = endpoint
380 .unix_path()
381 .map(|path| format!("unix:{}", path.display()))
382 .or_else(|| endpoint.named_pipe().map(|name| format!("pipe:{name}")))
383 .unwrap_or_else(|| "local-ipc".to_owned());
384 return Err(PolicyError::InvalidLocalIpcEndpoint {
385 endpoint: endpoint_value,
386 reason: "duplicate local-IPC endpoint",
387 });
388 }
389 let rule = LocalIpcRule::new(endpoint.clone(), access)?;
390 if let Some(path) = endpoint.unix_path() {
391 if self.mode == NetworkMode::Disabled {
397 self.mode = NetworkMode::Enabled;
398 }
399 if self.unix_socket_mode == UnixSocketMode::Disabled {
400 self.unix_socket_mode = UnixSocketMode::Restricted;
401 }
402 self.unix_sockets.push(UnixSocketRule {
408 path: path.to_path_buf(),
409 access,
410 });
411 }
412 self.local_ipc.push(rule);
413 Ok(self)
414 }
415
416 pub fn validate(&self) -> Result<(), PolicyError> {
418 if self.mode == NetworkMode::External
419 && (self.domain_mode != DomainMode::Disabled
420 || self.unix_socket_mode != UnixSocketMode::Disabled
421 || self.local_network_access != LocalNetworkAccess::Deny
422 || !self.domains.is_empty()
423 || !self.unix_sockets.is_empty()
424 || !self.local_ipc.is_empty())
425 {
426 return Err(PolicyError::InvalidRule {
427 message: "external network policies cannot contain local settings".to_string(),
428 });
429 }
430 Ok(())
431 }
432
433 pub fn normalized(&self) -> Result<Self, PolicyError> {
436 self.validate()?;
437 let mut domains: Vec<DomainRule> = Vec::with_capacity(self.domains.len());
438 let mut domain_positions: HashMap<String, usize> =
439 HashMap::with_capacity(self.domains.len());
440 for rule in &self.domains {
441 if let Some(&index) = domain_positions.get(rule.pattern()) {
442 if rule.access() == DomainAccess::Deny {
443 domains[index].access = DomainAccess::Deny;
444 }
445 } else {
446 domain_positions.insert(rule.pattern().to_owned(), domains.len());
447 domains.push(rule.clone());
448 }
449 }
450
451 let mut unix_sockets: Vec<UnixSocketRule> = Vec::with_capacity(self.unix_sockets.len());
452 let mut socket_positions: HashMap<NativePathKey, usize> =
453 HashMap::with_capacity(self.unix_sockets.len());
454 for rule in &self.unix_sockets {
455 let key = NativePathKey::new(rule.path());
456 if let Some(&index) = socket_positions.get(&key) {
457 if rule.access() == DomainAccess::Deny {
458 unix_sockets[index].access = DomainAccess::Deny;
459 }
460 } else {
461 socket_positions.insert(key, unix_sockets.len());
462 unix_sockets.push(rule.clone());
463 }
464 }
465
466 let mut local_ipc: Vec<LocalIpcRule> = Vec::with_capacity(self.local_ipc.len());
467 let mut local_ipc_positions: HashMap<LocalIpcEndpoint, usize> =
468 HashMap::with_capacity(self.local_ipc.len());
469 for rule in &self.local_ipc {
470 if let Some(&index) = local_ipc_positions.get(rule.endpoint()) {
471 if rule.access() == DomainAccess::Deny {
472 local_ipc[index].access = DomainAccess::Deny;
473 }
474 } else {
475 local_ipc_positions.insert(rule.endpoint().clone(), local_ipc.len());
476 local_ipc.push(rule.clone());
477 }
478 }
479
480 Ok(Self {
481 mode: self.mode,
482 domain_mode: self.domain_mode,
483 unix_socket_mode: self.unix_socket_mode,
484 local_network_access: self.local_network_access,
485 domains,
486 unix_sockets,
487 local_ipc,
488 })
489 }
490
491 pub fn decision_for_domain(&self, domain: &str) -> Result<NetworkDecision, PolicyError> {
497 let domain = normalize_domain(domain)?;
498 match self.mode {
499 NetworkMode::Disabled => Ok(NetworkDecision::Deny),
500 NetworkMode::External => Ok(NetworkDecision::ExternallyEnforced),
501 NetworkMode::Enabled => match self.domain_mode {
502 DomainMode::Disabled => Ok(NetworkDecision::Deny),
503 DomainMode::Enabled => Ok(
504 if matches!(
505 self.access_for_normalized_domain(&domain),
506 Some(DomainAccess::Deny)
507 ) {
508 NetworkDecision::Deny
509 } else {
510 NetworkDecision::Allow
511 },
512 ),
513 DomainMode::Restricted => Ok(
514 if matches!(
515 self.access_for_normalized_domain(&domain),
516 Some(DomainAccess::Allow)
517 ) {
518 NetworkDecision::Allow
519 } else {
520 NetworkDecision::Deny
521 },
522 ),
523 },
524 }
525 }
526
527 fn decision_for_resolved_target(
533 &self,
534 target: &ResolvedNetworkTarget,
535 ) -> Result<NetworkDecision, PolicyError> {
536 let resolved_ips: Vec<_> = target.addresses().iter().map(SocketAddr::ip).collect();
537 self.decision_for_domain_with_resolved_ips(target.domain(), &resolved_ips)
538 }
539
540 fn decision_for_connected_address(
546 &self,
547 target: &ResolvedNetworkTarget,
548 connected: SocketAddr,
549 ) -> Result<NetworkDecision, PolicyError> {
550 let decision = self.decision_for_resolved_target(target)?;
551 if !decision.is_allowed() {
552 return Ok(decision);
553 }
554 Ok(if target.contains_address(connected) {
555 NetworkDecision::Allow
556 } else {
557 NetworkDecision::Deny
558 })
559 }
560
561 pub fn authorize_connection(
567 &self,
568 target: &ResolvedNetworkTarget,
569 connected: SocketAddr,
570 ) -> Result<ConnectionAuthorization, PolicyError> {
571 Ok(
572 match self.decision_for_connected_address(target, connected)? {
573 NetworkDecision::Allow => {
574 ConnectionAuthorization::Allowed(AuthorizedSocketAddr(connected))
575 }
576 NetworkDecision::Deny => ConnectionAuthorization::Denied,
577 NetworkDecision::ExternallyEnforced => ConnectionAuthorization::ExternallyEnforced,
578 },
579 )
580 }
581
582 pub fn decision_for_domain_with_resolved_ips(
598 &self,
599 domain: &str,
600 resolved_ips: &[IpAddr],
601 ) -> Result<NetworkDecision, PolicyError> {
602 let normalized_domain = normalize_domain(domain)?;
603 let decision = self.decision_for_domain(&normalized_domain)?;
604 if !decision.is_allowed() {
605 return Ok(decision);
606 }
607
608 if let Some(literal) = parse_ip_literal(&normalized_domain) {
609 if resolved_ips.iter().any(|ip| *ip != literal) {
610 return Ok(NetworkDecision::Deny);
611 }
612 return Ok(
613 if self.has_exact_allow(&normalized_domain)
614 || self.local_network_access == LocalNetworkAccess::Allow
615 || !is_non_public_ip(literal)
616 {
617 NetworkDecision::Allow
618 } else {
619 NetworkDecision::Deny
620 },
621 );
622 }
623
624 let explicit_localhost_allow =
625 normalized_domain == "localhost" && self.has_exact_allow(&normalized_domain);
626 if normalized_domain == "localhost"
627 && self.local_network_access == LocalNetworkAccess::Deny
628 && !explicit_localhost_allow
629 {
630 return Ok(NetworkDecision::Deny);
631 }
632 if resolved_ips.is_empty() {
633 return Ok(NetworkDecision::Deny);
634 }
635 if self.local_network_access == LocalNetworkAccess::Deny
636 && resolved_ips
637 .iter()
638 .copied()
639 .any(|ip| is_non_public_ip(ip) && !(explicit_localhost_allow && ip.is_loopback()))
640 {
641 return Ok(NetworkDecision::Deny);
642 }
643 Ok(NetworkDecision::Allow)
644 }
645
646 pub fn decision_for_unix_socket(&self, path: &Path) -> Result<NetworkDecision, PolicyError> {
651 PathSelector::absolute(path.to_path_buf())?;
652 if self.mode == NetworkMode::External {
653 return Ok(NetworkDecision::ExternallyEnforced);
654 }
655 if self.mode == NetworkMode::Disabled || self.unix_socket_mode == UnixSocketMode::Disabled {
656 return Ok(NetworkDecision::Deny);
657 }
658 let mut result = None;
659 for rule in &self.unix_sockets {
660 if paths_equal(path, rule.path()) {
661 result = Some(match (result, rule.access()) {
662 (Some(DomainAccess::Deny), _) | (_, DomainAccess::Deny) => DomainAccess::Deny,
663 _ => DomainAccess::Allow,
664 });
665 }
666 }
667 let decision = match self.unix_socket_mode {
668 UnixSocketMode::Disabled => NetworkDecision::Deny,
669 UnixSocketMode::Enabled => {
670 if matches!(result, Some(DomainAccess::Deny)) {
671 NetworkDecision::Deny
672 } else {
673 NetworkDecision::Allow
674 }
675 }
676 UnixSocketMode::Restricted => {
677 if matches!(result, Some(DomainAccess::Allow)) {
678 NetworkDecision::Allow
679 } else {
680 NetworkDecision::Deny
681 }
682 }
683 };
684 Ok(decision)
685 }
686
687 fn access_for_normalized_domain(&self, domain: &str) -> Option<DomainAccess> {
688 let mut result = None;
689 for rule in &self.domains {
690 if rule.matches(domain) {
691 result = Some(match (result, rule.access()) {
692 (Some(DomainAccess::Deny), _) | (_, DomainAccess::Deny) => DomainAccess::Deny,
693 _ => DomainAccess::Allow,
694 });
695 }
696 }
697 result
698 }
699
700 fn has_exact_allow(&self, domain: &str) -> bool {
701 self.domains.iter().any(|rule| {
702 rule.access() == DomainAccess::Allow
703 && !rule.pattern().contains('*')
704 && !rule.pattern().contains('?')
705 && rule.pattern() == domain
706 })
707 }
708}
709
710fn normalize_domain_pattern(raw: &str) -> Result<String, PolicyError> {
711 let raw = raw.trim();
712 let invalid_syntax = raw.is_empty()
713 || raw.contains("://")
714 || raw.contains('/')
715 || raw.contains('#')
716 || raw.chars().any(char::is_whitespace)
717 || raw.chars().any(char::is_control);
718 if invalid_syntax {
719 return Err(PolicyError::InvalidDomainPattern {
720 pattern: raw.to_string(),
721 });
722 }
723
724 let (prefix, remainder) = if let Some(remainder) = raw.strip_prefix("**.") {
725 ("**.", remainder)
726 } else if let Some(remainder) = raw.strip_prefix("*.") {
727 ("*.", remainder)
728 } else {
729 ("", raw)
730 };
731 let remainder = normalize_host(remainder).ok_or_else(|| PolicyError::InvalidDomainPattern {
732 pattern: raw.to_string(),
733 })?;
734 let pattern = if prefix.is_empty() {
735 remainder
736 } else {
737 format!("{prefix}{remainder}")
738 };
739 if valid_domain_pattern(&pattern) {
740 Ok(pattern)
741 } else {
742 Err(PolicyError::InvalidDomainPattern {
743 pattern: raw.to_string(),
744 })
745 }
746}
747
748fn normalize_domain(raw: &str) -> Result<String, PolicyError> {
749 let normalized = normalize_domain_pattern(raw)?;
750 if !valid_concrete_host(&normalized) {
751 return Err(PolicyError::InvalidDomainPattern {
752 pattern: raw.to_string(),
753 });
754 }
755 Ok(normalized)
756}
757
758fn normalize_host(host: &str) -> Option<String> {
759 let host = host.trim();
760 if host.starts_with('[') {
761 let bracketed = host.strip_prefix('[')?;
762 let end = bracketed.find(']')?;
763 let inner = &bracketed[..end];
764 let suffix = &bracketed[end + 1..];
765 if normalize_ip_literal(inner).is_some() {
766 if !suffix.is_empty() && !valid_port_suffix(suffix) {
767 return None;
768 }
769 return normalize_ip_literal(inner).map(|ip| normalize_dns_host_or_ip_literal(&ip));
770 }
771 }
772 match host.bytes().filter(|byte| *byte == b':').count() {
773 0 => Some(normalize_dns_host_or_ip_literal(host)),
774 1 => {
775 let (host, port) = host.split_once(':')?;
776 if !valid_port(port) {
777 return None;
778 }
779 Some(normalize_dns_host_or_ip_literal(host))
780 }
781 _ => normalize_ip_literal(host).map(|ip| normalize_dns_host_or_ip_literal(&ip)),
782 }
783}
784
785fn valid_port_suffix(suffix: &str) -> bool {
786 let Some(port) = suffix.strip_prefix(':') else {
787 return false;
788 };
789 valid_port(port)
790}
791
792fn valid_port(port: &str) -> bool {
793 !port.is_empty()
794 && port.bytes().all(|byte| byte.is_ascii_digit())
795 && port.parse::<u16>().is_ok()
796}
797
798fn normalize_dns_host_or_ip_literal(host: &str) -> String {
799 let host = host.to_ascii_lowercase();
800 let host = host.trim_end_matches('.');
801 if let Some(ip) = normalize_ip_literal(host) {
802 return ip;
803 }
804 host.to_string()
805}
806
807fn normalize_ip_literal(host: &str) -> Option<String> {
808 if host.parse::<IpAddr>().is_ok() {
809 return Some(host.to_string());
810 }
811 for delimiter in ["%25", "%"] {
812 if let Some((ip, scope)) = host.split_once(delimiter)
813 && ip.parse::<IpAddr>().is_ok()
814 {
815 return Some(format!("{ip}%{scope}"));
816 }
817 }
818 None
819}
820
821fn parse_ip_literal(host: &str) -> Option<IpAddr> {
822 host.split_once('%').map_or_else(
823 || host.parse().ok(),
824 |(address, _scope)| address.parse().ok(),
825 )
826}
827
828fn is_non_public_ip(ip: IpAddr) -> bool {
829 match ip {
830 IpAddr::V4(address) => {
831 let value = u32::from(address);
832 address.is_loopback()
833 || address.is_private()
834 || address.is_link_local()
835 || address.is_unspecified()
836 || address.is_multicast()
837 || address.is_broadcast()
838 || (value & 0xff00_0000) == 0
839 || (value & 0xffc0_0000) == 0x6440_0000
840 || ((value & 0xffff_ff00) == 0xc000_0000
841 && !matches!(value, 0xc000_0009 | 0xc000_000a))
842 || (value & 0xffff_ff00) == 0xc000_0200
843 || (value & 0xffff_ff00) == 0xc058_6300
844 || (value & 0xfffe_0000) == 0xc612_0000
845 || (value & 0xffff_ff00) == 0xc633_6400
846 || (value & 0xffff_ff00) == 0xcb00_7100
847 || (value & 0xf000_0000) == 0xf000_0000
848 }
849 IpAddr::V6(address) => is_non_public_ipv6(address),
850 }
851}
852
853fn is_non_public_ipv6(address: std::net::Ipv6Addr) -> bool {
854 let segments = address.segments();
855 address.is_loopback()
856 || address.is_unspecified()
857 || address.is_multicast()
858 || address.is_unique_local()
859 || address.is_unicast_link_local()
860 || address
861 .to_ipv4()
862 .is_some_and(|address| is_non_public_ip(IpAddr::V4(address)))
863 || well_known_nat64_ipv4(segments)
866 .is_some_and(|address| is_non_public_ip(IpAddr::V4(address)))
867 || matches!(segments, [0x64, 0xff9b, 1, _, _, _, _, _])
869 || matches!(segments, [0x100, 0, 0, 0, _, _, _, _])
871 || matches!(segments, [0x100, 0, 0, 1, _, _, _, _])
873 || is_non_public_ietf_protocol_assignment(segments)
874 || matches!(segments, [0x2002, _, _, _, _, _, _, _])
876 || matches!(segments, [0x2001, 0x0db8, _, _, _, _, _, _])
878 || matches!(segments, [0x3fff, 0x0000..=0x0fff, _, _, _, _, _, _])
879 || matches!(segments, [0x5f00, _, _, _, _, _, _, _])
881 || (segments[0] & 0xffc0) == 0xfec0
884}
885
886fn well_known_nat64_ipv4(segments: [u16; 8]) -> Option<Ipv4Addr> {
887 match segments {
888 [0x64, 0xff9b, 0, 0, 0, 0, high, low] => {
889 let [first, second] = high.to_be_bytes();
890 let [third, fourth] = low.to_be_bytes();
891 Some(Ipv4Addr::new(first, second, third, fourth))
892 }
893 _ => None,
894 }
895}
896
897fn is_non_public_ietf_protocol_assignment(segments: [u16; 8]) -> bool {
898 if segments[0] != 0x2001 || segments[1] >= 0x0200 {
899 return false;
900 }
901 !matches!(
902 segments,
903 [0x2001, 0x0001, 0, 0, 0, 0, 0, 1..=3]
905 | [0x2001, 0x0003, _, _, _, _, _, _]
907 | [0x2001, 0x0004, 0x0112, _, _, _, _, _]
909 | [0x2001, 0x0020..=0x003f, _, _, _, _, _, _]
911 )
912}
913
914fn valid_domain_literal(pattern: &str) -> bool {
915 !pattern.is_empty()
916 && !pattern.contains('*')
917 && !pattern.contains('?')
918 && (!pattern.contains(':') || normalize_ip_literal(pattern).is_some())
919}
920
921fn valid_domain_pattern(pattern: &str) -> bool {
922 if pattern == "*" {
923 return true;
924 }
925 if pattern.contains(':') || pattern.contains('%') {
926 return valid_domain_literal(pattern);
927 }
928 let suffix = pattern
929 .strip_prefix("**.")
930 .or_else(|| pattern.strip_prefix("*."))
931 .unwrap_or(pattern);
932 !suffix.is_empty()
933 && suffix.split('.').all(|label| {
934 !label.is_empty()
935 && label.chars().all(|character| {
936 !character.is_whitespace()
937 && !character.is_control()
938 && !matches!(character, ':' | '%' | '@')
939 })
940 })
941}
942
943fn valid_concrete_host(host: &str) -> bool {
944 if parse_ip_literal(host).is_some() {
945 return true;
946 }
947 host.len() <= 253
948 && host.split('.').all(|label| {
949 !label.is_empty()
950 && label.len() <= 63
951 && !label.starts_with('-')
952 && !label.ends_with('-')
953 && label
954 .chars()
955 .all(|character| character.is_alphanumeric() || matches!(character, '-' | '_'))
956 })
957}
958
959fn compile_domain_matcher(pattern: &str) -> Result<DomainMatcher, PolicyError> {
960 if pattern == "*" {
961 return Ok(DomainMatcher::Any);
962 }
963 if let Some(suffix) = pattern.strip_prefix("**.") {
964 return Ok(DomainMatcher::Suffix {
965 labels: compile_domain_labels(suffix, pattern)?,
966 include_apex: true,
967 });
968 }
969 if let Some(suffix) = pattern.strip_prefix("*.") {
970 return Ok(DomainMatcher::Suffix {
971 labels: compile_domain_labels(suffix, pattern)?,
972 include_apex: false,
973 });
974 }
975 let matcher = GlobBuilder::new(pattern)
976 .case_insensitive(true)
977 .build()
978 .map_err(|_| PolicyError::InvalidDomainPattern {
979 pattern: pattern.to_owned(),
980 })?
981 .compile_matcher();
982 Ok(DomainMatcher::Full(matcher))
983}
984
985fn compile_domain_labels(suffix: &str, pattern: &str) -> Result<Vec<GlobMatcher>, PolicyError> {
986 suffix
987 .split('.')
988 .map(|label| {
989 GlobBuilder::new(label)
990 .case_insensitive(true)
991 .build()
992 .map(|glob| glob.compile_matcher())
993 .map_err(|_| PolicyError::InvalidDomainPattern {
994 pattern: pattern.to_owned(),
995 })
996 })
997 .collect()
998}