dwara_core/config/net.rs
1//! Minimal IP/CIDR support for trusted-proxy and IP-ACL matching
2//! (DW-009/DW-020).
3//!
4//! The trusted-proxy matcher is part of the CONFIG CONTRACT — validation
5//! (`snapshot::validate`) and every runtime consumer (the dataplane's
6//! forwarded-header rule, authorization's IP ACLs) must agree on what a
7//! well-formed `ip`/`ip/prefix` entry is, so the parser and the matcher
8//! live here with the schema instead of in any one consumer. No new
9//! dependency: `std::net` parsing plus a shift-compare.
10
11use std::net::IpAddr;
12
13/// Parse `ip`, `ip/prefix` into (network address, prefix length).
14/// Returns None for anything that is not a well-formed IPv4/IPv6 address
15/// or CIDR (including prefixes wider than the address family allows).
16pub fn parse_ip_or_cidr(s: &str) -> Option<(IpAddr, u8)> {
17 let s = s.trim();
18 if let Ok(ip) = s.parse::<IpAddr>() {
19 let bits = if ip.is_ipv4() { 32 } else { 128 };
20 return Some((ip, bits));
21 }
22 let (addr, prefix) = s.split_once('/')?;
23 let ip: IpAddr = addr.trim().parse().ok()?;
24 let max = if ip.is_ipv4() { 32 } else { 128 };
25 let prefix: u8 = prefix.trim().parse().ok()?;
26 if prefix > max {
27 return None;
28 }
29 Some((ip, prefix))
30}
31
32/// Whether `ip` falls inside `net/prefix` (same-family only). Public so
33/// authorization (DW-020) matches IP ACL entries with the exact
34/// DW-009 trusted-proxy semantics.
35pub fn ip_in_net(ip: IpAddr, net: IpAddr, prefix: u8) -> bool {
36 match (ip, net) {
37 (IpAddr::V4(a), IpAddr::V4(b)) => {
38 let (a, b) = (u32::from(a), u32::from(b));
39 prefix == 0 || ((a ^ b) >> (32 - prefix as u32)) == 0
40 }
41 (IpAddr::V6(a), IpAddr::V6(b)) => {
42 let (a, b) = (u128::from(a), u128::from(b));
43 prefix == 0 || ((a ^ b) >> (128 - prefix as u32)) == 0
44 }
45 _ => false,
46 }
47}
48
49/// Whether `peer` falls inside any configured trusted-proxy entry.
50/// Unparseable entries cannot occur in a validated config; they are
51/// conservatively treated as non-matching here (validation rejects the
52/// whole config before the dataplane ever sees it).
53pub fn peer_is_trusted(trusted: &[String], peer: IpAddr) -> bool {
54 trusted.iter().any(|entry| {
55 parse_ip_or_cidr(entry).is_some_and(|(net, prefix)| ip_in_net(peer, net, prefix))
56 })
57}