diff options
Diffstat (limited to 'crates/arti-testing/src')
| -rw-r--r-- | crates/arti-testing/src/config.rs | 14 | ||||
| -rw-r--r-- | crates/arti-testing/src/dirfilter.rs | 193 | ||||
| -rw-r--r-- | crates/arti-testing/src/main.rs | 6 |
3 files changed, 213 insertions, 0 deletions
diff --git a/crates/arti-testing/src/config.rs b/crates/arti-testing/src/config.rs index 2d15903ff..6f215b393 100644 --- a/crates/arti-testing/src/config.rs +++ b/crates/arti-testing/src/config.rs @@ -91,6 +91,13 @@ pub(crate) fn parse_cmdline() -> Result<Job> { .value_name("SECS") .global(true), ) + .arg( + Arg::with_name("dir-filter") + .long("dir-filter") + .takes_value(true) + .value_name("FILTER_NAME") + .global(true), + ) .subcommand( SubCommand::with_name("connect") .about("Try to bootstrap and connect to an address") @@ -169,6 +176,12 @@ pub(crate) fn parse_cmdline() -> Result<Job> { } }; + let dir_filter = matches + .value_of("dir-filter") + .map(crate::dirfilter::new_filter) + .transpose()? + .unwrap_or_else(crate::dirfilter::nil_filter); + let action = if let Some(_m) = matches.subcommand_matches("bootstrap") { Action::Bootstrap } else if let Some(matches) = matches.subcommand_matches("connect") { @@ -191,6 +204,7 @@ pub(crate) fn parse_cmdline() -> Result<Job> { config, timeout, tcp_breakage, + dir_filter, console_log, expectation, }) diff --git a/crates/arti-testing/src/dirfilter.rs b/crates/arti-testing/src/dirfilter.rs new file mode 100644 index 000000000..7df590ee3 --- /dev/null +++ b/crates/arti-testing/src/dirfilter.rs @@ -0,0 +1,193 @@ +//! Support for modifying directories in various ways in order to cause +//! different kinds of network failure. + +use anyhow::{anyhow, Result}; +use rand::Rng; +use std::sync::{Arc, Mutex}; +use tor_dirmgr::filter::DirFilter; +use tor_netdoc::{ + doc::{ + microdesc::Microdesc, + netstatus::{RouterStatus, UncheckedMdConsensus}, + }, + types::{family::RelayFamily, policy::PortPolicy}, +}; + +/// Return a new directory filter as configured by a specified string. +pub(crate) fn new_filter(s: &str) -> Result<Arc<dyn DirFilter + 'static>> { + Ok(match s { + "replace-onion-keys" => Arc::new(ReplaceOnionKeysFilter::default()), + "one-big-family" => Arc::new(OneBigFamilyFilter::default()), + "no-exit-ports" => Arc::new(NoExitPortsFilter::default()), + "bad-signatures" => Arc::new(BadSignaturesFilter::default()), + "non-existent-signing-keys" => Arc::new(NonexistentSigningKeysFilter::default()), + "bad-microdesc-digests" => Arc::new(BadMicrodescDigestsFilter::default()), + _ => { + return Err(anyhow!( + "Unrecognized filter. Options are: + replace-onion-keys, one-big-family, no-exit-ports, bad-signatures, + non-existent-signing-keys, bad-microdesc-digests." + )); + } + }) +} + +/// A filter that doesn't do anything. +/// +/// We define this so we can set a filter unconditionally and simplify our code a +/// little. +#[derive(Debug)] +struct NilFilter; +impl DirFilter for NilFilter {} + +/// Return a filter that doesn't do anything. +pub(crate) fn nil_filter() -> Arc<dyn DirFilter + 'static> { + Arc::new(NilFilter) +} + +/// A filter to replace onion keys with junk. +/// +/// Doing this means that all CREATE2 attempts via ntor will fail. (If any were +/// to succeed, they'd fail when they try to extend.) +#[derive(Debug, Default)] +struct ReplaceOnionKeysFilter; + +impl DirFilter for ReplaceOnionKeysFilter { + fn filter_md(&self, mut md: Microdesc) -> tor_dirmgr::Result<Microdesc> { + let junk_key: [u8; 32] = rand::thread_rng().gen(); + md.ntor_onion_key = junk_key.into(); + Ok(md) + } +} + +/// A filter to put all relays into a family with one another. +/// +/// This filter will prevent the client from generating any mult-hop circuits, +/// since they'll all violate our path constraints. +#[derive(Debug, Default)] +struct OneBigFamilyFilter { + /// The family we're going to put all the microdescs into. We set this to + /// contain all the identities, every time we load a consensus. + /// + /// (This filter won't do a very good job of ensuring consistency between + /// this family and the MDs we attach it to, but that's okay for the kind of + /// testing we want to do.) + new_family: Mutex<Arc<RelayFamily>>, +} + +impl DirFilter for OneBigFamilyFilter { + fn filter_consensus( + &self, + consensus: UncheckedMdConsensus, + ) -> tor_dirmgr::Result<UncheckedMdConsensus> { + let mut new_family = RelayFamily::new(); + for r in consensus.dangerously_peek().consensus.relays() { + new_family.push(*r.rsa_identity()); + } + + *self.new_family.lock().expect("poisoned lock") = Arc::new(new_family); + + Ok(consensus) + } + + fn filter_md(&self, mut md: Microdesc) -> tor_dirmgr::Result<Microdesc> { + let big_family = self.new_family.lock().expect("poisoned lock").clone(); + md.family = big_family; + Ok(md) + } +} + +/// A filter to remove all exit policies. +/// +/// With this change, any attempt to build a circuit connecting for to an +/// address will fail, since no exit will appear to support it. +#[derive(Debug)] +struct NoExitPortsFilter { + /// A "reject all ports" policy. + reject_all: Arc<PortPolicy>, +} + +impl Default for NoExitPortsFilter { + fn default() -> Self { + Self { + reject_all: Arc::new(PortPolicy::new_reject_all()), + } + } +} + +impl DirFilter for NoExitPortsFilter { + fn filter_md(&self, mut md: Microdesc) -> tor_dirmgr::Result<Microdesc> { + md.ipv4_policy = self.reject_all.clone(); + md.ipv6_policy = self.reject_all.clone(); + Ok(md) + } +} + +/// A filter to replace the signatures on a consensus with invalid ones. +/// +/// This change will cause directory validation to fail: we'll get good +/// certificates and discover that our directory is invalid. +#[derive(Debug, Default)] +struct BadSignaturesFilter; + +impl DirFilter for BadSignaturesFilter { + fn filter_consensus( + &self, + consensus: UncheckedMdConsensus, + ) -> tor_dirmgr::Result<UncheckedMdConsensus> { + let (mut consensus, time_bounds) = consensus.dangerously_into_parts(); + + // We retain the signatures, but change the declared digest of the + // document. This will make all the signatures invalid. + consensus.siggroup.sha1 = Some(*b"can you reverse sha1"); + consensus.siggroup.sha256 = Some(*b"sha256 preimage is harder so far"); + + Ok(UncheckedMdConsensus::new(consensus, time_bounds)) + } +} + +/// A filter that (nastily) claims all the authorities have changed their +/// signing keys. +/// +/// This change will make us go looking for a set of certificates that don't +/// exist so that we can verify the consensus. +#[derive(Debug, Default)] +struct NonexistentSigningKeysFilter; + +impl DirFilter for NonexistentSigningKeysFilter { + fn filter_consensus( + &self, + consensus: UncheckedMdConsensus, + ) -> tor_dirmgr::Result<UncheckedMdConsensus> { + let (mut consensus, time_bounds) = consensus.dangerously_into_parts(); + let mut rng = rand::thread_rng(); + for signature in consensus.siggroup.signatures.iter_mut() { + let sk_fingerprint: [u8; 20] = rng.gen(); + signature.key_ids.sk_fingerprint = sk_fingerprint.into(); + } + + Ok(UncheckedMdConsensus::new(consensus, time_bounds)) + } +} + +/// A filter that replaces all the microdesc digests with ones that don't exist. +/// +/// This filter will let us validate the consensus, but we'll look forever for +/// valid the microdescriptors it claims are present. +#[derive(Debug, Default)] +struct BadMicrodescDigestsFilter; + +impl DirFilter for BadMicrodescDigestsFilter { + fn filter_consensus( + &self, + consensus: UncheckedMdConsensus, + ) -> tor_dirmgr::Result<UncheckedMdConsensus> { + let (mut consensus, time_bounds) = consensus.dangerously_into_parts(); + let mut rng = rand::thread_rng(); + for rs in consensus.consensus.relays.iter_mut() { + rs.rs.doc_digest = rng.gen(); + } + + Ok(UncheckedMdConsensus::new(consensus, time_bounds)) + } +} diff --git a/crates/arti-testing/src/main.rs b/crates/arti-testing/src/main.rs index f12e084e1..c43060eb9 100644 --- a/crates/arti-testing/src/main.rs +++ b/crates/arti-testing/src/main.rs @@ -80,6 +80,7 @@ #![allow(clippy::print_stdout)] // Allowed in this crate only. mod config; +mod dirfilter; mod rt; mod traces; @@ -87,6 +88,7 @@ use arti::ArtiConfig; use arti_client::TorClient; use futures::task::SpawnExt; use rt::badtcp::BrokenTcpProvider; +use tor_dirmgr::filter::DirFilter; use tor_rtcompat::{PreferredRuntime, Runtime, SleepProviderExt}; use anyhow::{anyhow, Result}; @@ -201,6 +203,9 @@ struct Job { /// Describes how (if at all) to break the TCP connections. tcp_breakage: TcpBreakage, + /// Describes how (if at all) to mess with directories. + dir_filter: Arc<dyn DirFilter + 'static>, + /// The tracing configuration for our console log. console_log: String, @@ -220,6 +225,7 @@ impl Job { let config: ArtiConfig = self.config.load()?.try_into()?; let client = TorClient::with_runtime(runtime) .config(config.tor_client_config()?) + .dirfilter(self.dir_filter.clone()) .create_unbootstrapped()?; Ok(client) } |
