Files
rpki/src/query/vrp.rs

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);
}
}