Commit 8d26726f authored by eta's avatar eta
Browse files

Merge branch 'arti-testing-part1' into 'main'

arti-testing: Initial implementation

See merge request !378
parents 049d304e 06f0339b
Loading
Loading
Loading
Loading
+23 −0
Original line number Diff line number Diff line
@@ -183,6 +183,29 @@ dependencies = [
 "tracing-subscriber",
]

[[package]]
name = "arti-testing"
version = "0.1.0"
dependencies = [
 "anyhow",
 "arti-client",
 "arti-config",
 "async-trait",
 "cfg-if 1.0.0",
 "clap",
 "config",
 "futures",
 "notify",
 "pin-project",
 "rlimit",
 "serde",
 "tokio",
 "tor-rtcompat",
 "tracing",
 "tracing-appender",
 "tracing-subscriber",
]

[[package]]
name = "async-broadcast"
version = "0.3.4"
+1 −0
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ members = [
    "crates/arti-config",
    "crates/arti-bench",
    "crates/arti-hyper",
    "crates/arti-testing",
    "crates/arti"
]

+34 −0
Original line number Diff line number Diff line
[package]
name = "arti-testing"
version = "0.1.0"
authors = ["The Tor Project, Inc.", "Nick Mathewson <nickm@torproject.org>"]
edition = "2018"
license = "MIT OR Apache-2.0"
homepage = "https://gitlab.torproject.org/tpo/core/arti/-/wikis/home"
description = "Tools for testing the Arti Tor implementation."
keywords = ["tor", "arti", "privacy", "anonymity"]
categories = ["command-line-utilities", "cryptography"]
repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
publish = false

[features]

[dependencies]
arti-client = { package = "arti-client", path = "../arti-client", version = "0.1.0" }
tor-rtcompat = { path = "../tor-rtcompat", version = "0.1.0" }
arti-config = { path = "../arti-config", version = "0.1.0" }

anyhow = "1.0.23"
async-trait = "0.1.2"
config = { version = "0.12.0", default-features = false }
cfg-if = "1.0.0"
futures = "0.3.14"
tracing = "0.1.18"
notify = "4.0"
pin-project = "1"
rlimit = "0.7.0"
serde = { version = "1.0.103", features = ["derive"] }
tracing-subscriber = { version = "0.3.0", features = ["env-filter"] }
tokio = { version = "1.7", features = ["signal", "macros"] }
clap = "2.33.0"
tracing-appender = "0.2.0"
+144 −0
Original line number Diff line number Diff line
//! Reading configuration and command line issues in arti-testing.

use crate::{Action, Job};

use anyhow::{anyhow, Result};
use clap::{App, AppSettings, Arg, SubCommand};
use std::str::FromStr;
use std::time::Duration;

/// Helper: parse an optional string as a number of seconds.
fn int_str_to_secs(s: Option<&str>) -> Result<Option<Duration>> {
    match s {
        Some(s) => Ok(Some(Duration::from_secs(s.parse()?))),
        None => Ok(None),
    }
}

