use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct VrpRecord { pub run_id: String, pub asn: u32, pub prefix: String, pub max_length: u8, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct IpPrefix { addr: IpAddr, prefix_length: u8, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum VrpLookup { Ip(IpAddr), Prefix(IpPrefix), } impl VrpRecord { pub fn from_report( run_id: impl Into, asn: u32, prefix: &str, max_length: u8, ) -> Result { let network = IpPrefix::parse(prefix)?; if max_length < network.prefix_length { return Err(format!( "VRP max_length {max_length} is shorter than prefix length {}", network.prefix_length )); } if max_length > network.max_prefix_length() { return Err(format!( "VRP max_length {max_length} exceeds address family limit {}", network.max_prefix_length() )); } Ok(Self { run_id: run_id.into(), asn, prefix: network.to_cidr(), max_length, }) } pub fn prefix_data(&self) -> Result { IpPrefix::parse(&self.prefix) } pub fn index_key(&self) -> Result { let prefix = self.prefix_data()?; Ok(format!( "vrp/{}/{}/{:03}/{}/{:010}/{:03}", self.run_id, prefix.family(), prefix.prefix_length, prefix.network_hex(), self.asn, self.max_length )) } } impl IpPrefix { pub fn parse(value: &str) -> Result { let (addr, raw_length) = value .rsplit_once('/') .ok_or_else(|| format!("invalid CIDR prefix: {value}"))?; let ip = addr .parse::() .map_err(|_| format!("invalid IP address in prefix: {value}"))?; let prefix_length = raw_length .parse::() .map_err(|_| format!("invalid prefix length in: {value}"))?; Self::new(ip, prefix_length) } pub fn new(addr: IpAddr, prefix_length: u8) -> Result { if prefix_length > max_prefix_length(addr) { return Err(format!( "prefix length {prefix_length} exceeds address family limit {}", max_prefix_length(addr) )); } Ok(Self { addr: network_address(addr, prefix_length), prefix_length, }) } pub fn prefix_length(&self) -> u8 { self.prefix_length } pub fn max_prefix_length(&self) -> u8 { max_prefix_length(self.addr) } pub fn family(&self) -> &'static str { match self.addr { IpAddr::V4(_) => "v4", IpAddr::V6(_) => "v6", } } pub fn network_hex(&self) -> String { match self.addr { IpAddr::V4(addr) => hex::encode(addr.octets()), IpAddr::V6(addr) => hex::encode(addr.octets()), } } pub fn to_cidr(&self) -> String { format!("{}/{}", self.addr, self.prefix_length) } pub fn contains_ip(&self, ip: IpAddr) -> bool { if std::mem::discriminant(&self.addr) != std::mem::discriminant(&ip) { return false; } network_address(ip, self.prefix_length) == self.addr } pub fn contains_prefix(&self, other: &Self) -> bool { self.family() == other.family() && self.prefix_length <= other.prefix_length && self.contains_ip(other.addr) } pub fn ancestor_prefixes(&self) -> Vec { (0..=self.prefix_length) .filter_map(|length| Self::new(self.addr, length).ok()) .collect() } } impl VrpLookup { pub fn parse_ip(value: &str) -> Result { value .parse::() .map(VrpLookup::Ip) .map_err(|_| format!("invalid IP address: {value}")) } pub fn parse_prefix(value: &str) -> Result { IpPrefix::parse(value).map(VrpLookup::Prefix) } pub fn candidate_prefixes(&self) -> Vec { match self { VrpLookup::Ip(ip) => (0..=max_prefix_length(*ip)) .filter_map(|length| IpPrefix::new(*ip, length).ok()) .collect(), VrpLookup::Prefix(prefix) => prefix.ancestor_prefixes(), } } pub fn matches(&self, record: &VrpRecord) -> Result { let record_prefix = record.prefix_data()?; Ok(match self { VrpLookup::Ip(ip) => record_prefix.contains_ip(*ip), VrpLookup::Prefix(prefix) => { record_prefix.contains_prefix(prefix) && record.max_length >= prefix.prefix_length } }) } } pub fn vrp_key_prefix(run_id: &str, prefix: &IpPrefix) -> String { format!( "vrp/{}/{}/{:03}/{}/", run_id, prefix.family(), prefix.prefix_length, prefix.network_hex() ) } pub fn parse_cursor(cursor: Option<&str>) -> Result { let Some(cursor) = cursor else { return Ok(0); }; let offset = cursor .strip_prefix("vrp:") .ok_or_else(|| format!("invalid VRP cursor: {cursor}"))? .parse::() .map_err(|_| format!("invalid VRP cursor: {cursor}"))?; Ok(offset) } pub fn next_cursor(offset: usize, limit: usize, total: usize) -> Option { let next = offset.saturating_add(limit); (next < total).then(|| format!("vrp:{next}")) } fn max_prefix_length(addr: IpAddr) -> u8 { match addr { IpAddr::V4(_) => 32, IpAddr::V6(_) => 128, } } fn network_address(addr: IpAddr, prefix_length: u8) -> IpAddr { match addr { IpAddr::V4(addr) => { let value = u32::from(addr); let mask = if prefix_length == 0 { 0 } else { u32::MAX << (32 - prefix_length) }; IpAddr::V4(Ipv4Addr::from(value & mask)) } IpAddr::V6(addr) => { let mut octets = addr.octets(); let full_bytes = usize::from(prefix_length / 8); let remaining_bits = prefix_length % 8; if full_bytes < octets.len() { if remaining_bits > 0 { octets[full_bytes] &= 0xff << (8 - remaining_bits); } let first_zero = full_bytes + usize::from(remaining_bits > 0); for byte in &mut octets[first_zero..] { *byte = 0; } } IpAddr::V6(Ipv6Addr::from(octets)) } } } #[cfg(test)] mod tests { use super::*; #[test] fn normalizes_ipv4_and_ipv6_networks() { assert_eq!( IpPrefix::parse("192.0.2.129/24").unwrap().to_cidr(), "192.0.2.0/24" ); assert_eq!( IpPrefix::parse("2001:db8:1::1234/48").unwrap().to_cidr(), "2001:db8:1::/48" ); } #[test] fn prefix_lookup_uses_covering_and_max_length_semantics() { let lookup = VrpLookup::parse_prefix("192.0.2.0/24").unwrap(); let covering = VrpRecord::from_report("run_1", 64496, "192.0.0.0/16", 24).unwrap(); let too_short = VrpRecord::from_report("run_1", 64497, "192.0.0.0/16", 23).unwrap(); let intersecting = VrpRecord::from_report("run_1", 64498, "192.0.2.128/25", 25).unwrap(); assert!(lookup.matches(&covering).unwrap()); assert!(!lookup.matches(&too_short).unwrap()); assert!(!lookup.matches(&intersecting).unwrap()); } #[test] fn cursor_is_scoped_and_bounded() { assert_eq!(parse_cursor(None).unwrap(), 0); assert_eq!(parse_cursor(Some("vrp:12")).unwrap(), 12); assert!(parse_cursor(Some("12")).is_err()); assert_eq!(next_cursor(0, 10, 12).as_deref(), Some("vrp:10")); assert_eq!(next_cursor(10, 10, 12), None); } }