From 7aac4855bd7bc23f2eb79ab4df49460684530056 Mon Sep 17 00:00:00 2001 From: yuyr Date: Thu, 16 Jul 2026 09:29:57 +0800 Subject: [PATCH] 20260715 add configurable max CA depth --- src/cli.rs | 35 ++++++++++---- src/cli/tests.rs | 49 ++++++++++++++++++- src/validation/tree.rs | 24 ++++++--- src/validation/tree_parallel.rs | 22 ++++++--- tests/test_tree_traversal_m14.rs | 83 ++++++++++++++++++++++++++++++++ 5 files changed, 189 insertions(+), 24 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 3c0b465..6e7e75b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -38,7 +38,7 @@ use crate::validation::run_tree_from_tal::{ run_tree_from_tal_url_parallel_phase2_audit, run_tree_from_tal_url_parallel_phase2_audit_with_timing, }; -use crate::validation::tree::TreeRunConfig; +use crate::validation::tree::{DEFAULT_MAX_CA_DEPTH, TreeRunConfig}; #[cfg(test)] use output::write_json; use output::{ @@ -168,7 +168,7 @@ pub struct CliArgs { pub rsync_mirror_root: Option, pub rsync_scope_policy: RsyncScopePolicy, - pub max_depth: Option, + pub max_ca_depth: usize, pub max_instances: Option, pub validation_time: Option, @@ -259,7 +259,8 @@ Options: --rsync-timeout-secs rsync I/O timeout seconds (default: 60) --rsync-mirror-root Persist rsync mirrors under this directory (default: disabled) --rsync-scope rsync scope policy: host, publication-point, or module-root (default: module-root) - --max-depth Max CA instance depth (0 = root only) + --max-ca-depth Maximum CA depth from a trust anchor (root = 0, default: {DEFAULT_MAX_CA_DEPTH}) + --max-depth Deprecated alias for --max-ca-depth --max-instances Max number of CA instances to process --validation-time Validation time in RFC3339 (default: now UTC) --analyze Write timing analysis JSON under target/live/analyze// @@ -319,7 +320,8 @@ pub fn parse_args(argv: &[String]) -> Result { let mut rsync_timeout_secs: u64 = 30; let mut rsync_mirror_root: Option = None; let mut rsync_scope_policy = RsyncScopePolicy::default(); - let mut max_depth: Option = None; + let mut max_ca_depth: Option = None; + let mut max_ca_depth_option: Option<&'static str> = None; let mut max_instances: Option = None; let mut validation_time: Option = None; let mut analyze: bool = false; @@ -661,13 +663,26 @@ pub fn parse_args(argv: &[String]) -> Result { let v = argv.get(i).ok_or("--rsync-scope requires a value")?; rsync_scope_policy = RsyncScopePolicy::parse_cli_value(v)?; } - "--max-depth" => { + "--max-ca-depth" | "--max-depth" => { i += 1; - let v = argv.get(i).ok_or("--max-depth requires a value")?; - max_depth = Some( + let option = arg; + let v = argv + .get(i) + .ok_or_else(|| format!("{option} requires a value"))?; + if let Some(previous_option) = max_ca_depth_option { + return Err(format!( + "{option} cannot be combined with {previous_option}; use only --max-ca-depth" + )); + } + max_ca_depth = Some( v.parse::() - .map_err(|_| format!("invalid --max-depth: {v}"))?, + .map_err(|_| format!("invalid {option}: {v}"))?, ); + max_ca_depth_option = Some(match option { + "--max-ca-depth" => "--max-ca-depth", + "--max-depth" => "--max-depth", + _ => unreachable!("matched CA depth option"), + }); } "--max-instances" => { i += 1; @@ -989,7 +1004,7 @@ pub fn parse_args(argv: &[String]) -> Result { rsync_timeout_secs, rsync_mirror_root, rsync_scope_policy, - max_depth, + max_ca_depth: max_ca_depth.unwrap_or(DEFAULT_MAX_CA_DEPTH), max_instances, validation_time, analyze, @@ -2032,7 +2047,7 @@ pub fn run(argv: &[String]) -> Result<(), String> { Arc::new(RocksStore::open(&args.db_path).map_err(|e| e.to_string())?) }; let config = TreeRunConfig { - max_depth: args.max_depth, + max_depth: Some(args.max_ca_depth), max_instances: args.max_instances, compact_audit: args.skip_report_build && args.report_json_path.is_none() diff --git a/src/cli/tests.rs b/src/cli/tests.rs index 43e2c01..4d47d1f 100644 --- a/src/cli/tests.rs +++ b/src/cli/tests.rs @@ -21,6 +21,8 @@ fn parse_help_returns_usage() { assert!(err.contains("--memory-trim-after-validation"), "{err}"); assert!(err.contains("--enable-roa-validation-cache"), "{err}"); assert!(err.contains("--resource-validation-mode"), "{err}"); + assert!(err.contains("--max-ca-depth"), "{err}"); + assert!(err.contains("default: 32"), "{err}"); assert!( err.contains("--publication-point-cache-observe-only"), "{err}" @@ -80,6 +82,51 @@ fn parse_rejects_invalid_max_depth() { assert!(err.contains("invalid --max-depth"), "{err}"); } +#[test] +fn parse_rejects_invalid_max_ca_depth() { + let argv = vec![ + "rpki".to_string(), + "--db".to_string(), + "db".to_string(), + "--tal-url".to_string(), + "https://example.test/x.tal".to_string(), + "--max-ca-depth".to_string(), + "nope".to_string(), + ]; + let err = parse_args(&argv).unwrap_err(); + assert!(err.contains("invalid --max-ca-depth"), "{err}"); +} + +#[test] +fn parse_rejects_both_ca_depth_options() { + let argv = vec![ + "rpki".to_string(), + "--db".to_string(), + "db".to_string(), + "--tal-url".to_string(), + "https://example.test/x.tal".to_string(), + "--max-ca-depth".to_string(), + "32".to_string(), + "--max-depth".to_string(), + "12".to_string(), + ]; + let err = parse_args(&argv).unwrap_err(); + assert!(err.contains("cannot be combined"), "{err}"); +} + +#[test] +fn parse_defaults_max_ca_depth_to_32() { + let argv = vec![ + "rpki".to_string(), + "--db".to_string(), + "db".to_string(), + "--tal-url".to_string(), + "https://example.test/x.tal".to_string(), + ]; + let args = parse_args(&argv).expect("parse"); + assert_eq!(args.max_ca_depth, 32); +} + #[test] fn parse_accepts_ccr_out_path() { let argv = vec![ @@ -1103,7 +1150,7 @@ fn parse_accepts_offline_mode_requires_ta() { assert_eq!(args.ta_paths, vec![PathBuf::from("ta.cer")]); assert_eq!(args.tal_path.as_deref(), Some(Path::new("a.tal"))); assert_eq!(args.ta_path.as_deref(), Some(Path::new("ta.cer"))); - assert_eq!(args.max_depth, Some(0)); + assert_eq!(args.max_ca_depth, 0); } #[test] diff --git a/src/validation/tree.rs b/src/validation/tree.rs index 6bff590..3561a2b 100644 --- a/src/validation/tree.rs +++ b/src/validation/tree.rs @@ -10,6 +10,8 @@ use crate::validation::publication_point::PublicationPointSnapshot; use std::borrow::Cow; use std::sync::{Arc, OnceLock}; +pub const DEFAULT_MAX_CA_DEPTH: usize = 32; + #[derive(Clone, Debug, PartialEq, Eq)] pub struct TreeRunConfig { /// Max CA instance depth to process (0 = root only). @@ -37,7 +39,7 @@ pub struct TreeRunConfig { impl Default for TreeRunConfig { fn default() -> Self { Self { - max_depth: None, + max_depth: Some(DEFAULT_MAX_CA_DEPTH), max_instances: None, compact_audit: false, persist_vcir: true, @@ -51,6 +53,15 @@ impl Default for TreeRunConfig { } } +pub(crate) fn ca_depth_is_allowed(config: &TreeRunConfig, depth: usize) -> bool { + config.max_depth.is_none_or(|max_depth| depth <= max_depth) +} + +pub(crate) fn next_allowed_ca_depth(config: &TreeRunConfig, parent_depth: usize) -> Option { + let child_depth = parent_depth.checked_add(1)?; + ca_depth_is_allowed(config, child_depth).then_some(child_depth) +} + #[derive(Clone, Debug)] pub enum CaCertificateRef { InlineDer(Vec), @@ -347,10 +358,8 @@ pub fn run_tree_serial_audit_multi_root( } } - if let Some(max_depth) = config.max_depth { - if ca.depth > max_depth { - continue; - } + if !ca_depth_is_allowed(config, ca.depth) { + continue; } let res = match runner.run_publication_point(ca) { @@ -393,6 +402,9 @@ pub fn run_tree_serial_audit_multi_root( } publication_points.push(audit); + let Some(child_depth) = next_allowed_ca_depth(config, ca.depth) else { + continue; + }; let mut children = res.discovered_children; children.sort_by(|a, b| { a.handle @@ -413,7 +425,7 @@ pub fn run_tree_serial_audit_multi_root( for child in children { queue.push_back(QueuedCaInstance { id: next_id, - handle: child.handle.with_depth(ca.depth + 1), + handle: child.handle.with_depth(child_depth), parent_id: Some(node.id), discovered_from: Some(child.discovered_from), }); diff --git a/src/validation/tree_parallel.rs b/src/validation/tree_parallel.rs index de036a9..dfd5081 100644 --- a/src/validation/tree_parallel.rs +++ b/src/validation/tree_parallel.rs @@ -19,8 +19,8 @@ use crate::validation::objects::{ }; use crate::validation::tree::{ CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner, - TreeRunAuditOutput, TreeRunConfig, TreeRunError, TreeRunOutput, - run_tree_serial_audit_multi_root, + TreeRunAuditOutput, TreeRunConfig, TreeRunError, TreeRunOutput, ca_depth_is_allowed, + next_allowed_ca_depth, run_tree_serial_audit_multi_root, }; use crate::validation::tree_runner::{ FreshPublicationPointFinalizeOutput, FreshPublicationPointStage, Rpkiv1PublicationPointRunner, @@ -659,6 +659,7 @@ pub fn run_tree_parallel_phase2_audit_multi_root( &mut finished, ready, ready_queue_len_after_pop, + config, config.compact_audit, ); ready_batch_metrics.record(metrics); @@ -916,10 +917,8 @@ fn start_queued_ca_instances( if !visited_manifest_uris.insert(node.handle.manifest_rsync_uri.clone()) { continue; } - if let Some(max_depth) = config.max_depth { - if node.handle.depth > max_depth { - continue; - } + if !ca_depth_is_allowed(config, node.handle.depth) { + continue; } *instances_started += 1; match repo_runtime.request_publication_point_repo(&node.handle, 0) { @@ -959,6 +958,7 @@ fn stage_ready_publication_point( finished: &mut Vec, ready: ReadyCaInstance, ready_queue_len_after_pop: usize, + config: &TreeRunConfig, compact_audit: bool, ) -> ReadyStageMetrics { let publication_point_started = Instant::now(); @@ -991,6 +991,7 @@ fn stage_ready_publication_point( next_id, ca_queue, &ready.node, + config, result.discovered_children.clone(), ); metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started); @@ -1057,6 +1058,7 @@ fn stage_ready_publication_point( next_id, ca_queue, &ready.node, + config, result.discovered_children.clone(), ); metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started); @@ -1142,6 +1144,7 @@ fn stage_ready_publication_point( next_id, ca_queue, &ready.node, + config, fresh_stage.discovered_children.clone(), ); metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started); @@ -1412,8 +1415,13 @@ fn enqueue_discovered_children( next_id: &mut u64, ca_queue: &mut VecDeque, parent: &QueuedCaInstance, + config: &TreeRunConfig, mut children: Vec, ) { + let Some(child_depth) = next_allowed_ca_depth(config, parent.handle.depth) else { + return; + }; + children.sort_by(|a, b| { a.handle .manifest_rsync_uri @@ -1428,7 +1436,7 @@ fn enqueue_discovered_children( let _ = runtime.prefetch_discovered_children(&children); } for child in children { - let mut handle = child.handle.with_depth(parent.handle.depth + 1); + let mut handle = child.handle.with_depth(child_depth); handle.parent_manifest_rsync_uri = Some(parent.handle.manifest_rsync_uri.clone()); ca_queue.push_back(QueuedCaInstance { id: *next_id, diff --git a/tests/test_tree_traversal_m14.rs b/tests/test_tree_traversal_m14.rs index 37de233..3172adf 100644 --- a/tests/test_tree_traversal_m14.rs +++ b/tests/test_tree_traversal_m14.rs @@ -253,6 +253,7 @@ fn tree_enqueues_children_for_fresh_and_current_instance_vcir_results() { fn tree_respects_max_depth_and_max_instances() { let root_manifest = "rsync://example.test/repo/root.mft"; let child_manifest = "rsync://example.test/repo/child.mft"; + let grandchild_manifest = "rsync://example.test/repo/grandchild.mft"; let runner = MockRunner::default() .with( @@ -301,6 +302,32 @@ fn tree_respects_max_depth_and_max_instances() { audit: PublicationPointAudit::default(), cir_fresh_objects: Vec::new(), cir_cached_objects: Vec::new(), + discovered_children: vec![discovered_child(child_manifest, grandchild_manifest)], + }, + ) + .with( + grandchild_manifest, + PublicationPointRunResult { + source: PublicationPointSource::Fresh, + snapshot: Some(empty_snapshot( + grandchild_manifest, + "rsync://example.test/repo/grandchild/", + )), + warnings: Vec::new(), + objects: ObjectsOutput { + vrps: Vec::new(), + aspas: Vec::new(), + router_keys: Vec::new(), + local_outputs_cache: Vec::new(), + warnings: Vec::new(), + stats: ObjectsStats::default(), + audit: Vec::new(), + roa_cache_stats: Default::default(), + roa_cache_object_meta: Vec::new(), + }, + audit: PublicationPointAudit::default(), + cir_fresh_objects: Vec::new(), + cir_cached_objects: Vec::new(), discovered_children: Vec::new(), }, ); @@ -324,6 +351,62 @@ fn tree_respects_max_depth_and_max_instances() { .expect("run tree depth-limited"); assert_eq!(out.instances_processed, 1); assert_eq!(out.instances_failed, 0); + assert_eq!(runner.called(), vec![root_manifest]); + + let out = run_tree_serial( + ca_handle(root_manifest), + &runner, + &TreeRunConfig { + max_depth: Some(1), + max_instances: None, + compact_audit: false, + persist_vcir: true, + build_ccr_accumulator: true, + enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, + publication_point_cache_observe_only: false, + enable_publication_point_validation_cache: false, + enable_transport_request_prefetch: false, + }, + ) + .expect("run tree depth-one-limited"); + assert_eq!(out.instances_processed, 2); + assert_eq!(out.instances_failed, 0); + assert_eq!( + runner.called(), + vec![root_manifest, root_manifest, child_manifest] + ); + + let out = run_tree_serial( + ca_handle(root_manifest), + &runner, + &TreeRunConfig { + max_depth: Some(2), + max_instances: None, + compact_audit: false, + persist_vcir: true, + build_ccr_accumulator: true, + enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, + publication_point_cache_observe_only: false, + enable_publication_point_validation_cache: false, + enable_transport_request_prefetch: false, + }, + ) + .expect("run tree depth-two-limited"); + assert_eq!(out.instances_processed, 3); + assert_eq!(out.instances_failed, 0); + assert_eq!( + runner.called(), + vec![ + root_manifest, + root_manifest, + child_manifest, + root_manifest, + child_manifest, + grandchild_manifest, + ] + ); let out = run_tree_serial( ca_handle(root_manifest),