kirino 0.6.2

Zero-trust authentication and authorization framework with RBAC and dynamic risk assessment.
Documentation
use std::net::IpAddr;

pub struct NetworkSourceVerifier {
    allowed_networks: Vec<ParsedCidr>,
}

enum ParsedCidr {
    V4 { network: u32, mask: u32 },
    V6 { network: u128, mask: u128 },
    ExactIp(IpAddr),
}

impl NetworkSourceVerifier {
    pub fn new(allowed_networks: Vec<String>) -> Self {
        let parsed: Vec<ParsedCidr> = allowed_networks
            .iter()
            .filter_map(|cidr| parse_cidr(cidr))
            .collect();
        if parsed.len() != allowed_networks.len() {
            tracing::warn!(
                target: "kirino::verify::network_source",
                attempted = allowed_networks.len(),
                parsed = parsed.len(),
                "some CIDR entries failed to parse and were silently ignored"
            );
        }
        Self {
            allowed_networks: parsed,
        }
    }

    pub fn is_allowed(&self, source_ip: IpAddr) -> bool {
        for entry in &self.allowed_networks {
            match entry {
                ParsedCidr::ExactIp(ip) if *ip == source_ip => return true,
                ParsedCidr::V4 { network, mask } => {
                    if let IpAddr::V4(v4) = source_ip {
                        let bits = u32::from(v4);
                        if (bits & mask) == (*network & mask) {
                            return true;
                        }
                    }
                }
                ParsedCidr::V6 { network, mask } => {
                    if let IpAddr::V6(v6) = source_ip {
                        let bits = u128::from(v6);
                        if (bits & mask) == (*network & mask) {
                            return true;
                        }
                    }
                }
                _ => {}
            }
        }
        false
    }

    #[must_use]
    pub fn allowed_networks_count(&self) -> usize {
        self.allowed_networks.len()
    }
}

