Merge branch 'build_hs_circuits' into 'main'
First cut at a circuit launcher for onion services Closes #691 See merge request tpo/core/arti!1065
This commit is contained in:
commit
e08b2b7ffd
|
@ -3769,6 +3769,7 @@ dependencies = [
|
|||
"tor-rtcompat",
|
||||
"tor-rtmock",
|
||||
"tracing",
|
||||
"visibility",
|
||||
"weak-table",
|
||||
]
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ testing = []
|
|||
# These APIs are not covered by semantic versioning. Using this
|
||||
# feature voids your "semver warrantee".
|
||||
experimental = ["experimental-api", "hs-client", "hs-service"]
|
||||
experimental-api = []
|
||||
experimental-api = ["visibility"]
|
||||
hs-client = ["hs-common"]
|
||||
hs-service = ["hs-common"]
|
||||
hs-common = ["tor-hscrypto"]
|
||||
|
@ -63,15 +63,25 @@ tor-persist = { path = "../tor-persist", version = "0.6.2" }
|
|||
tor-proto = { path = "../tor-proto", version = "0.9.0" }
|
||||
tor-rtcompat = { path = "../tor-rtcompat", version = "0.8.1" }
|
||||
tracing = "0.1.18"
|
||||
visibility = { version = "0.0.1", optional = true }
|
||||
weak-table = "0.3.0"
|
||||
|
||||
[dev-dependencies]
|
||||
futures-await-test = "0.3.0"
|
||||
tor-guardmgr = { path = "../tor-guardmgr", version = "0.8.2", features = ["testing"] }
|
||||
tor-guardmgr = { path = "../tor-guardmgr", version = "0.8.2", features = [
|
||||
"testing",
|
||||
] }
|
||||
tor-llcrypto = { path = "../tor-llcrypto", version = "0.4.2" }
|
||||
tor-netdir = { path = "../tor-netdir", version = "0.8.0", features = ["testing"] }
|
||||
tor-persist = { path = "../tor-persist", version = "0.6.2", features = ["testing"] }
|
||||
tor-rtcompat = { path = "../tor-rtcompat", version = "0.8.1", features = ["tokio", "native-tls"] }
|
||||
tor-netdir = { path = "../tor-netdir", version = "0.8.0", features = [
|
||||
"testing",
|
||||
] }
|
||||
tor-persist = { path = "../tor-persist", version = "0.6.2", features = [
|
||||
"testing",
|
||||
] }
|
||||
tor-rtcompat = { path = "../tor-rtcompat", version = "0.8.1", features = [
|
||||
"tokio",
|
||||
"native-tls",
|
||||
] }
|
||||
tor-rtmock = { path = "../tor-rtmock", version = "0.7.0" }
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
|
|
@ -314,6 +314,11 @@ impl<R: Runtime, C: Buildable + Sync + Send + 'static> Builder<R, C> {
|
|||
pub(crate) fn runtime(&self) -> &R {
|
||||
&self.runtime
|
||||
}
|
||||
|
||||
/// Return a reference to this Builder's timeout estimator.
|
||||
pub(crate) fn estimator(&self) -> &timeouts::Estimator {
|
||||
&self.timeouts
|
||||
}
|
||||
}
|
||||
|
||||
/// A factory object to build circuits.
|
||||
|
@ -451,6 +456,11 @@ impl<R: Runtime> CircuitBuilder<R> {
|
|||
pub(crate) fn runtime(&self) -> &R {
|
||||
self.builder.runtime()
|
||||
}
|
||||
|
||||
/// Return a reference to this builder's timeout estimator.
|
||||
pub(crate) fn estimator(&self) -> &timeouts::Estimator {
|
||||
self.builder.estimator()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function: spawn a future as a background task, and run it with
|
||||
|
|
|
@ -0,0 +1,438 @@
|
|||
//! Manage a pool of circuits for usage with onion services.
|
||||
//
|
||||
// TODO HS: We need tests here. First, though, we need a testing strategy.
|
||||
mod pool;
|
||||
|
||||
use std::{
|
||||
sync::{Arc, Weak},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{CircMgr, Error, Result};
|
||||
use futures::{task::SpawnExt, StreamExt, TryFutureExt};
|
||||
use once_cell::sync::OnceCell;
|
||||
use tor_error::{bad_api_usage, internal, ErrorReport};
|
||||
use tor_linkspec::{OwnedChanTarget, OwnedCircTarget};
|
||||
use tor_netdir::{NetDir, NetDirProvider, Relay, SubnetConfig};
|
||||
use tor_proto::circuit::ClientCirc;
|
||||
use tor_rtcompat::{
|
||||
scheduler::{TaskHandle, TaskSchedule},
|
||||
Runtime, SleepProviderExt,
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
/// The (onion-service-related) purpose for which a given circuit is going to be
|
||||
/// used.
|
||||
///
|
||||
/// We will use this to tell how the path for a given circuit is to be
|
||||
/// constructed.
|
||||
#[cfg(feature = "hs-common")]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub enum HsCircKind {
|
||||
/// Circuit from an onion service to an HsDir.
|
||||
SvcHsDir,
|
||||
/// Circuit from an onion service to an Introduction Point.
|
||||
SvcIntro,
|
||||
/// Circuit from an onion service to a Rendezvous Point.
|
||||
SvcRend,
|
||||
/// Circuit from an onion service client to an HsDir.
|
||||
ClientHsDir,
|
||||
/// Circuit from an onion service client to an Introduction Point.
|
||||
ClientIntro,
|
||||
/// Circuit from an onion service client to a Rendezvous Point.
|
||||
ClientRend,
|
||||
}
|
||||
|
||||
/// An object to provide circuits for implementing onion services.
|
||||
pub struct HsCircPool<R: Runtime> {
|
||||
/// An underlying circuit manager, used for constructing circuits.
|
||||
circmgr: Arc<CircMgr<R>>,
|
||||
/// A collection of pre-constructed circuits.
|
||||
pool: pool::Pool,
|
||||
/// A task handle for making the background circuit launcher fire early.
|
||||
//
|
||||
// TODO: I think we may want to move this into the same Mutex as Pool
|
||||
// eventually. But for now, this is fine, since it's just an implementation
|
||||
// detail.
|
||||
launcher_handle: OnceCell<TaskHandle>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> HsCircPool<R> {
|
||||
/// Create a new `HsCircPool`.
|
||||
///
|
||||
/// This will not work properly before "launch_background_tasks" is called.
|
||||
pub fn new(circmgr: &Arc<CircMgr<R>>) -> Arc<Self> {
|
||||
let circmgr = Arc::clone(circmgr);
|
||||
let pool = pool::Pool::default();
|
||||
Arc::new(Self {
|
||||
circmgr,
|
||||
pool,
|
||||
launcher_handle: OnceCell::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Launch the periodic daemon tasks required by the manager to function properly.
|
||||
///
|
||||
/// Returns a set of [`TaskHandle`]s that can be used to manage the daemon tasks.
|
||||
pub fn launch_background_tasks(
|
||||
self: &Arc<Self>,
|
||||
runtime: &R,
|
||||
netdir_provider: &Arc<dyn NetDirProvider + 'static>,
|
||||
) -> Result<Vec<TaskHandle>> {
|
||||
let handle = self.launcher_handle.get_or_try_init(|| {
|
||||
runtime
|
||||
.spawn(remove_unusable_circuits(
|
||||
Arc::downgrade(self),
|
||||
Arc::downgrade(netdir_provider),
|
||||
))
|
||||
.map_err(|e| Error::from_spawn("preemptive onion circuit expiration task", e))?;
|
||||
|
||||
let (schedule, handle) = TaskSchedule::new(runtime.clone());
|
||||
runtime
|
||||
.spawn(launch_hs_circuits_as_needed(
|
||||
Arc::downgrade(self),
|
||||
Arc::downgrade(netdir_provider),
|
||||
schedule,
|
||||
))
|
||||
.map_err(|e| Error::from_spawn("preemptive onion circuit builder task", e))?;
|
||||
|
||||
Result::<TaskHandle>::Ok(handle)
|
||||
})?;
|
||||
|
||||
Ok(vec![handle.clone()])
|
||||
}
|
||||
|
||||
/// Create a circuit suitable for use as a rendezvous circuit by a client.
|
||||
///
|
||||
/// Return the circuit, along with a [`Relay`] from `netdir` representing its final hop.
|
||||
///
|
||||
/// Only makes a single attempt; the caller needs to loop if they want to retry.
|
||||
pub async fn get_or_launch_client_rend<'a>(
|
||||
&self,
|
||||
netdir: &'a NetDir,
|
||||
) -> Result<(ClientCirc, Relay<'a>)> {
|
||||
// For rendezvous points, clients use 3-hop circuits.
|
||||
let circ = self.take_or_launch_stub_circuit(netdir, None).await?;
|
||||
let path = circ.path();
|
||||
match path.last() {
|
||||
Some(ct) => match netdir.by_ids(ct) {
|
||||
Some(relay) => Ok((circ, relay)),
|
||||
// This can't happen, since launch_hs_unmanaged() only takes relays from the netdir
|
||||
// it is given, and circuit_compatible_with_target() ensures that
|
||||
// every relay in the circuit is listed.
|
||||
//
|
||||
// TODO: Still, it's an ugly place in our API; maybe we should return the last hop
|
||||
// from take_or_launch_stub_circuit()? But in many cases it won't be needed...
|
||||
None => Err(internal!("Got circuit with unknown last hop!?").into()),
|
||||
},
|
||||
None => Err(internal!("Circuit with an empty path!?").into()),
|
||||
}
|
||||
// TODO HS: We should retry attempts to build these circuits, either here or in
|
||||
// a higher-level crate.
|
||||
}
|
||||
|
||||
/// Create a circuit suitable for use for `kind`, ending at the chosen hop `target`.
|
||||
///
|
||||
/// Only makes a single attempt; the caller needs to loop if they want to retry.
|
||||
pub async fn get_or_launch_specific(
|
||||
&self,
|
||||
netdir: &NetDir,
|
||||
kind: HsCircKind,
|
||||
target: OwnedCircTarget,
|
||||
) -> Result<ClientCirc> {
|
||||
// The kind makes no difference yet, but it will at some point in the future.
|
||||
match kind {
|
||||
HsCircKind::ClientRend => {
|
||||
return Err(
|
||||
bad_api_usage!("get_or_launch_specific with ClientRend circuit!?").into(),
|
||||
)
|
||||
}
|
||||
HsCircKind::SvcIntro => {
|
||||
// TODO HS: In this case we will want to add an extra hop, once we have vanguards.
|
||||
// When this happens, the whole match statement will want to become
|
||||
// let extra_hop = match kind {...}
|
||||
}
|
||||
HsCircKind::SvcHsDir
|
||||
| HsCircKind::SvcRend
|
||||
| HsCircKind::ClientHsDir
|
||||
| HsCircKind::ClientIntro => {}
|
||||
}
|
||||
|
||||
// For most* of these circuit types, we want to build our circuit with
|
||||
// an extra hop, since the target hop is under somebody else's control.
|
||||
//
|
||||
// * The exceptions are ClientRend, which we handle in a different
|
||||
// method, and SvcIntro, where we will eventually want an extra hop
|
||||
// to avoid vanguard discovery attacks.
|
||||
|
||||
// Get an unfinished circuit that's compatible with our target.
|
||||
let circ = self
|
||||
.take_or_launch_stub_circuit(netdir, Some(&target))
|
||||
.await?;
|
||||
|
||||
// Estimate how long it will take to extend it one more hop, and
|
||||
// construct a timeout as appropriate.
|
||||
let n_hops = circ.n_hops();
|
||||
let (extend_timeout, _) = self.circmgr.mgr.peek_builder().estimator().timeouts(
|
||||
&crate::timeouts::Action::ExtendCircuit {
|
||||
initial_length: n_hops,
|
||||
final_length: n_hops + 1,
|
||||
},
|
||||
);
|
||||
|
||||
// Make a future to extend the circuit.
|
||||
let params = crate::DirInfo::from(netdir).circ_params();
|
||||
let extend_future = circ
|
||||
.extend_ntor(&target, ¶ms)
|
||||
.map_err(|error| Error::Protocol {
|
||||
action: "extending to chosen HS hop",
|
||||
peer: None, // Either party could be to blame.
|
||||
error,
|
||||
});
|
||||
|
||||
// Wait up to the timeout for the future to complete.
|
||||
self.circmgr
|
||||
.mgr
|
||||
.peek_runtime()
|
||||
.timeout(extend_timeout, extend_future)
|
||||
.await
|
||||
.map_err(|_| Error::CircTimeout)??;
|
||||
|
||||
// With any luck, return the circuit.
|
||||
Ok(circ)
|
||||
|
||||
// TODO HS: We should retry attempts to build these circuits, either here or in
|
||||
// a higher-level crate.
|
||||
}
|
||||
|
||||
/// Take and return a circuit from our pool suitable for being extended to `avoid_target`.
|
||||
///
|
||||
/// If there is no such circuit, build and return a new one.
|
||||
async fn take_or_launch_stub_circuit(
|
||||
&self,
|
||||
netdir: &NetDir,
|
||||
avoid_target: Option<&OwnedCircTarget>,
|
||||
) -> Result<ClientCirc> {
|
||||
// First, look for a circuit that is already built, if any is suitable.
|
||||
let mut rng = rand::thread_rng();
|
||||
let subnet_config = self.circmgr.builder().path_config().subnet_config();
|
||||
let target = avoid_target.map(|target| TargetInfo {
|
||||
target,
|
||||
relay: netdir.by_ids(target),
|
||||
});
|
||||
let found_usable_circ = self.pool.take_one_where(&mut rng, |circ| {
|
||||
circuit_compatible_with_target(netdir, subnet_config, circ, target.as_ref())
|
||||
});
|
||||
|
||||
/// Tell the background task to fire immediately if we have fewer than
|
||||
/// this many circuits left, or if we found nothing. Chosen arbitrarily.
|
||||
///
|
||||
/// TODO HS: This should change dynamically, and probably be a fixed
|
||||
/// fraction of TARGET_N.
|
||||
const LAUNCH_THRESHOLD: usize = 2;
|
||||
if self.pool.len() < LAUNCH_THRESHOLD || found_usable_circ.is_none() {
|
||||
let handle = self.launcher_handle.get().ok_or_else(|| {
|
||||
Error::from(bad_api_usage!("The circuit launcher wasn't initialized"))
|
||||
})?;
|
||||
handle.fire();
|
||||
}
|
||||
|
||||
// Return the circuit we found before, if any.
|
||||
if let Some(circuit) = found_usable_circ {
|
||||
return Ok(circuit);
|
||||
}
|
||||
|
||||
// TODO: There is a possible optimization here. Instead of only waiting
|
||||
// for the circuit we launch below to finish, we could also wait for any
|
||||
// of our in-progress preemptive circuits to finish. That would,
|
||||
// however, complexify our logic quite a bit.
|
||||
|
||||
// TODO: We could in launch multiple circuits in parallel here?
|
||||
self.circmgr.launch_hs_unmanaged(avoid_target, netdir).await
|
||||
}
|
||||
|
||||
/// Internal: Remove every closed circuit from this pool.
|
||||
fn remove_closed(&self) {
|
||||
self.pool.retain(|circ| !circ.is_closing());
|
||||
}
|
||||
|
||||
/// Internal: Remove every circuit form this pool for which any relay is not
|
||||
/// listed in `netdir`.
|
||||
fn remove_unlisted(&self, netdir: &NetDir) {
|
||||
self.pool
|
||||
.retain(|circ| all_circ_relays_are_listed_in(circ, netdir));
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around a target final hop, and any information about that target we
|
||||
/// were able to find from the directory.
|
||||
///
|
||||
/// TODO: This is possibly a bit redundant with path::MaybeOwnedRelay. We
|
||||
/// should consider merging them someday, once we have a better sense of what we
|
||||
/// truly want here.
|
||||
struct TargetInfo<'a> {
|
||||
/// The target to be used as a final hop.
|
||||
target: &'a OwnedCircTarget,
|
||||
/// A Relay reference for the targe, if we found one.
|
||||
relay: Option<Relay<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> TargetInfo<'a> {
|
||||
/// Return true if, according to the rules of `subnet_config`, this target can share a circuit with `r`.
|
||||
fn may_share_circuit_with(&self, r: &Relay<'_>, subnet_config: SubnetConfig) -> bool {
|
||||
if let Some(this_r) = &self.relay {
|
||||
if this_r.in_same_family(r) {
|
||||
return false;
|
||||
}
|
||||
// TODO: When bridge families are finally implemented (likely via
|
||||
// proposal `321-happy-families.md`), we should move family
|
||||
// functionality into CircTarget.
|
||||
}
|
||||
|
||||
!subnet_config.any_addrs_in_same_subnet(self.target, r)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if we can extend a pre-built circuit `circ` to `target`.
|
||||
///
|
||||
/// We require that the circuit is open, that every hop in the circuit is
|
||||
/// listed in `netdir`, and that no hop in the circuit shares a family with
|
||||
/// `target`.
|
||||
fn circuit_compatible_with_target(
|
||||
netdir: &NetDir,
|
||||
subnet_config: SubnetConfig,
|
||||
circ: &ClientCirc,
|
||||
target: Option<&TargetInfo<'_>>,
|
||||
) -> bool {
|
||||
if circ.is_closing() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO HS: I don't like having to copy the whole path out at this point; it
|
||||
// seems like that could get expensive. -nickm
|
||||
|
||||
let path = circ.path();
|
||||
path.iter().all(|c: &OwnedChanTarget| {
|
||||
match (target, netdir.by_ids(c)) {
|
||||
// We require that every relay in this circuit is still listed; an
|
||||
// unlisted relay means "reject".
|
||||
(_, None) => false,
|
||||
// If we have a target, the relay must be compatible with it.
|
||||
(Some(t), Some(r)) => t.may_share_circuit_with(&r, subnet_config),
|
||||
// If we have no target, any listed relay is okay.
|
||||
(None, Some(_)) => true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Return true if every relay in `circ` is listed in `netdir`.
|
||||
fn all_circ_relays_are_listed_in(circ: &ClientCirc, netdir: &NetDir) -> bool {
|
||||
// TODO HS: Again, I don't like having to copy the whole path out at this point.
|
||||
let path = circ.path();
|
||||
|
||||
// TODO HS: Are there any other checks we should do before declaring that
|
||||
// this is still usable?
|
||||
|
||||
// TODO HS: There is some duplicate logic here and in
|
||||
// circuit_compatible_with_target. I think that's acceptable for now, but
|
||||
// we should consider refactoring if these functions grow.
|
||||
path.iter()
|
||||
.all(|c: &OwnedChanTarget| netdir.by_ids(c).is_some())
|
||||
}
|
||||
|
||||
/// Background task to launch onion circuits as needed.
|
||||
async fn launch_hs_circuits_as_needed<R: Runtime>(
|
||||
pool: Weak<HsCircPool<R>>,
|
||||
netdir_provider: Weak<dyn NetDirProvider + 'static>,
|
||||
mut schedule: TaskSchedule<R>,
|
||||
) {
|
||||
/// Number of circuits to keep in the pool. Chosen arbitrarily.
|
||||
//
|
||||
// TODO HS: This should instead change dynamically based on observed needs.
|
||||
const TARGET_N: usize = 8;
|
||||
/// Default delay when not told to fire explicitly. Chosen arbitrarily.
|
||||
const DELAY: Duration = Duration::from_secs(30);
|
||||
|
||||
while schedule.next().await.is_some() {
|
||||
let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
|
||||
(Some(x), Some(y)) => (x, y),
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
};
|
||||
pool.remove_closed();
|
||||
let mut n_to_launch = pool.pool.len().saturating_sub(TARGET_N);
|
||||
let mut max_attempts = TARGET_N * 2;
|
||||
'inner: while n_to_launch > 1 {
|
||||
max_attempts -= 1;
|
||||
if max_attempts == 0 {
|
||||
// We want to avoid retrying over and over in a tight loop if all our attempts
|
||||
// are failing.
|
||||
warn!("Too many preemptive onion service circuits failed; waiting a while.");
|
||||
break 'inner;
|
||||
}
|
||||
if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
|
||||
// We want to launch a circuit, and we have a netdir that we can use
|
||||
// to launch it.
|
||||
//
|
||||
// TODO: Possibly we should be doing this in a background task, and
|
||||
// launching several of these in parallel. If we do, we should think about
|
||||
// whether taking the fastest will expose us to any attacks.
|
||||
let no_target: Option<&OwnedCircTarget> = None;
|
||||
// TODO HS: We should catch panics, here or in launch_hs_unmanaged.
|
||||
match pool.circmgr.launch_hs_unmanaged(no_target, &netdir).await {
|
||||
Ok(circ) => {
|
||||
pool.pool.insert(circ);
|
||||
n_to_launch -= 1;
|
||||
}
|
||||
Err(err) => {
|
||||
debug!(
|
||||
"Unable to build preemptive circuit for onion services: {}",
|
||||
err.report()
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We'd like to launch a circuit, but we don't have a netdir that we
|
||||
// can use.
|
||||
//
|
||||
// TODO HS possibly instead of a fixed delay we want to wait for more
|
||||
// netdir info?
|
||||
break 'inner;
|
||||
}
|
||||
}
|
||||
|
||||
// We have nothing to launch now, so we'll try after a while.
|
||||
schedule.fire_in(DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
/// Background task to remove unusable circuits whenever the directory changes.
|
||||
async fn remove_unusable_circuits<R: Runtime>(
|
||||
pool: Weak<HsCircPool<R>>,
|
||||
netdir_provider: Weak<dyn NetDirProvider + 'static>,
|
||||
) {
|
||||
let mut event_stream = match netdir_provider.upgrade() {
|
||||
Some(nd) => nd.events(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Note: We only look at the event stream here, not any kind of TaskSchedule.
|
||||
// That's fine, since this task only wants to fire when the directory changes,
|
||||
// and the directory will not change while we're dormant.
|
||||
//
|
||||
// Removing closed circuits is also handled above in launch_hs_circuits_as_needed.
|
||||
while event_stream.next().await.is_some() {
|
||||
let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
|
||||
(Some(x), Some(y)) => (x, y),
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
};
|
||||
pool.remove_closed();
|
||||
if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
|
||||
pool.remove_unlisted(&netdir);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
//! An internal pool object that we use to implement HsCircPool.
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use rand::{seq::IteratorRandom, Rng};
|
||||
use tor_proto::circuit::ClientCirc;
|
||||
|
||||
/// A collection of circuits used to fulfil onion-service-related requests.
|
||||
#[derive(Default)]
|
||||
pub(super) struct Pool {
|
||||
/// The collection of circuits themselves, in no particular order.
|
||||
circuits: Mutex<Vec<ClientCirc>>,
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
/// Return the number of circuits in this pool.
|
||||
pub(super) fn len(&self) -> usize {
|
||||
self.circuits.lock().expect("lock poisoned").len()
|
||||
}
|
||||
|
||||
/// Add `circ` to this pool
|
||||
pub(super) fn insert(&self, circ: ClientCirc) {
|
||||
self.circuits.lock().expect("lock poisoned").push(circ);
|
||||
}
|
||||
|
||||
/// Remove every circuit from this pool for which `f` returns false.
|
||||
pub(super) fn retain<F>(&self, f: F)
|
||||
where
|
||||
F: FnMut(&ClientCirc) -> bool,
|
||||
{
|
||||
self.circuits.lock().expect("lock poisoned").retain(f);
|
||||
}
|
||||
|
||||
/// If there is any circuit in this pool for which `f` returns true, return one such circuit at random, and remove it from the pool.
|
||||
pub(super) fn take_one_where<R, F>(&self, rng: &mut R, f: F) -> Option<ClientCirc>
|
||||
where
|
||||
R: Rng,
|
||||
F: Fn(&ClientCirc) -> bool,
|
||||
{
|
||||
let mut circuits = self.circuits.lock().expect("lock poisoned");
|
||||
// TODO HS: This ensures that we take a circuit at random, but at the
|
||||
// expense of searching every circuit. That could certainly be costly
|
||||
// if `circuits` is large! Perhaps we should instead stop at the first
|
||||
// matching circuit we find.
|
||||
let (idx, _) = circuits
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| f(c))
|
||||
.choose(rng)?;
|
||||
Some(circuits.remove(idx))
|
||||
}
|
||||
}
|
|
@ -61,6 +61,8 @@ pub use config::test_config::TestConfig;
|
|||
pub mod build;
|
||||
mod config;
|
||||
mod err;
|
||||
#[cfg(feature = "hs-common")]
|
||||
pub mod hspool;
|
||||
mod impls;
|
||||
pub mod isolation;
|
||||
mod mgr;
|
||||
|
@ -432,26 +434,33 @@ impl<R: Runtime> CircMgr<R> {
|
|||
.map(|(c, _)| c)
|
||||
}
|
||||
|
||||
/// Create and return a new (typically anonymous) circuit whose last hop is
|
||||
/// `target`.
|
||||
/// Create and return a new (typically anonymous) circuit for use as an
|
||||
/// onion service circuit of type `kind`.
|
||||
///
|
||||
/// This circuit is guaranteed not to have been used for any traffic
|
||||
/// previously, and it will not be given out for any other requests in the
|
||||
/// future unless explicitly re-registered with a circuit manager.
|
||||
///
|
||||
/// If `planned_target` is provided, then the circuit will be built so that
|
||||
/// it does not share any family members with the the provided target. (The
|
||||
/// circuit _will not be_ extended to that target itself!)
|
||||
///
|
||||
/// Used to implement onion service clients and services.
|
||||
#[cfg(feature = "hs-common")]
|
||||
#[allow(unused_variables, clippy::missing_panics_doc)]
|
||||
pub async fn launch_specific_isolated(
|
||||
#[allow(dead_code)] // TODO HS
|
||||
pub(crate) async fn launch_hs_unmanaged<T>(
|
||||
&self,
|
||||
target: tor_linkspec::OwnedCircTarget,
|
||||
// TODO hs: this should at least be an enum to define what kind of
|
||||
// circuit we want, in case we have different rules for different types.
|
||||
// It might also need to include a "anonymous?" flag for supporting
|
||||
// single onion services.
|
||||
preferences: (),
|
||||
) -> Result<ClientCirc> {
|
||||
todo!() // TODO hs implement.
|
||||
planned_target: Option<T>,
|
||||
dir: &NetDir,
|
||||
) -> Result<ClientCirc>
|
||||
where
|
||||
T: IntoOwnedChanTarget,
|
||||
{
|
||||
let usage = TargetCircUsage::HsCircBase {
|
||||
compatible_with_target: planned_target.map(IntoOwnedChanTarget::to_owned),
|
||||
};
|
||||
let (_, client_circ) = self.mgr.launch_unmanaged(&usage, dir.into()).await?;
|
||||
Ok(client_circ)
|
||||
}
|
||||
|
||||
/// Launch circuits preemptively, using the preemptive circuit predictor's
|
||||
|
@ -517,8 +526,9 @@ impl<R: Runtime> CircMgr<R> {
|
|||
/// Return a reference to the associated CircuitBuilder that this CircMgr
|
||||
/// will use to create its circuits.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
|
||||
#[cfg(feature = "experimental-api")]
|
||||
pub fn builder(&self) -> &build::CircuitBuilder<R> {
|
||||
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn builder(&self) -> &build::CircuitBuilder<R> {
|
||||
self.mgr.peek_builder()
|
||||
}
|
||||
|
||||
|
|
|
@ -1367,6 +1367,24 @@ impl<B: AbstractCircBuilder + 'static, R: Runtime> AbstractCircMgr<B, R> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Plan and launch a new circuit to a given target, bypassing our managed
|
||||
/// pool of circuits.
|
||||
///
|
||||
/// This method will always return a new circuit, and never return a circuit
|
||||
/// that this CircMgr gives out for anything else.
|
||||
///
|
||||
/// The new circuit will participate in the guard and timeout apparatus as
|
||||
/// appropriate, no retry attempt will be made if the circuit fails.
|
||||
#[cfg(feature = "hs-common")]
|
||||
pub(crate) async fn launch_unmanaged(
|
||||
&self,
|
||||
usage: &<B::Spec as AbstractSpec>::Usage,
|
||||
dir: DirInfo<'_>,
|
||||
) -> Result<(<B as AbstractCircBuilder>::Spec, B::Circ)> {
|
||||
let (_, plan) = self.plan_by_usage(dir, usage)?;
|
||||
self.builder.build_circuit(plan.plan).await
|
||||
}
|
||||
|
||||
/// Remove the circuit with a given `id` from this manager.
|
||||
///
|
||||
/// After this function is called, that circuit will no longer be handed
|
||||
|
|
|
@ -7,7 +7,7 @@ use std::time::SystemTime;
|
|||
use tor_basic_utils::iter::FilterCount;
|
||||
use tor_error::{bad_api_usage, internal};
|
||||
use tor_guardmgr::{GuardMgr, GuardMonitor, GuardUsable};
|
||||
use tor_linkspec::RelayIdSet;
|
||||
use tor_linkspec::{HasRelayIds, OwnedChanTarget, RelayIdSet};
|
||||
use tor_netdir::{NetDir, Relay, SubnetConfig, WeightRole};
|
||||
use tor_rtcompat::Runtime;
|
||||
|
||||
|
@ -23,15 +23,23 @@ enum ExitPathBuilderInner<'a> {
|
|||
strict: bool,
|
||||
},
|
||||
|
||||
/// Request a path to any relay, even those that cannot exit.
|
||||
AnyRelay,
|
||||
|
||||
/// Request a path that uses a given relay as exit node.
|
||||
ChosenExit(Relay<'a>),
|
||||
}
|
||||
|
||||
/// A PathBuilder that builds a path to an exit relay supporting a given
|
||||
/// set of ports.
|
||||
///
|
||||
/// NOTE: The name of this type is no longer completely apt: given some circuits,
|
||||
/// it is happy to build a circuit ending at a non-exit.
|
||||
pub struct ExitPathBuilder<'a> {
|
||||
/// The inner ExitPathBuilder state.
|
||||
inner: ExitPathBuilderInner<'a>,
|
||||
/// If present, a "target" that every chosen relay must be able to share a circuit with with.
|
||||
compatible_with: Option<OwnedChanTarget>,
|
||||
}
|
||||
|
||||
impl<'a> ExitPathBuilder<'a> {
|
||||
|
@ -46,6 +54,7 @@ impl<'a> ExitPathBuilder<'a> {
|
|||
}
|
||||
Self {
|
||||
inner: ExitPathBuilderInner::WantsPorts(ports),
|
||||
compatible_with: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,6 +63,7 @@ impl<'a> ExitPathBuilder<'a> {
|
|||
pub fn from_chosen_exit(exit_relay: Relay<'a>) -> Self {
|
||||
Self {
|
||||
inner: ExitPathBuilderInner::ChosenExit(exit_relay),
|
||||
compatible_with: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,6 +71,20 @@ impl<'a> ExitPathBuilder<'a> {
|
|||
pub fn for_any_exit() -> Self {
|
||||
Self {
|
||||
inner: ExitPathBuilderInner::AnyExit { strict: true },
|
||||
compatible_with: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new builder that will try to build a three-hop non-exit
|
||||
/// path to a given
|
||||
///
|
||||
/// TODO: This doesn't seem to belong in a type called ExitPathBuilder.
|
||||
/// Perhaps we should rename ExitPathBuilder, split it into multiple types,
|
||||
/// or move this method.
|
||||
pub(crate) fn for_any_compatible_with(compatible_with: Option<OwnedChanTarget>) -> Self {
|
||||
Self {
|
||||
inner: ExitPathBuilderInner::AnyRelay,
|
||||
compatible_with,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,6 +93,7 @@ impl<'a> ExitPathBuilder<'a> {
|
|||
pub(crate) fn for_timeout_testing() -> Self {
|
||||
Self {
|
||||
inner: ExitPathBuilderInner::AnyExit { strict: false },
|
||||
compatible_with: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,6 +136,15 @@ impl<'a> ExitPathBuilder<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
ExitPathBuilderInner::AnyRelay => netdir
|
||||
.pick_relay(rng, WeightRole::Middle, |r| {
|
||||
can_share.count(relays_can_share_circuit_opt(r, guard, config))
|
||||
})
|
||||
.ok_or(Error::NoExit {
|
||||
can_share,
|
||||
correct_ports,
|
||||
}),
|
||||
|
||||
ExitPathBuilderInner::WantsPorts(wantports) => Ok(netdir
|
||||
.pick_relay(rng, WeightRole::Exit, |r| {
|
||||
can_share.count(relays_can_share_circuit_opt(r, guard, config))
|
||||
|
@ -179,6 +213,15 @@ impl<'a> ExitPathBuilder<'a> {
|
|||
b.restrictions()
|
||||
.push(tor_guardmgr::GuardRestriction::AvoidAllIds(family));
|
||||
}
|
||||
if let Some(avoid_target) = &self.compatible_with {
|
||||
let mut family = RelayIdSet::new();
|
||||
family.extend(avoid_target.identities().map(|id| id.to_owned()));
|
||||
if let Some(avoid_relay) = netdir.by_ids(avoid_target) {
|
||||
family.extend(netdir.known_family_members(&avoid_relay).map(|r| *r.id()));
|
||||
}
|
||||
b.restrictions()
|
||||
.push(tor_guardmgr::GuardRestriction::AvoidAllIds(family));
|
||||
}
|
||||
let guard_usage = b.build().expect("Failed while building guard usage!");
|
||||
let (guard, mut mon, usable) = guardmgr.select_guard(guard_usage)?;
|
||||
let guard = if let Some(ct) = guard.as_circ_target() {
|
||||
|
|
|
@ -164,6 +164,19 @@ pub(crate) enum TargetCircUsage {
|
|||
/// and therefore to a specific relay (which need not be in any netdir).
|
||||
#[cfg(feature = "specific-relay")]
|
||||
DirSpecificTarget(OwnedChanTarget),
|
||||
|
||||
/// Used to build a circuit (currently always 3 hops) to serve as the basis of some
|
||||
/// onion-serivice-related operation.
|
||||
#[cfg(feature = "hs-common")]
|
||||
HsCircBase {
|
||||
/// A target to avoid when constructing this circuit.
|
||||
///
|
||||
/// This target is not appended to the end of the circuit; rather, the
|
||||
/// circuit is built so that its relays are all allowed to share a
|
||||
/// circuit with this target (without, for example, violating any
|
||||
/// family restrictions).
|
||||
compatible_with_target: Option<OwnedChanTarget>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The purposes for which a circuit is usable.
|
||||
|
@ -184,6 +197,10 @@ pub(crate) enum SupportedCircUsage {
|
|||
},
|
||||
/// This circuit is not suitable for any usage.
|
||||
NoUsage,
|
||||
/// This circuit is for some hs-related usage.
|
||||
/// (It should never be given to the circuit manager; the
|
||||
/// `HsPool` code will handle it instead.)
|
||||
HsOnly,
|
||||
/// Use only for BEGINDIR-based non-anonymous directory connections
|
||||
/// to a particular target (which may not be in the netdir).
|
||||
#[cfg(feature = "specific-relay")]
|
||||
|
@ -267,6 +284,16 @@ impl TargetCircUsage {
|
|||
let usage = SupportedCircUsage::DirSpecificTarget(target.clone());
|
||||
Ok((path, usage, None, None))
|
||||
}
|
||||
#[cfg(feature = "hs-common")]
|
||||
TargetCircUsage::HsCircBase {
|
||||
compatible_with_target,
|
||||
} => {
|
||||
let (path, mon, usable) =
|
||||
ExitPathBuilder::for_any_compatible_with(compatible_with_target.clone())
|
||||
.pick_path(rng, netdir, guards, config, now)?;
|
||||
let usage = SupportedCircUsage::HsOnly;
|
||||
Ok((path, usage, mon, usable))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -402,6 +429,7 @@ impl crate::mgr::AbstractSpec for SupportedCircUsage {
|
|||
SCU::DirSpecificTarget(_) => CU::Dir,
|
||||
SCU::Exit { .. } => CU::UserTraffic,
|
||||
SCU::NoUsage => CU::UselessCircuit,
|
||||
SCU::HsOnly => CU::UserTraffic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ADDED: ClientCirc::n_hops
|
|
@ -226,6 +226,10 @@ impl ClientCirc {
|
|||
}
|
||||
|
||||
/// Return a description of all the hops in this circuit.
|
||||
///
|
||||
/// Note that this method performs a deep copy over the `OwnedCircTarget`
|
||||
/// values in the path. This is undesirable for some applications; see
|
||||
/// [ticket #787](https://gitlab.torproject.org/tpo/core/arti/-/issues/787).
|
||||
pub fn path(&self) -> Vec<OwnedChanTarget> {
|
||||
self.path.all_hops()
|
||||
}
|
||||
|
@ -594,7 +598,12 @@ impl ClientCirc {
|
|||
self.unique_id
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Return the number of hops in this circuit.
|
||||
///
|
||||
/// NOTE: This function will currently return only the number of hops
|
||||
/// _currently_ in the circuit. If there is an extend operation in progress,
|
||||
/// the currently pending hop may or may not be counted, depending on whether
|
||||
/// the extend operation finishes before this call is done.
|
||||
pub fn n_hops(&self) -> usize {
|
||||
self.path.n_hops()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue