276 lines
8.1 KiB
Rust
276 lines
8.1 KiB
Rust
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<String>,
|
|
asn: u32,
|
|
prefix: &str,
|
|
max_length: u8,
|
|
) -> Result<Self, String> {
|
|
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, String> {
|
|
IpPrefix::parse(&self.prefix)
|
|
}
|
|
|
|
pub fn index_key(&self) -> Result<String, String> {
|
|
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<Self, String> {
|
|
let (addr, raw_length) = value
|
|
.rsplit_once('/')
|
|
.ok_or_else(|| format!("invalid CIDR prefix: {value}"))?;
|
|
let ip = addr
|
|
.parse::<IpAddr>()
|
|
.map_err(|_| format!("invalid IP address in prefix: {value}"))?;
|
|
let prefix_length = raw_length
|
|
.parse::<u8>()
|
|
.map_err(|_| format!("invalid prefix length in: {value}"))?;
|
|
Self::new(ip, prefix_length)
|
|
}
|
|
|
|
pub fn new(addr: IpAddr, prefix_length: u8) -> Result<Self, String> {
|
|
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<Self> {
|
|
(0..=self.prefix_length)
|
|
.filter_map(|length| Self::new(self.addr, length).ok())
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
impl VrpLookup {
|
|
pub fn parse_ip(value: &str) -> Result<Self, String> {
|
|
value
|
|
.parse::<IpAddr>()
|
|
.map(VrpLookup::Ip)
|
|
.map_err(|_| format!("invalid IP address: {value}"))
|
|
}
|
|
|
|
pub fn parse_prefix(value: &str) -> Result<Self, String> {
|
|
IpPrefix::parse(value).map(VrpLookup::Prefix)
|
|
}
|
|
|
|
pub fn candidate_prefixes(&self) -> Vec<IpPrefix> {
|
|
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<bool, String> {
|
|
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<usize, String> {
|
|
let Some(cursor) = cursor else {
|
|
return Ok(0);
|
|
};
|
|
let offset = cursor
|
|
.strip_prefix("vrp:")
|
|
.ok_or_else(|| format!("invalid VRP cursor: {cursor}"))?
|
|
.parse::<usize>()
|
|
.map_err(|_| format!("invalid VRP cursor: {cursor}"))?;
|
|
Ok(offset)
|
|
}
|
|
|
|
pub fn next_cursor(offset: usize, limit: usize, total: usize) -> Option<String> {
|
|
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);
|
|
}
|
|
}
|