Commit 259622bc authored by Nick Mathewson's avatar Nick Mathewson 🦀
Browse files

Merge branch 'dir-munger-v2' into 'main'

Implement a directory munger to simulate pathological cases in arti-testing (v2)

Closes #397

See merge request tpo/core/arti!442
parents a461ddc9 0725e388
Loading
Loading
Loading
Loading
+27 −0
Original line number Diff line number Diff line
@@ -211,6 +211,10 @@ dependencies = [
 "rlimit",
 "serde",
 "tokio",
 "tor-checkable",
 "tor-dirmgr",
 "tor-error",
 "tor-netdoc",
 "tor-rtcompat",
 "tracing",
 "tracing-appender",
@@ -3515,6 +3519,8 @@ dependencies = [
 "tor-error",
 "tor-llcrypto",
 "tor-protover",
 "visibility",
 "visible",
 "weak-table",
]

@@ -3866,6 +3872,27 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"

[[package]]
name = "visibility"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8881d5cc0ae34e3db2f1de5af81e5117a420d2f937506c2dc20d6f4cfb069051"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "visible"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a044005fd5c0fc1ebd79c622e5606431c6b879a6a19acafb754be9926a2de73e"
dependencies = [
 "quote",
 "syn",
]

[[package]]
name = "void"
version = "1.0.2"
+12 −1
Original line number Diff line number Diff line
@@ -15,8 +15,19 @@ publish = false

[dependencies]
arti = { package = "arti", path = "../arti", version = "0.1.0" }
arti-client = { package = "arti-client", path = "../arti-client", version = "0.1.0" }
arti-client = { package = "arti-client", path = "../arti-client", version = "0.1.0", features = [
    "dirfilter",
] }
tor-dirmgr = { package = "tor-dirmgr", path = "../tor-dirmgr", version = "0.1.0", features = [
    "dirfilter",
] }
tor-netdoc = { package = "tor-netdoc", path = "../tor-netdoc", version = "0.1.0", features = [
    "experimental-api",
    "dangerous-expose-struct-fields",
] }
tor-checkable = { path = "../tor-checkable", version = "0.1.0", features = ["experimental-api"] }
tor-rtcompat = { path = "../tor-rtcompat", version = "0.1.0" }
tor-error = { path = "../tor-error", version = "0.1.0" }
arti-config = { path = "../arti-config", version = "0.1.0" }

anyhow = "1.0.23"
+14 −0
Original line number Diff line number Diff line
@@ -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,
    })
+193 −0
Original line number Diff line number Diff line
//! 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))
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -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)
    }
Loading