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