arti-client: add ability to automatically bootstrap

The new `BootstrapBehavior` enum controls whether an unbootstrapped
`TorClient` will bootstrap itself automatically (`Ondemand`) when an
attempt is made to use it, or whether the user must perform
bootstrapping themselves (`Manual`).

The `lazy-init` example shows how you could write a simple
`get_tor_client()` function that used a global `OnceCell` to share
a Tor client across an entire application with this API.

closes arti#278
This commit is contained in:
eta 2022-02-16 15:57:24 +00:00
parent 13f39ed896
commit 790ea4af4b
6 changed files with 156 additions and 16 deletions

1
Cargo.lock generated
View File

@ -126,6 +126,7 @@ dependencies = [
"futures",
"humantime-serde",
"hyper",
"once_cell",
"pin-project",
"postage",
"serde",

View File

@ -54,3 +54,4 @@ tokio-util = { version = "0.6", features = ["compat"] }
anyhow = "1.0.5"
tracing-subscriber = "0.3.0"
tempfile = "3.3"
once_cell = "1.9"

View File

@ -0,0 +1,76 @@
use anyhow::Result;
use arti_client::{BootstrapBehavior, TorClient, TorClientConfig};
use tokio_crate as tokio;
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
use futures::io::{AsyncReadExt, AsyncWriteExt};
use once_cell::sync::OnceCell;
static TOR_CLIENT: OnceCell<TorClient<TokioNativeTlsRuntime>> = OnceCell::new();
/// Get a `TorClient` by copying the globally shared client stored in `TOR_CLIENT`.
/// If that client hasn't been initialized yet, initializes it first.
///
/// # Errors
///
/// Errors if called outside a Tokio runtime context, or creating the Tor client
/// failed.
pub fn get_tor_client() -> Result<TorClient<TokioNativeTlsRuntime>> {
let client = TOR_CLIENT.get_or_try_init(|| -> Result<TorClient<_>> {
// The client config includes things like where to store persistent Tor network state.
// The defaults provided are the same as the Arti standalone application, and save data
// to a conventional place depending on operating system (for example, ~/.local/share/arti
// on Linux platforms)
let config = TorClientConfig::default();
// Get a `tor_rtcompat::Runtime` from the currently running Tokio runtime.
let rt = TokioNativeTlsRuntime::current()?;
eprintln!("creating unbootstrapped Tor client");
// Create an unbootstrapped Tor client. Bootstrapping will happen when the client is used,
// since we specified `BootstrapBehavior::Ondemand`.
Ok(TorClient::create_unbootstrapped(
rt,
config,
BootstrapBehavior::OnDemand,
)?)
})?;
Ok(client.clone())
}
#[tokio::main]
async fn main() -> Result<()> {
// Arti uses the `tracing` crate for logging. Install a handler for this, to print Arti's logs.
tracing_subscriber::fmt::init();
eprintln!("getting shared Tor client...");
let tor_client = get_tor_client()?;
eprintln!("connecting to example.com...");
// Initiate a connection over Tor to example.com, port 80.
let mut stream = tor_client.connect(("example.com", 80)).await?;
eprintln!("sending request...");
stream
.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")
.await?;
// IMPORTANT: Make sure the request was written.
// Arti buffers data, so flushing the buffer is usually required.
stream.flush().await?;
eprintln!("reading response...");
// Read and print the result.
let mut buf = Vec::new();
stream.read_to_end(&mut buf).await?;
println!("{}", String::from_utf8_lossy(&buf));
Ok(())
}

View File

@ -80,6 +80,29 @@ pub struct TorClient<R: Runtime> {
/// mutex used to prevent two tasks from trying to bootstrap at once.
bootstrap_in_progress: Arc<AsyncMutex<()>>,
/// Whether or not we should call `bootstrap` before doing things that require
/// bootstrapping. If this is `false`, we will just call `wait_for_bootstrap`
/// instead.
should_bootstrap: BootstrapBehavior,
}
/// Preferences for whether a [`TorClient`] should bootstrap on its own or not.
///
/// *[See the documentation for `create_unbootstrapped` for a full explanation.](TorClient::create_unbootstrapped)*
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BootstrapBehavior {
/// Bootstrap the client automatically when requests are made that require the client to be
/// bootstrapped.
OnDemand,
/// Make no attempts to automatically bootstrap. [`TorClient::bootstrap`] must be manually
/// invoked in order for the [`TorClient`] to become useful.
///
/// Attempts to use the client (e.g. by creating connections or resolving hosts over the Tor
/// network) before calling [`bootstrap`](TorClient::bootstrap) will fail, and
/// return an error that has kind [`ErrorKind::BootstrapRequired`](crate::ErrorKind::BootstrapRequired).
Manual,
}
/// Preferences for how to route a stream over the Tor network.
@ -301,25 +324,49 @@ impl<R: Runtime> TorClient<R> {
runtime: R,
config: TorClientConfig,
) -> crate::Result<TorClient<R>> {
let ret = TorClient::create_unbootstrapped(runtime, config)?;
let ret = TorClient::create_unbootstrapped(runtime, config, BootstrapBehavior::Manual)?;
ret.bootstrap().await?;
Ok(ret)
}
/// Create a `TorClient` without bootstrapping a connection to the network. The returned client
/// will not be usable until [`bootstrap`](TorClient::bootstrap) is called.
/// Create a `TorClient` without bootstrapping a connection to the network.
///
/// The behaviour of this client depends on the value of `autobootstrap`.
///
/// ## If `autobootstrap` is [`Manual`](BootstrapBehavior::Manual)
///
/// If `autobootstrap` is set to `Manual`, the returned `TorClient` (and its clones) will never
/// attempt to bootstrap themselves. You must manually call [`bootstrap`](TorClient::bootstrap)
/// in order for the client(s) to become usable.
///
/// Attempts to use the client (e.g. by creating connections or resolving hosts over the Tor
/// network) before calling [`bootstrap`](TorClient::bootstrap) will fail, and
/// return an error that has kind [`ErrorKind::BootstrapRequired`](crate::ErrorKind::BootstrapRequired). However, if a bootstrap
/// attempt is in progress but not complete, attempts to use the client will block instead.
pub fn create_unbootstrapped(runtime: R, config: TorClientConfig) -> crate::Result<Self> {
TorClient::create_inner(runtime, config).map_err(ErrorDetail::into)
/// return an error that has kind [`ErrorKind::BootstrapRequired`](crate::ErrorKind::BootstrapRequired).
///
/// This option is useful if you wish to have control over the bootstrap process (for example,
/// you might wish to avoid initiating network connections until explicit user confirmation
/// is given).
///
/// ## If `autobootstrap` is [`OnDemand`](BootstrapBehavior::OnDemand)
///
/// If `autoboostrap` is set to `OnDemand`, the returned `TorClient` (and its clones) will
/// automatically bootstrap before doing any operation that would require them to be
/// bootstrapped.
pub fn create_unbootstrapped(
runtime: R,
config: TorClientConfig,
autobootstrap: BootstrapBehavior,
) -> crate::Result<Self> {
TorClient::create_inner(runtime, config, autobootstrap).map_err(ErrorDetail::into)
}
/// Implementation of `create_unbootstrapped`, split out in order to avoid manually specifying
/// double error conversions.
fn create_inner(runtime: R, config: TorClientConfig) -> StdResult<Self, ErrorDetail> {
fn create_inner(
runtime: R,
config: TorClientConfig,
autobootstrap: BootstrapBehavior,
) -> StdResult<Self, ErrorDetail> {
let circ_cfg = config.get_circmgr_config()?;
let dir_cfg = config.get_dirmgr_config()?;
let statemgr = FsStateMgr::from_path(config.storage.expand_state_dir()?)?;
@ -405,6 +452,7 @@ impl<R: Runtime> TorClient<R> {
reconfigure_lock: Arc::new(Mutex::new(())),
status_receiver,
bootstrap_in_progress: Arc::new(AsyncMutex::new(())),
should_bootstrap: autobootstrap,
})
}
@ -454,12 +502,25 @@ impl<R: Runtime> TorClient<R> {
Ok(())
}
/// ## For `BootstrapBehavior::Ondemand` clients
///
/// Initiate a bootstrap by calling `bootstrap` (which is idempotent, so attempts to
/// bootstrap twice will just do nothing).
///
/// ## For `BootstrapBehavior::Manual` clients
///
/// Check whether a bootstrap is in progress; if one is, wait until it finishes
/// and then return. (Otherwise, return immediately.)
async fn wait_for_bootstrap(&self) -> StdResult<(), ErrorDetail> {
// Grab the lock, and immediately release it. That will ensure that nobody else is trying to bootstrap.
self.bootstrap_in_progress.lock().await;
match self.should_bootstrap {
BootstrapBehavior::OnDemand => {
self.bootstrap_inner().await?;
}
BootstrapBehavior::Manual => {
// Grab the lock, and immediately release it. That will ensure that nobody else is trying to bootstrap.
self.bootstrap_in_progress.lock().await;
}
}
Ok(())
}
@ -1038,7 +1099,7 @@ mod test {
let cfg = TorClientConfigBuilder::from_directories(state_dir, cache_dir)
.build()
.unwrap();
let _ = TorClient::create_unbootstrapped(rt, cfg).unwrap();
let _ = TorClient::create_unbootstrapped(rt, cfg, BootstrapBehavior::Manual).unwrap();
});
}
@ -1050,7 +1111,8 @@ mod test {
let cfg = TorClientConfigBuilder::from_directories(state_dir, cache_dir)
.build()
.unwrap();
let client = TorClient::create_unbootstrapped(rt, cfg).unwrap();
let client =
TorClient::create_unbootstrapped(rt, cfg, BootstrapBehavior::Manual).unwrap();
let result = client.connect("example.com:80").await;
assert!(result.is_err());
assert_eq!(result.err().unwrap().kind(), ErrorKind::BootstrapRequired);

View File

@ -196,7 +196,7 @@ pub mod config;
pub mod status;
pub use address::{DangerouslyIntoTorAddr, IntoTorAddr, TorAddr, TorAddrError};
pub use client::{StreamPrefs, TorClient};
pub use client::{BootstrapBehavior, StreamPrefs, TorClient};
pub use config::TorClientConfig;
pub use tor_circmgr::IsolationToken;

View File

@ -78,7 +78,7 @@ use tor_netdoc::doc::netstatus::ConsensusFlavor;
use futures::{channel::oneshot, task::SpawnExt};
use tor_rtcompat::{Runtime, SleepProviderExt};
use tracing::{info, trace, warn};
use tracing::{debug, info, trace, warn};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
@ -270,7 +270,7 @@ impl<R: Runtime> DirMgr<R> {
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
info!("Attempted to bootstrap twice; ignoring.");
debug!("Attempted to bootstrap twice; ignoring.");
return Ok(());
}