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}