hsclient: Use a generic to provide a mock for connect()

This will allow us to test state.rs.
This commit is contained in:
Ian Jackson 2023-02-24 12:57:37 +00:00
parent feab6faa9e
commit d7602c5be4
5 changed files with 78 additions and 21 deletions

1
Cargo.lock generated
View File

@ -3968,6 +3968,7 @@ name = "tor-hsclient"
version = "0.1.1" version = "0.1.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"educe",
"futures", "futures",
"postage", "postage",
"rand_core 0.6.4", "rand_core 0.6.4",

View File

@ -18,6 +18,7 @@ default = []
[dependencies] [dependencies]
async-trait = "0.1.2" async-trait = "0.1.2"
educe = "0.4.6"
futures = "0.3.14" futures = "0.3.14"
postage = { version = "0.5.0", default-features = false, features = ["futures-traits"] } postage = { version = "0.5.0", default-features = false, features = ["futures-traits"] }
rand_core = "0.6.2" rand_core = "0.6.2"

View File

@ -2,15 +2,20 @@
//use std::time::SystemTime; //use std::time::SystemTime;
use async_trait::async_trait;
use tor_proto::circuit::ClientCirc; use tor_proto::circuit::ClientCirc;
use tor_rtcompat::Runtime; use tor_rtcompat::Runtime;
use crate::state::MockableConnectorData;
use crate::{HsClientConnError, HsClientConnector, HsClientSecretKeys}; use crate::{HsClientConnError, HsClientConnector, HsClientSecretKeys};
/// Information about a hidden service, including our connection history /// Information about a hidden service, including our connection history
#[allow(dead_code, unused_variables)] // TODO hs remove. #[allow(dead_code, unused_variables)] // TODO hs remove.
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub(crate) struct Data { // This type is actually crate-private, since it isn't re-exported, but it must
// be `pub` because it appears as a default for a type parameter in HsClientConnector.
pub struct Data {
// /// A time when we should check whether this descriptor is still the latest. // /// A time when we should check whether this descriptor is still the latest.
// desc_fresh_until: SystemTime, // desc_fresh_until: SystemTime,
// /// A time when we should expire this entry completely. // /// A time when we should expire this entry completely.
@ -47,3 +52,21 @@ pub(crate) async fn connect(
// - Return the rendezvous circuit. // - Return the rendezvous circuit.
todo!() todo!()
} }
#[async_trait]
impl MockableConnectorData for Data {
type ClientCirc = ClientCirc;
type MockGlobalState = ();
async fn connect<R: Runtime>(
connector: &HsClientConnector<R>,
data: &mut Self,
secret_keys: HsClientSecretKeys,
) -> Result<Self::ClientCirc, HsClientConnError> {
connect(connector, data, secret_keys).await
}
fn circuit_is_ok(circuit: &Self::ClientCirc) -> bool {
!circuit.is_closing()
}
}

View File

@ -46,6 +46,7 @@ mod state;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use async_trait::async_trait; use async_trait::async_trait;
use educe::Educe;
use tor_circmgr::isolation::Isolation; use tor_circmgr::isolation::Isolation;
use tor_circmgr::{CircMgr, OnionConnectError, OnionServiceConnector}; use tor_circmgr::{CircMgr, OnionConnectError, OnionServiceConnector};
@ -60,8 +61,9 @@ pub use keys::{HsClientSecretKeys, HsClientSecretKeysBuilder};
use state::Services; use state::Services;
/// An object that negotiates connections with onion services /// An object that negotiates connections with onion services
#[derive(Clone)] #[derive(Educe)]
pub struct HsClientConnector<R: Runtime> { #[educe(Clone)]
pub struct HsClientConnector<R: Runtime, D: state::MockableConnectorData = connect::Data> {
/// The runtime /// The runtime
runtime: R, runtime: R,
/// A [`CircMgr`] that we use to build circuits to HsDirs, introduction /// A [`CircMgr`] that we use to build circuits to HsDirs, introduction
@ -83,10 +85,12 @@ pub struct HsClientConnector<R: Runtime> {
// //
// TODO hs: if we implement cache isolation or state isolation, we might // TODO hs: if we implement cache isolation or state isolation, we might
// need multiple instances of this. // need multiple instances of this.
services: Arc<Mutex<state::Services>>, services: Arc<Mutex<state::Services<D>>>,
/// For mocking in tests of `state.rs`
mock_for_state: D::MockGlobalState,
} }
impl<R: Runtime> HsClientConnector<R> { impl<R: Runtime> HsClientConnector<R, connect::Data> {
/// Create a new `HsClientConnector` /// Create a new `HsClientConnector`
pub fn new( pub fn new(
runtime: R, runtime: R,
@ -100,6 +104,7 @@ impl<R: Runtime> HsClientConnector<R> {
circmgr, circmgr,
netdir_provider, netdir_provider,
services: Arc::new(Mutex::new(Services::default())), services: Arc::new(Mutex::new(Services::default())),
mock_for_state: (),
}) })
} }

View File

@ -2,6 +2,7 @@
//! about onion service history. //! about onion service history.
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Debug;
use std::mem; use std::mem;
use std::panic::AssertUnwindSafe; use std::panic::AssertUnwindSafe;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -9,6 +10,7 @@ use std::time::Instant;
use futures::FutureExt as _; use futures::FutureExt as _;
use async_trait::async_trait;
use postage::stream::Stream as _; use postage::stream::Stream as _;
use slotmap::dense::DenseSlotMap; use slotmap::dense::DenseSlotMap;
use tracing::{debug, error}; use tracing::{debug, error};
@ -16,10 +18,8 @@ use tracing::{debug, error};
use tor_circmgr::isolation::Isolation; use tor_circmgr::isolation::Isolation;
use tor_error::{internal, Bug, ErrorReport as _}; use tor_error::{internal, Bug, ErrorReport as _};
use tor_hscrypto::pk::HsId; use tor_hscrypto::pk::HsId;
use tor_proto::circuit::ClientCirc;
use tor_rtcompat::Runtime; use tor_rtcompat::Runtime;
use crate::connect::{self, Data};
use crate::{HsClientConnError, HsClientConnector, HsClientSecretKeys}; use crate::{HsClientConnError, HsClientConnector, HsClientSecretKeys};
slotmap::new_key_type! { slotmap::new_key_type! {
@ -54,7 +54,7 @@ const MAX_ATTEMPTS: u32 = 10;
/// linear search |________________| | ... .... | /// linear search |________________| | ... .... |
/// ``` |________________________| /// ``` |________________________|
#[derive(Default)] #[derive(Default)]
pub(crate) struct Services { pub(crate) struct Services<D: MockableConnectorData> {
/// Index, mapping key to entry in the data tble /// Index, mapping key to entry in the data tble
/// ///
/// There's a HashMap from HsId. /// There's a HashMap from HsId.
@ -69,7 +69,7 @@ pub(crate) struct Services {
/// place to put its results, without having to re-traverse the data structure. /// place to put its results, without having to re-traverse the data structure.
/// It also doesn't need a complete copy of the data structure key, /// It also doesn't need a complete copy of the data structure key,
/// nor to get involved with `Isolation` edge cases. /// nor to get involved with `Isolation` edge cases.
table: DenseSlotMap<TableIndex, ServiceState>, table: DenseSlotMap<TableIndex, ServiceState<D>>,
} }
/// Entry in the 2nd-level lookup array /// Entry in the 2nd-level lookup array
@ -90,11 +90,11 @@ struct IndexRecord {
// TODO HS actually expire old data // TODO HS actually expire old data
// //
// TODO unify this with channels and circuits. See arti#778. // TODO unify this with channels and circuits. See arti#778.
enum ServiceState { enum ServiceState<D: MockableConnectorData> {
/// We don't have a circuit /// We don't have a circuit
Closed { Closed {
/// The state /// The state
data: Data, data: D,
/// Last time we touched this, including reuse /// Last time we touched this, including reuse
#[allow(dead_code)] // TODO hs remove, when we do expiry #[allow(dead_code)] // TODO hs remove, when we do expiry
last_used: Instant, last_used: Instant,
@ -102,9 +102,9 @@ enum ServiceState {
/// We have an open circuit, which we can (hopefully) just use /// We have an open circuit, which we can (hopefully) just use
Open { Open {
/// The state /// The state
data: Data, data: D,
/// The circuit /// The circuit
circuit: ClientCirc, circuit: D::ClientCirc,
/// Last time we touched this, including reuse /// Last time we touched this, including reuse
last_used: Instant, last_used: Instant,
}, },
@ -123,23 +123,23 @@ enum ServiceState {
Dummy, Dummy,
} }
impl Services { impl<D: MockableConnectorData> Services<D> {
/// Connect to a hidden service /// Connect to a hidden service
// We *do* drop guard. There is *one* await point, just after drop(guard). // We *do* drop guard. There is *one* await point, just after drop(guard).
#[allow(clippy::await_holding_lock)] #[allow(clippy::await_holding_lock)]
pub(crate) async fn get_or_launch_connection( pub(crate) async fn get_or_launch_connection(
connector: &HsClientConnector<impl Runtime>, connector: &HsClientConnector<impl Runtime, D>,
hs_id: HsId, hs_id: HsId,
isolation: Box<dyn Isolation>, isolation: Box<dyn Isolation>,
secret_keys: HsClientSecretKeys, secret_keys: HsClientSecretKeys,
) -> Result<ClientCirc, HsClientConnError> { ) -> Result<D::ClientCirc, HsClientConnError> {
let mut guard = connector.services.lock() let mut guard = connector.services.lock()
.map_err(|_| internal!("HS connector poisoned"))?; .map_err(|_| internal!("HS connector poisoned"))?;
let services = &mut *guard; let services = &mut *guard;
let records = services.index.entry(hs_id).or_default(); let records = services.index.entry(hs_id).or_default();
let blank_state = || ServiceState::Closed { let blank_state = || ServiceState::Closed {
data: Data::default(), data: D::default(),
last_used: connector.runtime.now(), last_used: connector.runtime.now(),
}; };
@ -176,7 +176,7 @@ impl Services {
let (data, barrier_send) = match state { let (data, barrier_send) = match state {
ServiceState::Open { data:_, circuit, last_used } => { ServiceState::Open { data:_, circuit, last_used } => {
let now = connector.runtime.now(); let now = connector.runtime.now();
if circuit.is_closing() { if !D::circuit_is_ok(circuit) {
// Well that's no good, we need a fresh one, but keep the data // Well that's no good, we need a fresh one, but keep the data
let data = match mem::replace(state, ServiceState::Dummy) { let data = match mem::replace(state, ServiceState::Dummy) {
ServiceState::Open { data, last_used: _, circuit: _ } => data, ServiceState::Open { data, last_used: _, circuit: _ } => data,
@ -231,18 +231,18 @@ impl Services {
// Make a connection attempt // Make a connection attempt
let runtime = &connector.runtime; let runtime = &connector.runtime;
let connector = connector.clone(); let connector = (*connector).clone();
let secret_keys = secret_keys.clone(); let secret_keys = secret_keys.clone();
let connect_future = async move { let connect_future = async move {
let mut data = data; let mut data = data;
let got = AssertUnwindSafe( let got = AssertUnwindSafe(
connect::connect(&connector, &mut data, secret_keys) D::connect(&connector, &mut data, secret_keys)
) )
.catch_unwind() .catch_unwind()
.await .await
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
data = Data::default(); data = D::default();
Err(internal!("hidden service connector task panicked!").into()) Err(internal!("hidden service connector task panicked!").into())
}); });
let last_used = connector.runtime.now(); let last_used = connector.runtime.now();
@ -311,3 +311,30 @@ impl Services {
Err(internal!("HS connector state management malfunction (exceeded MAX_ATTEMPTS").into()) Err(internal!("HS connector state management malfunction (exceeded MAX_ATTEMPTS").into())
} }
} }
/// Mocking for actual HS connection work, to let us test the `Services` state machine
//
// Does *not* mock circmgr, chanmgr, etc. - those won't be used by the tests, since our
// `connect` won't call them. But mocking them pollutes many types with `R` and is
// generally tiresome. So let's not. Instead the tests can make dummy ones.
//
// This trait is actually crate-private, since it isn't re-exported, but it must
// be `pub` because it appears as a default for a type parameter in HsClientConnector.
#[async_trait]
pub trait MockableConnectorData: Default + Debug + Send + Sync + 'static {
/// Client circuit
type ClientCirc: Clone + Sync + Send + 'static;
/// Mock state
type MockGlobalState: Clone + Sync + Send + 'static;
/// Connect
async fn connect<R: Runtime>(
connector: &HsClientConnector<R, Self>,
data: &mut Self,
secret_keys: HsClientSecretKeys,
) -> Result<Self::ClientCirc, HsClientConnError>;
/// Is circuit OK? Ie, not `.is_closing()`.
fn circuit_is_ok(circuit: &Self::ClientCirc) -> bool;
}