From c0399fc6c12d35d8cf70d6b59eee70b93a608d57 Mon Sep 17 00:00:00 2001 From: Nick Mathewson <nickm@torproject.org> Date: Mon, 21 Mar 2022 13:33:38 -0400 Subject: [PATCH] dirmgr: Initial DirFilter code. This code sits behind a feature flag, and can be used to modify directories before storing them. This is part of the implementation for #397. --- crates/arti-client/src/config.rs | 1 + crates/tor-dirmgr/Cargo.toml | 1 + crates/tor-dirmgr/src/config.rs | 16 ++++++ crates/tor-dirmgr/src/filter.rs | 83 ++++++++++++++++++++++++++++++++ crates/tor-dirmgr/src/lib.rs | 11 +++++ crates/tor-dirmgr/src/state.rs | 29 +++++++++++ 6 files changed, 141 insertions(+) create mode 100644 crates/tor-dirmgr/src/filter.rs diff --git a/crates/arti-client/src/config.rs b/crates/arti-client/src/config.rs index 80d5398f17..98fe32936b 100644 --- a/crates/arti-client/src/config.rs +++ b/crates/arti-client/src/config.rs @@ -300,6 +300,7 @@ impl TryInto<dir::DirMgrConfig> for &TorClientConfig { schedule_config: self.download_schedule .clone(), cache_path: self.storage.expand_cache_dir()?, override_net_params: self.override_net_params.clone(), + extensions: Default::default(), }) } } diff --git a/crates/tor-dirmgr/Cargo.toml b/crates/tor-dirmgr/Cargo.toml index f75a67ad8d..ffd31afbe3 100644 --- a/crates/tor-dirmgr/Cargo.toml +++ b/crates/tor-dirmgr/Cargo.toml @@ -16,6 +16,7 @@ mmap = ["memmap2"] static = ["rusqlite/bundled"] # (Incomplete) support for downloading and storing router descriptors routerdesc = ["tor-dirclient/routerdesc"] +dirfilter = [] # Enable experimental APIs that are not yet officially supported. # diff --git a/crates/tor-dirmgr/src/config.rs b/crates/tor-dirmgr/src/config.rs index 54f037ae46..5ab6f26e31 100644 --- a/crates/tor-dirmgr/src/config.rs +++ b/crates/tor-dirmgr/src/config.rs @@ -199,6 +199,12 @@ pub struct DirMgrConfig { /// immediately. Users should _not_ assume that the effect of changing this /// option will always be delayed.) pub override_net_params: netstatus::NetParams<i32>, + + /// Extra fields for extension purposes. + /// + /// These are kept in a separate type so that the type can be marked as + /// `non_exhaustive` and used for optional features. + pub extensions: DirMgrExtensions, } impl DirMgrConfig { @@ -252,6 +258,7 @@ impl DirMgrConfig { }, schedule_config: new_config.schedule_config.clone(), override_net_params: new_config.override_net_params.clone(), + extensions: new_config.extensions.clone(), } } @@ -265,6 +272,15 @@ impl DirMgrConfig { } } +/// Optional extensions for configuring +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +pub struct DirMgrExtensions { + /// A filter to be used when installing new directory objects. + #[cfg(feature = "dirfilter")] + pub filter: Option<crate::filter::DynFilter>, +} + impl DownloadScheduleConfig { /// Return configuration for retrying our entire bootstrap /// operation at startup. diff --git a/crates/tor-dirmgr/src/filter.rs b/crates/tor-dirmgr/src/filter.rs new file mode 100644 index 0000000000..99d370fd9e --- /dev/null +++ b/crates/tor-dirmgr/src/filter.rs @@ -0,0 +1,83 @@ +//! A filtering mechanism for directory objects. +//! +//! This module and its members are only available when `tor-dirmgr` is built +//! with the `dirfilter` feature. +//! +//! This is unstable code, currently used for testing only. It might go away in +//! future versions, or its API might change completely. There are no semver +//! guarantees. + +use std::sync::Arc; + +use crate::Result; +use tor_netdoc::doc::{microdesc::Microdesc, netstatus::UncheckedMdConsensus}; + +/// An object that can filter directory documents before they're handled. +/// +/// Instances of DirFilter can be used for testing, to modify directory data +/// on-the-fly. +pub trait DirFilter { + /// Modify `consensus` in an unspecified way. + fn filter_consensus(&self, consensus: UncheckedMdConsensus) -> Result<UncheckedMdConsensus>; + /// Modify `md` in an unspecified way. + fn filter_md(&self, md: Microdesc) -> Result<Microdesc>; +} + +/// A dynamic [`DirFilter`] instance. +#[derive(Clone)] +pub struct DynFilter { + /// A reference to the DirFilter object + filter: Arc<dyn DirFilter + Send + Sync>, +} + +impl From<&Option<DynFilter>> for DynFilter { + fn from(option: &Option<DynFilter>) -> Self { + option.as_ref().map(Clone::clone).unwrap_or_default() + } +} + +impl Default for DynFilter { + fn default() -> Self { + DynFilter::new(NilFilter) + } +} + +impl DynFilter { + /// Wrap `filter` as a [`DynFilter`] + pub fn new<T>(filter: T) -> Self + where + T: DirFilter + Send + Sync + 'static, + { + DynFilter { + filter: Arc::new(filter), + } + } +} + +impl DirFilter for DynFilter { + fn filter_consensus(&self, consensus: UncheckedMdConsensus) -> Result<UncheckedMdConsensus> { + self.filter.filter_consensus(consensus) + } + + fn filter_md(&self, md: Microdesc) -> Result<Microdesc> { + self.filter.filter_md(md) + } +} + +impl std::fmt::Debug for DynFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DynFilter").finish_non_exhaustive() + } +} + +/// A [`DirFilter`] that does nothing. +struct NilFilter; + +impl DirFilter for NilFilter { + fn filter_consensus(&self, consensus: UncheckedMdConsensus) -> Result<UncheckedMdConsensus> { + Ok(consensus) + } + fn filter_md(&self, md: Microdesc) -> Result<Microdesc> { + Ok(md) + } +} diff --git a/crates/tor-dirmgr/src/lib.rs b/crates/tor-dirmgr/src/lib.rs index 5c966181ce..08695588d1 100644 --- a/crates/tor-dirmgr/src/lib.rs +++ b/crates/tor-dirmgr/src/lib.rs @@ -66,6 +66,9 @@ mod shared_ref; mod state; mod storage; +#[cfg(feature = "dirfilter")] +pub mod filter; + use crate::docid::{CacheUsage, ClientRequest, DocQuery}; #[cfg(not(feature = "experimental-api"))] use crate::shared_ref::SharedMutArc; @@ -222,6 +225,10 @@ pub struct DirMgr<R: Runtime> { /// /// (In offline mode, this does nothing.) bootstrap_started: AtomicBool, + + /// A filter that gets applied to directory objects before we use them. + #[cfg(feature = "dirfilter")] + filter: filter::DynFilter, } /// RAII guard to reset an AtomicBool on drop. @@ -702,6 +709,8 @@ impl<R: Runtime> DirMgr<R> { let receive_status = DirBootstrapEvents { inner: receive_status, }; + #[cfg(feature = "dirfilter")] + let filter = (&config.extensions.filter).into(); Ok(DirMgr { config: config.into(), @@ -714,6 +723,8 @@ impl<R: Runtime> DirMgr<R> { runtime, offline, bootstrap_started: AtomicBool::new(false), + #[cfg(feature = "dirfilter")] + filter, }) } diff --git a/crates/tor-dirmgr/src/state.rs b/crates/tor-dirmgr/src/state.rs index 214e5681b0..e6fff3ffe9 100644 --- a/crates/tor-dirmgr/src/state.rs +++ b/crates/tor-dirmgr/src/state.rs @@ -23,6 +23,8 @@ use tracing::{info, warn}; use crate::event::{DirStatus, DirStatusInner}; +#[cfg(feature = "dirfilter")] +use crate::filter::DirFilter; use crate::storage::{DynStore, EXPIRATION_DEFAULTS}; use crate::{ docmeta::{AuthCertMeta, ConsensusMeta}, @@ -84,6 +86,10 @@ pub(crate) trait WriteNetDir: 'static + Sync + Send { /// testing it is helpful to be able to mock our our current view /// of the time. fn now(&self) -> SystemTime; + + /// Return the currently configured DynFilter for this state. + #[cfg(feature = "dirfilter")] + fn filter(&self) -> crate::filter::DynFilter; } impl<R: Runtime> WriteNetDir for crate::DirMgr<R> { @@ -108,6 +114,11 @@ impl<R: Runtime> WriteNetDir for crate::DirMgr<R> { fn now(&self) -> SystemTime { SystemTime::now() } + + #[cfg(feature = "dirfilter")] + fn filter(&self) -> crate::filter::DynFilter { + self.filter.clone() + } } /// Initial state: fetching or loading a consensus directory. @@ -278,6 +289,12 @@ impl<DM: WriteNetDir> GetConsensusState<DM> { let (consensus_meta, unvalidated) = { let (signedval, remainder, parsed) = MdConsensus::parse(text).map_err(|e| Error::from_netdoc(source.clone(), e))?; + #[cfg(feature = "dirfilter")] + let parsed = if let Some(wd) = Weak::upgrade(&self.writedir) { + wd.filter().filter_consensus(parsed)? + } else { + parsed + }; let now = current_time(&self.writedir)?; if let Ok(timely) = parsed.check_valid_at(&now) { let meta = ConsensusMeta::from_unvalidated(signedval, remainder, &timely); @@ -651,6 +668,14 @@ impl<DM: WriteNetDir> GetMicrodescsState<DM> { where I: IntoIterator<Item = Microdesc>, { + #[cfg(feature = "dirfilter")] + let mds: Vec<Microdesc> = if let Some(wd) = Weak::upgrade(&self.writedir) { + mds.into_iter() + .filter_map(|m| wd.filter().filter_md(m).ok()) + .collect() + } else { + mds.into_iter().collect() + }; if let Some(p) = &mut self.partial { for md in mds { self.newly_listed.push(*md.digest()); @@ -1033,6 +1058,10 @@ mod test { fn now(&self) -> SystemTime { self.now } + #[cfg(feature = "dirfilter")] + fn filter(&self) -> crate::filter::DynFilter { + Default::default() + } } // Test data -- GitLab