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, pub vrps: Vec, pub vaps: Vec, } #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct CcrPayloadConversion { pub payloads: Vec, pub invalid_vrps: Vec, pub invalid_vaps: Vec, } pub fn load_ccr_snapshot_from_file(path: impl AsRef) -> Result { 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) -> Result> { let snapshot = load_ccr_snapshot_from_file(path)?; snapshot_to_payloads(&snapshot) } pub fn load_ccr_payloads_from_file_with_options( path: impl AsRef, strict: bool, ) -> Result { let snapshot = load_ccr_snapshot_from_file(path)?; snapshot_to_payloads_with_options(&snapshot, strict) } pub fn find_latest_ccr_file(dir: impl AsRef) -> Result { let dir = dir.as_ref(); let mut latest: Option = 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> { Ok(snapshot_to_payloads_with_options(snapshot, true)?.payloads) } pub fn snapshot_to_payloads_with_options( snapshot: &ParsedCcrSnapshot, strict: bool, ) -> Result { 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 { 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> { 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> { 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> { 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 { 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() }