将fetch pp cache改成使用vcir结构,跑通apnic全量同步
This commit is contained in:
@@ -10,8 +10,10 @@ use crate::data_model::roa::{IpPrefix, RoaAfi, RoaDecodeError, RoaObject, RoaVal
|
||||
use crate::data_model::signed_object::SignedObjectVerifyError;
|
||||
use crate::policy::{Policy, SignedObjectFailurePolicy};
|
||||
use crate::report::{RfcRef, Warning};
|
||||
use crate::storage::{FetchCachePpPack, PackFile};
|
||||
use crate::storage::PackFile;
|
||||
use crate::validation::cert_path::{CertPathError, validate_ee_cert_path_with_prevalidated_issuer};
|
||||
use crate::validation::manifest::PublicationPointData;
|
||||
use crate::validation::publication_point::PublicationPointSnapshot;
|
||||
use x509_parser::prelude::FromDer;
|
||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||
|
||||
@@ -81,10 +83,10 @@ pub struct ObjectsStats {
|
||||
pub publication_point_dropped: bool,
|
||||
}
|
||||
|
||||
/// Process objects from a fetch_cache_pp publication point pack using a known issuer CA certificate
|
||||
/// Process objects from a publication point snapshot using a known issuer CA certificate
|
||||
/// and its effective resources (resolved via the resource-path, RFC 6487 §7.2).
|
||||
pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
pack: &FetchCachePpPack,
|
||||
pub fn process_publication_point_for_issuer<P: PublicationPointData>(
|
||||
publication_point: &P,
|
||||
policy: &Policy,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
@@ -93,23 +95,24 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
) -> ObjectsOutput {
|
||||
let manifest_rsync_uri = publication_point.manifest_rsync_uri();
|
||||
let manifest_bytes = publication_point.manifest_bytes();
|
||||
let locked_files = publication_point.files();
|
||||
let mut warnings: Vec<Warning> = Vec::new();
|
||||
let mut stats = ObjectsStats::default();
|
||||
stats.roa_total = pack
|
||||
.files
|
||||
stats.roa_total = locked_files
|
||||
.iter()
|
||||
.filter(|f| f.rsync_uri.ends_with(".roa"))
|
||||
.count();
|
||||
stats.aspa_total = pack
|
||||
.files
|
||||
stats.aspa_total = locked_files
|
||||
.iter()
|
||||
.filter(|f| f.rsync_uri.ends_with(".asa"))
|
||||
.count();
|
||||
let mut audit: Vec<ObjectAuditEntry> = Vec::new();
|
||||
|
||||
// Enforce that `manifest_bytes` is actually a manifest object.
|
||||
let _manifest =
|
||||
ManifestObject::decode_der(&pack.manifest_bytes).expect("fetch_cache_pp manifest decodes");
|
||||
let _manifest = ManifestObject::decode_der(manifest_bytes)
|
||||
.expect("publication point snapshot manifest decodes");
|
||||
|
||||
// Decode issuer CA once; if it fails we cannot validate ROA/ASPA EE certificates.
|
||||
let issuer_ca = match ResourceCertificate::decode_der(issuer_ca_der) {
|
||||
@@ -121,9 +124,9 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
"dropping publication point: issuer CA decode failed: {e}"
|
||||
))
|
||||
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2"), RfcRef("RFC 5280 §6.1")])
|
||||
.with_context(&pack.manifest_rsync_uri),
|
||||
.with_context(manifest_rsync_uri),
|
||||
);
|
||||
for f in &pack.files {
|
||||
for f in locked_files {
|
||||
if f.rsync_uri.ends_with(".roa") {
|
||||
audit.push(ObjectAuditEntry {
|
||||
rsync_uri: f.rsync_uri.clone(),
|
||||
@@ -163,7 +166,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
rem.len()
|
||||
))
|
||||
.with_rfc_refs(&[RfcRef("RFC 5280 §4.1.2.7")])
|
||||
.with_context(&pack.manifest_rsync_uri),
|
||||
.with_context(manifest_rsync_uri),
|
||||
);
|
||||
return ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
@@ -180,7 +183,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
"dropping publication point: issuer SPKI parse failed: {e}"
|
||||
))
|
||||
.with_rfc_refs(&[RfcRef("RFC 5280 §4.1.2.7")])
|
||||
.with_context(&pack.manifest_rsync_uri),
|
||||
.with_context(manifest_rsync_uri),
|
||||
);
|
||||
return ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
@@ -192,8 +195,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
}
|
||||
};
|
||||
|
||||
let mut crl_cache: std::collections::HashMap<String, CachedIssuerCrl> = pack
|
||||
.files
|
||||
let mut crl_cache: std::collections::HashMap<String, CachedIssuerCrl> = locked_files
|
||||
.iter()
|
||||
.filter(|f| f.rsync_uri.ends_with(".crl"))
|
||||
.map(|f| {
|
||||
@@ -207,23 +209,26 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
let issuer_resources_index =
|
||||
build_issuer_resources_index(issuer_effective_ip, issuer_effective_as);
|
||||
|
||||
// If the pack has signed objects but no CRLs at all, we cannot validate any embedded EE
|
||||
// certificate paths deterministically (EE CRLDP must reference an rsync URI in the pack).
|
||||
// If the snapshot has signed objects but no CRLs at all, we cannot validate any embedded EE
|
||||
// certificate paths deterministically (EE CRLDP must reference an rsync URI in the snapshot).
|
||||
if crl_cache.is_empty() && (stats.roa_total > 0 || stats.aspa_total > 0) {
|
||||
stats.publication_point_dropped = true;
|
||||
warnings.push(
|
||||
Warning::new("dropping publication point: no CRL files in fetch_cache_pp")
|
||||
Warning::new("dropping publication point: no CRL files in validated publication point")
|
||||
.with_rfc_refs(&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §7")])
|
||||
.with_context(&pack.manifest_rsync_uri),
|
||||
.with_context(manifest_rsync_uri),
|
||||
);
|
||||
for f in &pack.files {
|
||||
for f in locked_files {
|
||||
if f.rsync_uri.ends_with(".roa") {
|
||||
audit.push(ObjectAuditEntry {
|
||||
rsync_uri: f.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&f.sha256),
|
||||
kind: AuditObjectKind::Roa,
|
||||
result: AuditObjectResult::Skipped,
|
||||
detail: Some("skipped due to missing CRL files in fetch_cache_pp".to_string()),
|
||||
detail: Some(
|
||||
"skipped due to missing CRL files in validated publication point"
|
||||
.to_string(),
|
||||
),
|
||||
});
|
||||
} else if f.rsync_uri.ends_with(".asa") {
|
||||
audit.push(ObjectAuditEntry {
|
||||
@@ -231,7 +236,10 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
sha256_hex: sha256_hex_from_32(&f.sha256),
|
||||
kind: AuditObjectKind::Aspa,
|
||||
result: AuditObjectResult::Skipped,
|
||||
detail: Some("skipped due to missing CRL files in fetch_cache_pp".to_string()),
|
||||
detail: Some(
|
||||
"skipped due to missing CRL files in validated publication point"
|
||||
.to_string(),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -247,7 +255,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
let mut vrps: Vec<Vrp> = Vec::new();
|
||||
let mut aspas: Vec<AspaAttestation> = Vec::new();
|
||||
|
||||
for (idx, file) in pack.files.iter().enumerate() {
|
||||
for (idx, file) in locked_files.iter().enumerate() {
|
||||
if file.rsync_uri.ends_with(".roa") {
|
||||
let _t = timing.as_ref().map(|t| t.span_phase("objects_roa_total"));
|
||||
match process_roa_with_issuer(
|
||||
@@ -300,7 +308,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(e.to_string()),
|
||||
});
|
||||
for f in pack.files.iter().skip(idx + 1) {
|
||||
for f in locked_files.iter().skip(idx + 1) {
|
||||
if f.rsync_uri.ends_with(".roa") {
|
||||
audit.push(ObjectAuditEntry {
|
||||
rsync_uri: f.rsync_uri.clone(),
|
||||
@@ -333,7 +341,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
file.rsync_uri
|
||||
))
|
||||
.with_rfc_refs(&refs)
|
||||
.with_context(&pack.manifest_rsync_uri),
|
||||
.with_context(manifest_rsync_uri),
|
||||
);
|
||||
return ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
@@ -397,7 +405,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(e.to_string()),
|
||||
});
|
||||
for f in pack.files.iter().skip(idx + 1) {
|
||||
for f in locked_files.iter().skip(idx + 1) {
|
||||
if f.rsync_uri.ends_with(".roa") {
|
||||
audit.push(ObjectAuditEntry {
|
||||
rsync_uri: f.rsync_uri.clone(),
|
||||
@@ -430,7 +438,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
file.rsync_uri
|
||||
))
|
||||
.with_rfc_refs(&refs)
|
||||
.with_context(&pack.manifest_rsync_uri),
|
||||
.with_context(manifest_rsync_uri),
|
||||
);
|
||||
return ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
@@ -453,6 +461,28 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
audit,
|
||||
}
|
||||
}
|
||||
/// Compatibility wrapper that processes a publication point snapshot.
|
||||
pub fn process_publication_point_snapshot_for_issuer(
|
||||
pack: &PublicationPointSnapshot,
|
||||
policy: &Policy,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
) -> ObjectsOutput {
|
||||
process_publication_point_for_issuer(
|
||||
pack,
|
||||
policy,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
timing,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum ObjectValidateError {
|
||||
@@ -480,12 +510,12 @@ enum ObjectValidateError {
|
||||
MissingCrlDpUris,
|
||||
|
||||
#[error(
|
||||
"no CRL available in fetch_cache_pp (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)"
|
||||
"no CRL available in publication point snapshot (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)"
|
||||
)]
|
||||
MissingCrlInPack,
|
||||
|
||||
#[error(
|
||||
"CRL referenced by CRLDistributionPoints not found in fetch_cache_pp: {0} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)"
|
||||
"CRL referenced by CRLDistributionPoints not found in publication point snapshot: {0} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)"
|
||||
)]
|
||||
CrlNotFound(String),
|
||||
|
||||
@@ -1283,7 +1313,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choose_crl_for_certificate_reports_missing_crl_in_pack() {
|
||||
fn choose_crl_for_certificate_reports_missing_crl_in_snapshot() {
|
||||
let roa_der =
|
||||
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
||||
let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
|
||||
@@ -1335,7 +1365,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choose_crl_for_certificate_reports_not_found_when_crldp_does_not_match_pack() {
|
||||
fn choose_crl_for_certificate_reports_not_found_when_crldp_does_not_match_snapshot() {
|
||||
let roa_der =
|
||||
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
||||
let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
|
||||
@@ -1421,4 +1451,225 @@ mod tests {
|
||||
let err = validate_ee_resources_subset(ee, Some(&issuer_ip), None, &idx).unwrap_err();
|
||||
assert!(matches!(err, ObjectValidateError::EeResourcesNotSubset));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_rfc_refs_for_crl_selection_distinguishes_crl_errors() {
|
||||
assert_eq!(
|
||||
extra_rfc_refs_for_crl_selection(&ObjectValidateError::MissingCrlDpUris),
|
||||
RFC_CRLDP
|
||||
);
|
||||
assert_eq!(
|
||||
extra_rfc_refs_for_crl_selection(&ObjectValidateError::CrlNotFound(
|
||||
"rsync://example.test/x.crl".to_string(),
|
||||
)),
|
||||
RFC_CRLDP_AND_LOCKED_PACK
|
||||
);
|
||||
assert!(
|
||||
extra_rfc_refs_for_crl_selection(&ObjectValidateError::MissingCrlInPack).is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_subset_helpers_cover_success_and_failure_paths() {
|
||||
let child = AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Id(5),
|
||||
AsIdOrRange::Range { min: 7, max: 9 },
|
||||
]);
|
||||
let parent =
|
||||
AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { min: 1, max: 10 }]);
|
||||
let parent_intervals = [(1, 10)];
|
||||
|
||||
assert!(as_choice_subset(None, Some(&parent)));
|
||||
assert!(!as_choice_subset(Some(&child), None));
|
||||
assert!(!as_choice_subset(
|
||||
Some(&AsIdentifierChoice::Inherit),
|
||||
Some(&parent)
|
||||
));
|
||||
assert!(!as_choice_subset(
|
||||
Some(&child),
|
||||
Some(&AsIdentifierChoice::Inherit)
|
||||
));
|
||||
assert!(as_choice_subset(Some(&child), Some(&parent)));
|
||||
|
||||
assert!(as_choice_subset_indexed(None, Some(&parent_intervals)));
|
||||
assert!(!as_choice_subset_indexed(Some(&child), None));
|
||||
assert!(!as_choice_subset_indexed(
|
||||
Some(&AsIdentifierChoice::Inherit),
|
||||
Some(&parent_intervals),
|
||||
));
|
||||
assert!(as_choice_subset_indexed(
|
||||
Some(&child),
|
||||
Some(&parent_intervals)
|
||||
));
|
||||
assert!(!as_choice_subset_indexed(
|
||||
Some(&AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Range { min: 11, max: 12 }
|
||||
])),
|
||||
Some(&parent_intervals),
|
||||
));
|
||||
|
||||
let child_set = AsResourceSet {
|
||||
asnum: Some(child.clone()),
|
||||
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(42)])),
|
||||
};
|
||||
let parent_set = AsResourceSet {
|
||||
asnum: Some(parent.clone()),
|
||||
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Range { min: 40, max: 50 },
|
||||
])),
|
||||
};
|
||||
assert!(as_resources_is_subset(&child_set, &parent_set));
|
||||
assert!(as_resources_is_subset_indexed(
|
||||
&child_set,
|
||||
&parent_set,
|
||||
&IssuerResourcesIndex {
|
||||
asnum: Some(vec![(1, 10)]),
|
||||
rdi: Some(vec![(40, 50)]),
|
||||
..IssuerResourcesIndex::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_subset_helpers_cover_strict_and_indexed_paths() {
|
||||
let parent = IpResourceSet {
|
||||
families: vec![
|
||||
IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![
|
||||
IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 8,
|
||||
addr: vec![10, 0, 0, 0],
|
||||
}),
|
||||
IpAddressOrRange::Range(IpAddressRange {
|
||||
min: vec![192, 0, 2, 0],
|
||||
max: vec![192, 0, 2, 255],
|
||||
}),
|
||||
]),
|
||||
},
|
||||
IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
},
|
||||
],
|
||||
};
|
||||
let child = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![
|
||||
IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![10, 1, 0, 0],
|
||||
}),
|
||||
IpAddressOrRange::Range(IpAddressRange {
|
||||
min: vec![192, 0, 2, 10],
|
||||
max: vec![192, 0, 2, 20],
|
||||
}),
|
||||
]),
|
||||
}],
|
||||
};
|
||||
let strict_bad = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
};
|
||||
|
||||
assert!(ip_resources_is_subset(&child, &parent));
|
||||
assert!(ip_resources_to_merged_intervals(&parent).contains_key(&AfiKey::V4));
|
||||
assert!(ip_resources_to_merged_intervals_strict(&child).is_ok());
|
||||
assert!(ip_resources_to_merged_intervals_strict(&strict_bad).is_err());
|
||||
|
||||
let idx = build_issuer_resources_index(
|
||||
Some(&parent),
|
||||
Some(&AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Range {
|
||||
min: 64500,
|
||||
max: 64510,
|
||||
},
|
||||
])),
|
||||
rdi: Some(AsIdentifierChoice::Inherit),
|
||||
}),
|
||||
);
|
||||
assert!(idx.ip_v4.is_some());
|
||||
assert!(idx.ip_v6.is_none());
|
||||
assert!(idx.asnum.is_some());
|
||||
assert!(idx.rdi.is_none());
|
||||
assert!(ip_resources_is_subset_indexed(&child, &parent, &idx));
|
||||
assert!(!ip_resources_is_subset_indexed(&strict_bad, &parent, &idx));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interval_and_byte_helpers_cover_edge_cases() {
|
||||
let parent = vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 10])];
|
||||
assert!(interval_is_covered(&parent, &[0, 0, 0, 1], &[0, 0, 0, 2]));
|
||||
assert!(!interval_is_covered(
|
||||
&parent,
|
||||
&[0, 0, 0, 11],
|
||||
&[0, 0, 0, 12]
|
||||
));
|
||||
assert!(intervals_are_covered(
|
||||
&parent,
|
||||
&[(vec![0, 0, 0, 1], vec![0, 0, 0, 2])]
|
||||
));
|
||||
assert!(!intervals_are_covered(
|
||||
&parent,
|
||||
&[(vec![0, 0, 0, 9], vec![0, 0, 0, 11])],
|
||||
));
|
||||
|
||||
let prefix = RcIpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![203, 0, 113, 7],
|
||||
};
|
||||
assert_eq!(
|
||||
prefix_to_range(&prefix),
|
||||
(vec![203, 0, 113, 0], vec![203, 0, 113, 255])
|
||||
);
|
||||
assert_eq!(increment_bytes(&[0, 0, 0, 255]), vec![0, 0, 1, 0]);
|
||||
assert!(bytes_is_next(&[0, 0, 1, 0], &[0, 0, 0, 255]));
|
||||
assert!(!bytes_is_next(&[1, 2], &[1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merged_interval_helpers_cover_empty_and_break_paths() {
|
||||
let mut empty: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
|
||||
merge_ip_intervals_in_place(&mut empty);
|
||||
assert!(empty.is_empty());
|
||||
|
||||
let mut v = vec![
|
||||
(vec![0, 0, 0, 20], vec![0, 0, 0, 30]),
|
||||
(vec![0, 0, 0, 0], vec![0, 0, 0, 10]),
|
||||
(vec![0, 0, 0, 11], vec![0, 0, 0, 19]),
|
||||
];
|
||||
v.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
merge_ip_intervals_in_place(&mut v);
|
||||
assert_eq!(v, vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 30])]);
|
||||
|
||||
assert_eq!(
|
||||
as_choice_to_merged_intervals(&AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Id(1),
|
||||
AsIdOrRange::Range { min: 2, max: 3 },
|
||||
AsIdOrRange::Range { min: 7, max: 9 },
|
||||
])),
|
||||
vec![(1, 3), (7, 9)]
|
||||
);
|
||||
assert!(as_interval_is_covered(&[(1, 3), (7, 9)], 2, 3));
|
||||
assert!(!as_interval_is_covered(&[(7, 9)], 2, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roa_output_helpers_cover_vrps_and_afi_strings() {
|
||||
let roa_der =
|
||||
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
||||
let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
|
||||
let vrps = roa_to_vrps(&roa);
|
||||
assert!(!vrps.is_empty());
|
||||
assert!(vrps.iter().all(|vrp| vrp.asn == roa.roa.as_id));
|
||||
assert_eq!(roa_afi_to_string(RoaAfi::Ipv4), "ipv4");
|
||||
assert_eq!(roa_afi_to_string(RoaAfi::Ipv6), "ipv6");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user