tor_hsclient/
connect.rs

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