356 lines
12 KiB
Rust
356 lines
12 KiB
Rust
use std::fs;
|
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use anyhow::{Context, Result, anyhow};
|
|
use der_parser::ber::{BerObject, BerObjectContent};
|
|
use der_parser::der::parse_der;
|
|
|
|
use crate::rtr::loader::{ParsedAspa, ParsedVrp, build_aspa, build_route_origin};
|
|
use crate::rtr::payload::Payload;
|
|
|
|
const VRPS_INDEX: usize = 3;
|
|
const VAPS_INDEX: usize = 4;
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct ParsedCcrSnapshot {
|
|
pub content_type_oid: String,
|
|
pub produced_at: Option<String>,
|
|
pub vrps: Vec<ParsedVrp>,
|
|
pub vaps: Vec<ParsedAspa>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
pub struct CcrPayloadConversion {
|
|
pub payloads: Vec<Payload>,
|
|
pub invalid_vrps: Vec<String>,
|
|
pub invalid_vaps: Vec<String>,
|
|
}
|
|
|
|
pub fn load_ccr_snapshot_from_file(path: impl AsRef<Path>) -> Result<ParsedCcrSnapshot> {
|
|
let path = path.as_ref();
|
|
let bytes =
|
|
fs::read(path).with_context(|| format!("failed to read CCR file: {}", path.display()))?;
|
|
parse_ccr_bytes(&bytes).with_context(|| format!("failed to parse CCR file: {}", path.display()))
|
|
}
|
|
|
|
pub fn load_ccr_payloads_from_file(path: impl AsRef<Path>) -> Result<Vec<Payload>> {
|
|
let snapshot = load_ccr_snapshot_from_file(path)?;
|
|
snapshot_to_payloads(&snapshot)
|
|
}
|
|
|
|
pub fn load_ccr_payloads_from_file_with_options(
|
|
path: impl AsRef<Path>,
|
|
strict: bool,
|
|
) -> Result<CcrPayloadConversion> {
|
|
let snapshot = load_ccr_snapshot_from_file(path)?;
|
|
snapshot_to_payloads_with_options(&snapshot, strict)
|
|
}
|
|
|
|
pub fn find_latest_ccr_file(dir: impl AsRef<Path>) -> Result<PathBuf> {
|
|
let dir = dir.as_ref();
|
|
let mut latest: Option<PathBuf> = None;
|
|
|
|
for entry in fs::read_dir(dir)
|
|
.with_context(|| format!("failed to read CCR directory: {}", dir.display()))?
|
|
{
|
|
let entry =
|
|
entry.with_context(|| format!("failed to iterate CCR directory: {}", dir.display()))?;
|
|
let path = entry.path();
|
|
if !path.is_file() {
|
|
continue;
|
|
}
|
|
if path.extension().and_then(|ext| ext.to_str()) != Some("ccr") {
|
|
continue;
|
|
}
|
|
|
|
if latest
|
|
.as_ref()
|
|
.is_none_or(|current| file_name_key(&path) > file_name_key(current))
|
|
{
|
|
latest = Some(path);
|
|
}
|
|
}
|
|
|
|
latest.ok_or_else(|| anyhow!("no .ccr files found in {}", dir.display()))
|
|
}
|
|
|
|
pub fn snapshot_to_payloads(snapshot: &ParsedCcrSnapshot) -> Result<Vec<Payload>> {
|
|
Ok(snapshot_to_payloads_with_options(snapshot, true)?.payloads)
|
|
}
|
|
|
|
pub fn snapshot_to_payloads_with_options(
|
|
snapshot: &ParsedCcrSnapshot,
|
|
strict: bool,
|
|
) -> Result<CcrPayloadConversion> {
|
|
let mut payloads = Vec::with_capacity(snapshot.vrps.len() + snapshot.vaps.len());
|
|
let mut invalid_vrps = Vec::new();
|
|
let mut invalid_vaps = Vec::new();
|
|
|
|
for vrp in &snapshot.vrps {
|
|
match build_route_origin(vrp.clone()) {
|
|
Ok(origin) => payloads.push(Payload::RouteOrigin(origin)),
|
|
Err(err) => {
|
|
let msg = format!("invalid CCR VRP: {:?}: {}", vrp, err);
|
|
if strict {
|
|
return Err(anyhow!(msg));
|
|
}
|
|
invalid_vrps.push(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
for vap in &snapshot.vaps {
|
|
match build_aspa(vap.clone()) {
|
|
Ok(aspa) => payloads.push(Payload::Aspa(aspa)),
|
|
Err(err) => {
|
|
let msg = format!("invalid CCR VAP/ASPA: {:?}: {}", vap, err);
|
|
if strict {
|
|
return Err(anyhow!(msg));
|
|
}
|
|
invalid_vaps.push(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(CcrPayloadConversion {
|
|
payloads,
|
|
invalid_vrps,
|
|
invalid_vaps,
|
|
})
|
|
}
|
|
|
|
pub fn parse_ccr_bytes(bytes: &[u8]) -> Result<ParsedCcrSnapshot> {
|
|
let (rem, root) = parse_der(bytes).map_err(|err| anyhow!("failed to parse CCR DER: {err}"))?;
|
|
if !rem.is_empty() {
|
|
return Err(anyhow!("CCR DER has {} trailing bytes", rem.len()));
|
|
}
|
|
|
|
let root_items = sequence_items(&root)?;
|
|
if root_items.len() != 2 {
|
|
return Err(anyhow!("CCR root must contain exactly 2 items"));
|
|
}
|
|
|
|
let content_type_oid = match &root_items[0].content {
|
|
BerObjectContent::OID(oid) => oid.to_string(),
|
|
other => {
|
|
return Err(anyhow!(
|
|
"CCR root first element must be content type OID, got {other:?}"
|
|
));
|
|
}
|
|
};
|
|
|
|
let payload = decode_context_wrapped_sequence(&root_items[1])?;
|
|
let payload_items = sequence_items(&payload)?;
|
|
|
|
let produced_at = payload_items.get(1).and_then(|obj| match &obj.content {
|
|
BerObjectContent::GeneralizedTime(t) => Some(t.to_string()),
|
|
_ => None,
|
|
});
|
|
|
|
let vrps = if let Some(vrps_field) = payload_items.get(VRPS_INDEX) {
|
|
parse_vrps(vrps_field)?
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
let vaps = if let Some(vaps_field) = payload_items.get(VAPS_INDEX) {
|
|
parse_vaps(vaps_field)?
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
Ok(ParsedCcrSnapshot {
|
|
content_type_oid,
|
|
produced_at,
|
|
vrps,
|
|
vaps,
|
|
})
|
|
}
|
|
|
|
fn parse_vrps(field: &BerObject<'_>) -> Result<Vec<ParsedVrp>> {
|
|
let vrp_state = decode_context_wrapped_sequence(field)?;
|
|
let vrp_state_items = sequence_items(&vrp_state)?;
|
|
let roa_payload_sets = vrp_state_items
|
|
.first()
|
|
.ok_or_else(|| anyhow!("ROA payload state missing payload set list"))?;
|
|
let roa_payload_sets = sequence_items(roa_payload_sets)?;
|
|
|
|
let mut vrps = Vec::new();
|
|
for payload_set in roa_payload_sets {
|
|
let payload_set_items = sequence_items(payload_set)?;
|
|
if payload_set_items.len() != 2 {
|
|
return Err(anyhow!(
|
|
"ROAPayloadSet must contain 2 items, got {}",
|
|
payload_set_items.len()
|
|
));
|
|
}
|
|
|
|
let asn = as_u32(&payload_set_items[0], "ROAPayloadSet.asID")?;
|
|
let families = sequence_items(&payload_set_items[1])?;
|
|
|
|
for family in families {
|
|
let family_items = sequence_items(family)?;
|
|
if family_items.len() != 2 {
|
|
return Err(anyhow!(
|
|
"ROAIPAddressFamily must contain 2 items, got {}",
|
|
family_items.len()
|
|
));
|
|
}
|
|
let address_family = as_octets(&family_items[0], "ROAIPAddressFamily.addressFamily")?;
|
|
let addresses = sequence_items(&family_items[1])?;
|
|
|
|
for address in addresses {
|
|
let address_items = sequence_items(address)?;
|
|
let (prefix_addr, prefix_len, max_len) =
|
|
parse_roa_address(address_family, address_items)?;
|
|
vrps.push(ParsedVrp {
|
|
prefix_addr,
|
|
prefix_len,
|
|
max_len,
|
|
asn,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(vrps)
|
|
}
|
|
|
|
fn parse_vaps(field: &BerObject<'_>) -> Result<Vec<ParsedAspa>> {
|
|
let vap_state = decode_context_wrapped_sequence(field)?;
|
|
let vap_state_items = sequence_items(&vap_state)?;
|
|
let aspa_payload_sets = vap_state_items
|
|
.first()
|
|
.ok_or_else(|| anyhow!("ASPA payload state missing payload set list"))?;
|
|
let aspa_payload_sets = sequence_items(aspa_payload_sets)?;
|
|
|
|
let mut vaps = Vec::new();
|
|
for payload_set in aspa_payload_sets {
|
|
let payload_set_items = sequence_items(payload_set)?;
|
|
if payload_set_items.len() != 2 {
|
|
return Err(anyhow!(
|
|
"ASPAPayloadSet must contain 2 items, got {}",
|
|
payload_set_items.len()
|
|
));
|
|
}
|
|
|
|
let customer_asn = as_u32(&payload_set_items[0], "ASPAPayloadSet.customerASID")?;
|
|
let provider_set = sequence_items(&payload_set_items[1])?;
|
|
let mut provider_asns = Vec::with_capacity(provider_set.len());
|
|
for provider in provider_set {
|
|
provider_asns.push(as_u32(provider, "ASPAPayloadSet.providerASID")?);
|
|
}
|
|
|
|
vaps.push(ParsedAspa {
|
|
customer_asn,
|
|
provider_asns,
|
|
});
|
|
}
|
|
|
|
Ok(vaps)
|
|
}
|
|
|
|
fn parse_roa_address(address_family: &[u8], items: &[BerObject<'_>]) -> Result<(IpAddr, u8, u8)> {
|
|
let address = items
|
|
.first()
|
|
.ok_or_else(|| anyhow!("ROAIPAddress missing address field"))?;
|
|
let (unused_bits, bit_string) = match &address.content {
|
|
BerObjectContent::BitString(unused_bits, bit_string) => (*unused_bits, bit_string),
|
|
other => {
|
|
return Err(anyhow!(
|
|
"ROAIPAddress.address must be BIT STRING, got {other:?}"
|
|
));
|
|
}
|
|
};
|
|
|
|
let prefix_len = (bit_string.data.len() * 8)
|
|
.checked_sub(usize::from(unused_bits))
|
|
.ok_or_else(|| anyhow!("invalid ROAIPAddress BIT STRING length"))?;
|
|
let prefix_len = u8::try_from(prefix_len)
|
|
.map_err(|_| anyhow!("prefix length {prefix_len} does not fit in u8"))?;
|
|
|
|
let max_len = match items.get(1) {
|
|
Some(value) => {
|
|
let max_len = as_u32(value, "ROAIPAddress.maxLength")?;
|
|
u8::try_from(max_len).map_err(|_| anyhow!("maxLength {max_len} does not fit in u8"))?
|
|
}
|
|
None => prefix_len,
|
|
};
|
|
|
|
let prefix_addr = match address_family {
|
|
[0, 1] | [0, 1, ..] => {
|
|
let mut octets = [0u8; 4];
|
|
if bit_string.data.len() > octets.len() {
|
|
return Err(anyhow!(
|
|
"IPv4 ROAIPAddress too long: {} bytes",
|
|
bit_string.data.len()
|
|
));
|
|
}
|
|
octets[..bit_string.data.len()].copy_from_slice(bit_string.data);
|
|
IpAddr::V4(Ipv4Addr::from(octets))
|
|
}
|
|
[0, 2] | [0, 2, ..] => {
|
|
let mut octets = [0u8; 16];
|
|
if bit_string.data.len() > octets.len() {
|
|
return Err(anyhow!(
|
|
"IPv6 ROAIPAddress too long: {} bytes",
|
|
bit_string.data.len()
|
|
));
|
|
}
|
|
octets[..bit_string.data.len()].copy_from_slice(bit_string.data);
|
|
IpAddr::V6(Ipv6Addr::from(octets))
|
|
}
|
|
_ => {
|
|
return Err(anyhow!(
|
|
"unsupported ROA address family octets: {:?}",
|
|
address_family
|
|
));
|
|
}
|
|
};
|
|
|
|
Ok((prefix_addr, prefix_len, max_len))
|
|
}
|
|
|
|
fn sequence_items<'a>(obj: &'a BerObject<'a>) -> Result<&'a [BerObject<'a>]> {
|
|
match &obj.content {
|
|
BerObjectContent::Sequence(items) => Ok(items),
|
|
other => Err(anyhow!("expected SEQUENCE, got {other:?}")),
|
|
}
|
|
}
|
|
|
|
fn decode_context_wrapped_sequence<'a>(obj: &'a BerObject<'a>) -> Result<BerObject<'a>> {
|
|
match &obj.content {
|
|
BerObjectContent::Unknown(any) => {
|
|
let (rem, inner) = parse_der(any.data)
|
|
.map_err(|err| anyhow!("failed to parse encapsulated DER: {err}"))?;
|
|
if !rem.is_empty() {
|
|
return Err(anyhow!("encapsulated DER has {} trailing bytes", rem.len()));
|
|
}
|
|
Ok(inner)
|
|
}
|
|
other => Err(anyhow!(
|
|
"expected context-specific wrapped field, got {other:?}"
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn as_u32(obj: &BerObject<'_>, field_name: &str) -> Result<u32> {
|
|
obj.as_u32()
|
|
.map_err(|err| anyhow!("{field_name} must be INTEGER fitting in u32: {err}"))
|
|
}
|
|
|
|
fn as_octets<'a>(obj: &'a BerObject<'a>, field_name: &str) -> Result<&'a [u8]> {
|
|
match &obj.content {
|
|
BerObjectContent::OctetString(bytes) => Ok(bytes),
|
|
other => Err(anyhow!("{field_name} must be OCTET STRING, got {other:?}")),
|
|
}
|
|
}
|
|
|
|
fn file_name_key(path: &Path) -> String {
|
|
path.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.map(|name| name.to_ascii_lowercase())
|
|
.unwrap_or_default()
|
|
}
|