use std::fmt::Debug;
use std::mem;
use std::panic::AssertUnwindSafe;
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant};
use futures::task::{SpawnError, SpawnExt as _};
use futures::FutureExt as _;
use async_trait::async_trait;
use educe::Educe;
use either::Either::{self, *};
use postage::stream::Stream as _;
use tracing::{debug, error, trace};
use safelog::sensitive as sv;
use tor_basic_utils::define_accessor_trait;
use tor_circmgr::isolation::Isolation;
use tor_error::{internal, Bug, ErrorReport as _};
use tor_hscrypto::pk::HsId;
use tor_netdir::NetDir;
use tor_rtcompat::Runtime;
use crate::isol_map;
use crate::{ConnError, HsClientConnector, HsClientSecretKeys};
slotmap::new_key_type! {
struct TableIndex;
}
#[derive(Default, Debug)]
pub struct Config {
pub(crate) retry: tor_circmgr::CircuitTiming,
}
define_accessor_trait! {
pub trait HsClientConnectorConfig {
circuit_timing: tor_circmgr::CircuitTiming,
}
}
const MAX_RECHECKS: u32 = 10;
const RETAIN_CIRCUIT_AFTER_LAST_USE: Duration = Duration::from_secs(10 * 60);
const RETAIN_DATA_AFTER_LAST_USE: Duration = Duration::from_secs(48 * 3600 );
#[derive(Default, Debug)]
pub(crate) struct Services<D: MockableConnectorData> {
records: isol_map::MultikeyIsolatedMap<TableIndex, HsId, HsClientSecretKeys, ServiceState<D>>,
config: Arc<Config>,
}
#[allow(dead_code)] type ServiceRecord<D> = isol_map::Record<HsClientSecretKeys, ServiceState<D>>;
#[derive(Educe)]
#[educe(Debug)]
enum ServiceState<D: MockableConnectorData> {
Closed {
data: D,
last_used: Instant,
},
Open {
data: D,
#[educe(Debug(ignore))]
circuit: Arc<D::ClientCirc>,
last_used: Instant,
circuit_expiry_task: CircuitExpiryTask,
},
Working {
barrier_recv: postage::barrier::Receiver,
error: Arc<Mutex<Option<ConnError>>>,
},
Dummy,
}
impl<D: MockableConnectorData> ServiceState<D> {
fn blank(runtime: &impl Runtime) -> Self {
ServiceState::Closed {
data: D::default(),
last_used: runtime.now(),
}
}
}
type Continuation = (Arc<Mutex<Option<ConnError>>>, postage::barrier::Receiver);
#[derive(Debug)] struct CircuitExpiryTask {}
impl Drop for CircuitExpiryTask {
fn drop(&mut self) {}
}
fn obtain_circuit_or_continuation_info<D: MockableConnectorData>(
connector: &HsClientConnector<impl Runtime, D>,
netdir: &Arc<NetDir>,
hsid: &HsId,
secret_keys: &HsClientSecretKeys,
table_index: TableIndex,
rechecks: &mut impl Iterator,
mut guard: MutexGuard<'_, Services<D>>,
) -> Result<Either<Continuation, Arc<D::ClientCirc>>, ConnError> {
let blank_state = || ServiceState::blank(&connector.runtime);
for _recheck in rechecks {
let record = guard
.records
.by_index_mut(table_index)
.ok_or_else(|| internal!("guard table entry vanished!"))?;
let state = &mut **record;
trace!("HS conn state: {state:?}");
let (data, barrier_send) = match state {
ServiceState::Open {
data: _,
circuit,
last_used,
circuit_expiry_task: _,
} => {
let now = connector.runtime.now();
if !D::circuit_is_ok(circuit) {
let data = match mem::replace(state, ServiceState::Dummy) {
ServiceState::Open {
data,
last_used: _,
circuit: _,
circuit_expiry_task: _,
} => data,
_ => panic!("state changed between matches"),
};
*state = ServiceState::Closed {
data,
last_used: now,
};
continue;
}
*last_used = now;
return Ok::<_, ConnError>(Right(circuit.clone()));
}
ServiceState::Working {
barrier_recv,
error,
} => {
if !matches!(
barrier_recv.try_recv(),
Err(postage::stream::TryRecvError::Pending)
) {
*state = blank_state();
continue;
}
let barrier_recv = barrier_recv.clone();
let error = error.clone();
return Ok(Left((error, barrier_recv)));
}
ServiceState::Closed { .. } => {
let (barrier_send, barrier_recv) = postage::barrier::channel();
let data = match mem::replace(
state,
ServiceState::Working {
barrier_recv,
error: Arc::new(Mutex::new(None)),
},
) {
ServiceState::Closed { data, .. } => data,
_ => panic!("state changed between matches"),
};
(data, barrier_send)
}
ServiceState::Dummy => {
*state = blank_state();
return Err(internal!("HS connector found dummy state").into());
}
};
let runtime = &connector.runtime;
let connector = (*connector).clone();
let config = guard.config.clone();
let netdir = netdir.clone();
let secret_keys = secret_keys.clone();
let hsid = *hsid;
let connect_future = async move {
let mut data = data;
let got = AssertUnwindSafe(D::connect(
&connector,
netdir,
config,
hsid,
&mut data,
secret_keys,
))
.catch_unwind()
.await
.unwrap_or_else(|_| {
data = D::default();
Err(internal!("hidden service connector task panicked!").into())
});
let now = connector.runtime.now();
let last_used = now;
let got = got.and_then(|circuit| {
let circuit_expiry_task = ServiceState::spawn_circuit_expiry_task(
&connector,
hsid,
table_index,
last_used,
now,
)
.map_err(|cause| ConnError::Spawn {
spawning: "circuit expiry task",
cause: cause.into(),
})?;
Ok((circuit, circuit_expiry_task))
});
let got_error = got.as_ref().map(|_| ()).map_err(Clone::clone);
let stored = async {
let mut guard = connector.services()?;
let record = guard
.records
.by_index_mut(table_index)
.ok_or_else(|| internal!("HS table entry removed while task running"))?;
let state = &mut **record;
let error_store = match state {
ServiceState::Working { error, .. } => error,
_ => return Err(internal!("HS task found state other than Working")),
};
match got {
Ok((circuit, circuit_expiry_task)) => {
*state = ServiceState::Open {
data,
circuit,
last_used,
circuit_expiry_task,
}
}
Err(error) => {
let mut error_store = error_store
.lock()
.map_err(|_| internal!("Working error poisoned, cannot store error"))?;
*error_store = Some(error);
}
};
Ok(())
}
.await;
match (got_error, stored) {
(Ok::<(), ConnError>(()), Ok::<(), Bug>(())) => {}
(Err(got_error), Ok(())) => {
debug!("HS connection failure: {}: {}", hsid, got_error.report(),);
}
(Ok(()), Err(bug)) => error!(
"internal error storing built HS circuit for {}: {}",
sv(hsid),
bug.report(),
),
(Err(got_error), Err(bug)) => error!(
"internal error storing HS connection error for {}: {}; {}",
sv(hsid),
got_error.report(),
bug.report(),
),
};
drop(barrier_send);
};
runtime
.spawn_obj(Box::new(connect_future).into())
.map_err(|cause| ConnError::Spawn {
spawning: "connection task",
cause: cause.into(),
})?;
}
Err(internal!("HS connector state management malfunction (exceeded MAX_RECHECKS").into())
}
impl<D: MockableConnectorData> Services<D> {
pub(crate) fn new(config: Config) -> Self {
Services {
records: Default::default(),
config: Arc::new(config),
}
}
pub(crate) async fn get_or_launch_connection(
connector: &HsClientConnector<impl Runtime, D>,
netdir: &Arc<NetDir>,
hs_id: HsId,
isolation: Box<dyn Isolation>,
secret_keys: HsClientSecretKeys,
) -> Result<Arc<D::ClientCirc>, ConnError> {
let blank_state = || ServiceState::blank(&connector.runtime);
let mut rechecks = 0..MAX_RECHECKS;
let mut obtain = |table_index, guard| {
obtain_circuit_or_continuation_info(
connector,
netdir,
&hs_id,
&secret_keys,
table_index,
&mut rechecks,
guard,
)
};
let mut got;
let table_index;
{
let mut guard = connector.services()?;
let services = &mut *guard;
trace!("HS conn get_or_launch: {hs_id:?} {isolation:?} {secret_keys:?}");
table_index =
services
.records
.index_or_insert_with(&hs_id, &secret_keys, isolation, blank_state);
let guard = guard;
got = obtain(table_index, guard);
}
loop {
{
let (error, mut barrier_recv) = match got? {
Right(ret) => return Ok(ret),
Left(continuation) => continuation,
};
barrier_recv.recv().await;
let error = error
.lock()
.map_err(|_| internal!("Working error poisoned"))?;
if let Some(error) = &*error {
return Err(error.clone());
}
}
let guard = connector.services()?;
got = obtain(table_index, guard);
}
}
pub(crate) fn run_housekeeping(&mut self, now: Instant) {
self.expire_old_data(now);
}
fn expire_old_data(&mut self, now: Instant) {
self.records.retain(|hsid, record, _table_index| match &**record {
ServiceState::Closed {
data: _,
last_used,
} => {
let Some(expiry_time) = last_used.checked_add(RETAIN_DATA_AFTER_LAST_USE) else { return false; };
now <= expiry_time
},
ServiceState::Open { .. } |
ServiceState::Working { .. } => true,
ServiceState::Dummy { .. } => {
error!("found dummy data during HS housekeeping, for {}", sv(hsid));
false
}
});
}
}
impl<D: MockableConnectorData> ServiceState<D> {
fn spawn_circuit_expiry_task(
connector: &HsClientConnector<impl Runtime, D>,
hsid: HsId,
table_index: TableIndex,
last_used: Instant,
now: Instant,
) -> Result<CircuitExpiryTask, SpawnError> {
fn calculate_expiry_wait(last_used: Instant, now: Instant) -> Option<Duration> {
let expiry = last_used
.checked_add(RETAIN_CIRCUIT_AFTER_LAST_USE)
.or_else(|| {
error!("time overflow calculating HS circuit expiry, killing circuit!");
None
})?;
let wait = expiry.checked_duration_since(now)?;
Some(wait)
}
let mut maybe_wait = calculate_expiry_wait(last_used, now);
let () = connector.runtime.spawn({
let connector = connector.clone();
async move {
loop {
if let Some(yes_wait) = maybe_wait {
connector.runtime.sleep(yes_wait).await;
}
let Ok(mut guard) = connector.services() else { break };
let Some(record) = guard.records.by_index_mut(table_index) else { break };
let state = &mut **record;
let last_used = match state {
ServiceState::Closed { .. } => break,
ServiceState::Open { last_used, .. } => *last_used,
ServiceState::Working { .. } => break, ServiceState::Dummy => break, };
maybe_wait = calculate_expiry_wait(last_used, connector.runtime.now());
if maybe_wait.is_none() {
match mem::replace(state, ServiceState::Dummy) {
ServiceState::Open {
data,
circuit,
last_used,
circuit_expiry_task,
} => {
debug!("HS connection expires: {hsid}");
drop(circuit);
drop(circuit_expiry_task); *state = ServiceState::Closed { data, last_used };
break;
}
_ => panic!("state now {state:?} even though we just saw it Open"),
}
}
}
}
})?;
Ok(CircuitExpiryTask {})
}
}
#[async_trait]
pub trait MockableConnectorData: Default + Debug + Send + Sync + 'static {
type ClientCirc: Sync + Send + 'static;
type MockGlobalState: Clone + Sync + Send + 'static;
async fn connect<R: Runtime>(
connector: &HsClientConnector<R, Self>,
netdir: Arc<NetDir>,
config: Arc<Config>,
hsid: HsId,
data: &mut Self,
secret_keys: HsClientSecretKeys,
) -> Result<Arc<Self::ClientCirc>, ConnError>;
fn circuit_is_ok(circuit: &Self::ClientCirc) -> bool;
}
#[cfg(test)]
pub(crate) mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
use super::*;
use crate::*;
use futures::{poll, SinkExt};
use std::fmt;
use std::task::Poll::{self, *};
use tokio::pin;
use tokio_crate as tokio;
use tor_rtcompat::{test_with_one_runtime, SleepProvider};
use tor_rtmock::MockSleepRuntime;
use tracing_test::traced_test;
use ConnError as E;
#[derive(Debug, Default)]
struct MockData {
connect_called: usize,
}
type MockGive = Poll<Result<(), E>>;
#[derive(Debug, Clone)]
struct MockGlobalState {
give: postage::watch::Receiver<MockGive>,
}
#[derive(Clone, Educe)]
#[educe(Debug)]
struct MockCirc {
#[educe(Debug(method = "debug_arc_mutex"))]
ok: Arc<Mutex<bool>>,
connect_called: usize,
}
fn debug_arc_mutex(val: &Arc<Mutex<impl Debug>>, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "@{:?}", Arc::as_ptr(val))?;
let guard = val.lock();
let guard = guard.or_else(|g| {
write!(f, ",POISON")?;
Ok::<_, fmt::Error>(g.into_inner())
})?;
write!(f, " ")?;
Debug::fmt(&*guard, f)
}
impl PartialEq for MockCirc {
fn eq(&self, other: &MockCirc) -> bool {
Arc::ptr_eq(&self.ok, &other.ok)
}
}
impl MockCirc {
fn new(connect_called: usize) -> Self {
let ok = Arc::new(Mutex::new(true));
MockCirc { ok, connect_called }
}
}
#[async_trait]
impl MockableConnectorData for MockData {
type ClientCirc = MockCirc;
type MockGlobalState = MockGlobalState;
async fn connect<R: Runtime>(
connector: &HsClientConnector<R, MockData>,
_netdir: Arc<NetDir>,
_config: Arc<Config>,
_hsid: HsId,
data: &mut MockData,
_secret_keys: HsClientSecretKeys,
) -> Result<Arc<Self::ClientCirc>, E> {
data.connect_called += 1;
let make = {
let connect_called = data.connect_called;
move |()| Arc::new(MockCirc::new(connect_called))
};
let mut give = connector.mock_for_state.give.clone();
if let Ready(ret) = &*give.borrow() {
return ret.clone().map(make);
}
loop {
match give.recv().await.expect("EOF on mock_global_state stream") {
Pending => {}
Ready(ret) => return ret.map(make),
}
}
}
fn circuit_is_ok(circuit: &Self::ClientCirc) -> bool {
*circuit.ok.lock().unwrap()
}
}
fn mk_keys(kk: u8) -> HsClientSecretKeys {
let mut ss = [0_u8; 32];
ss[0] = kk;
let secret = tor_llcrypto::pk::ed25519::SecretKey::from_bytes(&ss).unwrap();
let public = tor_llcrypto::pk::ed25519::PublicKey::from(&secret);
let kp = tor_llcrypto::pk::ed25519::Keypair { public, secret };
let mut b = HsClientSecretKeysBuilder::default();
b.ks_hsc_intro_auth(kp.into());
b.build().unwrap()
}
fn mk_hsconn<R: Runtime>(
runtime: R,
) -> (
HsClientConnector<R, MockData>,
HsClientSecretKeys,
postage::watch::Sender<MockGive>,
) {
let chanmgr = tor_chanmgr::ChanMgr::new(
runtime.clone(),
&Default::default(),
tor_chanmgr::Dormancy::Dormant,
&Default::default(),
);
let guardmgr = tor_guardmgr::GuardMgr::new(
runtime.clone(),
tor_persist::TestingStateMgr::new(),
&tor_guardmgr::TestConfig::default(),
)
.unwrap();
let circmgr = tor_circmgr::CircMgr::new(
&tor_circmgr::TestConfig::default(),
tor_persist::TestingStateMgr::new(),
&runtime,
Arc::new(chanmgr),
guardmgr,
)
.unwrap();
let circpool = HsCircPool::new(&circmgr);
let (give_send, give) = postage::watch::channel_with(Ready(Ok(())));
let mock_for_state = MockGlobalState { give };
#[allow(clippy::let_and_return)] let hscc = HsClientConnector {
runtime,
circpool,
services: Default::default(),
mock_for_state,
};
let keys = HsClientSecretKeysBuilder::default().build().unwrap();
(hscc, keys, give_send)
}
#[allow(clippy::unnecessary_wraps)]
fn mk_isol(s: &str) -> Option<NarrowableIsolation> {
Some(NarrowableIsolation(s.into()))
}
async fn launch_one(
hsconn: &HsClientConnector<impl Runtime, MockData>,
id: u8,
secret_keys: &HsClientSecretKeys,
isolation: Option<NarrowableIsolation>,
) -> Result<Arc<MockCirc>, ConnError> {
let netdir = tor_netdir::testnet::construct_netdir()
.unwrap_if_sufficient()
.unwrap();
let netdir = Arc::new(netdir);
let hs_id = {
let mut hs_id = [0_u8; 32];
hs_id[0] = id;
hs_id.into()
};
#[allow(clippy::redundant_closure)] let isolation = isolation.unwrap_or_default().into();
Services::get_or_launch_connection(hsconn, &netdir, hs_id, isolation, secret_keys.clone())
.await
}
#[derive(Default, Debug, Clone)]
pub(crate) struct NarrowableIsolation(pub(crate) String);
impl tor_circmgr::isolation::IsolationHelper for NarrowableIsolation {
fn compatible_same_type(&self, other: &Self) -> bool {
self.join_same_type(other).is_some()
}
fn join_same_type(&self, other: &Self) -> Option<Self> {
Some(if self.0.starts_with(&other.0) {
self.clone()
} else if other.0.starts_with(&self.0) {
other.clone()
} else {
return None;
})
}
}
#[test]
#[traced_test]
fn simple() {
test_with_one_runtime!(|runtime| async {
let (hsconn, keys, _give_send) = mk_hsconn(runtime);
let circuit = launch_one(&hsconn, 0, &keys, None).await.unwrap();
eprintln!("{:?}", circuit);
});
}
#[test]
#[traced_test]
fn expiry() {
test_with_one_runtime!(|outer_runtime| async move {
let runtime = MockSleepRuntime::new(outer_runtime.clone());
const BODGE_YIELD: Duration = Duration::from_millis(125);
const TIMEOUT_SLOP: Duration = Duration::from_secs(10);
let (hsconn, keys, _give_send) = mk_hsconn(runtime.clone());
let advance = |duration| {
let hsconn = hsconn.clone();
let runtime = &runtime;
let outer_runtime = &outer_runtime;
async move {
outer_runtime.sleep(BODGE_YIELD).await;
runtime.advance(duration).await;
outer_runtime.sleep(BODGE_YIELD).await;
hsconn.services().unwrap().run_housekeeping(runtime.now());
}
};
let circuit1 = launch_one(&hsconn, 0, &keys, None).await.unwrap();
advance(RETAIN_CIRCUIT_AFTER_LAST_USE + TIMEOUT_SLOP).await;
let circuit2a = launch_one(&hsconn, 0, &keys, None).await.unwrap();
assert_ne!(circuit1, circuit2a);
advance(RETAIN_CIRCUIT_AFTER_LAST_USE - TIMEOUT_SLOP).await;
let circuit2b = launch_one(&hsconn, 0, &keys, None).await.unwrap();
assert_eq!(circuit2a, circuit2b);
advance(RETAIN_CIRCUIT_AFTER_LAST_USE - TIMEOUT_SLOP).await;
let circuit2c = launch_one(&hsconn, 0, &keys, None).await.unwrap();
assert_eq!(circuit2a, circuit2c);
advance(RETAIN_CIRCUIT_AFTER_LAST_USE + TIMEOUT_SLOP).await;
let circuit3 = launch_one(&hsconn, 0, &keys, None).await.unwrap();
assert_ne!(circuit2c, circuit3);
assert_eq!(circuit3.connect_called, 3);
advance(RETAIN_DATA_AFTER_LAST_USE + Duration::from_secs(10)).await;
let circuit4 = launch_one(&hsconn, 0, &keys, None).await.unwrap();
assert_eq!(circuit4.connect_called, 1);
});
}
#[test]
#[traced_test]
fn coalesce() {
test_with_one_runtime!(|runtime| async {
let (hsconn, keys, mut give_send) = mk_hsconn(runtime);
give_send.send(Pending).await.unwrap();
let c1f = launch_one(&hsconn, 0, &keys, None);
pin!(c1f);
for _ in 0..10 {
assert!(poll!(&mut c1f).is_pending());
}
let c2f = launch_one(&hsconn, 0, &keys, None);
pin!(c2f);
for _ in 0..10 {
assert!(poll!(&mut c1f).is_pending());
assert!(poll!(&mut c2f).is_pending());
}
give_send.send(Ready(Ok(()))).await.unwrap();
let c1 = c1f.await.unwrap();
let c2 = c2f.await.unwrap();
assert_eq!(c1, c2);
let c3 = launch_one(&hsconn, 0, &keys, None).await.unwrap();
assert_eq!(c1, c3);
assert_ne!(c1, launch_one(&hsconn, 1, &keys, None).await.unwrap());
assert_ne!(
c1,
launch_one(&hsconn, 0, &mk_keys(42), None).await.unwrap()
);
let c_isol_1 = launch_one(&hsconn, 0, &keys, mk_isol("a")).await.unwrap();
assert_eq!(c1, c_isol_1);
let c_isol_2 = launch_one(&hsconn, 0, &keys, mk_isol("b")).await.unwrap();
assert_ne!(c1, c_isol_2);
});
}
}