1use std::net::IpAddr;
21use thiserror::Error;
22
23pub const MAX_ACL_ENTRY_LEN: usize = 256;
27
28const MAX_KEY_NAME_LEN: usize = 253;
31
32const ACL_KEYWORDS: [&str; 4] = ["any", "none", "localhost", "localnets"];
33
34const IPV4_MAX_PREFIX: u8 = 32;
35const IPV6_MAX_PREFIX: u8 = 128;
36
37#[derive(Debug, Error, PartialEq, Eq)]
38pub enum AclError {
39 #[error("ACL entry must not be empty")]
40 Empty,
41
42 #[error("ACL entry exceeds {MAX_ACL_ENTRY_LEN}-byte limit: {0:?}")]
43 TooLong(String),
44
45 #[error(
46 "ACL entry {0:?} is not a recognized address-match token. \
47 Accepted: any, none, localhost, localnets, an IPv4 or IPv6 address \
48 (optionally with /prefix), or \"key <name>\" — each optionally \
49 prefixed with '!'"
50 )]
51 InvalidToken(String),
52}
53
54pub fn validate_acl_entry(entry: &str) -> Result<(), AclError> {
60 let trimmed = entry.trim();
61 if trimmed.is_empty() {
62 return Err(AclError::Empty);
63 }
64 if trimmed.len() > MAX_ACL_ENTRY_LEN {
65 return Err(AclError::TooLong(trimmed.to_string()));
66 }
67
68 let core = trimmed.strip_prefix('!').unwrap_or(trimmed).trim_start();
69 if core.is_empty() {
70 return Err(AclError::InvalidToken(trimmed.to_string()));
71 }
72
73 if ACL_KEYWORDS.contains(&core) {
74 return Ok(());
75 }
76
77 if let Some(key_name) = core.strip_prefix("key ") {
78 if is_valid_key_name(key_name.trim()) {
79 return Ok(());
80 }
81 return Err(AclError::InvalidToken(trimmed.to_string()));
82 }
83
84 if is_valid_ip_or_cidr(core) {
85 return Ok(());
86 }
87
88 Err(AclError::InvalidToken(trimmed.to_string()))
89}
90
91pub fn build_acl_list(entries: &[String]) -> Result<String, AclError> {
98 let mut pieces = Vec::with_capacity(entries.len());
99 for entry in entries {
100 validate_acl_entry(entry)?;
101 pieces.push(entry.trim().to_string());
102 }
103 Ok(pieces.join("; "))
104}
105
106fn is_valid_key_name(name: &str) -> bool {
107 let unquoted = name
108 .strip_prefix('"')
109 .and_then(|n| n.strip_suffix('"'))
110 .unwrap_or(name);
111 if unquoted.is_empty() || unquoted.len() > MAX_KEY_NAME_LEN {
112 return false;
113 }
114 unquoted
115 .chars()
116 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
117}
118
119fn is_valid_ip_or_cidr(s: &str) -> bool {
120 let Some((addr_str, prefix_str)) = s.split_once('/') else {
121 return s.parse::<IpAddr>().is_ok();
122 };
123 let Ok(addr) = addr_str.parse::<IpAddr>() else {
124 return false;
125 };
126 let Ok(prefix) = prefix_str.parse::<u8>() else {
127 return false;
128 };
129 let max = if addr.is_ipv4() {
130 IPV4_MAX_PREFIX
131 } else {
132 IPV6_MAX_PREFIX
133 };
134 prefix <= max
135}