1mod clock;
4
5#[cfg(feature = "test-util")]
6pub use clock::ManualClock;
7pub use clock::{Clock, SystemClock};
8
9use std::net::{Ipv4Addr, Ipv6Addr};
10
11use serde::{Deserialize, Serialize};
12
13mod key;
14
15pub use key::{KeyRole, KeyState};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum Transport {
21 Udp,
22 Tcp,
23 Dot,
24 Doh,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(transparent)]
30pub struct UserId(pub i64);
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[serde(transparent)]
35pub struct TokenId(pub i64);
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum Principal {
40 Token { token: TokenId, user: UserId },
41 ClientCert { sha256: [u8; 32], subject: String },
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
46#[serde(rename_all = "lowercase")]
47pub enum Role {
48 Admin,
49 Operator,
50 Viewer,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
55pub enum Scope {
56 #[serde(rename = "dns:query")]
57 DnsQuery,
58 #[serde(rename = "api:read")]
59 ApiRead,
60 #[serde(rename = "api:write")]
61 ApiWrite,
62 #[serde(rename = "admin")]
63 Admin,
64}
65
66impl Role {
67 #[must_use]
69 pub fn allows(self, scope: Scope) -> bool {
70 match self {
71 Role::Admin => true,
72 Role::Operator => matches!(scope, Scope::ApiRead | Scope::ApiWrite | Scope::DnsQuery),
73 Role::Viewer => matches!(scope, Scope::ApiRead),
74 }
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
81#[serde(rename_all = "lowercase")]
82pub enum DnssecStatus {
83 Unchecked,
84 Secure,
85 Insecure,
86 Bogus,
87 Indeterminate,
88}
89
90pub mod hex {
92 use std::fmt::Write as _;
93
94 #[must_use]
96 pub fn upper(bytes: &[u8]) -> String {
97 bytes.iter().fold(String::new(), |mut s, b| {
98 let _ = write!(s, "{b:02X}");
99 s
100 })
101 }
102
103 #[must_use]
105 pub fn lower(bytes: &[u8]) -> String {
106 bytes.iter().fold(String::new(), |mut s, b| {
107 let _ = write!(s, "{b:02x}");
108 s
109 })
110 }
111
112 #[cfg(test)]
113 mod tests {
114 #[test]
115 fn encodes_both_cases() {
116 assert_eq!(super::upper(&[0x00, 0xab, 0xFF]), "00ABFF");
117 assert_eq!(super::lower(&[0x00, 0xab, 0xFF]), "00abff");
118 assert_eq!(super::upper(&[]), "");
119 }
120 }
121}
122
123pub mod net {
125 use std::net::{IpAddr, Ipv6Addr};
126
127 pub use ipnet::IpNet;
128
129 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
132 pub struct ClientKey(IpAddr);
133
134 impl ClientKey {
135 #[must_use]
137 pub fn ip(self) -> IpAddr {
138 self.0
139 }
140 }
141
142 impl std::fmt::Display for ClientKey {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 self.0.fmt(f)
145 }
146 }
147
148 #[must_use]
150 pub fn client_key(ip: IpAddr) -> ClientKey {
151 ClientKey(match ip.to_canonical() {
152 IpAddr::V4(v4) => IpAddr::V4(v4),
153 IpAddr::V6(v6) => IpAddr::V6(Ipv6Addr::from(u128::from(v6) & (!0u128 << 64))),
154 })
155 }
156
157 pub const MAX_TRUSTED_HOPS: usize = 16;
159
160 pub const UNKNOWN_FORWARDED: IpAddr = IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED);
166
167 #[must_use]
169 pub fn is_trusted(ip: IpAddr, trusted: &[IpNet]) -> bool {
170 let ip = ip.to_canonical();
171 trusted.iter().any(|net| net.contains(&ip))
172 }
173
174 #[must_use]
187 pub fn forwarded_client<'a>(
188 peer: IpAddr,
189 xff: impl DoubleEndedIterator<Item = &'a str>,
190 trusted: &[IpNet],
191 ) -> IpAddr {
192 if !is_trusted(peer, trusted) {
193 return peer;
194 }
195 let mut hops = 0usize;
196 for entry in xff.rev().flat_map(|value| value.rsplit(',')) {
197 let Some(ip) = parse_forwarded_entry(entry.trim()) else {
198 return UNKNOWN_FORWARDED;
199 };
200 if !is_trusted(ip, trusted) {
201 return ip.to_canonical();
202 }
203 hops += 1;
204 if hops >= MAX_TRUSTED_HOPS {
205 return UNKNOWN_FORWARDED;
206 }
207 }
208 UNKNOWN_FORWARDED
209 }
210
211 fn parse_forwarded_entry(entry: &str) -> Option<IpAddr> {
212 if let Some(inner) = entry.strip_prefix('[').and_then(|e| e.strip_suffix(']')) {
213 return inner.parse::<std::net::Ipv6Addr>().ok().map(IpAddr::V6);
214 }
215 entry.parse().ok()
216 }
217
218 #[cfg(test)]
219 mod tests {
220 use super::*;
221
222 fn ip(s: &str) -> IpAddr {
223 s.parse().unwrap()
224 }
225
226 #[test]
227 fn forwarded_client_rules() {
228 let trusted: Vec<IpNet> =
229 vec!["10.0.0.0/8".parse().unwrap(), "::1/128".parse().unwrap()];
230 let proxy = ip("10.0.0.1");
231 let unknown = "0.0.0.0";
232 let cases: &[(&str, IpAddr, &[&str], &str)] = &[
233 (
234 "untrusted peer ignores header",
235 ip("198.51.100.7"),
236 &["203.0.113.9"],
237 "198.51.100.7",
238 ),
239 (
240 "rightmost untrusted",
241 proxy,
242 &["1.1.1.1, 203.0.113.9"],
243 "203.0.113.9",
244 ),
245 (
246 "two trusted hops skipped",
247 proxy,
248 &["203.0.113.9, 10.1.1.1, 10.2.2.2"],
249 "203.0.113.9",
250 ),
251 ("all trusted", proxy, &["10.1.1.1, 10.2.2.2"], unknown),
252 ("no header", proxy, &[], unknown),
253 (
254 "garbage reached",
255 proxy,
256 &["203.0.113.9, evil.example"],
257 unknown,
258 ),
259 (
260 "garbage left of the client ignored",
261 proxy,
262 &["evil.example, 203.0.113.9"],
263 "203.0.113.9",
264 ),
265 ("empty entry reached", proxy, &["203.0.113.9, "], unknown),
266 ("bracketed v6", proxy, &["[2001:db8::5]"], "2001:db8::5"),
267 ("bare v6", proxy, &["2001:db8::5"], "2001:db8::5"),
268 ("port suffix", proxy, &["1.2.3.4:80"], unknown),
269 ("v6 with port", proxy, &["[2001:db8::5]:443"], unknown),
270 (
271 "two instances joined",
272 proxy,
273 &["203.0.113.1", "203.0.113.9"],
274 "203.0.113.9",
275 ),
276 (
277 "second instance trusted",
278 proxy,
279 &["203.0.113.1", "10.3.3.3"],
280 "203.0.113.1",
281 ),
282 (
283 "mapped peer is trusted",
284 ip("::ffff:10.0.0.1"),
285 &["203.0.113.9"],
286 "203.0.113.9",
287 ),
288 (
289 "mapped entry unmapped",
290 proxy,
291 &["::ffff:203.0.113.9"],
292 "203.0.113.9",
293 ),
294 ("v6 proxy", ip("::1"), &["203.0.113.9"], "203.0.113.9"),
295 ("proxy names itself", proxy, &["10.0.0.1"], unknown),
296 ];
297 for (name, peer, xff, want) in cases {
298 let got = forwarded_client(*peer, xff.iter().copied(), &trusted);
299 assert_eq!(got, ip(want), "{name}");
300 }
301 let padded = format!("{}, 203.0.113.9", vec!["evil"; 5000].join(","));
303 assert_eq!(
304 forwarded_client(proxy, [padded.as_str()].into_iter(), &trusted),
305 ip("203.0.113.9")
306 );
307 let hops = |n: usize| format!("203.0.113.9, {}", vec!["10.9.9.9"; n].join(", "));
309 let fifteen = hops(15);
310 assert_eq!(
311 forwarded_client(proxy, [fifteen.as_str()].into_iter(), &trusted),
312 ip("203.0.113.9")
313 );
314 let sixteen = hops(16);
315 assert_eq!(
316 forwarded_client(proxy, [sixteen.as_str()].into_iter(), &trusted),
317 UNKNOWN_FORWARDED
318 );
319 assert_eq!(
320 forwarded_client(proxy, ["203.0.113.9"].into_iter(), &[]),
321 proxy
322 );
323 }
324
325 #[test]
326 fn client_key_groups_ipv6_by_64() {
327 assert_eq!(
328 client_key(ip("2001:db8:0:1:ffff::3")).ip(),
329 ip("2001:db8:0:1::")
330 );
331 assert_eq!(
332 client_key(ip("2001:db8:0:1::1")),
333 client_key(ip("2001:db8:0:1:aaaa::2"))
334 );
335 assert_ne!(
336 client_key(ip("2001:db8:0:1::1")),
337 client_key(ip("2001:db8:0:2::1"))
338 );
339 }
340
341 #[test]
342 fn client_key_keeps_ipv4_and_unmaps_mapped() {
343 assert_eq!(client_key(ip("192.0.2.9")).ip(), ip("192.0.2.9"));
344 assert_eq!(client_key(ip("::ffff:192.0.2.9")).ip(), ip("192.0.2.9"));
345 assert_eq!(client_key(ip("192.0.2.9")).to_string(), "192.0.2.9");
346 }
347 }
348}
349
350#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
352#[serde(rename_all = "lowercase")]
353pub enum Validation {
354 Off,
356 Permissive,
358 #[default]
360 Strict,
361}
362
363#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
365#[serde(rename_all = "snake_case")]
366pub enum BlockMode {
367 #[serde(rename = "nxdomain")]
369 NxDomain,
370 #[default]
372 NullIp,
373 Refused,
375 #[serde(skip_deserializing)]
378 CustomIp {
379 v4: Option<Ipv4Addr>,
380 v6: Option<Ipv6Addr>,
381 },
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 const SCOPES: [(Scope, &str); 4] = [
389 (Scope::DnsQuery, "\"dns:query\""),
390 (Scope::ApiRead, "\"api:read\""),
391 (Scope::ApiWrite, "\"api:write\""),
392 (Scope::Admin, "\"admin\""),
393 ];
394
395 #[test]
396 fn scope_serde_roundtrip() {
397 for (scope, json) in SCOPES {
398 assert_eq!(serde_json::to_string(&scope).unwrap(), json);
399 assert_eq!(serde_json::from_str::<Scope>(json).unwrap(), scope);
400 }
401 assert!(serde_json::from_str::<Scope>("\"root\"").is_err());
402 }
403
404 #[test]
405 fn role_allows_scope() {
406 for (scope, _) in SCOPES {
407 assert!(Role::Admin.allows(scope));
408 assert_eq!(Role::Viewer.allows(scope), scope == Scope::ApiRead);
409 assert_eq!(Role::Operator.allows(scope), scope != Scope::Admin);
410 }
411 }
412
413 #[test]
414 fn dnssec_status_serde() {
415 let all = [
416 (DnssecStatus::Secure, "\"secure\""),
417 (DnssecStatus::Insecure, "\"insecure\""),
418 (DnssecStatus::Bogus, "\"bogus\""),
419 (DnssecStatus::Indeterminate, "\"indeterminate\""),
420 (DnssecStatus::Unchecked, "\"unchecked\""),
421 ];
422 for (status, json) in all {
423 assert_eq!(serde_json::to_string(&status).unwrap(), json);
424 assert_eq!(serde_json::from_str::<DnssecStatus>(json).unwrap(), status);
425 }
426 }
427
428 #[test]
429 fn ids_and_enums_serde() {
430 assert_eq!(serde_json::to_string(&UserId(7)).unwrap(), "7");
431 assert_eq!(serde_json::to_string(&TokenId(9)).unwrap(), "9");
432 assert_eq!(serde_json::to_string(&Transport::Doh).unwrap(), "\"doh\"");
433 assert_eq!(serde_json::to_string(&Role::Viewer).unwrap(), "\"viewer\"");
434 let p = Principal::ClientCert {
435 sha256: [0; 32],
436 subject: "CN=a".into(),
437 };
438 assert_eq!(p.clone(), p);
439 }
440
441 #[test]
442 fn validation_and_block_mode_serde_strings_are_pinned() {
443 for (v, text) in [
444 (Validation::Off, "\"off\""),
445 (Validation::Permissive, "\"permissive\""),
446 (Validation::Strict, "\"strict\""),
447 ] {
448 assert_eq!(serde_json::to_string(&v).unwrap(), text);
449 assert_eq!(serde_json::from_str::<Validation>(text).unwrap(), v);
450 }
451 for (m, text) in [
452 (BlockMode::NullIp, "\"null_ip\""),
453 (BlockMode::NxDomain, "\"nxdomain\""),
454 (BlockMode::Refused, "\"refused\""),
455 ] {
456 assert_eq!(serde_json::to_string(&m).unwrap(), text);
457 assert_eq!(serde_json::from_str::<BlockMode>(text).unwrap(), m);
458 }
459 assert_eq!(Validation::default(), Validation::Strict);
460 assert_eq!(BlockMode::default(), BlockMode::NullIp);
461 }
462}