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