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 {}, for period {}, using {} hsdirs",
487            &self.hsid,
488            self.netdir.hs_time_period(),
489            hs_dirs.len()
490        );
491
492        // We might consider launching requests to multiple HsDirs in parallel.
493        //   https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1118#note_2894463
494        // But C Tor doesn't and our HS experts don't consider that important:
495        //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914436
496        // (Additionally, making multiple HSDir requests at once may make us
497        // more vulnerable to traffic analysis.)
498        let mut attempts = hs_dirs.iter().cycle().take(max_total_attempts);
499        let mut errors = RetryError::in_attempt_to("retrieve hidden service descriptor");
500        let desc = loop {
501            let relay = match attempts.next() {
502                Some(relay) => relay,
503                None => {
504                    return Err(if errors.is_empty() {
505                        CE::NoHsDirs
506                    } else {
507                        CE::DescriptorDownload(errors)
508                    });
509                }
510            };
511            let hsdir_for_error: Sensitive<Ed25519Identity> = (*relay.id()).into();
512            match self
513                .runtime
514                .timeout(each_timeout, self.descriptor_fetch_attempt(relay))
515                .await
516                .unwrap_or(Err(DescriptorErrorDetail::Timeout))
517            {
518                Ok(desc) => break desc,
519                Err(error) => {
520                    if error.should_report_as_suspicious() {
521                        // Note that not every protocol violation is suspicious:
522                        // we only warn on the protocol violations that look like attempts
523                        // to do a traffic tagging attack via hsdir inflation.
524                        // (See proposal 360.)
525                        warn_report!(
526                            &error,
527                            "Suspicious failure while downloading hsdesc for {} from relay {}",
528                            &self.hsid,
529                            relay.display_relay_ids(),
530                        );
531                    } else {
532                        debug_report!(
533                            &error,
534                            "failed hsdir desc fetch for {} from {}/{}",
535                            &self.hsid,
536                            &relay.id(),
537                            &relay.rsa_id()
538                        );
539                    }
540                    errors.push_timed(
541                        tor_error::Report(DescriptorError {
542                            hsdir: hsdir_for_error,
543                            error,
544                        }),
545                        self.runtime.now(),
546                        Some(self.runtime.wallclock()),
547                    );
548                }
549            }
550        };
551
552        // Store the bounded value in the cache for reuse,
553        // but return a reference to the unwrapped `HsDesc`.
554        //
555        // The `HsDesc` must be owned by `data.desc`,
556        // so first add it to `data.desc`,
557        // and then dangerously_assume_timely to get a reference out again.
558        //
559        // It is safe to dangerously_assume_timely,
560        // as descriptor_fetch_attempt has already checked the timeliness of the descriptor.
561        let ret = data.insert(desc);
562        Ok(ret.as_ref().dangerously_assume_timely())
563    }
564
565    /// Make one attempt to fetch the descriptor from a specific hsdir
566    ///
567    /// No timeout
568    ///
569    /// On success, returns the descriptor.
570    ///
571    /// While the returned descriptor is `TimerangeBound`, its validity at the current time *has*
572    /// been checked.
573    #[instrument(level = "trace", skip_all)]
574    async fn descriptor_fetch_attempt(
575        &self,
576        hsdir: &Relay<'_>,
577    ) -> Result<TimerangeBound<HsDesc>, DescriptorErrorDetail> {
578        let max_len: usize = self
579            .netdir
580            .params()
581            .hsdir_max_desc_size
582            .get()
583            .try_into()
584            .map_err(into_internal!("BoundedInt was not truly bounded!"))?;
585        let request = {
586            let mut r = tor_dirclient::request::HsDescDownloadRequest::new(self.hs_blind_id);
587            r.set_max_len(max_len);
588            r
589        };
590        trace!(
591            "hsdir for {}, trying {}/{}, request {:?} (http request {:?})",
592            &self.hsid,
593            &hsdir.id(),
594            &hsdir.rsa_id(),
595            &request,
596            request.debug_request()
597        );
598
599        let circuit = self
600            .circpool
601            .m_get_or_launch_dir(&self.netdir, OwnedCircTarget::from_circ_target(hsdir))
602            .await?;
603        let source: Option<SourceInfo> = circuit
604            .m_source_info()
605            .map_err(into_internal!("Couldn't get SourceInfo for circuit"))?;
606        let mut stream = circuit
607            .m_begin_dir_stream()
608            .await
609            .map_err(DescriptorErrorDetail::Circuit)?;
610
611        let response = tor_dirclient::send_request(self.runtime, &request, &mut stream, source)
612            .await
613            .map_err(|dir_error| match dir_error {
614                tor_dirclient::Error::RequestFailed(rfe) => DescriptorErrorDetail::from(rfe.error),
615                tor_dirclient::Error::CircMgr(ce) => into_internal!(
616                    "tor-dirclient complains about circmgr going wrong but we gave it a stream"
617                )(ce)
618                .into(),
619                other => into_internal!(
620                    "tor-dirclient gave unexpected error, tor-hsclient code needs updating"
621                )(other)
622                .into(),
623            })?;
624
625        let desc_text = response.into_output_string().map_err(|rfe| rfe.error)?;
626        let hsc_desc_enc = self.secret_keys.keys.ks_hsc_desc_enc.as_ref();
627
628        let now = self.runtime.wallclock();
629
630        HsDesc::parse_decrypt_validate(
631            &desc_text,
632            &self.hs_blind_id,
633            now,
634            &self.subcredential,
635            hsc_desc_enc,
636        )
637        .map_err(DescriptorErrorDetail::from)
638    }
639
640    /// Given the descriptor, try to connect to service
641    ///
642    /// Does all necessary retries, timeouts, etc.
643    async fn intro_rend_connect(
644        &self,
645        desc: &HsDesc,
646        data: &mut DataIpts,
647    ) -> Result<DataTunnel!(R, M), CE> {
648        // Maximum number of rendezvous/introduction attempts we'll make
649        let max_total_attempts = self
650            .config
651            .retry
652            .hs_intro_rend_attempts()
653            .try_into()
654            // User specified a very large u32.  We must be downcasting it to 16bit!
655            // let's give them as many retries as we can manage.
656            .unwrap_or(usize::MAX);
657
658        // Limit on the duration of each attempt to establish a rendezvous point
659        //
660        // This *might* include establishing a fresh circuit,
661        // if the HsCircPool's pool is empty.
662        let rend_timeout = self.estimate_timeout(&[
663            (1, TimeoutsAction::BuildCircuit { length: HOPS }), // build circuit
664            (1, TimeoutsAction::RoundTrip { length: HOPS }),    // One ESTABLISH_RENDEZVOUS
665        ]);
666
667        // Limit on the duration of each attempt to negotiate with an introduction point
668        //
669        // *Does* include establishing the circuit.
670        let intro_timeout = self.estimate_timeout(&[
671            (1, TimeoutsAction::BuildCircuit { length: HOPS }), // build circuit
672            // This does some crypto too, but we don't account for that.
673            (1, TimeoutsAction::RoundTrip { length: HOPS }), // One INTRODUCE1/INTRODUCE_ACK
674        ]);
675
676        // Timeout estimator for the action that the HS will take in building
677        // its circuit to the RPT.
678        let hs_build_action = TimeoutsAction::BuildCircuit {
679            length: if desc.is_single_onion_service() {
680                1
681            } else {
682                HOPS
683            },
684        };
685        // Limit on the duration of each attempt for activities involving both
686        // RPT and IPT.
687        let rpt_ipt_timeout = self.estimate_timeout(&[
688            // The API requires us to specify a number of circuit builds and round trips.
689            // So what we tell the estimator is a rather imprecise description.
690            // (TODO it would be nice if the circmgr offered us a one-way trip Action).
691            //
692            // What we are timing here is:
693            //
694            //    INTRODUCE2 goes from IPT to HS
695            //    but that happens in parallel with us waiting for INTRODUCE_ACK,
696            //    which is controlled by `intro_timeout` so not pat of `ipt_rpt_timeout`.
697            //    and which has to come HOPS hops.  So don't count INTRODUCE2 here.
698            //
699            //    HS builds to our RPT
700            (1, hs_build_action),
701            //
702            //    RENDEZVOUS1 goes from HS to RPT.  `hs_hops`, one-way.
703            //    RENDEZVOUS2 goes from RPT to us.  HOPS, one-way.
704            //    Together, we squint a bit and call this a HOPS round trip:
705            (1, TimeoutsAction::RoundTrip { length: HOPS }),
706        ]);
707
708        // We can't reliably distinguish IPT failure from RPT failure, so we iterate over IPTs
709        // (best first) and each time use a random RPT.
710
711        // We limit the number of rendezvous establishment attempts, separately, since we don't
712        // try to talk to the intro pt until we've established the rendezvous circuit.
713        let mut rend_attempts = 0..max_total_attempts;
714
715        // But, we put all the errors into the same bucket, since we might have a mixture.
716        let mut errors = RetryError::in_attempt_to("make circuit to hidden service");
717
718        // Note that IntroPtIndex is *not* the index into this Vec.
719        // It is the index into the original list of introduction points in the descriptor.
720        let mut usable_intros: Vec<UsableIntroPt> = desc
721            .intro_points()
722            .iter()
723            .enumerate()
724            .map(|(intro_index, intro_desc)| {
725                let intro_index = intro_index.into();
726                let intro_target = ipt_to_circtarget(intro_desc, &self.netdir)
727                    .map_err(|error| FAE::UnusableIntro { error, intro_index })?;
728                // Lack of TAIT means this clone
729                let intro_target = OwnedCircTarget::from_circ_target(&intro_target);
730                Ok::<_, FailedAttemptError>(UsableIntroPt {
731                    intro_index,
732                    intro_desc,
733                    intro_target,
734                    sort_rand: self.mocks.thread_rng().random(),
735                })
736            })
737            .filter_map(|entry| match entry {
738                Ok(y) => Some(y),
739                Err(e) => {
740                    errors.push_timed(e, self.runtime.now(), Some(self.runtime.wallclock()));
741                    None
742                }
743            })
744            .collect_vec();
745
746        // Delete experience information for now-unlisted intro points
747        // Otherwise, as the IPTs change `Data` might grow without bound,
748        // if we keep reconnecting to the same HS.
749        data.retain(|k, _v| {
750            usable_intros
751                .iter()
752                .any(|ipt| RelayIdForExperience::for_lookup(&ipt.intro_target).any(|id| &id == k))
753        });
754
755        // Join with existing state recording our experiences,
756        // sort by descending goodness, and then randomly
757        // (so clients without any experience don't all pile onto the same, first, IPT)
758        usable_intros.sort_by_key(|ipt: &UsableIntroPt| {
759            let experience =
760                RelayIdForExperience::for_lookup(&ipt.intro_target).find_map(|id| data.get(&id));
761            IptSortKey {
762                outcome: experience.into(),
763                sort_rand: ipt.sort_rand,
764            }
765        });
766        self.mocks.test_got_ipts(&usable_intros);
767
768        let mut intro_attempts = usable_intros.iter().cycle().take(max_total_attempts);
769
770        // We retain a rendezvous we managed to set up in here.  That way if we created it, and
771        // then failed before we actually needed it, we can reuse it.
772        // If we exit with an error, we will waste it - but because we isolate things we do
773        // for different services, it wouldn't be reusable anyway.
774        let mut saved_rendezvous = None;
775
776        // If we are using proof-of-work DoS mitigation, this chooses an
777        // algorithm and initial effort, and adjusts that effort when we retry.
778        let mut pow_client = HsPowClient::new(&self.hs_blind_id, desc);
779
780        // We might consider making multiple INTRODUCE attempts to different
781        // IPTs in parallel, and somehow aggregating the errors and
782        // experiences.
783        // However our HS experts don't consider that important:
784        //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914438
785        // Parallelizing our HsCircPool circuit building would likely have
786        // greater impact. (See #1149.)
787        loop {
788            // When did we start doing things that depended on the IPT?
789            //
790            // Used for recording our experience with the selected IPT
791            let mut ipt_use_started = None::<Instant>;
792
793            // Error handling inner async block (analogous to an IEFE):
794            //  * Ok(Some()) means this attempt succeeded
795            //  * Ok(None) means all attempts exhausted
796            //  * Err(error) means this attempt failed
797            //
798            // Error handling is rather complex here.  It's the primary job of *this* code to
799            // make sure that it's done right for timeouts.  (The individual component
800            // functions handle non-timeout errors.)  The different timeout errors have
801            // different amounts of information about the identity of the RPT and IPT: in each
802            // case, the error only mentions the RPT or IPT if that node is implicated in the
803            // timeout.
804            let outcome = async {
805                // We establish a rendezvous point first.  Although it appears from reading
806                // this code that this means we serialise establishment of the rendezvous and
807                // introduction circuits, this isn't actually the case.  The circmgr maintains
808                // a pool of circuits.  What actually happens in the "standing start" case is
809                // that we obtain a circuit for rendezvous from the circmgr's pool, expecting
810                // one to be available immediately; the circmgr will then start to build a new
811                // one to replenish its pool, and that happens in parallel with the work we do
812                // here - but in arrears.  If the circmgr pool is empty, then we must wait.
813                //
814                // Perhaps this should be parallelised here.  But that's really what the pool
815                // is for, since we expect building the rendezvous circuit and building the
816                // introduction circuit to take about the same length of time.
817                //
818                // We *do* serialise the ESTABLISH_RENDEZVOUS exchange, with the
819                // building of the introduction circuit.  That could be improved, at the cost
820                // of some additional complexity here.
821                //
822                // Our HS experts don't consider it important to increase the parallelism:
823                //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914444
824                //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914445
825                if saved_rendezvous.is_none() {
826                    debug!("hs conn to {}: setting up rendezvous point", &self.hsid);
827                    // Establish a rendezvous circuit.
828                    let Some(_): Option<usize> = rend_attempts.next() else {
829                        return Ok(None);
830                    };
831
832                    let mut using_rend_pt = None;
833                    saved_rendezvous = Some(
834                        self.runtime
835                            .timeout(rend_timeout, self.establish_rendezvous(&mut using_rend_pt))
836                            .await
837                            .map_err(|_: TimeoutError| match using_rend_pt {
838                                None => FAE::RendezvousCircuitObtain {
839                                    error: tor_circmgr::Error::CircTimeout(None),
840                                },
841                                Some(rend_pt) => FAE::RendezvousEstablishTimeout { rend_pt },
842                            })??,
843                    );
844                }
845
846                let Some(ipt) = intro_attempts.next() else {
847                    return Ok(None);
848                };
849                let intro_index = ipt.intro_index;
850
851                let proof_of_work = match pow_client.solve().await {
852                    Ok(solution) => solution,
853                    Err(e) => {
854                        debug!(
855                            "failing to compute proof-of-work, trying without. ({:?})",
856                            e
857                        );
858                        None
859                    }
860                };
861
862                // We record how long things take, starting from here, as
863                // as a statistic we'll use for the IPT in future.
864                // This is stored in a variable outside this async block,
865                // so that the outcome handling can use it.
866                ipt_use_started = Some(self.runtime.now());
867
868                // No `Option::get_or_try_insert_with`, or we'd avoid this expect()
869                let rend_pt_for_error = rend_pt_identity_for_error(
870                    &saved_rendezvous
871                        .as_ref()
872                        .expect("just made Some")
873                        .rend_relay,
874                );
875                debug!(
876                    "hs conn to {}: RPT {}",
877                    &self.hsid,
878                    rend_pt_for_error.as_inner()
879                );
880
881                let (rendezvous, introduced) = self
882                    .runtime
883                    .timeout(
884                        intro_timeout,
885                        self.exchange_introduce(ipt, &mut saved_rendezvous,
886                            proof_of_work),
887                    )
888                    .await
889                    .map_err(|_: TimeoutError| {
890                        // The intro point ought to give us a prompt ACK regardless of HS
891                        // behaviour or whatever is happening at the RPT, so blame the IPT.
892                        FAE::IntroductionTimeout { intro_index }
893                    })?
894                    // TODO: Maybe try, once, to extend-and-reuse the intro circuit.
895                    //
896                    // If the introduction fails, the introduction circuit is in principle
897                    // still usable.  We believe that in this case, C Tor extends the intro
898                    // circuit by one hop to the next IPT to try.  That saves on building a
899                    // whole new 3-hop intro circuit.  However, our HS experts tell us that
900                    // if introduction fails at one IPT it is likely to fail at the others too,
901                    // so that optimisation might reduce our network impact and time to failure,
902                    // but isn't likely to improve our chances of success.
903                    //
904                    // However, it's not clear whether this approach risks contaminating
905                    // the 2nd attempt with some fault relating to the introduction point.
906                    // The 1st ipt might also gain more knowledge about which HS we're talking to.
907                    //
908                    // TODO SPEC: Discuss extend-and-reuse HS intro circuit after nack
909                    ?;
910                #[allow(unused_variables)] // it's *supposed* to be unused
911                let saved_rendezvous = (); // don't use `saved_rendezvous` any more, use rendezvous
912
913                let rend_pt = rend_pt_identity_for_error(&rendezvous.rend_relay);
914                let circ = self
915                    .runtime
916                    .timeout(
917                        rpt_ipt_timeout,
918                        self.complete_rendezvous(ipt, rendezvous, introduced),
919                    )
920                    .await
921                    .map_err(|_: TimeoutError| FAE::RendezvousCompletionTimeout {
922                        intro_index,
923                        rend_pt: rend_pt.clone(),
924                    })??;
925
926                debug!(
927                    "hs conn to {}: RPT {} IPT {}: success",
928                    &self.hsid,
929                    rend_pt.as_inner(),
930                    intro_index,
931                );
932                Ok::<_, FAE>(Some((intro_index, circ)))
933            }
934            .await;
935
936            // Store the experience `outcome` we had with IPT `intro_index`, in `data`
937            #[allow(clippy::unused_unit)] // -> () is here for error handling clarity
938            let mut store_experience = |intro_index, outcome| -> () {
939                (|| {
940                    let ipt = usable_intros
941                        .iter()
942                        .find(|ipt| ipt.intro_index == intro_index)
943                        .ok_or_else(|| internal!("IPT not found by index"))?;
944                    let id = RelayIdForExperience::for_store(&ipt.intro_target)?;
945                    let started = ipt_use_started.ok_or_else(|| {
946                        internal!("trying to record IPT use but no IPT start time noted")
947                    })?;
948                    let duration = self
949                        .runtime
950                        .now()
951                        .checked_duration_since(started)
952                        .ok_or_else(|| internal!("clock overflow calculating IPT use duration"))?;
953                    data.insert(id, IptExperience { duration, outcome });
954                    Ok::<_, Bug>(())
955                })()
956                .unwrap_or_else(|e| warn_report!(e, "error recording HS IPT use experience"));
957            };
958
959            match outcome {
960                Ok(Some((intro_index, y))) => {
961                    // Record successful outcome in Data
962                    store_experience(intro_index, Ok(()));
963                    return Ok(y);
964                }
965                Ok(None) => return Err(CE::Failed(errors)),
966                Err(error) => {
967                    debug_report!(&error, "hs conn to {}: attempt failed", &self.hsid);
968                    // Record error outcome in Data, if in fact we involved the IPT
969                    // at all.  The IPT information is be retrieved from `error`,
970                    // since only some of the errors implicate the introduction point.
971                    if let Some(intro_index) = error.intro_index() {
972                        store_experience(intro_index, Err(error.retry_time()));
973                    }
974                    errors.push_timed(error, self.runtime.now(), Some(self.runtime.wallclock()));
975
976                    // If we are using proof-of-work DoS mitigation, try harder next time
977                    pow_client.increase_effort();
978                }
979            }
980        }
981    }
982
983    /// Make one attempt to establish a rendezvous circuit
984    ///
985    /// This doesn't really depend on anything,
986    /// other than (obviously) the isolation implied by our circuit pool.
987    /// In particular it doesn't depend on the introduction point.
988    ///
989    /// Does not apply a timeout.
990    ///
991    /// On entry `using_rend_pt` is `None`.
992    /// This function will store `Some` when it finds out which relay
993    /// it is talking to and starts to converse with it.
994    /// That way, if a timeout occurs, the caller can add that information to the error.
995    #[instrument(level = "trace", skip_all)]
996    async fn establish_rendezvous(
997        &'c self,
998        using_rend_pt: &mut Option<RendPtIdentityForError>,
999    ) -> Result<Rendezvous<'c, R, M>, FAE> {
1000        let (rend_tunnel, rend_relay) = self
1001            .circpool
1002            .m_get_or_launch_client_rend(&self.netdir)
1003            .await
1004            .map_err(|error| FAE::RendezvousCircuitObtain { error })?;
1005
1006        let rend_pt = rend_pt_identity_for_error(&rend_relay);
1007        *using_rend_pt = Some(rend_pt.clone());
1008
1009        let rend_cookie: RendCookie = self.mocks.thread_rng().random();
1010        let message = EstablishRendezvous::new(rend_cookie);
1011
1012        let (rend_established_tx, rend_established_rx) = proto_oneshot::channel();
1013        let (rend2_tx, rend2_rx) = proto_oneshot::channel();
1014
1015        /// Handler which expects `RENDEZVOUS_ESTABLISHED` and then
1016        /// `RENDEZVOUS2`.   Returns each message via the corresponding `oneshot`.
1017        struct Handler {
1018            /// Sender for a RENDEZVOUS_ESTABLISHED message.
1019            rend_established_tx: proto_oneshot::Sender<RendezvousEstablished>,
1020            /// Sender for a RENDEZVOUS2 message.
1021            rend2_tx: proto_oneshot::Sender<Rendezvous2>,
1022        }
1023        impl MsgHandler for Handler {
1024            fn handle_msg(
1025                &mut self,
1026                msg: AnyRelayMsg,
1027            ) -> Result<MetaCellDisposition, tor_proto::Error> {
1028                // The first message we expect is a RENDEZVOUS_ESTABALISHED.
1029                if self.rend_established_tx.still_expected() {
1030                    self.rend_established_tx
1031                        .deliver_expected_message(msg, MetaCellDisposition::Consumed)
1032                } else {
1033                    self.rend2_tx
1034                        .deliver_expected_message(msg, MetaCellDisposition::ConversationFinished)
1035                }
1036            }
1037        }
1038
1039        debug!(
1040            "hs conn to {}: RPT {}: sending ESTABLISH_RENDEZVOUS",
1041            &self.hsid,
1042            rend_pt.as_inner(),
1043        );
1044
1045        let failed_map_err = |error| FAE::RendezvousEstablish {
1046            error,
1047            rend_pt: rend_pt.clone(),
1048        };
1049        let handler = Handler {
1050            rend_established_tx,
1051            rend2_tx,
1052        };
1053
1054        // TODO(conflux) This error handling is horrible. Problem is that this Mock system requires
1055        // to send back a tor_circmgr::Error while our reply handler requires a tor_proto::Error.
1056        // And unifying both is hard here considering it needs to be converted to yet another Error
1057        // type "FAE" so we have to do these hoops and jumps.
1058        rend_tunnel
1059            .m_start_conversation_last_hop(Some(message.into()), handler)
1060            .await
1061            .map_err(|e| {
1062                let proto_error = match e {
1063                    tor_circmgr::Error::Protocol { error, .. } => error,
1064                    _ => tor_proto::Error::CircuitClosed,
1065                };
1066                FAE::RendezvousEstablish {
1067                    error: proto_error,
1068                    rend_pt: rend_pt.clone(),
1069                }
1070            })?;
1071
1072        // `start_conversation` returns as soon as the control message has been sent.
1073        // We need to obtain the RENDEZVOUS_ESTABLISHED message, which is "returned" via the oneshot.
1074        let _: RendezvousEstablished = rend_established_rx.recv(failed_map_err).await?;
1075
1076        debug!(
1077            "hs conn to {}: RPT {}: got RENDEZVOUS_ESTABLISHED",
1078            &self.hsid,
1079            rend_pt.as_inner(),
1080        );
1081
1082        Ok(Rendezvous {
1083            rend_tunnel,
1084            rend_cookie,
1085            rend_relay,
1086            rend2_rx,
1087            marker: PhantomData,
1088        })
1089    }
1090
1091    /// Attempt (once) to send an INTRODUCE1 and wait for the INTRODUCE_ACK
1092    ///
1093    /// `take`s the input `rendezvous` (but only takes it if it gets that far)
1094    /// and, if successful, returns it.
1095    /// (This arranges that the rendezvous is "used up" precisely if
1096    /// we sent its secret somewhere.)
1097    ///
1098    /// Although this function handles the `Rendezvous`,
1099    /// nothing in it actually involves the rendezvous point.
1100    /// So if there's a failure, it's purely to do with the introduction point.
1101    ///
1102    /// Does not apply a timeout.
1103    #[allow(clippy::cognitive_complexity, clippy::type_complexity)] // TODO: Refactor
1104    #[instrument(level = "trace", skip_all)]
1105    async fn exchange_introduce(
1106        &'c self,
1107        ipt: &UsableIntroPt<'_>,
1108        rendezvous: &mut Option<Rendezvous<'c, R, M>>,
1109        proof_of_work: Option<ProofOfWork>,
1110    ) -> Result<(Rendezvous<'c, R, M>, Introduced<R, M>), FAE> {
1111        let intro_index = ipt.intro_index;
1112
1113        debug!(
1114            "hs conn to {}: IPT {}: obtaining intro circuit",
1115            &self.hsid, intro_index,
1116        );
1117
1118        let intro_circ = self
1119            .circpool
1120            .m_get_or_launch_intro(
1121                &self.netdir,
1122                ipt.intro_target.clone(), // &OwnedCircTarget isn't CircTarget apparently
1123            )
1124            .await
1125            .map_err(|error| FAE::IntroductionCircuitObtain { error, intro_index })?;
1126
1127        let rendezvous = rendezvous.take().ok_or_else(|| internal!("no rend"))?;
1128
1129        let rend_pt = rend_pt_identity_for_error(&rendezvous.rend_relay);
1130
1131        debug!(
1132            "hs conn to {}: RPT {} IPT {}: making introduction",
1133            &self.hsid,
1134            rend_pt.as_inner(),
1135            intro_index,
1136        );
1137
1138        // Now we construct an introduce1 message and perform the first part of the
1139        // rendezvous handshake.
1140        //
1141        // This process is tricky because the header of the INTRODUCE1 message
1142        // -- which depends on the IntroPt configuration -- is authenticated as
1143        // part of the HsDesc handshake.
1144
1145        // Construct the header, since we need it as input to our encryption.
1146        let intro_header = {
1147            let ipt_sid_key = ipt.intro_desc.ipt_sid_key();
1148            let intro1 = Introduce1::new(
1149                AuthKeyType::ED25519_SHA3_256,
1150                ipt_sid_key.as_bytes().to_vec(),
1151                vec![],
1152            );
1153            let mut header = vec![];
1154            intro1
1155                .encode_onto(&mut header)
1156                .map_err(into_internal!("couldn't encode intro1 header"))?;
1157            header
1158        };
1159
1160        // Construct the introduce payload, which tells the onion service how to find
1161        // our rendezvous point.  (We could do this earlier if we wanted.)
1162        let intro_payload = {
1163            let onion_key =
1164                intro_payload::OnionKey::NtorOnionKey(*rendezvous.rend_relay.ntor_onion_key());
1165            let linkspecs = rendezvous
1166                .rend_relay
1167                .linkspecs()
1168                .map_err(into_internal!("Couldn't encode link specifiers"))?;
1169            let payload = IntroduceHandshakePayload::new(
1170                rendezvous.rend_cookie,
1171                onion_key,
1172                linkspecs,
1173                proof_of_work,
1174            );
1175            let mut encoded = vec![];
1176            payload
1177                .write_onto(&mut encoded)
1178                .map_err(into_internal!("Couldn't encode introduce1 payload"))?;
1179            encoded
1180        };
1181
1182        // Perform the cryptographic handshake with the onion service.
1183        let service_info = hs_ntor::HsNtorServiceInfo::new(
1184            ipt.intro_desc.svc_ntor_key().clone(),
1185            ipt.intro_desc.ipt_sid_key().clone(),
1186            self.subcredential,
1187        );
1188        let handshake_state =
1189            hs_ntor::HsNtorClientState::new(&mut self.mocks.thread_rng(), service_info);
1190        let encrypted_body = handshake_state
1191            .client_send_intro(&intro_header, &intro_payload)
1192            .map_err(into_internal!("can't begin hs-ntor handshake"))?;
1193
1194        // Build our actual INTRODUCE1 message.
1195        let intro1_real = Introduce1::new(
1196            AuthKeyType::ED25519_SHA3_256,
1197            ipt.intro_desc.ipt_sid_key().as_bytes().to_vec(),
1198            encrypted_body,
1199        );
1200
1201        /// Handler which expects just `INTRODUCE_ACK`
1202        struct Handler {
1203            /// Sender for `INTRODUCE_ACK`
1204            intro_ack_tx: proto_oneshot::Sender<IntroduceAck>,
1205        }
1206        impl MsgHandler for Handler {
1207            fn handle_msg(
1208                &mut self,
1209                msg: AnyRelayMsg,
1210            ) -> Result<MetaCellDisposition, tor_proto::Error> {
1211                self.intro_ack_tx
1212                    .deliver_expected_message(msg, MetaCellDisposition::ConversationFinished)
1213            }
1214        }
1215        let failed_map_err = |error| FAE::IntroductionExchange { error, intro_index };
1216        let (intro_ack_tx, intro_ack_rx) = proto_oneshot::channel();
1217        let handler = Handler { intro_ack_tx };
1218
1219        debug!(
1220            "hs conn to {}: RPT {} IPT {}: making introduction - sending INTRODUCE1",
1221            &self.hsid,
1222            rend_pt.as_inner(),
1223            intro_index,
1224        );
1225
1226        // TODO(conflux) This error handling is horrible. Problem is that this Mock system requires
1227        // to send back a tor_circmgr::Error while our reply handler requires a tor_proto::Error.
1228        // And unifying both is hard here considering it needs to be converted to yet another Error
1229        // type "FAE" so we have to do these hoops and jumps.
1230        intro_circ
1231            .m_start_conversation_last_hop(Some(intro1_real.into()), handler)
1232            .await
1233            .map_err(|e| {
1234                let proto_error = match e {
1235                    tor_circmgr::Error::Protocol { error, .. } => error,
1236                    _ => tor_proto::Error::CircuitClosed,
1237                };
1238                FAE::IntroductionExchange {
1239                    error: proto_error,
1240                    intro_index,
1241                }
1242            })?;
1243
1244        // Status is checked by `.success()`, and we don't look at the extensions;
1245        // just discard the known-successful `IntroduceAck`
1246        let _: IntroduceAck =
1247            intro_ack_rx
1248                .recv(failed_map_err)
1249                .await?
1250                .success()
1251                .map_err(|status| FAE::IntroductionFailed {
1252                    status,
1253                    intro_index,
1254                })?;
1255
1256        debug!(
1257            "hs conn to {}: RPT {} IPT {}: making introduction - success",
1258            &self.hsid,
1259            rend_pt.as_inner(),
1260            intro_index,
1261        );
1262
1263        // Having received INTRODUCE_ACK. we can forget about this circuit
1264        // (and potentially tear it down).
1265        drop(intro_circ);
1266
1267        Ok((
1268            rendezvous,
1269            Introduced {
1270                handshake_state,
1271                marker: PhantomData,
1272            },
1273        ))
1274    }
1275
1276    /// Attempt (once) to connect a rendezvous circuit using the given intro pt
1277    ///
1278    /// Timeouts here might be due to the IPT, RPT, service,
1279    /// or any of the intermediate relays.
1280    ///
1281    /// If, rather than a timeout, we actually encounter some kind of error,
1282    /// we'll return the appropriate `FailedAttemptError`.
1283    /// (Who is responsible may vary, so the `FailedAttemptError` variant will reflect that.)
1284    ///
1285    /// Does not apply a timeout
1286    async fn complete_rendezvous(
1287        &'c self,
1288        ipt: &UsableIntroPt<'_>,
1289        rendezvous: Rendezvous<'c, R, M>,
1290        introduced: Introduced<R, M>,
1291    ) -> Result<DataTunnel!(R, M), FAE> {
1292        use tor_proto::client::circuit::handshake;
1293
1294        let rend_pt = rend_pt_identity_for_error(&rendezvous.rend_relay);
1295        let intro_index = ipt.intro_index;
1296        let failed_map_err = |error| FAE::RendezvousCompletionCircuitError {
1297            error,
1298            intro_index,
1299            rend_pt: rend_pt.clone(),
1300        };
1301
1302        debug!(
1303            "hs conn to {}: RPT {} IPT {}: awaiting rendezvous completion",
1304            &self.hsid,
1305            rend_pt.as_inner(),
1306            intro_index,
1307        );
1308
1309        let rend2_msg: Rendezvous2 = rendezvous.rend2_rx.recv(failed_map_err).await?;
1310
1311        debug!(
1312            "hs conn to {}: RPT {} IPT {}: received RENDEZVOUS2",
1313            &self.hsid,
1314            rend_pt.as_inner(),
1315            intro_index,
1316        );
1317
1318        // In theory would be great if we could have multiple introduction attempts in parallel
1319        // with similar x,X values but different IPTs.  However, our HS experts don't
1320        // think increasing parallelism here is important:
1321        //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914438
1322        let handshake_state = introduced.handshake_state;
1323
1324        // Try to complete the cryptographic handshake.
1325        let keygen = handshake_state
1326            .client_receive_rend(rend2_msg.handshake_info())
1327            // If this goes wrong. either the onion service has mangled the crypto,
1328            // or the rendezvous point has misbehaved (that that is possible is a protocol bug),
1329            // or we have used the wrong handshake_state (let's assume that's not true).
1330            //
1331            // If this happens we'll go and try another RPT.
1332            .map_err(|error| FAE::RendezvousCompletionHandshake {
1333                error,
1334                intro_index,
1335                rend_pt: rend_pt.clone(),
1336            })?;
1337
1338        let params = onion_circparams_from_netparams(self.netdir.params())
1339            .map_err(into_internal!("Failed to build CircParameters"))?;
1340        // TODO: We may be able to infer more about the supported protocols of the other side from our
1341        // handshake, and from its descriptors.
1342        //
1343        // TODO CC: This is relevant for congestion control!
1344        let protocols = self.netdir.client_protocol_status().required_protocols();
1345
1346        rendezvous
1347            .rend_tunnel
1348            .m_extend_virtual(
1349                handshake::RelayProtocol::HsV3,
1350                handshake::HandshakeRole::Initiator,
1351                keygen,
1352                params,
1353                protocols,
1354            )
1355            .await
1356            .map_err(into_internal!(
1357                "actually this is probably a 'circuit closed' error" // TODO HS
1358            ))?;
1359
1360        debug!(
1361            "hs conn to {}: RPT {} IPT {}: HS circuit established",
1362            &self.hsid,
1363            rend_pt.as_inner(),
1364            intro_index,
1365        );
1366
1367        Ok(rendezvous.rend_tunnel)
1368    }
1369
1370    /// Helper to estimate a timeout for a complicated operation
1371    ///
1372    /// `actions` is a list of `(count, action)`, where each entry
1373    /// represents doing `action`, `count` times sequentially.
1374    ///
1375    /// Combines the timeout estimates and returns an overall timeout.
1376    fn estimate_timeout(&self, actions: &[(u32, TimeoutsAction)]) -> Duration {
1377        // This algorithm is, perhaps, wrong.  For uncorrelated variables, a particular
1378        // percentile estimate for a sum of random variables, is not calculated by adding the
1379        // percentile estimates of the individual variables.
1380        //
1381        // But the actual lengths of times of the operations aren't uncorrelated.
1382        // If they were *perfectly* correlated, then this addition would be correct.
1383        // It will do for now; it just might be rather longer than it ought to be.
1384        actions
1385            .iter()
1386            .map(|(count, action)| {
1387                self.circpool
1388                    .m_estimate_timeout(action)
1389                    .saturating_mul(*count)
1390            })
1391            .fold(Duration::ZERO, Duration::saturating_add)
1392    }
1393}
1394
1395/// Mocks used for testing `connect.rs`
1396///
1397/// This is different to `MockableConnectorData`,
1398/// which is used to *replace* this file, when testing `state.rs`.
1399///
1400/// `MocksForConnect` provides mock facilities for *testing* this file.
1401//
1402// TODO this should probably live somewhere else, maybe tor-circmgr even?
1403// TODO this really ought to be made by macros or something
1404trait MocksForConnect<R>: Clone {
1405    /// HS circuit pool
1406    type HsCircPool: MockableCircPool<R>;
1407
1408    /// A random number generator
1409    type Rng: rand::Rng + rand::CryptoRng;
1410
1411    /// Tell tests we got this descriptor text
1412    fn test_got_desc(&self, _: &HsDesc) {}
1413    /// Tell tests we got this data tunnel.
1414    fn test_got_tunnel(&self, _: &DataTunnel!(R, Self)) {}
1415    /// Tell tests we have obtained and sorted the intros like this
1416    fn test_got_ipts(&self, _: &[UsableIntroPt]) {}
1417
1418    /// Return a random number generator
1419    fn thread_rng(&self) -> Self::Rng;
1420}
1421/// Mock for `HsCircPool`
1422///
1423/// Methods start with `m_` to avoid the following problem:
1424/// `ClientCirc::start_conversation` (say) means
1425/// to use the inherent method if one exists,
1426/// but will use a trait method if there isn't an inherent method.
1427///
1428/// So if the inherent method is renamed, the call in the impl here
1429/// turns into an always-recursive call.
1430/// This is not detected by the compiler due to the situation being
1431/// complicated by futures, `#[async_trait]` etc.
1432/// <https://github.com/rust-lang/rust/issues/111177>
1433#[async_trait]
1434trait MockableCircPool<R> {
1435    /// Directory tunnel.
1436    type DirTunnel: MockableClientDir;
1437    /// Data tunnel.
1438    type DataTunnel: MockableClientData;
1439    /// Intro tunnel.
1440    type IntroTunnel: MockableClientIntro;
1441
1442    async fn m_get_or_launch_dir(
1443        &self,
1444        netdir: &NetDir,
1445        target: impl CircTarget + Send + Sync + 'async_trait,
1446    ) -> tor_circmgr::Result<Self::DirTunnel>;
1447
1448    async fn m_get_or_launch_intro(
1449        &self,
1450        netdir: &NetDir,
1451        target: impl CircTarget + Send + Sync + 'async_trait,
1452    ) -> tor_circmgr::Result<Self::IntroTunnel>;
1453
1454    /// Client circuit
1455    async fn m_get_or_launch_client_rend<'a>(
1456        &self,
1457        netdir: &'a NetDir,
1458    ) -> tor_circmgr::Result<(Self::DataTunnel, Relay<'a>)>;
1459
1460    /// Estimate timeout
1461    fn m_estimate_timeout(&self, action: &TimeoutsAction) -> Duration;
1462}
1463
1464/// Mock for onion service client directory tunnel.
1465#[async_trait]
1466trait MockableClientDir: Debug {
1467    /// Client circuit
1468    type DirStream: AsyncRead + AsyncWrite + Send + Unpin;
1469    async fn m_begin_dir_stream(&self) -> tor_circmgr::Result<Self::DirStream>;
1470
1471    /// Get a tor_dirclient::SourceInfo for this circuit, if possible.
1472    fn m_source_info(&self) -> tor_proto::Result<Option<SourceInfo>>;
1473}
1474
1475/// Mock for onion service client data tunnel.
1476#[async_trait]
1477trait MockableClientData: Debug {
1478    /// Conversation
1479    type Conversation<'r>
1480    where
1481        Self: 'r;
1482    /// Converse
1483    async fn m_start_conversation_last_hop(
1484        &self,
1485        msg: Option<AnyRelayMsg>,
1486        reply_handler: impl MsgHandler + Send + 'static,
1487    ) -> tor_circmgr::Result<Self::Conversation<'_>>;
1488
1489    /// Add a virtual hop to the circuit.
1490    async fn m_extend_virtual(
1491        &self,
1492        protocol: tor_proto::client::circuit::handshake::RelayProtocol,
1493        role: tor_proto::client::circuit::handshake::HandshakeRole,
1494        handshake: impl tor_proto::client::circuit::handshake::KeyGenerator + Send,
1495        params: CircParameters,
1496        capabilities: &tor_protover::Protocols,
1497    ) -> tor_circmgr::Result<()>;
1498}
1499
1500/// Mock for onion service client introduction tunnel.
1501#[async_trait]
1502trait MockableClientIntro: Debug {
1503    /// Conversation
1504    type Conversation<'r>
1505    where
1506        Self: 'r;
1507    /// Converse
1508    async fn m_start_conversation_last_hop(
1509        &self,
1510        msg: Option<AnyRelayMsg>,
1511        reply_handler: impl MsgHandler + Send + 'static,
1512    ) -> tor_circmgr::Result<Self::Conversation<'_>>;
1513}
1514
1515impl<R: Runtime> MocksForConnect<R> for () {
1516    type HsCircPool = HsCircPool<R>;
1517    type Rng = rand::rngs::ThreadRng;
1518
1519    fn thread_rng(&self) -> Self::Rng {
1520        rand::rng()
1521    }
1522}
1523#[async_trait]
1524impl<R: Runtime> MockableCircPool<R> for HsCircPool<R> {
1525    type DirTunnel = ClientOnionServiceDirTunnel;
1526    type DataTunnel = ClientOnionServiceDataTunnel;
1527    type IntroTunnel = ClientOnionServiceIntroTunnel;
1528
1529    #[instrument(level = "trace", skip_all)]
1530    async fn m_get_or_launch_dir(
1531        &self,
1532        netdir: &NetDir,
1533        target: impl CircTarget + Send + Sync + 'async_trait,
1534    ) -> tor_circmgr::Result<Self::DirTunnel> {
1535        Ok(HsCircPool::get_or_launch_client_dir(self, netdir, target).await?)
1536    }
1537    #[instrument(level = "trace", skip_all)]
1538    async fn m_get_or_launch_intro(
1539        &self,
1540        netdir: &NetDir,
1541        target: impl CircTarget + Send + Sync + 'async_trait,
1542    ) -> tor_circmgr::Result<Self::IntroTunnel> {
1543        Ok(HsCircPool::get_or_launch_client_intro(self, netdir, target).await?)
1544    }
1545    #[instrument(level = "trace", skip_all)]
1546    async fn m_get_or_launch_client_rend<'a>(
1547        &self,
1548        netdir: &'a NetDir,
1549    ) -> tor_circmgr::Result<(Self::DataTunnel, Relay<'a>)> {
1550        HsCircPool::get_or_launch_client_rend(self, netdir).await
1551    }
1552    fn m_estimate_timeout(&self, action: &TimeoutsAction) -> Duration {
1553        HsCircPool::estimate_timeout(self, action)
1554    }
1555}
1556#[async_trait]
1557impl MockableClientDir for ClientOnionServiceDirTunnel {
1558    /// Client circuit
1559    type DirStream = tor_proto::client::stream::DataStream;
1560    async fn m_begin_dir_stream(&self) -> tor_circmgr::Result<Self::DirStream> {
1561        Self::begin_dir_stream(self).await
1562    }
1563
1564    /// Get a tor_dirclient::SourceInfo for this circuit, if possible.
1565    fn m_source_info(&self) -> tor_proto::Result<Option<SourceInfo>> {
1566        SourceInfo::from_tunnel(self)
1567    }
1568}
1569
1570#[async_trait]
1571impl MockableClientData for ClientOnionServiceDataTunnel {
1572    type Conversation<'r> = tor_proto::Conversation<'r>;
1573
1574    async fn m_start_conversation_last_hop(
1575        &self,
1576        msg: Option<AnyRelayMsg>,
1577        reply_handler: impl MsgHandler + Send + 'static,
1578    ) -> tor_circmgr::Result<Self::Conversation<'_>> {
1579        Self::start_conversation(self, msg, reply_handler, TargetHop::LastHop).await
1580    }
1581
1582    async fn m_extend_virtual(
1583        &self,
1584        protocol: tor_proto::client::circuit::handshake::RelayProtocol,
1585        role: tor_proto::client::circuit::handshake::HandshakeRole,
1586        handshake: impl tor_proto::client::circuit::handshake::KeyGenerator + Send,
1587        params: CircParameters,
1588        capabilities: &tor_protover::Protocols,
1589    ) -> tor_circmgr::Result<()> {
1590        Self::extend_virtual(self, protocol, role, handshake, params, capabilities).await
1591    }
1592}
1593
1594#[async_trait]
1595impl MockableClientIntro for ClientOnionServiceIntroTunnel {
1596    type Conversation<'r> = tor_proto::Conversation<'r>;
1597
1598    async fn m_start_conversation_last_hop(
1599        &self,
1600        msg: Option<AnyRelayMsg>,
1601        reply_handler: impl MsgHandler + Send + 'static,
1602    ) -> tor_circmgr::Result<Self::Conversation<'_>> {
1603        Self::start_conversation(self, msg, reply_handler, TargetHop::LastHop).await
1604    }
1605}
1606
1607#[async_trait]
1608impl MockableConnectorData for Data {
1609    type DataTunnel = ClientOnionServiceDataTunnel;
1610    type MockGlobalState = ();
1611
1612    async fn connect<R: Runtime>(
1613        connector: &HsClientConnector<R>,
1614        netdir: Arc<NetDir>,
1615        config: Arc<Config>,
1616        hsid: HsId,
1617        data: &mut Self,
1618        secret_keys: HsClientSecretKeys,
1619    ) -> Result<Self::DataTunnel, ConnError> {
1620        connect(connector, netdir, config, hsid, data, secret_keys).await
1621    }
1622
1623    fn tunnel_is_ok(tunnel: &Self::DataTunnel) -> bool {
1624        !tunnel.is_closed()
1625    }
1626}
1627
1628#[cfg(test)]
1629mod test {
1630    // @@ begin test lint list maintained by maint/add_warning @@
1631    #![allow(clippy::bool_assert_comparison)]
1632    #![allow(clippy::clone_on_copy)]
1633    #![allow(clippy::dbg_macro)]
1634    #![allow(clippy::mixed_attributes_style)]
1635    #![allow(clippy::print_stderr)]
1636    #![allow(clippy::print_stdout)]
1637    #![allow(clippy::single_char_pattern)]
1638    #![allow(clippy::unwrap_used)]
1639    #![allow(clippy::unchecked_time_subtraction)]
1640    #![allow(clippy::useless_vec)]
1641    #![allow(clippy::needless_pass_by_value)]
1642    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
1643
1644    #![allow(dead_code, unused_variables)] // TODO HS TESTS delete, after tests are completed
1645
1646    use super::*;
1647    use crate::*;
1648    use futures::FutureExt as _;
1649    use std::{iter, panic::AssertUnwindSafe};
1650    use tokio_crate as tokio;
1651    use tor_async_utils::JoinReadWrite;
1652    use tor_basic_utils::test_rng::{TestingRng, testing_rng};
1653    use tor_hscrypto::pk::{HsClientDescEncKey, HsClientDescEncKeypair};
1654    use tor_llcrypto::pk::curve25519;
1655    use tor_netdoc::doc::{hsdesc::test_data, netstatus::Lifetime};
1656    use tor_rtcompat::RuntimeSubstExt as _;
1657    use tor_rtcompat::tokio::TokioNativeTlsRuntime;
1658    use tor_rtmock::simple_time::SimpleMockTimeProvider;
1659    use tracing_test::traced_test;
1660
1661    #[derive(Debug, Default)]
1662    struct MocksGlobal {
1663        hsdirs_asked: Vec<OwnedCircTarget>,
1664        got_desc: Option<HsDesc>,
1665    }
1666    #[derive(Clone, Debug)]
1667    struct Mocks<I> {
1668        mglobal: Arc<Mutex<MocksGlobal>>,
1669        id: I,
1670    }
1671
1672    impl<I> Mocks<I> {
1673        fn map_id<J>(&self, f: impl FnOnce(&I) -> J) -> Mocks<J> {
1674            Mocks {
1675                mglobal: self.mglobal.clone(),
1676                id: f(&self.id),
1677            }
1678        }
1679    }
1680
1681    impl<R: Runtime> MocksForConnect<R> for Mocks<()> {
1682        type HsCircPool = Mocks<()>;
1683        type Rng = TestingRng;
1684
1685        fn test_got_desc(&self, desc: &HsDesc) {
1686            self.mglobal.lock().unwrap().got_desc = Some(desc.clone());
1687        }
1688
1689        fn test_got_ipts(&self, desc: &[UsableIntroPt]) {}
1690
1691        fn thread_rng(&self) -> Self::Rng {
1692            testing_rng()
1693        }
1694    }
1695    #[allow(clippy::diverging_sub_expression)] // async_trait + todo!()
1696    #[async_trait]
1697    impl<R: Runtime> MockableCircPool<R> for Mocks<()> {
1698        type DataTunnel = Mocks<()>;
1699        type DirTunnel = Mocks<()>;
1700        type IntroTunnel = Mocks<()>;
1701
1702        async fn m_get_or_launch_dir(
1703            &self,
1704            _netdir: &NetDir,
1705            target: impl CircTarget + Send + Sync + 'async_trait,
1706        ) -> tor_circmgr::Result<Self::DirTunnel> {
1707            let target = OwnedCircTarget::from_circ_target(&target);
1708            self.mglobal.lock().unwrap().hsdirs_asked.push(target);
1709            // Adding the `Arc` here is a little ugly, but that's what we get
1710            // for using the same Mocks for everything.
1711            Ok(self.clone())
1712        }
1713        async fn m_get_or_launch_intro(
1714            &self,
1715            _netdir: &NetDir,
1716            target: impl CircTarget + Send + Sync + 'async_trait,
1717        ) -> tor_circmgr::Result<Self::IntroTunnel> {
1718            todo!()
1719        }
1720        /// Client circuit
1721        async fn m_get_or_launch_client_rend<'a>(
1722            &self,
1723            netdir: &'a NetDir,
1724        ) -> tor_circmgr::Result<(Self::DataTunnel, Relay<'a>)> {
1725            todo!()
1726        }
1727
1728        fn m_estimate_timeout(&self, action: &TimeoutsAction) -> Duration {
1729            Duration::from_secs(10)
1730        }
1731    }
1732    #[allow(clippy::diverging_sub_expression)] // async_trait + todo!()
1733    #[async_trait]
1734    impl MockableClientDir for Mocks<()> {
1735        type DirStream = JoinReadWrite<futures::io::Cursor<Box<[u8]>>, futures::io::Sink>;
1736        async fn m_begin_dir_stream(&self) -> tor_circmgr::Result<Self::DirStream> {
1737            let response = format!(
1738                r#"HTTP/1.1 200 OK
1739
1740{}"#,
1741                test_data::TEST_DATA_2
1742            )
1743            .into_bytes()
1744            .into_boxed_slice();
1745
1746            Ok(JoinReadWrite::new(
1747                futures::io::Cursor::new(response),
1748                futures::io::sink(),
1749            ))
1750        }
1751
1752        fn m_source_info(&self) -> tor_proto::Result<Option<SourceInfo>> {
1753            Ok(None)
1754        }
1755    }
1756
1757    #[allow(clippy::diverging_sub_expression)] // async_trait + todo!()
1758    #[async_trait]
1759    impl MockableClientData for Mocks<()> {
1760        type Conversation<'r> = &'r ();
1761        async fn m_start_conversation_last_hop(
1762            &self,
1763            msg: Option<AnyRelayMsg>,
1764            reply_handler: impl MsgHandler + Send + 'static,
1765        ) -> tor_circmgr::Result<Self::Conversation<'_>> {
1766            todo!()
1767        }
1768
1769        async fn m_extend_virtual(
1770            &self,
1771            protocol: tor_proto::client::circuit::handshake::RelayProtocol,
1772            role: tor_proto::client::circuit::handshake::HandshakeRole,
1773            handshake: impl tor_proto::client::circuit::handshake::KeyGenerator + Send,
1774            params: CircParameters,
1775            capabilities: &tor_protover::Protocols,
1776        ) -> tor_circmgr::Result<()> {
1777            todo!()
1778        }
1779    }
1780
1781    #[allow(clippy::diverging_sub_expression)] // async_trait + todo!()
1782    #[async_trait]
1783    impl MockableClientIntro for Mocks<()> {
1784        type Conversation<'r> = &'r ();
1785        async fn m_start_conversation_last_hop(
1786            &self,
1787            msg: Option<AnyRelayMsg>,
1788            reply_handler: impl MsgHandler + Send + 'static,
1789        ) -> tor_circmgr::Result<Self::Conversation<'_>> {
1790            todo!()
1791        }
1792    }
1793
1794    #[traced_test]
1795    #[tokio::test]
1796    async fn test_connect() {
1797        let valid_after = humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap();
1798        let fresh_until = valid_after + humantime::parse_duration("1 hours").unwrap();
1799        let valid_until = valid_after + humantime::parse_duration("24 hours").unwrap();
1800        let lifetime = Lifetime::new(valid_after, fresh_until, valid_until).unwrap();
1801
1802        let netdir = tor_netdir::testnet::construct_custom_netdir_with_params(
1803            tor_netdir::testnet::simple_net_func,
1804            iter::empty::<(&str, _)>(),
1805            Some(lifetime),
1806        )
1807        .expect("failed to build default testing netdir");
1808
1809        let netdir = Arc::new(netdir.unwrap_if_sufficient().unwrap());
1810        let runtime = TokioNativeTlsRuntime::current().unwrap();
1811        let now = humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap();
1812        let mock_sp = SimpleMockTimeProvider::from_wallclock(now);
1813        let runtime = runtime
1814            .with_sleep_provider(mock_sp.clone())
1815            .with_coarse_time_provider(mock_sp);
1816        let time_period = netdir.hs_time_period();
1817
1818        let mglobal = Arc::new(Mutex::new(MocksGlobal::default()));
1819        let mocks = Mocks { mglobal, id: () };
1820        // From C Tor src/test/test_hs_common.c test_build_address
1821        let hsid = test_data::TEST_HSID_2.into();
1822        let mut data = Data::default();
1823
1824        let pk: HsClientDescEncKey = curve25519::PublicKey::from(test_data::TEST_PUBKEY_2).into();
1825        let sk = curve25519::StaticSecret::from(test_data::TEST_SECKEY_2).into();
1826        let mut secret_keys_builder = HsClientSecretKeysBuilder::default();
1827        secret_keys_builder.ks_hsc_desc_enc(HsClientDescEncKeypair::new(pk.clone(), sk));
1828        let secret_keys = secret_keys_builder.build().unwrap();
1829
1830        let ctx = Context::new(
1831            &runtime,
1832            &mocks,
1833            netdir,
1834            Default::default(),
1835            hsid,
1836            secret_keys,
1837            mocks.clone(),
1838        )
1839        .unwrap();
1840
1841        let _got = AssertUnwindSafe(ctx.connect(&mut data))
1842            .catch_unwind() // TODO HS TESTS: remove this and the AssertUnwindSafe
1843            .await;
1844
1845        let (hs_blind_id_key, subcredential) = HsIdKey::try_from(hsid)
1846            .unwrap()
1847            .compute_blinded_key(time_period)
1848            .unwrap();
1849        let hs_blind_id = hs_blind_id_key.id();
1850
1851        let sk = curve25519::StaticSecret::from(test_data::TEST_SECKEY_2).into();
1852
1853        let hsdesc = HsDesc::parse_decrypt_validate(
1854            test_data::TEST_DATA_2,
1855            &hs_blind_id,
1856            now,
1857            &subcredential,
1858            Some(&HsClientDescEncKeypair::new(pk, sk)),
1859        )
1860        .unwrap()
1861        .dangerously_assume_timely();
1862
1863        let mglobal = mocks.mglobal.lock().unwrap();
1864        assert_eq!(mglobal.hsdirs_asked.len(), 1);
1865        // TODO hs: here and in other places, consider implementing PartialEq instead, or creating
1866        // an assert_dbg_eq macro (which would be part of a test_helpers crate or something)
1867        assert_eq!(
1868            format!("{:?}", mglobal.got_desc),
1869            format!("{:?}", Some(hsdesc))
1870        );
1871
1872        // Check how long the descriptor is valid for
1873        let (start_time, end_time) = data.desc.as_ref().unwrap().bounds();
1874        assert_eq!(start_time, None);
1875
1876        let desc_valid_until = humantime::parse_rfc3339("2023-02-11T20:00:00Z").unwrap();
1877        assert_eq!(end_time, Some(desc_valid_until));
1878
1879        // TODO HS TESTS: check the circuit in got is the one we gave out
1880
1881        // TODO HS TESTS: continue with this
1882    }
1883
1884    // TODO HS TESTS: Test IPT state management and expiry:
1885    //   - obtain a test descriptor with only a broken ipt
1886    //     (broken in the sense that intro can be attempted, but will fail somehow)
1887    //   - try to make a connection and expect it to fail
1888    //   - assert that the ipt data isn't empty
1889    //   - cause the descriptor to expire (advance clock)
1890    //   - start using a mocked RNG if we weren't already and pin its seed here
1891    //   - make a new descriptor with two IPTs: the broken one from earlier, and a new one
1892    //   - make a new connection
1893    //   - use test_got_ipts to check that the random numbers
1894    //     would sort the bad intro first, *and* that the good one is appears first
1895    //   - assert that connection succeeded
1896    //   - cause the circuit and descriptor to expire (advance clock)
1897    //   - go back to the previous descriptor contents, but with a new validity period
1898    //   - try to make a connection
1899    //   - use test_got_ipts to check that only the broken ipt is present
1900
1901    // TODO HS TESTS: test retries (of every retry loop we have here)
1902    // TODO HS TESTS: test error paths
1903}