/// Parse the command line into a Job description.
pub(crate) fn parse_cmdline() -> Result<Job> {
    let matches = App::new("Arti testing tool")
        .version(env!("CARGO_PKG_VERSION"))
        .author("The Tor Project Developers")
        .about("Testing program for unusual arti behaviors")
        // HACK: see note in arti/src/main.rs
        .usage("arti-testing <SUBCOMMAND> [OPTIONS]")
        .arg(
            Arg::with_name("config-files")
                .short("c")
                .long("config")
                .takes_value(true)
                .value_name("FILE")
                .multiple(true)
                .global(true),
        )
        .arg(
            Arg::with_name("option")
                .short("o")
                .takes_value(true)
                .value_name("KEY=VALUE")
                .multiple(true)
                .global(true),
        )
        .arg(
            Arg::with_name("log")
                .short("l")
                .long("log")
                .takes_value(true)
                .value_name("FILTER")
                .global(true),
        )
        .arg(
            Arg::with_name("timeout")
                .long("timeout")
                .takes_value(true)
                .value_name("SECS")
                .global(true),
        )
        .arg(
            Arg::with_name("expect")
                .long("expect")
                .takes_value(true)
                .value_name("success|failure|timeout")
                .global(true),
        )
        .subcommand(
            SubCommand::with_name("connect")
                .about("Try to bootstrap and connect to an address")
                .arg(
                    Arg::with_name("target")
                        .long("target")
                        .takes_value(true)
                        .value_name("ADDR:PORT")
                        .required(true),
                )
                .arg(
                    Arg::with_name("retry")
                        .long("retry")
                        .takes_value(true)
                        .value_name("DELAY")
                        .required(false),
                ),
        )
        .subcommand(SubCommand::with_name("bootstrap").about("Try to bootstrap only"))
        .setting(AppSettings::SubcommandRequiredElseHelp)
        .get_matches();

    let config = {
        // TODO: this is mostly duplicate code.
        let mut cfg_sources = arti_config::ConfigurationSources::new();

        let config_files = matches.values_of_os("config-files").unwrap_or_default();

        if config_files.len() == 0 {
            // Not using the regular default here; we don't want interference
            // from the user's regular setup.
            // Maybe change this later on if we decide it's silly.
            return Err(anyhow!("Sorry, you need to give me a configuration file."));
        } else {
            config_files.for_each(|f| cfg_sources.push_file(f));
        }

        matches
            .values_of("option")
            .unwrap_or_default()
            .for_each(|s| cfg_sources.push_option(s));

        cfg_sources
    };

    let timeout =
        int_str_to_secs(matches.value_of("timeout"))?.unwrap_or_else(|| Duration::from_secs(30));

    let console_log = matches.value_of("log").unwrap_or("debug").to_string();

    let expectation = matches
        .value_of("expect")
        .map(crate::Expectation::from_str)
        .transpose()?;

    let action = if let Some(_m) = matches.subcommand_matches("bootstrap") {
        Action::Bootstrap
    } else if let Some(matches) = matches.subcommand_matches("connect") {
        let target = matches
            .value_of("target")
            .unwrap_or("www.torproject.org:443")
            .to_owned();
        let retry_delay = int_str_to_secs(matches.value_of("retry"))?;

        Action::Connect {
            target,
            retry_delay,
        }
    } else {
        return Err(anyhow!("No subcommand given?"));
    };

    Ok(Job {
        action,
        config,
        timeout,
        console_log,
        expectation,
    })
}
+249 −0
Original line number Diff line number Diff line
//! Tool for running an Arti client with unusual behavior or limitations.
//!
//! Example use:
//!
//! ```ignore
//! $ cat ~/.arti_testing.toml
//! [storage]
//!
//! cache_dir = "${USER_HOME}/.arti_testing/cache"
//! state_dir = "${USER_HOME}/.arti_testing/state"
//!
//! $ ./target/debug/arti-testing bootstrap --config ~/.arti-testing.toml \
//!           --timeout 120 --expect=success
//! [...lots of logs]
//! Operation succeeded [as expected]
//! TCP stats: TcpCount { n_connect_attempt: 4, n_connect_ok: 2, n_accept: 0, n_bytes_send: 461102, n_bytes_recv: 3502811 }
//! Total events: Trace: 6943, Debug: 17, Info: 13, Warn: 0, Error: 0
//!
//! $ faketime '1 year ago' ./target/debug/arti-testing connect \
//!           --config ~/.arti-testing.toml
//!           --target www.torproject.org:80
//!           --timeout 60
//!           --expect=timeout
//! [...lots of logs...]
//! Timeout occurred [as expected]
//! TCP stats: TcpCount { n_connect_attempt: 3, n_connect_ok: 3, n_accept: 0, n_bytes_send: 10917, n_bytes_recv: 16704 }
//! Total events: Trace: 77, Debug: 21, Info: 10, Warn: 2, Error: 0
//! ```
//!
//! # TODO
//!
//! - make TCP connections fail
//! - do something on the connection
//! - look at bootstrapping status and events
//! - look at trace messages
//! - Make sure we can replicate all/most test situations from arti#329
//! - Actually implement those tests.

#![allow(dead_code)]
#![deny(missing_docs)]
#![warn(noop_method_call)]
#![deny(unreachable_pub)]
#![warn(clippy::all)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::checked_conversions)]
#![warn(clippy::cognitive_complexity)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::implicit_clone)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![deny(clippy::missing_panics_doc)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::trait_duplication_in_bounds)]
#![deny(clippy::unnecessary_wraps)]
#![warn(clippy::unseparated_literal_suffix)]
#![deny(clippy::unwrap_used)]
#![allow(clippy::print_stderr)] // Allowed in this crate only.
#![allow(clippy::print_stdout)] // Allowed in this crate only.

mod config;
mod rt;
mod traces;

use arti_client::TorClient;
use arti_config::ArtiConfig;
use tor_rtcompat::{PreferredRuntime, Runtime, SleepProviderExt};

