Skip to main content

tor_hsclient/
connect.rs

1//! Main implementation of the connection functionality
2
3use std::collections::HashMap;
4use std::fmt::Debug;
5use std::marker::PhantomData;
6use std::sync::Arc;
7
8use async_trait::async_trait;
9use educe::Educe;
10use futures::{AsyncRead, AsyncWrite};
11use itertools::Itertools;
12use rand::RngExt;
13use tor_bytes::Writeable;
14use tor_cell::relaycell::hs::IntroduceAckStatus;
15use tor_cell::relaycell::hs::intro_payload::{self, IntroduceHandshakePayload};
16use tor_cell::relaycell::hs::pow::ProofOfWork;
17use tor_cell::relaycell::msg::{AnyRelayMsg, Introduce1, Rendezvous2};
18use tor_circmgr::build::onion_circparams_from_netparams;
19use tor_circmgr::{
20    ClientOnionServiceDataTunnel, ClientOnionServiceDirTunnel, ClientOnionServiceIntroTunnel,
21};
22use tor_dirclient::SourceInfo;
23use tor_error::{Bug, debug_report, warn_report};
24use tor_hscrypto::Subcredential;
25use tor_hscrypto::time::TimePeriod;
26use tor_proto::TargetHop;
27use tor_proto::client::circuit::handshake::hs_ntor::{self, HsNtorHkdfKeyGenerator};
28use tracing::{debug, instrument, trace, warn};
29use web_time_compat::{Duration, Instant, SystemTime};
30
31use retry_error::RetryError;
32use safelog::{DispRedacted, Sensitive};
33use tor_cell::relaycell::RelayMsg;
34use tor_cell::relaycell::hs::{
35    AuthKeyType, EstablishRendezvous, IntroduceAck, RendezvousEstablished,
36};
37use tor_checkable::{Timebound, timed::TimerangeBound};
38use tor_circmgr::hspool::HsCircPool;
39use tor_circmgr::timeouts::Action as TimeoutsAction;
40use tor_dirclient::request::Requestable as _;
41use tor_error::{HasRetryTime as _, RetryTime};
42use tor_error::{internal, into_internal};
43use tor_hscrypto::RendCookie;
44use tor_hscrypto::pk::{HsBlindId, HsId, HsIdKey};
45use tor_linkspec::{CircTarget, HasRelayIds, OwnedCircTarget, RelayId};
46use tor_llcrypto::pk::ed25519::Ed25519Identity;
47use tor_netdir::{NetDir, Relay};
48use tor_netdoc::doc::hsdesc::{HsDesc, IntroPointDesc};
49use tor_proto::client::circuit::{CircParameters, handshake};
50use tor_proto::{MetaCellDisposition, MsgHandler};
51use tor_rtcompat::{Runtime, SleepProviderExt as _, TimeoutError};
52
53use crate::Config;
54use crate::err::RendPtIdentityForError;
55use crate::pow::HsPowClient;
56use crate::proto_oneshot;
57use crate::relay_info::ipt_to_circtarget;
58use crate::state::MockableConnectorData;
59use crate::{ConnError, DescriptorError, DescriptorErrorDetail};
60use crate::{FailedAttemptError, IntroPtIndex, rend_pt_identity_for_error};
61use crate::{HsClientConnector, HsClientSecretKeys};
62
63use ConnError as CE;
64use FailedAttemptError as FAE;
65
66/// Given `R, M` where `M: MocksForConnect<M>`, expand to the mockable `ClientCirc`
67// This is quite annoying.  But the alternative is to write out `<... as // ...>`
68// each time, since otherwise the compile complains about ambiguous associated types.
69macro_rules! DataTunnel{ { $R:ty, $M:ty } => {
70    <<$M as MocksForConnect<$R>>::HsCircPool as MockableCircPool<$R>>::DataTunnel
71} }
72
73/// Information about a hidden service, including our connection history
74#[derive(Default, Educe)]
75#[educe(Debug)]
76// This type is actually crate-private, since it isn't re-exported, but it must
77// be `pub` because it appears as a default for a type parameter in HsClientConnector.
78pub struct Data {
79    /// The latest known onion service descriptor for this service.
80    desc: DataHsDesc,
81    /// Information about the latest status of trying to connect to this service
82    /// through each of its introduction points.
83    ipts: DataIpts,
84    /// Information about the requery period of each HsDir we have recently queried.
85    ///
86    /// Each entry represents an HsDir that we cannot requery until
87    /// its specified timestamp elapses.
88    ///
89    /// Any HsDir that does not have an entry in this map can be requeried.
90    hsdirs: DataHsDirs,
91}
92
93/// An onion service descriptor and its associated HsBlindId.
94#[derive(Debug)]
95struct HsDescForTp {
96    /// The TP this descriptor is for.
97    ///
98    /// Used for determining whether a newly fetched descriptor
99    /// is for the same time period as this one.
100    time_period: TimePeriod,
101    /// The descriptor
102    desc: TimerangeBound<HsDesc>,
103}
104
105/// Part of `Data` that relates to our information about the HsDir requery periods
106type DataHsDirs = HashMap<RelayIdForRequeryPeriod, SystemTime>;
107
108/// Marker type, to make typed HsDir [`RelayIdFor`] keys
109#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Debug)]
110struct RequeryPeriodMap;
111
112/// Lookup key for looking up and recording our IPT use experiences
113type RelayIdForRequeryPeriod = RelayIdFor<RequeryPeriodMap>;
114
115/// Part of `Data` that relates to the HS descriptor
116type DataHsDesc = Option<HsDescForTp>;
117
118/// Part of `Data` that relates to our information about introduction points
119type DataIpts = HashMap<RelayIdForExperience, IptExperience>;
120
121/// How things went last time we tried to use this introduction point
122///
123/// Neither this data structure, nor [`Data`], is responsible for arranging that we expire this
124/// information eventually.  If we keep reconnecting to the service, we'll retain information
125/// about each IPT indefinitely, at least so long as they remain listed in the descriptors we
126/// receive.
127///
128/// Expiry of unused data is handled by `state.rs`, according to `last_used` in `ServiceState`.
129///
130/// Choosing which IPT to prefer is done by obtaining an `IptSortKey`
131/// (from this and other information).
132//
133// Don't impl Ord for IptExperience.  We obtain `Option<&IptExperience>` from our
134// data structure, and if IptExperience were Ord then Option<&IptExperience> would be Ord
135// but it would be the wrong sort order: it would always prefer None, ie untried IPTs.
136#[derive(Debug)]
137struct IptExperience {
138    /// How long it took us to get whatever outcome occurred
139    ///
140    /// We prefer fast successes to slow ones.
141    /// Then, we prefer failures with earlier `RetryTime`,
142    /// and, lastly, faster failures to slower ones.
143    duration: Duration,
144
145    /// What happened and when we might try again
146    ///
147    /// Note that we don't actually *enforce* the `RetryTime` here, just sort by it
148    /// using `RetryTime::loose_cmp`.
149    ///
150    /// We *do* return an error that is itself `HasRetryTime` and expect our callers
151    /// to honour that.
152    outcome: Result<(), RetryTime>,
153}
154
155/// Actually make a HS connection, updating our recorded state as necessary
156///
157/// `connector` is provided only for obtaining the runtime and netdir (and `mock_for_state`).
158/// Obviously, `connect` is not supposed to go looking in `services`.
159///
160/// This function handles all necessary retrying of fallible operations,
161/// (and, therefore, must also limit the total work done for a particular call).
162///
163/// This function has a minimum of functionality, since it is the boundary
164/// between "mock connection, used for testing `state.rs`" and
165/// "mock circuit and netdir, used for testing `connect.rs`",
166/// so it is not, itself, unit-testable.
167#[instrument(level = "trace", skip_all)]
168pub(crate) async fn connect<R: Runtime>(
169    connector: &HsClientConnector<R>,
170    netdir: Arc<NetDir>,
171    config: Arc<Config>,
172    hsid: HsId,
173    data: &mut Data,
174    secret_keys: HsClientSecretKeys,
175) -> Result<ClientOnionServiceDataTunnel, ConnError> {
176    Context::new(
177        &connector.runtime,
178        &*connector.circpool,
179        netdir,
180        config,
181        hsid,
182        secret_keys,
183        (),
184    )?
185    .connect(data)
186    .await
187}
188
189/// Common context for a single request to connect to a hidden service
190///
191/// This saves on passing this same set of (immutable) values (or subsets thereof)
192/// to each method in the principal functional code, everywhere.
193/// It also provides a convenient type to be `Self`.
194///
195/// Its lifetime is one request to make a new client circuit to a hidden service,
196/// including all the retries and timeouts.
197struct Context<'c, R: Runtime, M: MocksForConnect<R>> {
198    /// Runtime
199    runtime: &'c R,
200    /// Circpool
201    circpool: &'c M::HsCircPool,
202    /// Netdir
203    //
204    // TODO holding onto the netdir for the duration of our attempts is not ideal
205    // but doing better is fairly complicated.  See discussions here:
206    //   https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1228#note_2910545
207    //   https://gitlab.torproject.org/tpo/core/arti/-/issues/884
208    netdir: Arc<NetDir>,
209    /// Configuration
210    config: Arc<Config>,
211    /// Secret keys to use
212    secret_keys: HsClientSecretKeys,
213    /// HS ID
214    hsid: DispRedacted<HsId>,
215    /// Blinded HS ID
216    hs_blind_id: HsBlindId,
217    /// The subcredential to use during this time period
218    subcredential: Subcredential,
219    /// Mock data
220    mocks: M,
221}
222
223/// Details of an established rendezvous point
224///
225/// Intermediate value for progress during a connection attempt.
226struct Rendezvous<'r, R: Runtime, M: MocksForConnect<R>> {
227    /// RPT as a `Relay`
228    rend_relay: Relay<'r>,
229    /// Rendezvous circuit
230    rend_tunnel: DataTunnel!(R, M),
231    /// Rendezvous cookie
232    rend_cookie: RendCookie,
233
234    /// Receiver that will give us the RENDEZVOUS2 message.
235    ///
236    /// The sending ended is owned by the handler
237    /// which receives control messages on the rendezvous circuit,
238    /// and which was installed when we sent `ESTABLISH_RENDEZVOUS`.
239    ///
240    /// (`RENDEZVOUS2` is the message containing the onion service's side of the handshake.)
241    rend2_rx: proto_oneshot::Receiver<Rendezvous2>,
242
243    /// Dummy, to placate compiler
244    ///
245    /// Covariant without dropck or interfering with Send/Sync will do fine.
246    marker: PhantomData<fn() -> (R, M)>,
247}
248
249/// Random value used as part of IPT selection
250type IptSortRand = u32;
251
252/// Details of an apparently-useable introduction point
253///
254/// Intermediate value for progress during a connection attempt.
255struct UsableIntroPt<'i> {
256    /// Index in HS descriptor
257    intro_index: IntroPtIndex,
258    /// IPT descriptor
259    intro_desc: &'i IntroPointDesc,
260    /// IPT `CircTarget`
261    intro_target: OwnedCircTarget,
262    /// Random value used as part of IPT selection
263    sort_rand: IptSortRand,
264}
265
266/// Lookup key for looking up and recording information about a relay
267///
268/// Used to identify a relay when looking to see what happened last time we used it,
269/// and storing that information after we tried it.
270///
271/// We store the experience information under an arbitrary one of the relay's identities,
272/// as returned by the `HasRelayIds::identities().next()`.
273/// When we do lookups, we check all the relay's identities to see if we find
274/// anything relevant.
275/// If relay identities permute in strange ways, whether we find our previous
276/// knowledge about them is not particularly well defined, but that's fine.
277///
278/// While this is, structurally, a relay identity, it is not suitable for other purposes.
279#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Debug)]
280struct RelayIdFor<K> {
281    /// The relay id
282    inner: RelayId,
283
284    /// Phantom data to allow parameterizing over `K`
285    ///
286    /// `K` is a marker type that represents the kind of map
287    /// this key will be used in.
288    marker: PhantomData<K>,
289}
290
291/// Marker type, to make typed Ipt exprience [`RelayIdFor`] keys
292#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Debug)]
293struct IptExperienceMap;
294
295/// Lookup key for looking up and recording our IPT use experiences
296type RelayIdForExperience = RelayIdFor<IptExperienceMap>;
297
298/// Details of an apparently-successful INTRODUCE exchange
299///
300/// Intermediate value for progress during a connection attempt.
301struct Introduced<R: Runtime, M: MocksForConnect<R>> {
302    /// End-to-end crypto NTORv3 handshake with the service
303    ///
304    /// Created as part of generating our `INTRODUCE1`,
305    /// and then used when processing `RENDEZVOUS2`.
306    handshake_state: hs_ntor::HsNtorClientState,
307
308    /// Dummy, to placate compiler
309    ///
310    /// `R` and `M` only used for getting to mocks.
311    /// Covariant without dropck or interfering with Send/Sync will do fine.
312    marker: PhantomData<fn() -> (R, M)>,
313}
314
315impl<K> RelayIdFor<K> {
316    /// Create a new key for use with `T`
317    fn new(inner: RelayId) -> Self {
318        Self {
319            inner,
320            marker: Default::default(),
321        }
322    }
323
324    /// Identities to use to try to find previous experience information about this IPT
325    fn for_lookup<T: HasRelayIds>(ids: &T) -> impl Iterator<Item = Self> + '_ {
326        ids.identities().map(|id| RelayIdFor::new(id.to_owned()))
327    }
328
329    /// Identity to use to store previous experience information about this IPT
330    fn for_store<T: HasRelayIds>(ids: &T) -> Result<Self, Bug> {
331        let id = ids
332            .identities()
333            .next()
334            .ok_or_else(|| internal!("introduction point relay with no identities"))?
335            .to_owned();
336        Ok(RelayIdFor::new(id))
337    }
338}
339
340/// Sort key for an introduction point, for selecting the best IPTs to try first
341///
342/// Ordering is most preferable first.
343///
344/// We use this to sort our `UsableIpt`s using `.sort_by_key`.
345/// (This implementation approach ensures that we obey all the usual ordering invariants.)
346#[derive(Ord, PartialOrd, Eq, PartialEq, Debug)]
347struct IptSortKey {
348    /// Sort by how preferable the experience was
349    outcome: IptSortKeyOutcome,
350    /// Failing that, choose randomly
351    sort_rand: IptSortRand,
352}
353
354/// Component of the [`IptSortKey`] representing outcome of our last attempt, if any
355///
356/// This is the main thing we use to decide which IPTs to try first.
357/// It is calculated for each IPT
358/// (via `.sort_by_key`, so repeatedly - it should therefore be cheap to make.)
359///
360/// Ordering is most preferable first.
361#[derive(Ord, PartialOrd, Eq, PartialEq, Debug)]
362enum IptSortKeyOutcome {
363    /// Prefer successes
364    Success {
365        /// Prefer quick ones
366        duration: Duration,
367    },
368    /// Failing that, try one we don't know to have failed
369    Untried,
370    /// Failing that, it'll have to be ones that didn't work last time
371    Failed {
372        /// Prefer failures with an earlier retry time
373        retry_time: tor_error::LooseCmpRetryTime,
374        /// Failing that, prefer quick failures (rather than slow ones eg timeouts)
375        duration: Duration,
376    },
377}
378
379impl From<Option<&IptExperience>> for IptSortKeyOutcome {
380    fn from(experience: Option<&IptExperience>) -> IptSortKeyOutcome {
381        use IptSortKeyOutcome as O;
382        match experience {
383            None => O::Untried,
384            Some(IptExperience { duration, outcome }) => match outcome {
385                Ok(()) => O::Success {
386                    duration: *duration,
387                },
388                Err(retry_time) => O::Failed {
389                    retry_time: (*retry_time).into(),
390                    duration: *duration,
391                },
392            },
393        }
394    }
395}
396
397/// Token indicating that a descriptor fetch is wanted
398#[derive(Clone, Copy, Eq, PartialEq, Debug)]
399struct RefetchDescriptor;
400
401impl<'c, R: Runtime, M: MocksForConnect<R>> Context<'c, R, M> {
402    /// Make a new `Context` from the input data
403    fn new(
404        runtime: &'c R,
405        circpool: &'c M::HsCircPool,
406        netdir: Arc<NetDir>,
407        config: Arc<Config>,
408        hsid: HsId,
409        secret_keys: HsClientSecretKeys,
410        mocks: M,
411    ) -> Result<Self, ConnError> {
412        let time_period = netdir.hs_time_period();
413        let (hs_blind_id_key, subcredential) = HsIdKey::try_from(hsid)
414            .map_err(|_| CE::InvalidHsId)?
415            .compute_blinded_key(time_period)
416            .map_err(
417                // TODO HS what on earth do these errors mean, in practical terms ?
418                // In particular, we'll want to convert them to a ConnError variant,
419                // but what ErrorKind should they have ?
420                into_internal!("key blinding error, don't know how to handle"),
421            )?;
422        let hs_blind_id = hs_blind_id_key.id();
423
424        Ok(Context {
425            netdir,
426            config,
427            hsid: DispRedacted(hsid),
428            hs_blind_id,
429            subcredential,
430            circpool,
431            runtime,
432            secret_keys,
433            mocks,
434        })
435    }
436
437    /// Actually make a HS connection, updating our recorded state as necessary
438    ///
439    /// Called by the `connect` function in this module.
440    ///
441    /// This function handles all necessary retrying of fallible operations,
442    /// (and, therefore, must also limit the total work done for a particular call).
443    #[instrument(level = "trace", skip_all)]
444    async fn connect(&self, data: &mut Data) -> Result<DataTunnel!(R, M), ConnError> {
445        // This function must do the following, retrying as appropriate.
446        //  - Look up the onion descriptor in the state.
447        //  - Download the onion descriptor if one isn't there.
448        //  - In parallel:
449        //    - Pick a rendezvous point from the netdirprovider and launch a
450        //      rendezvous circuit to it. Then send ESTABLISH_INTRO.
451        //    - Pick a number of introduction points (1 or more) and try to
452        //      launch circuits to them.
453        //  - On a circuit to an introduction point, send an INTRODUCE1 cell.
454        //  - Wait for a RENDEZVOUS2 cell on the rendezvous circuit
455        //  - Add a virtual hop to the rendezvous circuit.
456        //  - Return the rendezvous circuit.
457
458        let mocks = self.mocks.clone();
459
460        let desc = self
461            .descriptor_ensure(&mut data.desc, &mut data.hsdirs, None)
462            .await?;
463
464        mocks.test_got_desc(desc);
465
466        let tunnel = match self.intro_rend_connect(desc, &mut data.ipts).await {
467            Ok(tunnel) => tunnel,
468            Err(e) => {
469                let is_intro_nack = |e| {
470                    if let FAE::IntroductionFailed { status, .. } = e {
471                        status == IntroduceAckStatus::NOT_RECOGNIZED
472                    } else {
473                        false
474                    }
475                };
476
477                let retry = if let CE::Failed(ref errors) = e {
478                    // If any of the errors are an INTRODUCE_NACK,
479                    // then it's worth retrying one more time
480                    // with a fresh descriptor.
481                    errors
482                        .clone()
483                        .into_iter()
484                        .any(is_intro_nack)
485                        .then_some(RefetchDescriptor)
486                } else {
487                    None
488                };
489
490                if let Some(RefetchDescriptor) = retry {
491                    debug!(
492                        "Introduction to {} NACKed, refetching descriptor and retrying",
493                        &self.hsid,
494                    );
495                    // Refetch the descriptor and try one more time
496                    let desc = self
497                        .descriptor_ensure(&mut data.desc, &mut data.hsdirs, retry)
498                        .await?;
499                    mocks.test_got_desc(desc);
500                    self.intro_rend_connect(desc, &mut data.ipts).await?
501                } else {
502                    return Err(e);
503                }
504            }
505        };
506
507        mocks.test_got_tunnel(&tunnel);
508
509        Ok(tunnel)
510    }
511
512    /// Ensure that `Data.desc` contains the HS descriptor
513    ///
514    /// If we have a previously-downloaded descriptor, which is still valid,
515    /// just returns a reference to it.
516    ///
517    /// Otherwise, tries to obtain the descriptor by downloading it from hsdir(s).
518    ///
519    /// If `refetch` is `true`, a new descriptor will be refetched
520    /// from the hsdir(s) unconditionally.
521    ///
522    /// Does all necessary retries and timeouts.
523    /// Returns an error if no valid descriptor could be found.
524    #[allow(clippy::cognitive_complexity)] // TODO: Refactor
525    #[instrument(level = "trace", skip_all)]
526    async fn descriptor_ensure<'d>(
527        &self,
528        data: &'d mut DataHsDesc,
529        recent_hsdirs: &'d mut DataHsDirs,
530        refetch: Option<RefetchDescriptor>,
531    ) -> Result<&'d HsDesc, CE> {
532        // Maximum number of hsdir connection and retrieval attempts we'll make
533        let max_total_attempts = self
534            .config
535            .retry
536            .hs_desc_fetch_attempts()
537            .try_into()
538            // User specified a very large u32.  We must be downcasting it to 16bit!
539            // let's give them as many retries as we can manage.
540            .unwrap_or(usize::MAX);
541
542        let now = self.runtime.wallclock();
543        let unwrap_valid_desc = |data: &'d mut DataHsDesc| -> &'d HsDesc {
544            data.as_ref()
545                .expect("Some but now None")
546                .desc
547                .as_ref()
548                .check_valid_at(&now)
549                .expect("Ok but now Err")
550        };
551
552        // We retain a previously obtained descriptor precisely until its lifetime expires,
553        // or until we refetch a more recent one
554        // as a result of an `intro_rend_connect()` failure caused by introduce NACK.
555        //
556        // When it expires, we discard it completely and try to obtain a new one.
557        //
558        // We only replace our cached descriptor if the new one has a higher revision counter.
559        //
560        // TODO SPEC: Discuss HS descriptor lifetime and expiry client behaviour
561        let now = self.runtime.wallclock();
562
563        let stored_revision = data.as_ref().and_then(|previously| {
564            if let Ok(desc) = previously.desc.as_ref().check_valid_at(&now) {
565                // Ideally we would just return desc but that confuses borrowck,
566                // so we have to use unwrap_valid_desc() each time
567                // we need the known-to-be-Some descriptor instead.
568                //
569                // https://github.com/rust-lang/rust/issues/51545
570                Some((desc.revision(), previously.time_period))
571            } else {
572                // Seems to be not valid now.  Try to fetch a fresh one.
573                None
574            }
575        });
576
577        match (stored_revision, refetch) {
578            (Some(_), None) => {
579                // Our cached descriptor is still timely,
580                // and we don't need to fetch a new one.
581                return Ok(unwrap_valid_desc(data));
582            }
583            (None, _) => {
584                // We don't have a timely descriptor,
585                // so ignore the requery_interval,
586                // and reach out to all HsDirs
587                recent_hsdirs.clear();
588            }
589            (_, Some(RefetchDescriptor)) => {
590                // We have been asked to try to fetch a new descriptor.
591                // We will only reach out to the HsDirs that are
592                // not within the `hs_dir_requery_interval`
593            }
594        }
595
596        // First, filter out any HsDirs that we *can* requery
597        recent_hsdirs.retain(|_hsdir, requery| *requery > now);
598
599        let working_tp = self.netdir.hs_time_period();
600        let hs_dirs = self.netdir.hs_dirs_download(
601            self.hs_blind_id,
602            working_tp,
603            &mut self.mocks.thread_rng(),
604        )?;
605
606        trace!(
607            "HS desc fetch for {}, for period {}, using {} hsdirs",
608            &self.hsid,
609            working_tp,
610            hs_dirs.len()
611        );
612
613        let hs_dirs = hs_dirs
614            .into_iter()
615            .filter(|hsdir| {
616                // Skip over any HsDirs that we are not allowed to requery right now
617                let should_skip = recent_hsdirs.keys().any(|recent| {
618                    RelayIdForRequeryPeriod::for_lookup(hsdir).any(|id| id == *recent)
619                });
620
621                !should_skip
622            })
623            .collect::<Vec<_>>();
624
625        if hs_dirs.is_empty() {
626            warn!(
627                "Tried to fetch HS desc for {}, for period {}, but all hsdirs are rate-limited",
628                &self.hsid, working_tp,
629            );
630
631            if stored_revision.is_none() {
632                // We can't fetch a new descriptor, and we don't have a cached one.
633                return Err(CE::NoUsableHsDirs);
634            } else {
635                // Return our cached descriptor
636                return Ok(unwrap_valid_desc(data));
637            }
638        }
639
640        // We might consider launching requests to multiple HsDirs in parallel.
641        //   https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1118#note_2894463
642        // But C Tor doesn't and our HS experts don't consider that important:
643        //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914436
644        // (Additionally, making multiple HSDir requests at once may make us
645        // more vulnerable to traffic analysis.)
646        let mut attempts = hs_dirs.iter().cycle().take(max_total_attempts);
647        let mut errors = RetryError::in_attempt_to("retrieve hidden service descriptor");
648        let desc = loop {
649            let relay = match attempts.next() {
650                Some(relay) => relay,
651                None => {
652                    return Err(if errors.is_empty() {
653                        CE::NoHsDirs
654                    } else {
655                        CE::DescriptorDownload(errors)
656                    });
657                }
658            };
659            let hsdir_for_error: Sensitive<Ed25519Identity> = (*relay.id()).into();
660
661            let hsdir = RelayIdForRequeryPeriod::for_store(relay)?;
662            // Ensure we wait at least hs_dir_requery_interval() until we try to
663            // fecth from this HsDir again
664            recent_hsdirs.insert(hsdir, now + self.config.retry.hs_dir_requery_interval());
665
666            match self.descriptor_fetch_attempt(relay).await {
667                Ok(desc) => break desc,
668                Err(error) => {
669                    if error.should_report_as_suspicious() {
670                        // Note that not every protocol violation is suspicious:
671                        // we only warn on the protocol violations that look like attempts
672                        // to do a traffic tagging attack via hsdir inflation.
673                        // (See proposal 360.)
674                        warn_report!(
675                            &error,
676                            "Suspicious failure while downloading hsdesc for {} from relay {}",
677                            &self.hsid,
678                            relay.display_relay_ids(),
679                        );
680                    } else {
681                        debug_report!(
682                            &error,
683                            "failed hsdir desc fetch for {} from {}/{}",
684                            &self.hsid,
685                            &relay.id(),
686                            &relay.rsa_id()
687                        );
688                    }
689                    errors.push_timed(
690                        tor_error::Report(DescriptorError {
691                            hsdir: hsdir_for_error,
692                            error,
693                        }),
694                        self.runtime.now(),
695                        Some(self.runtime.wallclock()),
696                    );
697                }
698            }
699        };
700
701        // If our existing descriptor is newer than the one we have just fetched,
702        // we should retain it.
703        if let Some(stored_revision) = stored_revision {
704            // It is safe to dangerously_assume_timely,
705            // as descriptor_fetch_attempt has already checked the timeliness of the descriptor.
706            let new_desc = desc.as_ref().dangerously_assume_timely();
707
708            // Revision counters are monotonically increasing within a given time period.
709            // If our newly fetched descriptor has the same HsBlindId as our cached one,
710            // it means they are both used for the same time period,
711            // and so we should only update our cache if the new descriptor is more recent
712            // (i.e. it has a higher revision counter).
713            if stored_revision >= (new_desc.revision(), working_tp) {
714                // Our cached descriptor is still timely, and has a higher revision counter
715                // than the one we've just fetched, so we retain it.
716                return Ok(unwrap_valid_desc(data));
717            }
718        }
719
720        // Store the bounded value in the cache for reuse,
721        // but return a reference to the unwrapped `HsDesc`.
722        //
723        // The `HsDesc` must be owned by `data.desc`,
724        // so first add it to `data.desc`,
725        // and then dangerously_assume_timely to get a reference out again.
726        //
727        // It is safe to dangerously_assume_timely,
728        // as descriptor_fetch_attempt has already checked the timeliness of the descriptor.
729        let desc = HsDescForTp {
730            time_period: working_tp,
731            desc,
732        };
733        let ret = data.insert(desc);
734        Ok(ret.desc.as_ref().dangerously_assume_timely())
735    }
736
737    /// Make one attempt to fetch the descriptor from a specific hsdir
738    ///
739    /// No timeout
740    ///
741    /// On success, returns the descriptor.
742    ///
743    /// While the returned descriptor is `TimerangeBound`, its validity at the current time *has*
744    /// been checked.
745    #[instrument(level = "trace", skip_all)]
746    async fn descriptor_fetch_attempt(
747        &self,
748        hsdir: &Relay<'_>,
749    ) -> Result<TimerangeBound<HsDesc>, DescriptorErrorDetail> {
750        let max_len: usize = self
751            .netdir
752            .params()
753            .hsdir_max_desc_size
754            .get()
755            .try_into()
756            .map_err(into_internal!("BoundedInt was not truly bounded!"))?;
757        let request = {
758            let mut r = tor_dirclient::request::HsDescDownloadRequest::new(self.hs_blind_id);
759            r.set_max_len(max_len);
760            r
761        };
762        trace!(
763            "hsdir for {}, trying {}/{}, request {:?} (http request {:?})",
764            &self.hsid,
765            &hsdir.id(),
766            &hsdir.rsa_id(),
767            &request,
768            request.debug_request()
769        );
770
771        let circuit = self
772            .circpool
773            .m_get_or_launch_dir(&self.netdir, OwnedCircTarget::from_circ_target(hsdir))
774            .await?;
775        let n_hops = circuit.m_num_hops()?;
776        let timeout_roundtrip =
777            self.estimate_timeout(&[(1, TimeoutsAction::RoundTrip { length: n_hops })]);
778
779        let source: Option<SourceInfo> = circuit
780            .m_source_info()
781            .map_err(into_internal!("Couldn't get SourceInfo for circuit"))?;
782
783        let mut stream = self
784            .runtime
785            // NOTE: In fact this timeout is overkill: this operation should succeed immediately,
786            // since we always send BEGINDIR messages optimistically (without waiting for a reply).
787            // But since our code is complex, and since it could become possible for this to block
788            // if the circuit is saturated or we implement proposal 367 or something,
789            // we may as well have _some_ timeout here.
790            .timeout(timeout_roundtrip, circuit.m_begin_dir_stream())
791            .await?
792            .map_err(DescriptorErrorDetail::Circuit)?;
793
794        let request_future =
795            tor_dirclient::send_request(self.runtime, &request, &mut stream, source);
796        let response = self
797            .runtime
798            .timeout(timeout_roundtrip, request_future)
799            .await?
800            .map_err(|dir_error| match dir_error {
801                tor_dirclient::Error::RequestFailed(rfe) => DescriptorErrorDetail::from(rfe.error),
802                tor_dirclient::Error::CircMgr(ce) => into_internal!(
803                    "tor-dirclient complains about circmgr going wrong but we gave it a stream"
804                )(ce)
805                .into(),
806                other => into_internal!(
807                    "tor-dirclient gave unexpected error, tor-hsclient code needs updating"
808                )(other)
809                .into(),
810            })?;
811
812        let desc_text = response.into_output_string().map_err(|rfe| rfe.error)?;
813        let hsc_desc_enc = self.secret_keys.keys.ks_hsc_desc_enc.as_ref();
814
815        let now = self.runtime.wallclock();
816
817        HsDesc::parse_decrypt_validate(
818            &desc_text,
819            &self.hs_blind_id,
820            now,
821            &self.subcredential,
822            hsc_desc_enc,
823        )
824        .map_err(DescriptorErrorDetail::from)
825    }
826
827    /// Given the descriptor, try to connect to service
828    ///
829    /// Does all necessary retries, timeouts, etc.
830    async fn intro_rend_connect(
831        &self,
832        desc: &HsDesc,
833        data: &mut DataIpts,
834    ) -> Result<DataTunnel!(R, M), CE> {
835        // Maximum number of rendezvous/introduction attempts we'll make
836        let max_total_attempts = self
837            .config
838            .retry
839            .hs_intro_rend_attempts()
840            .try_into()
841            // User specified a very large u32.  We must be downcasting it to 16bit!
842            // let's give them as many retries as we can manage.
843            .unwrap_or(usize::MAX);
844
845        // We can't reliably distinguish IPT failure from RPT failure, so we iterate over IPTs
846        // (best first) and each time use a random RPT.
847
848        // We limit the number of rendezvous establishment attempts, separately, since we don't
849        // try to talk to the intro pt until we've established the rendezvous circuit.
850        let mut rend_attempts = 0..max_total_attempts;
851
852        // But, we put all the errors into the same bucket, since we might have a mixture.
853        let mut errors = RetryError::in_attempt_to("make circuit to hidden service");
854
855        // Note that IntroPtIndex is *not* the index into this Vec.
856        // It is the index into the original list of introduction points in the descriptor.
857        let mut usable_intros: Vec<UsableIntroPt> = desc
858            .intro_points()
859            .iter()
860            .enumerate()
861            .map(|(intro_index, intro_desc)| {
862                let intro_index = intro_index.into();
863                let intro_target = ipt_to_circtarget(intro_desc, &self.netdir)
864                    .map_err(|error| FAE::UnusableIntro { error, intro_index })?;
865                // Lack of TAIT means this clone
866                let intro_target = OwnedCircTarget::from_circ_target(&intro_target);
867                Ok::<_, FailedAttemptError>(UsableIntroPt {
868                    intro_index,
869                    intro_desc,
870                    intro_target,
871                    sort_rand: self.mocks.thread_rng().random(),
872                })
873            })
874            .filter_map(|entry| match entry {
875                Ok(y) => Some(y),
876                Err(e) => {
877                    errors.push_timed(e, self.runtime.now(), Some(self.runtime.wallclock()));
878                    None
879                }
880            })
881            .collect_vec();
882
883        // Delete experience information for now-unlisted intro points
884        // Otherwise, as the IPTs change `Data` might grow without bound,
885        // if we keep reconnecting to the same HS.
886        data.retain(|k, _v| {
887            usable_intros
888                .iter()
889                .any(|ipt| RelayIdForExperience::for_lookup(&ipt.intro_target).any(|id| &id == k))
890        });
891
892        // Join with existing state recording our experiences,
893        // sort by descending goodness, and then randomly
894        // (so clients without any experience don't all pile onto the same, first, IPT)
895        usable_intros.sort_by_key(|ipt: &UsableIntroPt| {
896            let experience =
897                RelayIdForExperience::for_lookup(&ipt.intro_target).find_map(|id| data.get(&id));
898            IptSortKey {
899                outcome: experience.into(),
900                sort_rand: ipt.sort_rand,
901            }
902        });
903        self.mocks.test_got_ipts(&usable_intros);
904
905        let mut intro_attempts = usable_intros.iter().cycle().take(max_total_attempts);
906
907        // We retain a rendezvous we managed to set up in here.  That way if we created it, and
908        // then failed before we actually needed it, we can reuse it.
909        // If we exit with an error, we will waste it - but because we isolate things we do
910        // for different services, it wouldn't be reusable anyway.
911        let mut saved_rendezvous = None;
912
913        // If we are using proof-of-work DoS mitigation, this chooses an
914        // algorithm and initial effort, and adjusts that effort when we retry.
915        let mut pow_client = HsPowClient::new(&self.hs_blind_id, desc);
916
917        // We might consider making multiple INTRODUCE attempts to different
918        // IPTs in parallel, and somehow aggregating the errors and
919        // experiences.
920        // However our HS experts don't consider that important:
921        //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914438
922        // Parallelizing our HsCircPool circuit building would likely have
923        // greater impact. (See #1149.)
924        loop {
925            // When did we start doing things that depended on the IPT?
926            //
927            // Used for recording our experience with the selected IPT
928            let mut ipt_use_started = None::<Instant>;
929
930            // Error handling inner async block (analogous to an IEFE):
931            //  * Ok(Some()) means this attempt succeeded
932            //  * Ok(None) means all attempts exhausted
933            //  * Err(error) means this attempt failed
934            //
935            let outcome = async {
936                // We establish a rendezvous point first.  Although it appears from reading
937                // this code that this means we serialise establishment of the rendezvous and
938                // introduction circuits, this isn't actually the case.  The circmgr maintains
939                // a pool of circuits.  What actually happens in the "standing start" case is
940                // that we obtain a circuit for rendezvous from the circmgr's pool, expecting
941                // one to be available immediately; the circmgr will then start to build a new
942                // one to replenish its pool, and that happens in parallel with the work we do
943                // here - but in arrears.  If the circmgr pool is empty, then we must wait.
944                //
945                // Perhaps this should be parallelised here.  But that's really what the pool
946                // is for, since we expect building the rendezvous circuit and building the
947                // introduction circuit to take about the same length of time.
948                //
949                // We *do* serialise the ESTABLISH_RENDEZVOUS exchange, with the
950                // building of the introduction circuit.  That could be improved, at the cost
951                // of some additional complexity here.
952                //
953                // Our HS experts don't consider it important to increase the parallelism:
954                //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914444
955                //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914445
956                if saved_rendezvous.is_none() {
957                    debug!("hs conn to {}: setting up rendezvous point", &self.hsid);
958                    // Establish a rendezvous circuit.
959                    let Some(_): Option<usize> = rend_attempts.next() else {
960                        return Ok(None);
961                    };
962
963                    saved_rendezvous = Some(self.establish_rendezvous().await?);
964                }
965
966                let Some(ipt) = intro_attempts.next() else {
967                    return Ok(None);
968                };
969                let intro_index = ipt.intro_index;
970                let is_single_onion_service = desc.is_single_onion_service();
971
972                let proof_of_work = match pow_client.solve().await {
973                    Ok(solution) => solution,
974                    Err(e) => {
975                        debug!(
976                            "failing to compute proof-of-work, trying without. ({:?})",
977                            e
978                        );
979                        None
980                    }
981                };
982
983                // We record how long things take, starting from here, as
984                // as a statistic we'll use for the IPT in future.
985                // This is stored in a variable outside this async block,
986                // so that the outcome handling can use it.
987                ipt_use_started = Some(self.runtime.now());
988
989                // No `Option::get_or_try_insert_with`, or we'd avoid this expect()
990                let rend_pt_for_error = rend_pt_identity_for_error(
991                    &saved_rendezvous
992                        .as_ref()
993                        .expect("just made Some")
994                        .rend_relay,
995                );
996                debug!(
997                    "hs conn to {}: RPT {}",
998                    &self.hsid,
999                    rend_pt_for_error.as_inner()
1000                );
1001
1002                let (rendezvous, introduced) =
1003                    self.exchange_introduce(ipt, &mut saved_rendezvous, proof_of_work)
1004                    .await
1005                    // TODO: Maybe try, once, to extend-and-reuse the intro circuit.
1006                    //
1007                    // If the introduction fails, the introduction circuit is in principle
1008                    // still usable.  We believe that in this case, C Tor extends the intro
1009                    // circuit by one hop to the next IPT to try.  That saves on building a
1010                    // whole new 3-hop intro circuit.  However, our HS experts tell us that
1011                    // if introduction fails at one IPT it is likely to fail at the others too,
1012                    // so that optimisation might reduce our network impact and time to failure,
1013                    // but isn't likely to improve our chances of success.
1014                    //
1015                    // However, it's not clear whether this approach risks contaminating
1016                    // the 2nd attempt with some fault relating to the introduction point.
1017                    // The 1st ipt might also gain more knowledge about which HS we're talking to.
1018                    //
1019                    // TODO SPEC: Discuss extend-and-reuse HS intro circuit after nack
1020                    ?;
1021                #[allow(unused_variables)] // it's *supposed* to be unused
1022                let saved_rendezvous = (); // don't use `saved_rendezvous` any more, use rendezvous
1023
1024                let rend_pt = rend_pt_identity_for_error(&rendezvous.rend_relay);
1025                let circ = self.complete_rendezvous(ipt, rendezvous, introduced, is_single_onion_service)
1026                    .await?;
1027
1028                debug!(
1029                    "hs conn to {}: RPT {} IPT {}: success",
1030                    &self.hsid,
1031                    rend_pt.as_inner(),
1032                    intro_index,
1033                );
1034                Ok::<_, FAE>(Some((intro_index, circ)))
1035            }
1036            .await;
1037
1038            // Store the experience `outcome` we had with IPT `intro_index`, in `data`
1039            #[allow(clippy::unused_unit)] // -> () is here for error handling clarity
1040            let mut store_experience = |intro_index, outcome| -> () {
1041                (|| {
1042                    let ipt = usable_intros
1043                        .iter()
1044                        .find(|ipt| ipt.intro_index == intro_index)
1045                        .ok_or_else(|| internal!("IPT not found by index"))?;
1046                    let id = RelayIdForExperience::for_store(&ipt.intro_target)?;
1047                    let started = ipt_use_started.ok_or_else(|| {
1048                        internal!("trying to record IPT use but no IPT start time noted")
1049                    })?;
1050                    let duration = self
1051                        .runtime
1052                        .now()
1053                        .checked_duration_since(started)
1054                        .ok_or_else(|| internal!("clock overflow calculating IPT use duration"))?;
1055                    data.insert(id, IptExperience { duration, outcome });
1056                    Ok::<_, Bug>(())
1057                })()
1058                .unwrap_or_else(|e| warn_report!(e, "error recording HS IPT use experience"));
1059            };
1060
1061            match outcome {
1062                Ok(Some((intro_index, y))) => {
1063                    // Record successful outcome in Data
1064                    store_experience(intro_index, Ok(()));
1065                    return Ok(y);
1066                }
1067                Ok(None) => return Err(CE::Failed(errors)),
1068                Err(error) => {
1069                    debug_report!(&error, "hs conn to {}: attempt failed", &self.hsid);
1070                    // Record error outcome in Data, if in fact we involved the IPT
1071                    // at all.  The IPT information is be retrieved from `error`,
1072                    // since only some of the errors implicate the introduction point.
1073                    if let Some(intro_index) = error.intro_index() {
1074                        store_experience(intro_index, Err(error.retry_time()));
1075                    }
1076                    errors.push_timed(error, self.runtime.now(), Some(self.runtime.wallclock()));
1077
1078                    // If we are using proof-of-work DoS mitigation, try harder next time
1079                    pow_client.increase_effort();
1080                }
1081            }
1082        }
1083    }
1084
1085    /// Make one attempt to establish a rendezvous circuit
1086    ///
1087    /// This doesn't really depend on anything,
1088    /// other than (obviously) the isolation implied by our circuit pool.
1089    /// In particular it doesn't depend on the introduction point.
1090    ///
1091    /// Applies timeouts as appropriate.
1092    #[instrument(level = "trace", skip_all)]
1093    async fn establish_rendezvous(&'c self) -> Result<Rendezvous<'c, R, M>, FAE> {
1094        let (rend_tunnel, rend_relay) = self
1095            .circpool
1096            .m_get_or_launch_client_rend(&self.netdir)
1097            .await
1098            .map_err(|error| FAE::RendezvousCircuitObtain { error })?;
1099
1100        let rend_pt = rend_pt_identity_for_error(&rend_relay);
1101
1102        let rend_cookie: RendCookie = self.mocks.thread_rng().random();
1103        let message = EstablishRendezvous::new(rend_cookie);
1104
1105        let (rend_established_tx, rend_established_rx) = proto_oneshot::channel();
1106        let (rend2_tx, rend2_rx) = proto_oneshot::channel();
1107
1108        /// Handler which expects `RENDEZVOUS_ESTABLISHED` and then
1109        /// `RENDEZVOUS2`.   Returns each message via the corresponding `oneshot`.
1110        struct Handler {
1111            /// Sender for a RENDEZVOUS_ESTABLISHED message.
1112            rend_established_tx: proto_oneshot::Sender<RendezvousEstablished>,
1113            /// Sender for a RENDEZVOUS2 message.
1114            rend2_tx: proto_oneshot::Sender<Rendezvous2>,
1115        }
1116        impl MsgHandler for Handler {
1117            fn handle_msg(
1118                &mut self,
1119                msg: AnyRelayMsg,
1120            ) -> Result<MetaCellDisposition, tor_proto::Error> {
1121                // The first message we expect is a RENDEZVOUS_ESTABALISHED.
1122                if self.rend_established_tx.still_expected() {
1123                    self.rend_established_tx
1124                        .deliver_expected_message(msg, MetaCellDisposition::Consumed)
1125                } else {
1126                    self.rend2_tx
1127                        .deliver_expected_message(msg, MetaCellDisposition::ConversationFinished)
1128                }
1129            }
1130        }
1131
1132        debug!(
1133            "hs conn to {}: RPT {}: sending ESTABLISH_RENDEZVOUS",
1134            &self.hsid,
1135            rend_pt.as_inner(),
1136        );
1137
1138        let failed_map_err = |error| FAE::RendezvousEstablish {
1139            error,
1140            rend_pt: rend_pt.clone(),
1141        };
1142        let handler = Handler {
1143            rend_established_tx,
1144            rend2_tx,
1145        };
1146
1147        let num_hops = rend_tunnel
1148            .m_num_own_hops()
1149            .map_err(|error| FAE::RendezvousCircuitObtain { error })?;
1150
1151        let timeout_roundtrip =
1152            self.estimate_timeout(&[(1, TimeoutsAction::RoundTrip { length: num_hops })]);
1153
1154        // TODO(conflux) This error handling is horrible. Problem is that this Mock system requires
1155        // to send back a tor_circmgr::Error while our reply handler requires a tor_proto::Error.
1156        // And unifying both is hard here considering it needs to be converted to yet another Error
1157        // type "FAE" so we have to do these hoops and jumps.
1158        rend_tunnel
1159            .m_start_conversation_last_hop(Some(message.into()), handler)
1160            .await
1161            .map_err(|e| {
1162                let proto_error = match e {
1163                    tor_circmgr::Error::Protocol { error, .. } => error,
1164                    _ => tor_proto::Error::CircuitClosed,
1165                };
1166                FAE::RendezvousEstablish {
1167                    error: proto_error,
1168                    rend_pt: rend_pt.clone(),
1169                }
1170            })?;
1171
1172        // `start_conversation` returns as soon as the control message has been sent.
1173        // We need to obtain the RENDEZVOUS_ESTABLISHED message, which is "returned" via the oneshot.
1174        let _: RendezvousEstablished = self
1175            .runtime
1176            .timeout(timeout_roundtrip, rend_established_rx.recv(failed_map_err))
1177            .await
1178            .map_err(
1179                |_timeout: tor_rtcompat::TimeoutError| FAE::RendezvousEstablishTimeout {
1180                    rend_pt: rend_pt.clone(),
1181                },
1182            )??;
1183
1184        debug!(
1185            "hs conn to {}: RPT {}: got RENDEZVOUS_ESTABLISHED",
1186            &self.hsid,
1187            rend_pt.as_inner(),
1188        );
1189
1190        Ok(Rendezvous {
1191            rend_tunnel,
1192            rend_cookie,
1193            rend_relay,
1194            rend2_rx,
1195            marker: PhantomData,
1196        })
1197    }
1198
1199    /// Attempt (once) to send an INTRODUCE1 and wait for the INTRODUCE_ACK
1200    ///
1201    /// `take`s the input `rendezvous` (but only takes it if it gets that far)
1202    /// and, if successful, returns it.
1203    /// (This arranges that the rendezvous is "used up" precisely if
1204    /// we sent its secret somewhere.)
1205    ///
1206    /// Although this function handles the `Rendezvous`,
1207    /// nothing in it actually involves the rendezvous point.
1208    /// So if there's a failure, it's purely to do with the introduction point.
1209    ///
1210    /// Applies timeouts as appropriate.
1211    #[allow(clippy::cognitive_complexity, clippy::type_complexity)] // TODO: Refactor
1212    #[instrument(level = "trace", skip_all)]
1213    async fn exchange_introduce(
1214        &'c self,
1215        ipt: &UsableIntroPt<'_>,
1216        rendezvous: &mut Option<Rendezvous<'c, R, M>>,
1217        proof_of_work: Option<ProofOfWork>,
1218    ) -> Result<(Rendezvous<'c, R, M>, Introduced<R, M>), FAE> {
1219        let intro_index = ipt.intro_index;
1220
1221        debug!(
1222            "hs conn to {}: IPT {}: obtaining intro circuit",
1223            &self.hsid, intro_index,
1224        );
1225
1226        let intro_circ = self
1227            .circpool
1228            .m_get_or_launch_intro(
1229                &self.netdir,
1230                ipt.intro_target.clone(), // &OwnedCircTarget isn't CircTarget apparently
1231            )
1232            .await
1233            .map_err(|error| FAE::IntroductionCircuitObtain { error, intro_index })?;
1234
1235        let rendezvous = rendezvous.take().ok_or_else(|| internal!("no rend"))?;
1236
1237        let rend_pt = rend_pt_identity_for_error(&rendezvous.rend_relay);
1238
1239        debug!(
1240            "hs conn to {}: RPT {} IPT {}: making introduction",
1241            &self.hsid,
1242            rend_pt.as_inner(),
1243            intro_index,
1244        );
1245
1246        // Now we construct an introduce1 message and perform the first part of the
1247        // rendezvous handshake.
1248        //
1249        // This process is tricky because the header of the INTRODUCE1 message
1250        // -- which depends on the IntroPt configuration -- is authenticated as
1251        // part of the HsDesc handshake.
1252
1253        // Construct the header, since we need it as input to our encryption.
1254        let intro_header = {
1255            let ipt_sid_key = ipt.intro_desc.ipt_sid_key();
1256            let intro1 = Introduce1::new(
1257                AuthKeyType::ED25519_SHA3_256,
1258                ipt_sid_key.as_bytes().to_vec(),
1259                vec![],
1260            );
1261            let mut header = vec![];
1262            intro1
1263                .encode_onto(&mut header)
1264                .map_err(into_internal!("couldn't encode intro1 header"))?;
1265            header
1266        };
1267
1268        // Construct the introduce payload, which tells the onion service how to find
1269        // our rendezvous point.  (We could do this earlier if we wanted.)
1270        let intro_payload = {
1271            let onion_key =
1272                intro_payload::OnionKey::NtorOnionKey(*rendezvous.rend_relay.ntor_onion_key());
1273            let linkspecs = rendezvous
1274                .rend_relay
1275                .linkspecs()
1276                .map_err(into_internal!("Couldn't encode link specifiers"))?;
1277            let payload = IntroduceHandshakePayload::new(
1278                rendezvous.rend_cookie,
1279                onion_key,
1280                linkspecs,
1281                proof_of_work,
1282            );
1283            let mut encoded = vec![];
1284            payload
1285                .write_onto(&mut encoded)
1286                .map_err(into_internal!("Couldn't encode introduce1 payload"))?;
1287            encoded
1288        };
1289
1290        // Perform the cryptographic handshake with the onion service.
1291        let service_info = hs_ntor::HsNtorServiceInfo::new(
1292            ipt.intro_desc.svc_ntor_key().clone(),
1293            ipt.intro_desc.ipt_sid_key().clone(),
1294            self.subcredential,
1295        );
1296        let handshake_state =
1297            hs_ntor::HsNtorClientState::new(&mut self.mocks.thread_rng(), service_info);
1298        let encrypted_body = handshake_state
1299            .client_send_intro(&intro_header, &intro_payload)
1300            .map_err(into_internal!("can't begin hs-ntor handshake"))?;
1301
1302        // Build our actual INTRODUCE1 message.
1303        let intro1_real = Introduce1::new(
1304            AuthKeyType::ED25519_SHA3_256,
1305            ipt.intro_desc.ipt_sid_key().as_bytes().to_vec(),
1306            encrypted_body,
1307        );
1308
1309        /// Handler which expects just `INTRODUCE_ACK`
1310        struct Handler {
1311            /// Sender for `INTRODUCE_ACK`
1312            intro_ack_tx: proto_oneshot::Sender<IntroduceAck>,
1313        }
1314        impl MsgHandler for Handler {
1315            fn handle_msg(
1316                &mut self,
1317                msg: AnyRelayMsg,
1318            ) -> Result<MetaCellDisposition, tor_proto::Error> {
1319                self.intro_ack_tx
1320                    .deliver_expected_message(msg, MetaCellDisposition::ConversationFinished)
1321            }
1322        }
1323        let failed_map_err = |error| FAE::IntroductionExchange { error, intro_index };
1324        let (intro_ack_tx, intro_ack_rx) = proto_oneshot::channel();
1325        let handler = Handler { intro_ack_tx };
1326
1327        let num_hops = intro_circ
1328            .m_num_hops()
1329            .map_err(|error| FAE::IntroductionCircuitObtain { error, intro_index })?;
1330        // NOTE: Should we allow this to be longer in case the introduction point is grievously
1331        // overloaded?
1332        let timeout_roundtrip =
1333            self.estimate_timeout(&[(1, TimeoutsAction::RoundTrip { length: num_hops })]);
1334
1335        debug!(
1336            "hs conn to {}: RPT {} IPT {}: making introduction - sending INTRODUCE1",
1337            &self.hsid,
1338            rend_pt.as_inner(),
1339            intro_index,
1340        );
1341
1342        // TODO(conflux) This error handling is horrible. Problem is that this Mock system requires
1343        // to send back a tor_circmgr::Error while our reply handler requires a tor_proto::Error.
1344        // And unifying both is hard here considering it needs to be converted to yet another Error
1345        // type "FAE" so we have to do these hoops and jumps.
1346        intro_circ
1347            .m_start_conversation_last_hop(Some(intro1_real.into()), handler)
1348            .await
1349            .map_err(|e| {
1350                let proto_error = match e {
1351                    tor_circmgr::Error::Protocol { error, .. } => error,
1352                    _ => tor_proto::Error::CircuitClosed,
1353                };
1354                FAE::IntroductionExchange {
1355                    error: proto_error,
1356                    intro_index,
1357                }
1358            })?;
1359
1360        // Status is checked by `.success()`, and we don't look at the extensions;
1361        // just discard the known-successful `IntroduceAck`
1362        let _: IntroduceAck = self
1363            .runtime
1364            .timeout(timeout_roundtrip, intro_ack_rx.recv(failed_map_err))
1365            .await
1366            .map_err(|_timeout: TimeoutError| FAE::IntroductionTimeout { intro_index })??
1367            .success()
1368            .map_err(|status| FAE::IntroductionFailed {
1369                status,
1370                intro_index,
1371            })?;
1372
1373        debug!(
1374            "hs conn to {}: RPT {} IPT {}: making introduction - success",
1375            &self.hsid,
1376            rend_pt.as_inner(),
1377            intro_index,
1378        );
1379
1380        // Having received INTRODUCE_ACK. we can forget about this circuit
1381        // (and potentially tear it down).
1382        drop(intro_circ);
1383
1384        Ok((
1385            rendezvous,
1386            Introduced {
1387                handshake_state,
1388                marker: PhantomData,
1389            },
1390        ))
1391    }
1392
1393    /// Attempt (once) to connect a rendezvous circuit using the given intro pt.
1394    ///
1395    /// That is to say, we simply wait for a RENDEZVOUS2 message,
1396    /// and if we get one, we add a virtual hop.
1397    ///
1398    /// Timeouts here might be due to the IPT, RPT, service,
1399    /// or any of the intermediate relays.
1400    ///
1401    /// If, rather than a timeout, we actually encounter some kind of error,
1402    /// we'll return the appropriate `FailedAttemptError`.
1403    /// (Who is responsible may vary, so the `FailedAttemptError` variant will reflect that.)
1404    async fn complete_rendezvous(
1405        &'c self,
1406        ipt: &UsableIntroPt<'_>,
1407        rendezvous: Rendezvous<'c, R, M>,
1408        introduced: Introduced<R, M>,
1409        is_single_onion_service: bool,
1410    ) -> Result<DataTunnel!(R, M), FAE> {
1411        /// Largest number of hops that the onion service must build for _its_
1412        /// circuits to our rendezvous points.
1413        ///
1414        /// This is 4 hops (assuming that it has full vanguards enabled) plus one for the
1415        /// renedezvous point itself.
1416        const MAX_PEER_REND_HOPS: usize = 5;
1417
1418        /// Largest number of retries that we think the peer might make if its
1419        /// circuits are failing.
1420        const MAX_PEER_CIRC_RETRIES: u32 = 3;
1421
1422        let rend_pt = rend_pt_identity_for_error(&rendezvous.rend_relay);
1423        let intro_index = ipt.intro_index;
1424        let failed_map_err = |error| FAE::RendezvousCompletionCircuitError {
1425            error,
1426            intro_index,
1427            rend_pt: rend_pt.clone(),
1428        };
1429
1430        debug!(
1431            "hs conn to {}: RPT {} IPT {}: awaiting rendezvous completion",
1432            &self.hsid,
1433            rend_pt.as_inner(),
1434            intro_index,
1435        );
1436
1437        let num_hops = rendezvous
1438            .rend_tunnel
1439            .m_num_own_hops()
1440            // This is not necessarily the best error, but it isn't totally wrong.
1441            // We can't wrap the tor_circuit error in anything else that makes sense.
1442            // See #2513.
1443            .map_err(|error| FAE::RendezvousCircuitObtain { error })?;
1444
1445        // Maximum length of the circuit that the peer will build to the rendezvous point.
1446        let peer_rend_circ_len = if is_single_onion_service {
1447            1
1448        } else {
1449            MAX_PEER_REND_HOPS
1450        };
1451
1452        // The total number of hops from the peer to us.
1453        //
1454        // We subtract 1 because both circuits terminate at the rendezvous point.
1455        let total_circ_len = peer_rend_circ_len + num_hops - 1;
1456
1457        // Limit on the duration of each attempt for activities involving both
1458        // RPT and IPT.
1459        let rpt_ipt_timeout = self.estimate_timeout(&[
1460            // The API requires us to specify a number of circuit builds and round trips.
1461            // So what we tell the estimator is a rather imprecise description.
1462            //
1463            // What we are timing here is:
1464            //
1465            //    INTRODUCE2 goes from IPT to HS.
1466            //    This happens in parallel with our waiting for the INTRODUCE_ACK,
1467            //    and we know that our own introduction circuit is always at least
1468            //    as long as the peer's (even if they are using full vanguards),
1469            //    so we don't need any additional delay here.
1470            //
1471            //    HS builds to our RPT
1472            (
1473                MAX_PEER_CIRC_RETRIES,
1474                TimeoutsAction::BuildCircuit {
1475                    length: peer_rend_circ_len,
1476                },
1477            ),
1478            //
1479            //    RENDEZVOUS1 goes from HS to RPT.  `peer_circ_len`, one-way.
1480            //    RENDEZVOUS2 goes from RPT to us.  `num_hops`, one-way.
1481            (
1482                1,
1483                TimeoutsAction::OneWay {
1484                    length: total_circ_len,
1485                },
1486            ),
1487        ]);
1488
1489        let rend2_msg: Rendezvous2 = self
1490            .runtime
1491            .timeout(rpt_ipt_timeout, rendezvous.rend2_rx.recv(failed_map_err))
1492            .await
1493            .map_err(|_: TimeoutError| FAE::RendezvousCompletionTimeout {
1494                intro_index,
1495                rend_pt: rend_pt.clone(),
1496            })??;
1497
1498        debug!(
1499            "hs conn to {}: RPT {} IPT {}: received RENDEZVOUS2",
1500            &self.hsid,
1501            rend_pt.as_inner(),
1502            intro_index,
1503        );
1504
1505        // In theory would be great if we could have multiple introduction attempts in parallel
1506        // with similar x,X values but different IPTs.  However, our HS experts don't
1507        // think increasing parallelism here is important:
1508        //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914438
1509        let handshake_state = introduced.handshake_state;
1510
1511        // Try to complete the cryptographic handshake.
1512        let keygen =
1513            self.mocks
1514                .rendezvous_handshake(handshake_state, rend2_msg, intro_index, &rend_pt)?;
1515
1516        let params = onion_circparams_from_netparams(self.netdir.params())
1517            .map_err(into_internal!("Failed to build CircParameters"))?;
1518        // TODO: We may be able to infer more about the supported protocols of the other side from our
1519        // handshake, and from its descriptors.
1520        //
1521        // TODO CC: This is relevant for congestion control!
1522        let protocols = self.netdir.client_protocol_status().required_protocols();
1523
1524        rendezvous
1525            .rend_tunnel
1526            .m_extend_virtual(
1527                handshake::RelayProtocol::HsV3,
1528                handshake::HandshakeRole::Initiator,
1529                keygen,
1530                params,
1531                protocols,
1532            )
1533            .await
1534            .map_err(into_internal!(
1535                "actually this is probably a 'circuit closed' error" // TODO HS
1536            ))?;
1537
1538        debug!(
1539            "hs conn to {}: RPT {} IPT {}: HS circuit established",
1540            &self.hsid,
1541            rend_pt.as_inner(),
1542            intro_index,
1543        );
1544
1545        Ok(rendezvous.rend_tunnel)
1546    }
1547
1548    /// Helper to estimate a timeout for a complicated operation
1549    ///
1550    /// `actions` is a list of `(count, action)`, where each entry
1551    /// represents doing `action`, `count` times sequentially.
1552    ///
1553    /// Combines the timeout estimates and returns an overall timeout.
1554    fn estimate_timeout(&self, actions: &[(u32, TimeoutsAction)]) -> Duration {
1555        // This algorithm is, perhaps, wrong.  For uncorrelated variables, a particular
1556        // percentile estimate for a sum of random variables, is not calculated by adding the
1557        // percentile estimates of the individual variables.
1558        //
1559        // But the actual lengths of times of the operations aren't uncorrelated.
1560        // If they were *perfectly* correlated, then this addition would be correct.
1561        // It will do for now; it just might be rather longer than it ought to be.
1562        actions
1563            .iter()
1564            .map(|(count, action)| {
1565                self.circpool
1566                    .m_estimate_timeout(action)
1567                    .saturating_mul(*count)
1568            })
1569            .fold(Duration::ZERO, Duration::saturating_add)
1570    }
1571}
1572
1573/// Mocks used for testing `connect.rs`
1574///
1575/// This is different to `MockableConnectorData`,
1576/// which is used to *replace* this file, when testing `state.rs`.
1577///
1578/// `MocksForConnect` provides mock facilities for *testing* this file.
1579//
1580// TODO this should probably live somewhere else, maybe tor-circmgr even?
1581// TODO this really ought to be made by macros or something
1582trait MocksForConnect<R>: Clone {
1583    /// HS circuit pool
1584    type HsCircPool: MockableCircPool<R>;
1585
1586    /// A random number generator
1587    type Rng: rand::Rng + rand::CryptoRng;
1588
1589    /// Key generator used for generating the keys for the virtual hop.
1590    type KeyGenerator: tor_proto::client::circuit::handshake::KeyGenerator + Send;
1591
1592    /// Tell tests we got this descriptor text
1593    fn test_got_desc(&self, _: &HsDesc) {}
1594    /// Tell tests we got this data tunnel.
1595    fn test_got_tunnel(&self, _: &DataTunnel!(R, Self)) {}
1596    /// Tell tests we have obtained and sorted the intros like this
1597    fn test_got_ipts(&self, _: &[UsableIntroPt]) {}
1598
1599    /// Return a random number generator
1600    fn thread_rng(&self) -> Self::Rng;
1601
1602    /// Complete the rendezvous handshake, returning the resulting keygen
1603    fn rendezvous_handshake(
1604        &self,
1605        handshake_state: hs_ntor::HsNtorClientState,
1606        rend2_msg: Rendezvous2,
1607        intro_index: IntroPtIndex,
1608        rend_pt: &RendPtIdentityForError,
1609    ) -> Result<Self::KeyGenerator, FAE>;
1610}
1611/// Mock for `HsCircPool`
1612///
1613/// Methods start with `m_` to avoid the following problem:
1614/// `ClientCirc::start_conversation` (say) means
1615/// to use the inherent method if one exists,
1616/// but will use a trait method if there isn't an inherent method.
1617///
1618/// So if the inherent method is renamed, the call in the impl here
1619/// turns into an always-recursive call.
1620/// This is not detected by the compiler due to the situation being
1621/// complicated by futures, `#[async_trait]` etc.
1622/// <https://github.com/rust-lang/rust/issues/111177>
1623#[async_trait]
1624trait MockableCircPool<R> {
1625    /// Directory tunnel.
1626    type DirTunnel: MockableClientDir;
1627    /// Data tunnel.
1628    type DataTunnel: MockableClientData;
1629    /// Intro tunnel.
1630    type IntroTunnel: MockableClientIntro;
1631
1632    async fn m_get_or_launch_dir(
1633        &self,
1634        netdir: &NetDir,
1635        target: impl CircTarget + Send + Sync + 'async_trait,
1636    ) -> tor_circmgr::Result<Self::DirTunnel>;
1637
1638    async fn m_get_or_launch_intro(
1639        &self,
1640        netdir: &NetDir,
1641        target: impl CircTarget + Send + Sync + 'async_trait,
1642    ) -> tor_circmgr::Result<Self::IntroTunnel>;
1643
1644    /// Client circuit
1645    async fn m_get_or_launch_client_rend<'a>(
1646        &self,
1647        netdir: &'a NetDir,
1648    ) -> tor_circmgr::Result<(Self::DataTunnel, Relay<'a>)>;
1649
1650    /// Estimate timeout
1651    fn m_estimate_timeout(&self, action: &TimeoutsAction) -> Duration;
1652}
1653
1654/// Mock for onion service client directory tunnel.
1655#[async_trait]
1656trait MockableClientDir: Debug {
1657    /// Client circuit
1658    type DirStream: AsyncRead + AsyncWrite + Send + Unpin;
1659    async fn m_begin_dir_stream(&self) -> tor_circmgr::Result<Self::DirStream>;
1660
1661    /// Get a tor_dirclient::SourceInfo for this circuit, if possible.
1662    fn m_source_info(&self) -> tor_proto::Result<Option<SourceInfo>>;
1663
1664    /// Return the length of this circuit.
1665    fn m_num_hops(&self) -> tor_circmgr::Result<usize>;
1666}
1667
1668/// Mock for onion service client data tunnel.
1669#[async_trait]
1670trait MockableClientData: Debug {
1671    /// Conversation
1672    type Conversation<'r>
1673    where
1674        Self: 'r;
1675    /// Converse
1676    async fn m_start_conversation_last_hop(
1677        &self,
1678        msg: Option<AnyRelayMsg>,
1679        reply_handler: impl MsgHandler + Send + 'static,
1680    ) -> tor_circmgr::Result<Self::Conversation<'_>>;
1681
1682    /// Add a virtual hop to the circuit.
1683    async fn m_extend_virtual(
1684        &self,
1685        protocol: handshake::RelayProtocol,
1686        role: handshake::HandshakeRole,
1687        handshake: impl handshake::KeyGenerator + Send,
1688        params: CircParameters,
1689        capabilities: &tor_protover::Protocols,
1690    ) -> tor_circmgr::Result<()>;
1691
1692    /// Return the number of our own hops in this circuit.
1693    ///
1694    /// This does not count any hops for the service's rendezvous circuit.
1695    /// It does count our virtual hop, if we have one.
1696    /// (That isn't a problem, since we only use this method to calculate
1697    /// timeouts, and we only calculate timeouts _before_ we establish
1698    /// the virtual hop.)
1699    fn m_num_own_hops(&self) -> tor_circmgr::Result<usize>;
1700}
1701
1702/// Mock for onion service client introduction tunnel.
1703#[async_trait]
1704trait MockableClientIntro: Debug {
1705    /// Conversation
1706    type Conversation<'r>
1707    where
1708        Self: 'r;
1709    /// Converse
1710    async fn m_start_conversation_last_hop(
1711        &self,
1712        msg: Option<AnyRelayMsg>,
1713        reply_handler: impl MsgHandler + Send + 'static,
1714    ) -> tor_circmgr::Result<Self::Conversation<'_>>;
1715
1716    /// Return the number of hops in this circuit.
1717    fn m_num_hops(&self) -> tor_circmgr::Result<usize>;
1718}
1719
1720impl<R: Runtime> MocksForConnect<R> for () {
1721    type HsCircPool = HsCircPool<R>;
1722    type Rng = rand::rngs::ThreadRng;
1723    type KeyGenerator = HsNtorHkdfKeyGenerator;
1724
1725    fn thread_rng(&self) -> Self::Rng {
1726        rand::rng()
1727    }
1728
1729    fn rendezvous_handshake(
1730        &self,
1731        handshake_state: hs_ntor::HsNtorClientState,
1732        rend2_msg: Rendezvous2,
1733        intro_index: IntroPtIndex,
1734        rend_pt: &RendPtIdentityForError,
1735    ) -> Result<Self::KeyGenerator, FAE> {
1736        // Try to complete the cryptographic handshake.
1737        handshake_state
1738            .client_receive_rend(rend2_msg.handshake_info())
1739            // If this goes wrong. either the onion service has mangled the crypto,
1740            // or the rendezvous point has misbehaved (that that is possible is a protocol bug),
1741            // or we have used the wrong handshake_state (let's assume that's not true).
1742            //
1743            // If this happens we'll go and try another RPT.
1744            .map_err(|error| FAE::RendezvousCompletionHandshake {
1745                error,
1746                intro_index,
1747                rend_pt: rend_pt.clone(),
1748            })
1749    }
1750}
1751#[async_trait]
1752impl<R: Runtime> MockableCircPool<R> for HsCircPool<R> {
1753    type DirTunnel = ClientOnionServiceDirTunnel;
1754    type DataTunnel = ClientOnionServiceDataTunnel;
1755    type IntroTunnel = ClientOnionServiceIntroTunnel;
1756
1757    #[instrument(level = "trace", skip_all)]
1758    async fn m_get_or_launch_dir(
1759        &self,
1760        netdir: &NetDir,
1761        target: impl CircTarget + Send + Sync + 'async_trait,
1762    ) -> tor_circmgr::Result<Self::DirTunnel> {
1763        Ok(HsCircPool::get_or_launch_client_dir(self, netdir, target).await?)
1764    }
1765    #[instrument(level = "trace", skip_all)]
1766    async fn m_get_or_launch_intro(
1767        &self,
1768        netdir: &NetDir,
1769        target: impl CircTarget + Send + Sync + 'async_trait,
1770    ) -> tor_circmgr::Result<Self::IntroTunnel> {
1771        Ok(HsCircPool::get_or_launch_client_intro(self, netdir, target).await?)
1772    }
1773    #[instrument(level = "trace", skip_all)]
1774    async fn m_get_or_launch_client_rend<'a>(
1775        &self,
1776        netdir: &'a NetDir,
1777    ) -> tor_circmgr::Result<(Self::DataTunnel, Relay<'a>)> {
1778        HsCircPool::get_or_launch_client_rend(self, netdir).await
1779    }
1780    fn m_estimate_timeout(&self, action: &TimeoutsAction) -> Duration {
1781        HsCircPool::estimate_timeout(self, action)
1782    }
1783}
1784#[async_trait]
1785impl MockableClientDir for ClientOnionServiceDirTunnel {
1786    /// Client circuit
1787    type DirStream = tor_proto::client::stream::DataStream;
1788    async fn m_begin_dir_stream(&self) -> tor_circmgr::Result<Self::DirStream> {
1789        Self::begin_dir_stream(self).await
1790    }
1791
1792    /// Get a tor_dirclient::SourceInfo for this circuit, if possible.
1793    fn m_source_info(&self) -> tor_proto::Result<Option<SourceInfo>> {
1794        SourceInfo::from_tunnel(self)
1795    }
1796
1797    fn m_num_hops(&self) -> tor_circmgr::Result<usize> {
1798        self.n_hops()
1799    }
1800}
1801
1802#[async_trait]
1803impl MockableClientData for ClientOnionServiceDataTunnel {
1804    type Conversation<'r> = tor_proto::Conversation<'r>;
1805
1806    async fn m_start_conversation_last_hop(
1807        &self,
1808        msg: Option<AnyRelayMsg>,
1809        reply_handler: impl MsgHandler + Send + 'static,
1810    ) -> tor_circmgr::Result<Self::Conversation<'_>> {
1811        Self::start_conversation(self, msg, reply_handler, TargetHop::LastHop).await
1812    }
1813
1814    async fn m_extend_virtual(
1815        &self,
1816        protocol: handshake::RelayProtocol,
1817        role: handshake::HandshakeRole,
1818        handshake: impl handshake::KeyGenerator + Send,
1819        params: CircParameters,
1820        capabilities: &tor_protover::Protocols,
1821    ) -> tor_circmgr::Result<()> {
1822        Self::extend_virtual(self, protocol, role, handshake, params, capabilities).await
1823    }
1824
1825    fn m_num_own_hops(&self) -> tor_circmgr::Result<usize> {
1826        self.n_hops()
1827    }
1828}
1829
1830#[async_trait]
1831impl MockableClientIntro for ClientOnionServiceIntroTunnel {
1832    type Conversation<'r> = tor_proto::Conversation<'r>;
1833
1834    async fn m_start_conversation_last_hop(
1835        &self,
1836        msg: Option<AnyRelayMsg>,
1837        reply_handler: impl MsgHandler + Send + 'static,
1838    ) -> tor_circmgr::Result<Self::Conversation<'_>> {
1839        Self::start_conversation(self, msg, reply_handler, TargetHop::LastHop).await
1840    }
1841
1842    fn m_num_hops(&self) -> tor_circmgr::Result<usize> {
1843        self.n_hops()
1844    }
1845}
1846
1847#[async_trait]
1848impl MockableConnectorData for Data {
1849    type DataTunnel = ClientOnionServiceDataTunnel;
1850    type MockGlobalState = ();
1851
1852    async fn connect<R: Runtime>(
1853        connector: &HsClientConnector<R>,
1854        netdir: Arc<NetDir>,
1855        config: Arc<Config>,
1856        hsid: HsId,
1857        data: &mut Self,
1858        secret_keys: HsClientSecretKeys,
1859    ) -> Result<Self::DataTunnel, ConnError> {
1860        connect(connector, netdir, config, hsid, data, secret_keys).await
1861    }
1862
1863    fn tunnel_is_ok(tunnel: &Self::DataTunnel) -> bool {
1864        !tunnel.is_closed()
1865    }
1866}
1867
1868#[cfg(test)]
1869mod test {
1870    // @@ begin test lint list maintained by maint/add_warning @@
1871    #![allow(clippy::bool_assert_comparison)]
1872    #![allow(clippy::clone_on_copy)]
1873    #![allow(clippy::dbg_macro)]
1874    #![allow(clippy::mixed_attributes_style)]
1875    #![allow(clippy::print_stderr)]
1876    #![allow(clippy::print_stdout)]
1877    #![allow(clippy::single_char_pattern)]
1878    #![allow(clippy::unwrap_used)]
1879    #![allow(clippy::unchecked_time_subtraction)]
1880    #![allow(clippy::useless_vec)]
1881    #![allow(clippy::needless_pass_by_value)]
1882    #![allow(clippy::string_slice)] // See arti#2571
1883    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
1884
1885    #![allow(dead_code, unused_variables)] // TODO HS TESTS delete, after tests are completed
1886
1887    use super::*;
1888    use crate::*;
1889    use itertools::chain;
1890    use std::iter;
1891    use tokio_crate as tokio;
1892    use tor_async_utils::JoinReadWrite;
1893    use tor_basic_utils::test_rng::{TestingRng, testing_rng};
1894    use tor_hscrypto::pk::{HsClientDescEncKey, HsClientDescEncKeypair};
1895    use tor_llcrypto::pk::curve25519;
1896    use tor_netdoc::doc::{hsdesc::test_data, netstatus::Lifetime};
1897    use tor_rtcompat::RuntimeSubstExt as _;
1898    use tor_rtcompat::tokio::TokioNativeTlsRuntime;
1899    use tor_rtmock::simple_time::SimpleMockTimeProvider;
1900    use tracing_test::traced_test;
1901
1902    #[derive(derive_more::Debug, Default)]
1903    struct MocksGlobal {
1904        hsdirs_asked: Vec<OwnedCircTarget>,
1905        got_desc: Option<HsDesc>,
1906        #[debug(skip)]
1907        rendezvous: Option<Box<dyn MsgHandler + Send + 'static>>,
1908        intro_acks: Vec<(IntroduceAck, MetaCellDisposition)>,
1909    }
1910
1911    #[derive(Clone, Debug)]
1912    struct Mocks<I> {
1913        mglobal: Arc<Mutex<MocksGlobal>>,
1914        id: I,
1915    }
1916
1917    struct MockKeyGenerator;
1918
1919    impl handshake::KeyGenerator for MockKeyGenerator {
1920        fn expand(self, _keylen: usize) -> tor_proto::Result<tor_bytes::SecretBuf> {
1921            todo!()
1922        }
1923    }
1924
1925    impl<R: Runtime> MocksForConnect<R> for Mocks<()> {
1926        type HsCircPool = Mocks<()>;
1927        type Rng = TestingRng;
1928        type KeyGenerator = MockKeyGenerator;
1929
1930        fn test_got_desc(&self, desc: &HsDesc) {
1931            self.mglobal.lock().unwrap().got_desc = Some(desc.clone());
1932        }
1933
1934        fn test_got_ipts(&self, desc: &[UsableIntroPt]) {}
1935
1936        fn thread_rng(&self) -> Self::Rng {
1937            testing_rng()
1938        }
1939
1940        fn rendezvous_handshake(
1941            &self,
1942            _handshake_state: hs_ntor::HsNtorClientState,
1943            _rend2_msg: Rendezvous2,
1944            _intro_index: IntroPtIndex,
1945            _rend_pt: &RendPtIdentityForError,
1946        ) -> Result<Self::KeyGenerator, FAE> {
1947            Ok(MockKeyGenerator)
1948        }
1949    }
1950    #[async_trait]
1951    impl<R: Runtime> MockableCircPool<R> for Mocks<()> {
1952        type DataTunnel = Mocks<()>;
1953        type DirTunnel = Mocks<()>;
1954        type IntroTunnel = Mocks<()>;
1955
1956        async fn m_get_or_launch_dir(
1957            &self,
1958            _netdir: &NetDir,
1959            target: impl CircTarget + Send + Sync + 'async_trait,
1960        ) -> tor_circmgr::Result<Self::DirTunnel> {
1961            let target = OwnedCircTarget::from_circ_target(&target);
1962            self.mglobal.lock().unwrap().hsdirs_asked.push(target);
1963            Ok(self.clone())
1964        }
1965        async fn m_get_or_launch_intro(
1966            &self,
1967            _netdir: &NetDir,
1968            target: impl CircTarget + Send + Sync + 'async_trait,
1969        ) -> tor_circmgr::Result<Self::IntroTunnel> {
1970            Ok(self.clone())
1971        }
1972        /// Client circuit
1973        async fn m_get_or_launch_client_rend<'a>(
1974            &self,
1975            netdir: &'a NetDir,
1976        ) -> tor_circmgr::Result<(Self::DataTunnel, Relay<'a>)> {
1977            // Pick one of the relays we know to be in the test net as our RPT
1978            let rpt = netdir.by_id(&Ed25519Identity::from([12; 32])).unwrap();
1979
1980            Ok((self.clone(), rpt))
1981        }
1982
1983        fn m_estimate_timeout(&self, action: &TimeoutsAction) -> Duration {
1984            Duration::from_secs(10)
1985        }
1986    }
1987    #[async_trait]
1988    impl MockableClientDir for Mocks<()> {
1989        type DirStream = JoinReadWrite<futures::io::Cursor<Box<[u8]>>, futures::io::Sink>;
1990        async fn m_begin_dir_stream(&self) -> tor_circmgr::Result<Self::DirStream> {
1991            let response = format!(
1992                r#"HTTP/1.1 200 OK
1993
1994{}"#,
1995                test_data::TEST_DATA_2
1996            )
1997            .into_bytes()
1998            .into_boxed_slice();
1999
2000            Ok(JoinReadWrite::new(
2001                futures::io::Cursor::new(response),
2002                futures::io::sink(),
2003            ))
2004        }
2005
2006        fn m_source_info(&self) -> tor_proto::Result<Option<SourceInfo>> {
2007            Ok(None)
2008        }
2009
2010        fn m_num_hops(&self) -> tor_circmgr::Result<usize> {
2011            Ok(4)
2012        }
2013    }
2014
2015    #[async_trait]
2016    impl MockableClientData for Mocks<()> {
2017        type Conversation<'r> = &'r ();
2018        async fn m_start_conversation_last_hop(
2019            &self,
2020            msg: Option<AnyRelayMsg>,
2021            mut reply_handler: impl MsgHandler + Send + 'static,
2022        ) -> tor_circmgr::Result<Self::Conversation<'_>> {
2023            match msg {
2024                Some(AnyRelayMsg::EstablishRendezvous(_)) => {
2025                    let reply = RendezvousEstablished::default();
2026                    let disp = reply_handler.handle_msg(reply.into()).unwrap();
2027                    assert_eq!(disp, MetaCellDisposition::Consumed);
2028                    // Save this, because we'll need to use it later,
2029                    // when handling the INTRODUCE1
2030                    let mut global = self.mglobal.lock().unwrap();
2031                    global.rendezvous = Some(Box::new(reply_handler));
2032                }
2033                _ => panic!("unexpected msg {msg:?}"),
2034            }
2035
2036            Ok(&())
2037        }
2038
2039        async fn m_extend_virtual(
2040            &self,
2041            protocol: handshake::RelayProtocol,
2042            role: handshake::HandshakeRole,
2043            handshake: impl handshake::KeyGenerator + Send,
2044            params: CircParameters,
2045            capabilities: &tor_protover::Protocols,
2046        ) -> tor_circmgr::Result<()> {
2047            Ok(())
2048        }
2049
2050        fn m_num_own_hops(&self) -> tor_circmgr::Result<usize> {
2051            Ok(4)
2052        }
2053    }
2054
2055    #[async_trait]
2056    impl MockableClientIntro for Mocks<()> {
2057        type Conversation<'r> = &'r ();
2058        async fn m_start_conversation_last_hop(
2059            &self,
2060            msg: Option<AnyRelayMsg>,
2061            mut reply_handler: impl MsgHandler + Send + 'static,
2062        ) -> tor_circmgr::Result<Self::Conversation<'_>> {
2063            match msg {
2064                Some(AnyRelayMsg::Introduce1(introduce1)) => {
2065                    let mut global = self.mglobal.lock().unwrap();
2066                    let (reply, expected_disp) = global.intro_acks.remove(0);
2067                    let disp = reply_handler.handle_msg(reply.into()).unwrap();
2068                    assert_eq!(disp, expected_disp);
2069
2070                    // Mock the service's response
2071                    let rendezvous = global
2072                        .rendezvous
2073                        .as_mut()
2074                        .expect("got INTRODUCE1 before ESTABLISH_RENDEZVOUS?!");
2075                    let reply = Rendezvous2::new(b"dummy handshake info, ignored");
2076                    let disp = rendezvous.handle_msg(reply.into()).unwrap();
2077                    assert_eq!(disp, MetaCellDisposition::ConversationFinished);
2078                }
2079                _ => panic!("unexpected msg {msg:?}"),
2080            }
2081
2082            Ok(&())
2083        }
2084
2085        fn m_num_hops(&self) -> tor_circmgr::Result<usize> {
2086            Ok(4)
2087        }
2088    }
2089
2090    fn ks_hsc_desc_enc() -> HsClientDescEncKeypair {
2091        let pk: HsClientDescEncKey = curve25519::PublicKey::from(test_data::TEST_PUBKEY_2).into();
2092        let sk = curve25519::StaticSecret::from(test_data::TEST_SECKEY_2).into();
2093        HsClientDescEncKeypair::new(pk, sk)
2094    }
2095
2096    fn expected_hsdesc(hsid: HsId, netdir: &NetDir, now: SystemTime) -> HsDesc {
2097        let time_period = netdir.hs_time_period();
2098        let (hs_blind_id_key, subcredential) = HsIdKey::try_from(hsid)
2099            .unwrap()
2100            .compute_blinded_key(time_period)
2101            .unwrap();
2102        let hs_blind_id = hs_blind_id_key.id();
2103
2104        HsDesc::parse_decrypt_validate(
2105            test_data::TEST_DATA_2,
2106            &hs_blind_id,
2107            now,
2108            &subcredential,
2109            Some(&ks_hsc_desc_enc()),
2110        )
2111        .unwrap()
2112        .dangerously_assume_timely()
2113    }
2114
2115    fn build_test_netdir() -> Arc<NetDir> {
2116        let valid_after = humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap();
2117        let fresh_until = valid_after + humantime::parse_duration("1 hours").unwrap();
2118        let valid_until = valid_after + humantime::parse_duration("24 hours").unwrap();
2119        let lifetime = Lifetime::new(valid_after, fresh_until, valid_until).unwrap();
2120
2121        let netdir = tor_netdir::testnet::construct_custom_netdir_with_params(
2122            tor_netdir::testnet::simple_net_func,
2123            iter::empty::<(&str, _)>(),
2124            Some(lifetime),
2125        )
2126        .expect("failed to build default testing netdir");
2127
2128        Arc::new(netdir.unwrap_if_sufficient().unwrap())
2129    }
2130
2131    #[traced_test]
2132    #[tokio::test]
2133    async fn test_connect() {
2134        use MetaCellDisposition::*;
2135        let netdir = build_test_netdir();
2136        let runtime = TokioNativeTlsRuntime::current().unwrap();
2137        let now = humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap();
2138        let mock_sp = SimpleMockTimeProvider::from_wallclock(now);
2139        let runtime = runtime
2140            .with_sleep_provider(mock_sp.clone())
2141            .with_coarse_time_provider(mock_sp.clone());
2142
2143        let success = (
2144            IntroduceAck::new(IntroduceAckStatus::SUCCESS),
2145            ConversationFinished,
2146        );
2147
2148        let nack = (
2149            IntroduceAck::new(IntroduceAckStatus::NOT_RECOGNIZED),
2150            ConversationFinished,
2151        );
2152
2153        // The number of times to make Context:connect() fail due to intro NACK
2154        //
2155        // Set to 5 in order to trigger a rate-limit for all 6 HsDirs:
2156        //
2157        // there are 6 HsDirs in total, one of which is "used up" by the
2158        // first (successful) connect() attempt below.
2159        const INTRO_FAIL_COUNT: usize = 5;
2160
2161        /// The number of times we expect the client to retry the
2162        /// introduction per connect() call
2163        /// (it will essentially try two rounds of `intro_rend_connect()`,
2164        /// once with the cached descriptor, and once with the potentially
2165        /// new descriptor).
2166        const IPT_RETRY_COUNT: usize = 12;
2167
2168        // The first introduction will succeed
2169        let intro_acks = chain!(
2170            [&success],
2171            // But the next INTRO_FAIL_COUNT connect() will fail
2172            // (+1 because we want to fail *again*, in order to find
2173            // that there's now a limit on all our HsDirs)
2174            [&nack; IPT_RETRY_COUNT * (INTRO_FAIL_COUNT + 1)],
2175            // One more round of failures, to trigger a refecth after the rate-limit is lifted
2176            [&nack; IPT_RETRY_COUNT - 1],
2177            // After refetching the descriptor, the client will retry the introduction,
2178            // and succeed.
2179            [&success],
2180        )
2181        .cloned()
2182        .collect();
2183
2184        let mglobal = Arc::new(Mutex::new(MocksGlobal {
2185            intro_acks,
2186            ..Default::default()
2187        }));
2188
2189        let mocks = Mocks { mglobal, id: () };
2190        // From C Tor src/test/test_hs_common.c test_build_address
2191        let hsid = test_data::TEST_HSID_2.into();
2192        let mut data = Data::default();
2193        let mut expected_hsdirs_asked = 1;
2194
2195        let mut secret_keys_builder = HsClientSecretKeysBuilder::default();
2196        secret_keys_builder.ks_hsc_desc_enc(ks_hsc_desc_enc());
2197        let secret_keys = secret_keys_builder.build().unwrap();
2198
2199        let ctx = Context::new(
2200            &runtime,
2201            &mocks,
2202            Arc::clone(&netdir),
2203            Default::default(),
2204            hsid,
2205            secret_keys,
2206            mocks.clone(),
2207        )
2208        .unwrap();
2209
2210        let _got = ctx.connect(&mut data).await.unwrap();
2211
2212        // Our mock IPT hasn't sent any NACKs yet
2213        assert!(!logs_contain("NACKed, refetching descriptor and retrying"));
2214
2215        let hsdesc = expected_hsdesc(hsid, &netdir, now);
2216        {
2217            let mglobal = mocks.mglobal.lock().unwrap();
2218            assert_eq!(mglobal.hsdirs_asked.len(), expected_hsdirs_asked);
2219            // TODO hs: here and in other places, consider implementing PartialEq instead, or creating
2220            // an assert_dbg_eq macro (which would be part of a test_helpers crate or something)
2221            assert_eq!(
2222                format!("{:?}", mglobal.got_desc),
2223                format!("{:?}", Some(hsdesc.clone()))
2224            );
2225        }
2226
2227        // Check how long the descriptor is valid for
2228        let (start_time, end_time) = data.desc.as_ref().unwrap().desc.bounds();
2229        assert_eq!(start_time, None);
2230
2231        let desc_valid_until = humantime::parse_rfc3339("2023-02-11T20:00:00Z").unwrap();
2232        assert_eq!(end_time, Some(desc_valid_until));
2233
2234        // These attempts will all fail due to intro NACK,
2235        // and trigger a rate-limit for all 6 HsDirs
2236        for i in 1..=INTRO_FAIL_COUNT + 1 {
2237            let err = ctx.connect(&mut data).await.unwrap_err();
2238
2239            let is_intro_nack = |e| matches!(e, FAE::IntroductionFailed { status, .. });
2240
2241            // All attempts failed because of our repeated intro NACKs
2242            assert!(matches!(err, CE::Failed(e) if e.clone().into_iter().all(is_intro_nack)));
2243
2244            {
2245                assert!(logs_contain("NACKed, refetching descriptor and retrying"));
2246                let mglobal = mocks.mglobal.lock().unwrap();
2247                // Because all intro attempts failed with NACK (NOT_RECOGNIZED),
2248                // the client must've tried to refetch the descriptor
2249                if i <= INTRO_FAIL_COUNT {
2250                    // No rate limiting yet, so the client must've tried to fetch a new
2251                    // descriptor, before failing again.
2252                    expected_hsdirs_asked += 1;
2253                    assert!(!logs_contain("but all hsdirs are rate-limited"));
2254                    assert_eq!(mglobal.hsdirs_asked.len(), expected_hsdirs_asked);
2255                } else {
2256                    // The final failure won't lead to an HsDir fetch
2257                    // because all HsDirs will be rate-limited at that point
2258                    assert!(logs_contain("but all hsdirs are rate-limited"));
2259                    assert_eq!(mglobal.hsdirs_asked.len(), expected_hsdirs_asked);
2260                }
2261
2262                // Same descriptor each time
2263                // TODO hs: here and in other places, consider implementing PartialEq instead, or creating
2264                // an assert_dbg_eq macro (which would be part of a test_helpers crate or something)
2265                assert_eq!(
2266                    format!("{:?}", mglobal.got_desc),
2267                    format!("{:?}", Some(hsdesc.clone()))
2268                );
2269            }
2270
2271            let (start_time, end_time) = data.desc.as_ref().unwrap().desc.bounds();
2272            assert_eq!(start_time, None);
2273
2274            let desc_valid_until = humantime::parse_rfc3339("2023-02-11T20:00:00Z").unwrap();
2275            assert_eq!(end_time, Some(desc_valid_until));
2276        }
2277
2278        // By default, the HsDir fetches are rate-limited for 15min
2279        mock_sp.advance(Duration::from_secs(15 * 60));
2280        // Finally, we succeed.
2281        let _got = ctx.connect(&mut data).await.unwrap();
2282
2283        // And it turns out we did, in fact refetch the descriptor
2284
2285        // Finally, we try again, but find that all HsDirs are now rate-limited!
2286        // So now we advance the time to lift the rate limit, and hope that
2287        //
2288        // TODO HS TESTS: we could extend our mock infrastructure
2289        // to support returning a different hsdesc this time,
2290        // with various revision counters, to check that the client is indeed
2291        // keeping the newest one.
2292        {
2293            assert!(logs_contain("NACKed, refetching descriptor and retrying"));
2294            let mglobal = mocks.mglobal.lock().unwrap();
2295            // Because all intro attempts failed with NACK (NOT_RECOGNIZED),
2296            // the client must've tried to refetch the descriptor
2297            expected_hsdirs_asked += 1;
2298            assert_eq!(mglobal.hsdirs_asked.len(), expected_hsdirs_asked);
2299        }
2300
2301        // TODO HS TESTS: check the circuit in got is the one we gave out
2302
2303        // TODO HS TESTS: continue with this
2304    }
2305
2306    // TODO HS TESTS: Test IPT state management and expiry:
2307    //   - obtain a test descriptor with only a broken ipt
2308    //     (broken in the sense that intro can be attempted, but will fail somehow)
2309    //   - try to make a connection and expect it to fail
2310    //   - assert that the ipt data isn't empty
2311    //   - cause the descriptor to expire (advance clock)
2312    //   - start using a mocked RNG if we weren't already and pin its seed here
2313    //   - make a new descriptor with two IPTs: the broken one from earlier, and a new one
2314    //   - make a new connection
2315    //   - use test_got_ipts to check that the random numbers
2316    //     would sort the bad intro first, *and* that the good one is appears first
2317    //   - assert that connection succeeded
2318    //   - cause the circuit and descriptor to expire (advance clock)
2319    //   - go back to the previous descriptor contents, but with a new validity period
2320    //   - try to make a connection
2321    //   - use test_got_ipts to check that only the broken ipt is present
2322
2323    // TODO HS TESTS: test retries (of every retry loop we have here)
2324    // TODO HS TESTS: test error paths
2325}