1use std::net::IpAddr;
2
3#[derive(Clone, Debug, Eq, Hash, PartialEq)]
5pub struct ServerBlockKey {
6 pub transport: String,
8 pub domain: String,
10 pub port: String,
12}
13
14pub fn normalize_key(key: &str) -> Result<Vec<ServerBlockKey>, String> {
16 let (transport, host) = ["dns", "tls", "quic", "grpc", "https", "https3", "unix"]
17 .into_iter()
18 .find_map(|transport| {
19 key.strip_prefix(&format!("{transport}://"))
20 .map(|host| (transport, host))
21 })
22 .unwrap_or(("dns", key));
23
24 if host.is_empty() || host.ends_with(':') {
25 return Err(format!("expecting data after last colon: {host:?}"));
26 }
27 let (host, port) = host
28 .rsplit_once(':')
29 .and_then(|(host, port)| {
30 port.parse::<i64>()
31 .ok()
32 .map(|port| (host, port.to_string()))
33 })
34 .unwrap_or((host, String::new()));
35 if host.len() > 255 || !valid_domain(host) {
36 return Err(format!("invalid Domain: {host}"));
37 }
38
39 let domains = match parse_cidr(host) {
40 Some((ip, prefix)) => {
41 if host.starts_with(':') || (host.starts_with('0') && host.contains(':')) {
42 return Err(format!("invalid CIDR {host}"));
43 }
44 reverse_domains(ip, prefix)
45 }
46 None => vec![fqdn(host)],
47 };
48 let port = if port.is_empty() {
49 match transport {
50 "dns" => "53",
51 "tls" | "quic" => "853",
52 "grpc" | "https" | "https3" => "443",
53 _ => "",
54 }
55 .to_owned()
56 } else {
57 port
58 };
59 domains
60 .into_iter()
61 .map(|domain| {
62 if domain.len() > 255 || !valid_domain(&domain) {
63 return Err(format!("invalid Domain: {domain}"));
64 }
65 Ok(ServerBlockKey {
66 transport: transport.to_owned(),
67 domain: domain.chars().map(go_lowercase).collect(),
68 port: port.clone(),
69 })
70 })
71 .collect()
72}
73
74fn go_lowercase(c: char) -> char {
75 match c {
77 '\u{1c89}'
78 | '\u{a7cb}'
79 | '\u{a7cc}'
80 | '\u{a7ce}'
81 | '\u{a7d2}'
82 | '\u{a7d4}'
83 | '\u{a7da}'
84 | '\u{a7dc}'
85 | '\u{10d50}'..='\u{10d65}'
86 | '\u{16ea0}'..='\u{16eb8}' => c,
87 _ => c.to_lowercase().next().unwrap_or(c),
88 }
89}
90
91fn fqdn(name: &str) -> String {
92 let is_fqdn = name.strip_suffix('.').is_some_and(|prefix| {
93 if !prefix.ends_with('\\') {
94 return true;
95 }
96 let last_non_escape = prefix.char_indices().rfind(|(_, c)| *c != '\\');
98 let distance = last_non_escape.map_or(prefix.len() + 1, |(index, _)| prefix.len() - index);
99 !distance.is_multiple_of(2)
100 });
101 if is_fqdn {
102 name.to_owned()
103 } else {
104 format!("{name}.")
105 }
106}
107
108fn valid_domain(name: &str) -> bool {
109 if name.is_empty() {
110 return false;
111 }
112 let qualified = fqdn(name);
113 let bytes = qualified.as_bytes();
114 let (mut i, mut begin, mut offset) = (0, 0, 0);
115 let (mut was_dot, mut escape) = (false, false);
116 while i < bytes.len() {
117 match bytes[i] {
118 b'\\' => {
119 escape = !escape;
120 if offset + 1 > 256 {
121 return false;
122 }
123 let digits = bytes
124 .get(i + 1..i + 4)
125 .is_some_and(|digits| digits.iter().all(u8::is_ascii_digit));
126 let escaped = if digits { 3 } else { 1 };
127 i += escaped;
128 begin += escaped;
129 was_dot = false;
130 }
131 b'.' => {
132 escape = false;
133 if (i == 0 && bytes.len() > 1) || was_dot {
134 return false;
135 }
136 let label_len = i - begin;
137 if label_len >= 64 {
139 return false;
140 }
141 offset += 1 + label_len;
142 if offset > 256 {
143 return false;
144 }
145 begin = i + 1;
146 was_dot = true;
147 }
148 _ => {
149 escape = false;
150 was_dot = false;
151 }
152 }
153 i += 1;
154 }
155 !escape
156}
157
158fn parse_cidr(host: &str) -> Option<(IpAddr, u32)> {
159 let (address, prefix) = host.split_once('/')?;
160 if prefix.is_empty() || !prefix.bytes().all(|b| b.is_ascii_digit()) {
161 return None;
162 }
163 let ip: IpAddr = address.parse().ok()?;
164 let prefix: u32 = prefix.parse().ok()?;
165 let bits = if ip.is_ipv4() { 32 } else { 128 };
166 (prefix <= bits).then_some((ip, prefix))
167}
168
169fn reverse_domains(ip: IpAddr, prefix: u32) -> Vec<String> {
170 let (value, bits, step, suffix) = match ip {
173 IpAddr::V4(ip) => (u128::from(u32::from(ip)), 32, 8, "in-addr.arpa."),
174 IpAddr::V6(ip) => (u128::from(ip), 128, 4, "ip6.arpa."),
175 };
176 let rounded = prefix.div_ceil(step) * step;
177 let network = if prefix == 0 {
178 0
179 } else {
180 (value >> (bits - prefix)) << (bits - prefix)
181 };
182 let count = 1_u128 << (rounded - prefix);
183 (0..count)
184 .map(|index| {
185 let value = if rounded == 0 {
186 network
187 } else {
188 network | (index << (bits - rounded))
189 };
190 let mut domain = String::new();
191 for label in (0..rounded / step).rev() {
192 let digit = (value >> (bits - step * (label + 1))) & ((1 << step) - 1);
193 if step == 8 {
194 domain.push_str(&format!("{digit}."));
195 } else {
196 domain.push_str(&format!("{digit:x}."));
197 }
198 }
199 domain.push_str(suffix);
200 domain
201 })
202 .collect()
203}
204
205#[cfg(test)]
206mod tests {
207 use super::{ServerBlockKey, normalize_key};
208
209 fn key(transport: &str, domain: &str, port: &str) -> ServerBlockKey {
210 ServerBlockKey {
211 transport: transport.into(),
212 domain: domain.into(),
213 port: port.into(),
214 }
215 }
216
217 #[test]
218 fn transport_and_port_defaults() {
219 for (input, transport, port) in [
220 (".", "dns", "53"),
221 ("dns://Example.ORG", "dns", "53"),
222 ("tls://Example.ORG.", "tls", "853"),
223 ("quic://example.org", "quic", "853"),
224 ("grpc://example.org", "grpc", "443"),
225 ("https://example.org", "https", "443"),
226 ("https3://example.org", "https3", "443"),
227 ("unix://example.org", "unix", ""),
228 ] {
229 let domain = if input == "." { "." } else { "example.org." };
230 assert_eq!(
231 normalize_key(input).unwrap(),
232 [key(transport, domain, port)],
233 "{input}"
234 );
235 }
236 }
237
238 #[test]
239 fn numeric_ports_are_canonicalized_without_range_validation() {
240 for (input, domain, port) in [
241 ("Example.Org:0053", "example.org.", "53"),
242 ("example.org:+53", "example.org.", "53"),
243 ("example.org:-1", "example.org.", "-1"),
244 ("example.org:99999", "example.org.", "99999"),
245 ("example.org:http", "example.org:http.", "53"),
246 ("unknown://Example.org", "unknown://example.org.", "53"),
247 ("2001:db8::1", "2001:db8:.", "1"),
248 ] {
249 assert_eq!(
250 normalize_key(input).unwrap(),
251 [key("dns", domain, port)],
252 "{input}"
253 );
254 }
255 }
256
257 #[test]
258 fn expands_ipv4_networks_on_octet_boundaries() {
259 assert_eq!(
260 normalize_key("10.0.0.0/8:53").unwrap(),
261 [key("dns", "10.in-addr.arpa.", "53")]
262 );
263 assert_eq!(
264 normalize_key("tls://192.168.1.1/23:0054").unwrap(),
265 [
266 key("tls", "0.168.192.in-addr.arpa.", "54"),
267 key("tls", "1.168.192.in-addr.arpa.", "54"),
268 ]
269 );
270 let expanded = normalize_key("10.9.3.0/18").unwrap();
271 assert_eq!(expanded.len(), 64);
272 assert_eq!(expanded[0], key("dns", "0.9.10.in-addr.arpa.", "53"));
273 assert_eq!(expanded[63], key("dns", "63.9.10.in-addr.arpa.", "53"));
274 assert_eq!(
275 normalize_key("0.0.0.0/0").unwrap(),
276 [key("dns", "in-addr.arpa.", "53")]
277 );
278 }
279
280 #[test]
281 fn expands_ipv6_networks_on_nibble_boundaries() {
282 assert_eq!(
283 normalize_key("2001:db8::/31").unwrap(),
284 [
285 key("dns", "8.b.d.0.1.0.0.2.ip6.arpa.", "53"),
286 key("dns", "9.b.d.0.1.0.0.2.ip6.arpa.", "53"),
287 ]
288 );
289 let expanded = normalize_key("2001:db8::/29:853").unwrap();
290 assert_eq!(expanded.len(), 8);
291 assert_eq!(expanded[7], key("dns", "f.b.d.0.1.0.0.2.ip6.arpa.", "853"));
292 assert!(normalize_key("::1/128").is_err());
293 assert!(normalize_key("0:1::/32").is_err());
294 assert_eq!(
295 normalize_key("2001:db8::/0").unwrap(),
296 [key("dns", "ip6.arpa.", "53")]
297 );
298 }
299
300 #[test]
301 fn trailing_dot_and_invalid_cidr_remain_domains() {
302 for (input, domain) in [
303 ("10.0.0.0/8.", "10.0.0.0/8."),
304 ("2001:db8::/32.", "2001:db8::/32."),
305 ("10.0.0.0/99", "10.0.0.0/99."),
306 ("10.0.0/8", "10.0.0/8."),
307 ] {
308 assert_eq!(
309 normalize_key(input).unwrap(),
310 [key("dns", domain, "53")],
311 "{input}"
312 );
313 }
314 }
315
316 #[test]
317 fn validates_domain_names_like_coredns() {
318 for input in ["", "dns://", "example:", ".example", "a..b", "a\\", "ȶ\\."] {
319 assert!(normalize_key(input).is_err(), "{input:?}");
320 }
321 assert!(normalize_key(&format!("{}.example", "a".repeat(64))).is_err());
322 assert!(normalize_key(&"a".repeat(256)).is_err());
323 assert_eq!(
324 normalize_key("A\\.B.Example").unwrap(),
325 [key("dns", "a\\.b.example.", "53")]
326 );
327 assert_eq!(normalize_key("A\\.").unwrap(), [key("dns", "a\\..", "53")]);
328 assert_eq!(
329 normalize_key("İ\u{10d50}\u{16ea0}").unwrap(),
330 [key("dns", "i\u{10d50}\u{16ea0}.", "53")]
331 );
332 }
333}