use anyhow::{anyhow, Result};
use tracing_subscriber::prelude::*;
//use std::path::PathBuf;
use std::convert::TryInto;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;

/// A possible action for the tool to try to take
#[derive(Debug, Clone)]
enum Action {
    /// Bootstrap the client and exit.
    Bootstrap,
    /// Bootstrap the client, then try to connect to a target
    ///
    /// Exit when successful.
    Connect {
        /// The target address.
        target: String,
        /// How long to wait between attempts?  If None, exit on the first
        /// failure.
        retry_delay: Option<Duration>,
    },
}

/// What we expect to happen when we run a given job.
#[derive(Debug, Clone)]
enum Expectation {
    /// The operation should complete successfully.
    Success,
    /// The operation should terminate with an error.
    Failure,
    /// The operation should time out
    Timeout,
}

impl FromStr for Expectation {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s {
            "success" => Expectation::Success,
            "failure" => Expectation::Failure,
            "timeout" => Expectation::Timeout,
            _ => return Err(anyhow!("Unrecognized expectation {:?}", s)),
        })
    }
}

/// Descriptions of an action to take, and what to expect as an outcome.
#[derive(Debug, Clone)]
struct Job {
    /// The action that the client should try to take
    action: Action,

    /// The tracing configuration for our console log.
    console_log: String,

    /// Where we're getting our configuration from.
    config: arti_config::ConfigurationSources,

    /// What we expect to happen.
    expectation: Option<Expectation>,

    /// How long to wait for the action to succeed or fail.
    timeout: Duration,
}

impl Job {
    /// Make a new unbootstrapped client for this job.
    fn make_client<R: Runtime>(&self, runtime: R) -> Result<TorClient<R>> {
        let config: ArtiConfig = self.config.load()?.try_into()?;
        let client = TorClient::with_runtime(runtime)
            .config(config.tor_client_config()?)
            .create_unbootstrapped()?;
        Ok(client)
    }

    /// Run the body of a job.
    async fn run_job_inner<R: Runtime>(&self, client: TorClient<R>) -> Result<()> {
        client.bootstrap().await?; // all jobs currently start with a bootstrap.

        match &self.action {
            Action::Bootstrap => {}
            Action::Connect {
                target,
                retry_delay,
            } => {
                loop {
                    let outcome = client.connect(target).await;
                    match (outcome, retry_delay) {
                        (Ok(_stream), _) => break,
                        (Err(e), None) => return Err(e.into()),
                        (Err(_e), Some(delay)) => client.runtime().sleep(*delay).await, // XXXX log error
                    }
                }
            }
        }

        Ok(())
    }

    /// Run a provided job.
    ///
    /// XXXX Eventually this should come up with some kind of result that's meaningful.
    async fn run_job(&self) -> Result<()> {
        let runtime = PreferredRuntime::current()?;
        let tcp = rt::count::Counting::new_zeroed(runtime.clone());
        let runtime = tor_rtcompat::CompoundRuntime::new(
            runtime.clone(),
            runtime.clone(),
            tcp.clone(),
            runtime,
        );
        let client = self.make_client(runtime)?;

        let outcome = client
            .clone()
            .runtime()
            .timeout(self.timeout, self.run_job_inner(client))
            .await;

        let result = match (&self.expectation, outcome) {
            (Some(Expectation::Timeout), Err(tor_rtcompat::TimeoutError)) => {
                println!("Timeout occurred [as expected]");
                Ok(())
            }
            (Some(Expectation::Failure), Ok(Err(e))) => {
                println!("Got an error as [as expected]");
                println!("Error was: {}", e);
                Ok(())
            }
            (Some(Expectation::Success), Ok(Ok(()))) => {
                println!("Operation succeeded [as expected]");
                Ok(())
            }
            (Some(expectation), outcome) => Err(anyhow!(
                "Test failed. Expected {:?} but got: {:?}",
                expectation,
                outcome
            )),
            (None, outcome) => {
                // no expectation.
                println!("Outcome: {:?}", outcome);
                Ok(())
            }
        };

        println!("TCP stats: {:?}", tcp.counts());

        result
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let job = config::parse_cmdline()?;

    let targets: tracing_subscriber::filter::Targets = job.console_log.parse()?;
    let console_layer = tracing_subscriber::fmt::Layer::default().with_filter(targets);
    let trace_count = Arc::new(traces::TraceCount::default());
    tracing_subscriber::registry()
        .with(console_layer)
        .with(traces::TraceCounter(trace_count.clone()))
        .init();

    let outcome = job.run_job().await;

    println!("Total events: {}", trace_count);

    outcome
}
Loading