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:
Nick Mathewson 2023-03-22 17:07:54 +00:00
commit e08b2b7ffd
11 changed files with 641 additions and 21 deletions

1
Cargo.lock generated
View File

@ -3769,6 +3769,7 @@ dependencies = [
"tor-rtcompat",
"tor-rtmock",
"tracing",
"visibility",
"weak-table",
]

View File

@ -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

View File

@ -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

View File

@ -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, &params)
.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);
}
}
}

View File

@ -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))
}
}

View File

@ -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()
}

View File

@ -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

View File

@ -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() {

View File

@ -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,
}
}
}

View File

@ -0,0 +1 @@
ADDED: ClientCirc::n_hops

View File

@ -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()
}