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