20260718 新增查询服务 VRP 检索接口
This commit is contained in:
275
src/query/vrp.rs
Normal file
275
src/query/vrp.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user