fn parse_cidr(cidr: &str) -> Option<ParsedCidr> {
    let parts: Vec<&str> = cidr.split('/').collect();
    if parts.len() != 2 {
        return match cidr.parse::<std::net::Ipv4Addr>() {
            Ok(v4) => Some(ParsedCidr::ExactIp(IpAddr::V4(v4))),
            Err(_) => cidr
                .parse::<std::net::Ipv6Addr>()
                .ok()
                .map(|v6| ParsedCidr::ExactIp(IpAddr::V6(v6))),
        };
    }

    let prefix_len: u32 = parts[1].parse().ok()?;

    if let Ok(network) = parts[0].parse::<std::net::Ipv4Addr>() {
        if prefix_len > 32 {
            return None;
        }
        let mask = if prefix_len == 0 {
            0
        } else {
            !0u32 << (32 - prefix_len)
        };
        Some(ParsedCidr::V4 {
            network: u32::from(network),
            mask,
        })
    } else if let Ok(network) = parts[0].parse::<std::net::Ipv6Addr>() {
        if prefix_len > 128 {
            return None;
        }
        let mask = if prefix_len == 0 {
            0u128
        } else {
            !0u128 << (128 - prefix_len)
        };
        Some(ParsedCidr::V6 {
            network: u128::from(network),
            mask,
        })
    } else {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

    #[test]
    fn test_single_ip_match() {
        let v = NetworkSourceVerifier::new(vec!["192.168.1.1".to_string()]);
        let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
        assert!(v.is_allowed(ip));
    }

    #[test]
    fn test_single_ip_no_match() {
        let v = NetworkSourceVerifier::new(vec!["192.168.1.1".to_string()]);
        let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
        assert!(!v.is_allowed(ip));
    }

    #[test]
    fn test_cidr_24_match() {
        let v = NetworkSourceVerifier::new(vec!["192.168.1.0/24".to_string()]);
        let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100));
        assert!(v.is_allowed(ip));
    }

    #[test]
    fn test_cidr_24_no_match() {
        let v = NetworkSourceVerifier::new(vec!["192.168.1.0/24".to_string()]);
        let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 2, 1));
        assert!(!v.is_allowed(ip));
    }

    #[test]
    fn test_cidr_16() {
        let v = NetworkSourceVerifier::new(vec!["10.0.0.0/16".to_string()]);
        let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 255, 255));
        assert!(v.is_allowed(ip));
        let ip2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 0));
        assert!(!v.is_allowed(ip2));
    }

    #[test]
    fn test_cidr_32_exact_match() {
        let v = NetworkSourceVerifier::new(vec!["192.168.1.1/32".to_string()]);
        assert!(v.is_allowed(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
        assert!(!v.is_allowed(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))));
    }

    #[test]
    fn test_cidr_0_match_all() {
        let v = NetworkSourceVerifier::new(vec!["0.0.0.0/0".to_string()]);
        assert!(v.is_allowed(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))));
        assert!(v.is_allowed(IpAddr::V4(Ipv4Addr::BROADCAST)));
    }

    #[test]
    fn test_invalid_cidr_returns_false() {
        let v = NetworkSourceVerifier::new(vec!["not-a-cidr".to_string()]);
        assert!(!v.is_allowed(IpAddr::V4(Ipv4Addr::LOCALHOST)));
    }

    #[test]
    fn test_invalid_cidr_prefix_returns_false() {
        let v = NetworkSourceVerifier::new(vec!["10.0.0.0/abc".to_string()]);
        assert!(!v.is_allowed(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
    }

    #[test]
    fn test_ipv6_loopback() {
        let v = NetworkSourceVerifier::new(vec!["::1/128".to_string()]);
        let ip = IpAddr::V6(Ipv6Addr::LOCALHOST);
        assert!(v.is_allowed(ip));
    }

    #[test]
    fn test_ipv6_subnet() {
        let v = NetworkSourceVerifier::new(vec!["2001:db8::/32".to_string()]);
        assert!(v.is_allowed(IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))));
        assert!(!v.is_allowed(IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb9, 0, 0, 0, 0, 0, 1))));
    }

    #[test]
    fn test_ipv6_all_any() {
        let v = NetworkSourceVerifier::new(vec!["::/0".to_string()]);
        assert!(v.is_allowed(IpAddr::V6(Ipv6Addr::LOCALHOST)));
        assert!(v.is_allowed(IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))));
    }

    #[test]
    fn test_ipv6_exact() {
        let v = NetworkSourceVerifier::new(vec!["fe80::1".to_string()]);
        assert!(v.is_allowed(IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1))));
        assert!(!v.is_allowed(IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 2))));
    }

    #[test]
    fn test_empty_allowed() {
        let v = NetworkSourceVerifier::new(vec![]);
        let ip = IpAddr::V4(Ipv4Addr::LOCALHOST);
        assert!(!v.is_allowed(ip));
    }

    #[test]
    fn test_multiple_networks() {
        let v = NetworkSourceVerifier::new(vec![
            "192.168.0.0/16".to_string(),
            "10.0.0.0/8".to_string(),
        ]);
        assert!(v.is_allowed(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
        assert!(v.is_allowed(IpAddr::V4(Ipv4Addr::new(10, 42, 0, 1))));
        assert!(!v.is_allowed(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1))));
    }

    #[test]
    fn test_allowed_networks_count() {
        let v = NetworkSourceVerifier::new(vec!["10.0.0.0/8".to_string()]);
        assert_eq!(v.allowed_networks_count(), 1);
    }

    #[test]
    fn test_mixed_ipv4_ipv6_networks() {
        let v =
            NetworkSourceVerifier::new(vec!["192.168.1.0/24".to_string(), "::1/128".to_string()]);
        assert!(v.is_allowed(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
        assert!(v.is_allowed(IpAddr::V6(Ipv6Addr::LOCALHOST)));
        assert!(!v.is_allowed(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
    }

    #[test]
    fn test_ipv4_network_against_ipv6_ip() {
        let v = NetworkSourceVerifier::new(vec!["192.168.1.0/24".to_string()]);
        assert!(!v.is_allowed(IpAddr::V6(Ipv6Addr::LOCALHOST)));
    }

    #[test]
    fn test_prefix_len_over_32_returns_false() {
        let v = NetworkSourceVerifier::new(vec!["10.0.0.0/33".to_string()]);
        assert!(!v.is_allowed(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
    }

    #[test]
    fn test_prefix_len_over_128_returns_false() {
        let v = NetworkSourceVerifier::new(vec!["::1/129".to_string()]);
        assert!(!v.is_allowed(IpAddr::V6(Ipv6Addr::LOCALHOST)));
    }

    #[test]
    fn test_prefix_len_exactly_32() {
        let v = NetworkSourceVerifier::new(vec!["10.0.0.0/32".to_string()]);
        assert!(v.is_allowed(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0))));
        assert!(!v.is_allowed(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
    }
}