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