1
//! Main implementation of the connection functionality
2

            
3
use std::collections::HashMap;
4
use std::fmt::Debug;
5
use std::marker::PhantomData;
6
use std::sync::Arc;
7

            
8
use async_trait::async_trait;
9
use educe::Educe;
10
use futures::{AsyncRead, AsyncWrite};
11
use itertools::Itertools;
12
use rand::RngExt;
13
use tor_bytes::Writeable;
14
use tor_cell::relaycell::hs::IntroduceAckStatus;
15
use tor_cell::relaycell::hs::intro_payload::{self, IntroduceHandshakePayload};
16
use tor_cell::relaycell::hs::pow::ProofOfWork;
17
use tor_cell::relaycell::msg::{AnyRelayMsg, Introduce1, Rendezvous2};
18
use tor_circmgr::build::onion_circparams_from_netparams;
19
use tor_circmgr::{
20
    ClientOnionServiceDataTunnel, ClientOnionServiceDirTunnel, ClientOnionServiceIntroTunnel,
21
};
22
use tor_dirclient::SourceInfo;
23
use tor_error::{Bug, debug_report, warn_report};
24
use tor_hscrypto::Subcredential;
25
use tor_hscrypto::time::TimePeriod;
26
use tor_netdir::params::NetParameters;
27
use tor_proto::TargetHop;
28
use tor_proto::client::circuit::handshake::hs_ntor::{self, HsNtorHkdfKeyGenerator};
29
use tracing::{debug, instrument, trace, warn};
30
use web_time_compat::{Duration, Instant, SystemTime};
31

            
32
use retry_error::RetryError;
33
use safelog::{DispRedacted, Sensitive};
34
use tor_cell::relaycell::RelayMsg;
35
use tor_cell::relaycell::hs::{
36
    AuthKeyType, EstablishRendezvous, IntroduceAck, RendezvousEstablished,
37
};
38
use tor_checkable::{Timebound, timed::TimerangeBound};
39
use tor_circmgr::hspool::HsCircPool;
40
use tor_circmgr::timeouts::Action as TimeoutsAction;
41
use tor_dirclient::request::Requestable as _;
42
use tor_error::{HasRetryTime as _, RetryTime};
43
use tor_error::{internal, into_internal};
44
use tor_hscrypto::RendCookie;
45
use tor_hscrypto::pk::{HsBlindId, HsId, HsIdKey};
46
use tor_linkspec::{CircTarget, HasRelayIds, OwnedCircTarget, RelayId};
47
use tor_llcrypto::pk::ed25519::Ed25519Identity;
48
use tor_netdir::{NetDir, Relay};
49
use tor_netdoc::doc::hsdesc::{HsDesc, IntroPointDesc};
50
use tor_proto::client::circuit::{CircParameters, handshake};
51
use tor_proto::{MetaCellDisposition, MsgHandler};
52
use tor_rtcompat::{Runtime, SleepProviderExt as _, TimeoutError};
53

            
54
use crate::Config;
55
use crate::caps;
56
use crate::err::RendPtIdentityForError;
57
use crate::pow::HsPowClient;
58
use crate::proto_oneshot;
59
use crate::relay_info::ipt_to_circtarget;
60
use crate::state::MockableConnectorData;
61
use crate::{ConnError, DescriptorError, DescriptorErrorDetail};
62
use crate::{FailedAttemptError, IntroPtIndex, rend_pt_identity_for_error};
63
use crate::{HsClientConnector, HsClientSecretKeys};
64

            
65
use ConnError as CE;
66
use FailedAttemptError as FAE;
67

            
68
/// Given `R, M` where `M: MocksForConnect<M>`, expand to the mockable `ClientCirc`
69
// This is quite annoying.  But the alternative is to write out `<... as // ...>`
70
// each time, since otherwise the compile complains about ambiguous associated types.
71
macro_rules! DataTunnel{ { $R:ty, $M:ty } => {
72
    <<$M as MocksForConnect<$R>>::HsCircPool as MockableCircPool<$R>>::DataTunnel
73
} }
74

            
75
/// Information about a hidden service, including our connection history
76
#[derive(Default, Educe)]
77
#[educe(Debug)]
78
// This type is actually crate-private, since it isn't re-exported, but it must
79
// be `pub` because it appears as a default for a type parameter in HsClientConnector.
80
pub struct Data {
81
    /// The latest known onion service descriptor for this service.
82
    desc: DataHsDesc,
83

            
84
    /// Information about the latest status of trying to connect to this service
85
    /// through each of its introduction points.
86
    ipts: DataIpts,
87
    /// Information about the requery period of each HsDir we have recently queried.
88
    ///
89
    /// Each entry represents an HsDir that we cannot requery until
90
    /// its specified timestamp elapses.
91
    ///
92
    /// Any HsDir that does not have an entry in this map can be requeried.
93
    hsdirs: DataHsDirs,
94
}
95

            
96
/// An onion service descriptor and its associated HsBlindId.
97
#[derive(Debug)]
98
struct HsDescForTp {
99
    /// The TP this descriptor is for.
100
    ///
101
    /// Used for determining whether a newly fetched descriptor
102
    /// is for the same time period as this one.
103
    time_period: TimePeriod,
104
    /// The descriptor
105
    desc: TimerangeBound<HsDesc>,
106
}
107

            
108
/// Part of `Data` that relates to our information about the HsDir requery periods
109
type DataHsDirs = HashMap<RelayIdForRequeryPeriod, SystemTime>;
110

            
111
/// Marker type, to make typed HsDir [`RelayIdFor`] keys
112
#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Debug)]
113
struct RequeryPeriodMap;
114

            
115
/// Lookup key for looking up and recording our IPT use experiences
116
type RelayIdForRequeryPeriod = RelayIdFor<RequeryPeriodMap>;
117

            
118
/// Part of `Data` that relates to the HS descriptor
119
type DataHsDesc = Option<HsDescForTp>;
120

            
121
/// Part of `Data` that relates to our information about introduction points
122
type DataIpts = HashMap<RelayIdForExperience, IptExperience>;
123

            
124
/// How things went last time we tried to use this introduction point
125
///
126
/// Neither this data structure, nor [`Data`], is responsible for arranging that we expire this
127
/// information eventually.  If we keep reconnecting to the service, we'll retain information
128
/// about each IPT indefinitely, at least so long as they remain listed in the descriptors we
129
/// receive.
130
///
131
/// Expiry of unused data is handled by `state.rs`, according to `last_used` in `ServiceState`.
132
///
133
/// Choosing which IPT to prefer is done by obtaining an `IptSortKey`
134
/// (from this and other information).
135
//
136
// Don't impl Ord for IptExperience.  We obtain `Option<&IptExperience>` from our
137
// data structure, and if IptExperience were Ord then Option<&IptExperience> would be Ord
138
// but it would be the wrong sort order: it would always prefer None, ie untried IPTs.
139
#[derive(Debug)]
140
struct IptExperience {
141
    /// How long it took us to get whatever outcome occurred
142
    ///
143
    /// We prefer fast successes to slow ones.
144
    /// Then, we prefer failures with earlier `RetryTime`,
145
    /// and, lastly, faster failures to slower ones.
146
    duration: Duration,
147

            
148
    /// What happened and when we might try again
149
    ///
150
    /// Note that we don't actually *enforce* the `RetryTime` here, just sort by it
151
    /// using `RetryTime::loose_cmp`.
152
    ///
153
    /// We *do* return an error that is itself `HasRetryTime` and expect our callers
154
    /// to honour that.
155
    outcome: Result<(), RetryTime>,
156
}
157

            
158
/// Actually make a HS connection, updating our recorded state as necessary
159
///
160
/// `connector` is provided only for obtaining the runtime and netdir (and `mock_for_state`).
161
/// Obviously, `connect` is not supposed to go looking in `services`.
162
///
163
/// This function handles all necessary retrying of fallible operations,
164
/// (and, therefore, must also limit the total work done for a particular call).
165
///
166
/// This function has a minimum of functionality, since it is the boundary
167
/// between "mock connection, used for testing `state.rs`" and
168
/// "mock circuit and netdir, used for testing `connect.rs`",
169
/// so it is not, itself, unit-testable.
170
#[instrument(level = "trace", skip_all)]
171
pub(crate) async fn connect<R: Runtime>(
172
    connector: &HsClientConnector<R>,
173
    netdir: Arc<NetDir>,
174
    config: Arc<Config>,
175
    hsid: HsId,
176
    data: &mut Data,
177
    secret_keys: HsClientSecretKeys,
178
) -> Result<ClientOnionServiceDataTunnel, ConnError> {
179
    Context::new(
180
        &connector.runtime,
181
        &*connector.circpool,
182
        netdir,
183
        config,
184
        hsid,
185
        secret_keys,
186
        (),
187
    )?
188
    .connect(data)
189
    .await
190
}
191

            
192
/// Common context for a single request to connect to a hidden service
193
///
194
/// This saves on passing this same set of (immutable) values (or subsets thereof)
195
/// to each method in the principal functional code, everywhere.
196
/// It also provides a convenient type to be `Self`.
197
///
198
/// Its lifetime is one request to make a new client circuit to a hidden service,
199
/// including all the retries and timeouts.
200
struct Context<'c, R: Runtime, M: MocksForConnect<R>> {
201
    /// Runtime
202
    runtime: &'c R,
203
    /// Circpool
204
    circpool: &'c M::HsCircPool,
205
    /// Netdir
206
    //
207
    // TODO holding onto the netdir for the duration of our attempts is not ideal
208
    // but doing better is fairly complicated.  See discussions here:
209
    //   https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1228#note_2910545
210
    //   https://gitlab.torproject.org/tpo/core/arti/-/issues/884
211
    netdir: Arc<NetDir>,
212
    /// Configuration
213
    config: Arc<Config>,
214
    /// Secret keys to use
215
    secret_keys: HsClientSecretKeys,
216
    /// HS ID
217
    hsid: DispRedacted<HsId>,
218
    /// Blinded HS ID
219
    hs_blind_id: HsBlindId,
220
    /// The subcredential to use during this time period
221
    subcredential: Subcredential,
222
    /// Mock data
223
    mocks: M,
224
}
225

            
226
/// Details of an established rendezvous point
227
///
228
/// Intermediate value for progress during a connection attempt.
229
struct Rendezvous<'r, R: Runtime, M: MocksForConnect<R>> {
230
    /// RPT as a `Relay`
231
    rend_relay: Relay<'r>,
232
    /// Rendezvous circuit
233
    rend_tunnel: DataTunnel!(R, M),
234
    /// Rendezvous cookie
235
    rend_cookie: RendCookie,
236

            
237
    /// Receiver that will give us the RENDEZVOUS2 message.
238
    ///
239
    /// The sending ended is owned by the handler
240
    /// which receives control messages on the rendezvous circuit,
241
    /// and which was installed when we sent `ESTABLISH_RENDEZVOUS`.
242
    ///
243
    /// (`RENDEZVOUS2` is the message containing the onion service's side of the handshake.)
244
    rend2_rx: proto_oneshot::Receiver<Rendezvous2>,
245

            
246
    /// Dummy, to placate compiler
247
    ///
248
    /// Covariant without dropck or interfering with Send/Sync will do fine.
249
    marker: PhantomData<fn() -> (R, M)>,
250
}
251

            
252
/// Random value used as part of IPT selection
253
type IptSortRand = u32;
254

            
255
/// Details of an apparently-useable introduction point
256
///
257
/// Intermediate value for progress during a connection attempt.
258
struct UsableIntroPt<'i> {
259
    /// Index in HS descriptor
260
    intro_index: IntroPtIndex,
261
    /// IPT descriptor
262
    intro_desc: &'i IntroPointDesc,
263
    /// IPT `CircTarget`
264
    intro_target: OwnedCircTarget,
265
    /// Random value used as part of IPT selection
266
    sort_rand: IptSortRand,
267
}
268

            
269
/// Lookup key for looking up and recording information about a relay
270
///
271
/// Used to identify a relay when looking to see what happened last time we used it,
272
/// and storing that information after we tried it.
273
///
274
/// We store the experience information under an arbitrary one of the relay's identities,
275
/// as returned by the `HasRelayIds::identities().next()`.
276
/// When we do lookups, we check all the relay's identities to see if we find
277
/// anything relevant.
278
/// If relay identities permute in strange ways, whether we find our previous
279
/// knowledge about them is not particularly well defined, but that's fine.
280
///
281
/// While this is, structurally, a relay identity, it is not suitable for other purposes.
282
#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Debug)]
283
struct RelayIdFor<K> {
284
    /// The relay id
285
    inner: RelayId,
286

            
287
    /// Phantom data to allow parameterizing over `K`
288
    ///
289
    /// `K` is a marker type that represents the kind of map
290
    /// this key will be used in.
291
    marker: PhantomData<K>,
292
}
293

            
294
/// Marker type, to make typed Ipt exprience [`RelayIdFor`] keys
295
#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Debug)]
296
struct IptExperienceMap;
297

            
298
/// Lookup key for looking up and recording our IPT use experiences
299
type RelayIdForExperience = RelayIdFor<IptExperienceMap>;
300

            
301
/// Details of an apparently-successful INTRODUCE exchange
302
///
303
/// Intermediate value for progress during a connection attempt.
304
struct Introduced<R: Runtime, M: MocksForConnect<R>> {
305
    /// End-to-end crypto NTORv3 handshake with the service
306
    ///
307
    /// Created as part of generating our `INTRODUCE1`,
308
    /// and then used when processing `RENDEZVOUS2`.
309
    handshake_state: hs_ntor::HsNtorClientState,
310

            
311
    /// A set of peer extensions that we decided to negotiate and use.
312
    peer_caps: caps::PeerCaps,
313

            
314
    /// Dummy, to placate compiler
315
    ///
316
    /// `R` and `M` only used for getting to mocks.
317
    /// Covariant without dropck or interfering with Send/Sync will do fine.
318
    marker: PhantomData<fn() -> (R, M)>,
319
}
320

            
321
impl<K> RelayIdFor<K> {
322
    /// Create a new key for use with `T`
323
926
    fn new(inner: RelayId) -> Self {
324
926
        Self {
325
926
            inner,
326
926
            marker: Default::default(),
327
926
        }
328
926
    }
329

            
330
    /// Identities to use to try to find previous experience information about this IPT
331
504
    fn for_lookup<T: HasRelayIds>(ids: &T) -> impl Iterator<Item = Self> + '_ {
332
742
        ids.identities().map(|id| RelayIdFor::new(id.to_owned()))
333
504
    }
334

            
335
    /// Identity to use to store previous experience information about this IPT
336
184
    fn for_store<T: HasRelayIds>(ids: &T) -> Result<Self, Bug> {
337
184
        let id = ids
338
184
            .identities()
339
184
            .next()
340
184
            .ok_or_else(|| internal!("introduction point relay with no identities"))?
341
184
            .to_owned();
342
184
        Ok(RelayIdFor::new(id))
343
184
    }
344
}
345

            
346
/// Sort key for an introduction point, for selecting the best IPTs to try first
347
///
348
/// Ordering is most preferable first.
349
///
350
/// We use this to sort our `UsableIpt`s using `.sort_by_key`.
351
/// (This implementation approach ensures that we obey all the usual ordering invariants.)
352
#[derive(Ord, PartialOrd, Eq, PartialEq, Debug)]
353
struct IptSortKey {
354
    /// Sort by how preferable the experience was
355
    outcome: IptSortKeyOutcome,
356
    /// Failing that, choose randomly
357
    sort_rand: IptSortRand,
358
}
359

            
360
/// Component of the [`IptSortKey`] representing outcome of our last attempt, if any
361
///
362
/// This is the main thing we use to decide which IPTs to try first.
363
/// It is calculated for each IPT
364
/// (via `.sort_by_key`, so repeatedly - it should therefore be cheap to make.)
365
///
366
/// Ordering is most preferable first.
367
#[derive(Ord, PartialOrd, Eq, PartialEq, Debug)]
368
enum IptSortKeyOutcome {
369
    /// Prefer successes
370
    Success {
371
        /// Prefer quick ones
372
        duration: Duration,
373
    },
374
    /// Failing that, try one we don't know to have failed
375
    Untried,
376
    /// Failing that, it'll have to be ones that didn't work last time
377
    Failed {
378
        /// Prefer failures with an earlier retry time
379
        retry_time: tor_error::LooseCmpRetryTime,
380
        /// Failing that, prefer quick failures (rather than slow ones eg timeouts)
381
        duration: Duration,
382
    },
383
}
384

            
385
impl From<Option<&IptExperience>> for IptSortKeyOutcome {
386
164
    fn from(experience: Option<&IptExperience>) -> IptSortKeyOutcome {
387
        use IptSortKeyOutcome as O;
388
164
        match experience {
389
20
            None => O::Untried,
390
144
            Some(IptExperience { duration, outcome }) => match outcome {
391
4
                Ok(()) => O::Success {
392
4
                    duration: *duration,
393
4
                },
394
140
                Err(retry_time) => O::Failed {
395
140
                    retry_time: (*retry_time).into(),
396
140
                    duration: *duration,
397
140
                },
398
            },
399
        }
400
164
    }
401
}
402

            
403
/// Token indicating that a descriptor fetch is wanted
404
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
405
struct RefetchDescriptor;
406

            
407
impl<'c, R: Runtime, M: MocksForConnect<R>> Context<'c, R, M> {
408
    /// Make a new `Context` from the input data
409
2
    fn new(
410
2
        runtime: &'c R,
411
2
        circpool: &'c M::HsCircPool,
412
2
        netdir: Arc<NetDir>,
413
2
        config: Arc<Config>,
414
2
        hsid: HsId,
415
2
        secret_keys: HsClientSecretKeys,
416
2
        mocks: M,
417
2
    ) -> Result<Self, ConnError> {
418
2
        let time_period = netdir.hs_time_period();
419
2
        let (hs_blind_id_key, subcredential) = HsIdKey::try_from(hsid)
420
2
            .map_err(|_| CE::InvalidHsId)?
421
2
            .compute_blinded_key(time_period)
422
2
            .map_err(
423
                // TODO HS what on earth do these errors mean, in practical terms ?
424
                // In particular, we'll want to convert them to a ConnError variant,
425
                // but what ErrorKind should they have ?
426
2
                into_internal!("key blinding error, don't know how to handle"),
427
            )?;
428
2
        let hs_blind_id = hs_blind_id_key.id();
429

            
430
2
        Ok(Context {
431
2
            netdir,
432
2
            config,
433
2
            hsid: DispRedacted(hsid),
434
2
            hs_blind_id,
435
2
            subcredential,
436
2
            circpool,
437
2
            runtime,
438
2
            secret_keys,
439
2
            mocks,
440
2
        })
441
2
    }
442

            
443
    /// Actually make a HS connection, updating our recorded state as necessary
444
    ///
445
    /// Called by the `connect` function in this module.
446
    ///
447
    /// This function handles all necessary retrying of fallible operations,
448
    /// (and, therefore, must also limit the total work done for a particular call).
449
    #[instrument(level = "trace", skip_all)]
450
16
    async fn connect(&self, data: &mut Data) -> Result<DataTunnel!(R, M), ConnError> {
451
        // This function must do the following, retrying as appropriate.
452
        //  - Look up the onion descriptor in the state.
453
        //  - Download the onion descriptor if one isn't there.
454
        //  - In parallel:
455
        //    - Pick a rendezvous point from the netdirprovider and launch a
456
        //      rendezvous circuit to it. Then send ESTABLISH_INTRO.
457
        //    - Pick a number of introduction points (1 or more) and try to
458
        //      launch circuits to them.
459
        //  - On a circuit to an introduction point, send an INTRODUCE1 cell.
460
        //  - Wait for a RENDEZVOUS2 cell on the rendezvous circuit
461
        //  - Add a virtual hop to the rendezvous circuit.
462
        //  - Return the rendezvous circuit.
463

            
464
        let mocks = self.mocks.clone();
465

            
466
        let desc = self
467
            .descriptor_ensure(&mut data.desc, &mut data.hsdirs, None)
468
            .await?;
469

            
470
        mocks.test_got_desc(desc);
471

            
472
        let tunnel = match self.intro_rend_connect(desc, &mut data.ipts).await {
473
            Ok(tunnel) => tunnel,
474
            Err(e) => {
475
14
                let is_intro_nack = |e| {
476
14
                    if let FAE::IntroductionFailed { status, .. } = e {
477
14
                        status == IntroduceAckStatus::NOT_RECOGNIZED
478
                    } else {
479
                        false
480
                    }
481
14
                };
482

            
483
                let retry = if let CE::Failed(ref errors) = e {
484
                    // If any of the errors are an INTRODUCE_NACK,
485
                    // then it's worth retrying one more time
486
                    // with a fresh descriptor.
487
                    errors
488
                        .clone()
489
                        .into_iter()
490
                        .any(is_intro_nack)
491
                        .then_some(RefetchDescriptor)
492
                } else {
493
                    None
494
                };
495

            
496
                if let Some(RefetchDescriptor) = retry {
497
                    debug!(
498
                        "Introduction to {} NACKed, refetching descriptor and retrying",
499
                        &self.hsid,
500
                    );
501
                    // Refetch the descriptor and try one more time
502
                    let desc = self
503
                        .descriptor_ensure(&mut data.desc, &mut data.hsdirs, retry)
504
                        .await?;
505
                    mocks.test_got_desc(desc);
506
                    self.intro_rend_connect(desc, &mut data.ipts).await?
507
                } else {
508
                    return Err(e);
509
                }
510
            }
511
        };
512

            
513
        mocks.test_got_tunnel(&tunnel);
514

            
515
        Ok(tunnel)
516
16
    }
517

            
518
    /// Ensure that `Data.desc` contains the HS descriptor
519
    ///
520
    /// If we have a previously-downloaded descriptor, which is still valid,
521
    /// just returns a reference to it.
522
    ///
523
    /// Otherwise, tries to obtain the descriptor by downloading it from hsdir(s).
524
    ///
525
    /// If `refetch` is `true`, a new descriptor will be refetched
526
    /// from the hsdir(s) unconditionally.
527
    ///
528
    /// Does all necessary retries and timeouts.
529
    /// Returns an error if no valid descriptor could be found.
530
    #[allow(clippy::cognitive_complexity)] // TODO: Refactor
531
    #[instrument(level = "trace", skip_all)]
532
30
    async fn descriptor_ensure<'d>(
533
30
        &self,
534
30
        data: &'d mut DataHsDesc,
535
30
        recent_hsdirs: &'d mut DataHsDirs,
536
30
        refetch: Option<RefetchDescriptor>,
537
30
    ) -> Result<&'d HsDesc, CE> {
538
        // Maximum number of hsdir connection and retrieval attempts we'll make
539
        let max_total_attempts = self
540
            .config
541
            .retry
542
            .hs_desc_fetch_attempts()
543
            .try_into()
544
            // User specified a very large u32.  We must be downcasting it to 16bit!
545
            // let's give them as many retries as we can manage.
546
            .unwrap_or(usize::MAX);
547

            
548
        let now = self.runtime.wallclock();
549
28
        let unwrap_valid_desc = |data: &'d mut DataHsDesc| -> &'d HsDesc {
550
28
            data.as_ref()
551
28
                .expect("Some but now None")
552
28
                .desc
553
28
                .as_ref()
554
28
                .check_valid_at(&now)
555
28
                .expect("Ok but now Err")
556
28
        };
557

            
558
        // We retain a previously obtained descriptor precisely until its lifetime expires,
559
        // or until we refetch a more recent one
560
        // as a result of an `intro_rend_connect()` failure caused by introduce NACK.
561
        //
562
        // When it expires, we discard it completely and try to obtain a new one.
563
        //
564
        // We only replace our cached descriptor if the new one has a higher revision counter.
565
        //
566
        // TODO SPEC: Discuss HS descriptor lifetime and expiry client behaviour
567
        let now = self.runtime.wallclock();
568

            
569
28
        let stored_revision = data.as_ref().and_then(|previously| {
570
28
            if let Ok(desc) = previously.desc.as_ref().check_valid_at(&now) {
571
                // Ideally we would just return desc but that confuses borrowck,
572
                // so we have to use unwrap_valid_desc() each time
573
                // we need the known-to-be-Some descriptor instead.
574
                //
575
                // https://github.com/rust-lang/rust/issues/51545
576
28
                Some((desc.revision(), previously.time_period))
577
            } else {
578
                // Seems to be not valid now.  Try to fetch a fresh one.
579
                None
580
            }
581
28
        });
582

            
583
        match (stored_revision, refetch) {
584
            (Some(_), None) => {
585
                // Our cached descriptor is still timely,
586
                // and we don't need to fetch a new one.
587
                return Ok(unwrap_valid_desc(data));
588
            }
589
            (None, _) => {
590
                // We don't have a timely descriptor,
591
                // so ignore the requery_interval,
592
                // and reach out to all HsDirs
593
                recent_hsdirs.clear();
594
            }
595
            (_, Some(RefetchDescriptor)) => {
596
                // We have been asked to try to fetch a new descriptor.
597
                // We will only reach out to the HsDirs that are
598
                // not within the `hs_dir_requery_interval`
599
            }
600
        }
601

            
602
        // First, filter out any HsDirs that we *can* requery
603
54
        recent_hsdirs.retain(|_hsdir, requery| *requery > now);
604

            
605
        let working_tp = self.netdir.hs_time_period();
606
        let hs_dirs = self.netdir.hs_dirs_download(
607
            self.hs_blind_id,
608
            working_tp,
609
            &mut self.mocks.thread_rng(),
610
        )?;
611

            
612
        trace!(
613
            "HS desc fetch for {}, for period {}, using {} hsdirs",
614
            &self.hsid,
615
            working_tp,
616
            hs_dirs.len()
617
        );
618

            
619
        let hs_dirs = hs_dirs
620
            .into_iter()
621
96
            .filter(|hsdir| {
622
                // Skip over any HsDirs that we are not allowed to requery right now
623
182
                let should_skip = recent_hsdirs.keys().any(|recent| {
624
322
                    RelayIdForRequeryPeriod::for_lookup(hsdir).any(|id| id == *recent)
625
182
                });
626

            
627
96
                !should_skip
628
96
            })
629
            .collect::<Vec<_>>();
630

            
631
        if hs_dirs.is_empty() {
632
            warn!(
633
                "Tried to fetch HS desc for {}, for period {}, but all hsdirs are rate-limited",
634
                &self.hsid, working_tp,
635
            );
636

            
637
            if stored_revision.is_none() {
638
                // We can't fetch a new descriptor, and we don't have a cached one.
639
                return Err(CE::NoUsableHsDirs);
640
            } else {
641
                // Return our cached descriptor
642
                return Ok(unwrap_valid_desc(data));
643
            }
644
        }
645

            
646
        let params = self.netdir.params();
647

            
648
        // We might consider launching requests to multiple HsDirs in parallel.
649
        //   https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1118#note_2894463
650
        // But C Tor doesn't and our HS experts don't consider that important:
651
        //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914436
652
        // (Additionally, making multiple HSDir requests at once may make us
653
        // more vulnerable to traffic analysis.)
654
        let mut attempts = hs_dirs.iter().cycle().take(max_total_attempts);
655
        let mut errors = RetryError::in_attempt_to("retrieve hidden service descriptor");
656
        let desc = loop {
657
            let relay = match attempts.next() {
658
                Some(relay) => relay,
659
                None => {
660
                    return Err(if errors.is_empty() {
661
                        CE::NoHsDirs
662
                    } else {
663
                        CE::DescriptorDownload(errors)
664
                    });
665
                }
666
            };
667
            let hsdir_for_error: Sensitive<Ed25519Identity> = (*relay.id()).into();
668

            
669
            let hsdir = RelayIdForRequeryPeriod::for_store(relay)?;
670
            // Ensure we wait at least hs_dir_requery_interval() until we try to
671
            // fecth from this HsDir again
672
            recent_hsdirs.insert(hsdir, now + self.config.retry.hs_dir_requery_interval());
673

            
674
            match self.descriptor_fetch_attempt(relay, params).await {
675
                Ok(desc) => break desc,
676
                Err(error) => {
677
                    if error.should_report_as_suspicious() {
678
                        // Note that not every protocol violation is suspicious:
679
                        // we only warn on the protocol violations that look like attempts
680
                        // to do a traffic tagging attack via hsdir inflation.
681
                        // (See proposal 360.)
682
                        warn_report!(
683
                            &error,
684
                            "Suspicious failure while downloading hsdesc for {} from relay {}",
685
                            &self.hsid,
686
                            relay.display_relay_ids(),
687
                        );
688
                    } else {
689
                        debug_report!(
690
                            &error,
691
                            "failed hsdir desc fetch for {} from {}/{}",
692
                            &self.hsid,
693
                            &relay.id(),
694
                            &relay.rsa_id()
695
                        );
696
                    }
697
                    errors.push_timed(
698
                        tor_error::Report(DescriptorError {
699
                            hsdir: hsdir_for_error,
700
                            error,
701
                        }),
702
                        self.runtime.now(),
703
                        Some(self.runtime.wallclock()),
704
                    );
705
                }
706
            }
707
        };
708

            
709
        // If our existing descriptor is newer than the one we have just fetched,
710
        // we should retain it.
711
        if let Some(stored_revision) = stored_revision {
712
            // It is safe to dangerously_assume_timely,
713
            // as descriptor_fetch_attempt has already checked the timeliness of the descriptor.
714
            let new_desc = desc.as_ref().dangerously_assume_timely();
715

            
716
            // Revision counters are monotonically increasing within a given time period.
717
            // If our newly fetched descriptor has the same HsBlindId as our cached one,
718
            // it means they are both used for the same time period,
719
            // and so we should only update our cache if the new descriptor is more recent
720
            // (i.e. it has a higher revision counter).
721
            if stored_revision >= (new_desc.revision(), working_tp) {
722
                // Our cached descriptor is still timely, and has a higher revision counter
723
                // than the one we've just fetched, so we retain it.
724
                return Ok(unwrap_valid_desc(data));
725
            }
726
        }
727

            
728
        // Store the bounded value in the cache for reuse,
729
        // but return a reference to the unwrapped `HsDesc`.
730
        //
731
        // The `HsDesc` must be owned by `data.desc`,
732
        // so first add it to `data.desc`,
733
        // and then dangerously_assume_timely to get a reference out again.
734
        //
735
        // It is safe to dangerously_assume_timely,
736
        // as descriptor_fetch_attempt has already checked the timeliness of the descriptor.
737
        let desc = HsDescForTp {
738
            time_period: working_tp,
739
            desc,
740
        };
741
        let ret = data.insert(desc);
742
        Ok(ret.desc.as_ref().dangerously_assume_timely())
743
30
    }
744

            
745
    /// Make one attempt to fetch the descriptor from a specific hsdir
746
    ///
747
    /// No timeout
748
    ///
749
    /// On success, returns the descriptor.
750
    ///
751
    /// While the returned descriptor is `TimerangeBound`, its validity at the current time *has*
752
    /// been checked.
753
    #[instrument(level = "trace", skip_all)]
754
14
    async fn descriptor_fetch_attempt(
755
14
        &self,
756
14
        hsdir: &Relay<'_>,
757
14
        params: &NetParameters,
758
14
    ) -> Result<TimerangeBound<HsDesc>, DescriptorErrorDetail> {
759
        let max_len: usize = self
760
            .netdir
761
            .params()
762
            .hsdir_max_desc_size
763
            .get()
764
            .try_into()
765
            .map_err(into_internal!("BoundedInt was not truly bounded!"))?;
766
        let request = {
767
            let mut r = tor_dirclient::request::HsDescDownloadRequest::new(self.hs_blind_id);
768
            r.set_max_len(max_len);
769
            r
770
        };
771
        trace!(
772
            "hsdir for {}, trying {}/{}, request {:?} (http request {:?})",
773
            &self.hsid,
774
            &hsdir.id(),
775
            &hsdir.rsa_id(),
776
            &request,
777
            request.debug_request()
778
        );
779

            
780
        let circuit = self
781
            .circpool
782
            .m_get_or_launch_dir(&self.netdir, OwnedCircTarget::from_circ_target(hsdir))
783
            .await?;
784
        let n_hops = circuit.m_num_hops()?;
785
        let timeout_roundtrip =
786
            self.estimate_timeout(&[(1, TimeoutsAction::RoundTrip { length: n_hops })]);
787

            
788
        let source: Option<SourceInfo> = circuit
789
            .m_source_info()
790
            .map_err(into_internal!("Couldn't get SourceInfo for circuit"))?;
791

            
792
        let mut stream = self
793
            .runtime
794
            // NOTE: In fact this timeout is overkill: this operation should succeed immediately,
795
            // since we always send BEGINDIR messages optimistically (without waiting for a reply).
796
            // But since our code is complex, and since it could become possible for this to block
797
            // if the circuit is saturated or we implement proposal 367 or something,
798
            // we may as well have _some_ timeout here.
799
            .timeout(timeout_roundtrip, circuit.m_begin_dir_stream())
800
            .await?
801
            .map_err(DescriptorErrorDetail::Circuit)?;
802

            
803
        let request_future =
804
            tor_dirclient::send_request(self.runtime, &request, &mut stream, source);
805
        let response = self
806
            .runtime
807
            .timeout(timeout_roundtrip, request_future)
808
            .await?
809
            .map_err(|dir_error| match dir_error {
810
                tor_dirclient::Error::RequestFailed(rfe) => DescriptorErrorDetail::from(rfe.error),
811
                tor_dirclient::Error::CircMgr(ce) => into_internal!(
812
                    "tor-dirclient complains about circmgr going wrong but we gave it a stream"
813
                )(ce)
814
                .into(),
815
                other => into_internal!(
816
                    "tor-dirclient gave unexpected error, tor-hsclient code needs updating"
817
                )(other)
818
                .into(),
819
            })?;
820

            
821
        let desc_text = response.into_output_string().map_err(|rfe| rfe.error)?;
822
        let hsc_desc_enc = self.secret_keys.keys.ks_hsc_desc_enc.as_ref();
823

            
824
        let now = self.runtime.wallclock();
825

            
826
        let desc = HsDesc::parse_decrypt_validate(
827
            &desc_text,
828
            &self.hs_blind_id,
829
            now,
830
            &self.subcredential,
831
            hsc_desc_enc,
832
        )
833
        .map_err(DescriptorErrorDetail::from)?;
834

            
835
        // Validate cc_sendme_inc, if present, is within ±1 of params.cc_sendme_inc.
836
        ensure_descriptor_compatible_with_params(desc.dangerously_peek(), params)?;
837

            
838
        Ok(desc)
839
14
    }
840

            
841
    /// Given the descriptor, try to connect to service
842
    ///
843
    /// Does all necessary retries, timeouts, etc.
844
30
    async fn intro_rend_connect(
845
30
        &self,
846
30
        desc: &HsDesc,
847
30
        data: &mut DataIpts,
848
30
    ) -> Result<DataTunnel!(R, M), CE> {
849
        // Maximum number of rendezvous/introduction attempts we'll make
850
30
        let max_total_attempts = self
851
30
            .config
852
30
            .retry
853
30
            .hs_intro_rend_attempts()
854
30
            .try_into()
855
            // User specified a very large u32.  We must be downcasting it to 16bit!
856
            // let's give them as many retries as we can manage.
857
30
            .unwrap_or(usize::MAX);
858

            
859
        // We can't reliably distinguish IPT failure from RPT failure, so we iterate over IPTs
860
        // (best first) and each time use a random RPT.
861

            
862
        // We limit the number of rendezvous establishment attempts, separately, since we don't
863
        // try to talk to the intro pt until we've established the rendezvous circuit.
864
30
        let mut rend_attempts = 0..max_total_attempts;
865

            
866
        // But, we put all the errors into the same bucket, since we might have a mixture.
867
30
        let mut errors = RetryError::in_attempt_to("make circuit to hidden service");
868

            
869
        // Note that IntroPtIndex is *not* the index into this Vec.
870
        // It is the index into the original list of introduction points in the descriptor.
871
30
        let mut usable_intros: Vec<UsableIntroPt> = desc
872
30
            .intro_points()
873
30
            .iter()
874
30
            .enumerate()
875
90
            .map(|(intro_index, intro_desc)| {
876
90
                let intro_index = intro_index.into();
877
90
                let intro_target = ipt_to_circtarget(intro_desc, &self.netdir)
878
90
                    .map_err(|error| FAE::UnusableIntro { error, intro_index })?;
879
                // Lack of TAIT means this clone
880
90
                let intro_target = OwnedCircTarget::from_circ_target(&intro_target);
881
90
                Ok::<_, FailedAttemptError>(UsableIntroPt {
882
90
                    intro_index,
883
90
                    intro_desc,
884
90
                    intro_target,
885
90
                    sort_rand: self.mocks.thread_rng().random(),
886
90
                })
887
90
            })
888
90
            .filter_map(|entry| match entry {
889
90
                Ok(y) => Some(y),
890
                Err(e) => {
891
                    errors.push_timed(e, self.runtime.now(), Some(self.runtime.wallclock()));
892
                    None
893
                }
894
90
            })
895
30
            .collect_vec();
896

            
897
        // Delete experience information for now-unlisted intro points
898
        // Otherwise, as the IPTs change `Data` might grow without bound,
899
        // if we keep reconnecting to the same HS.
900
80
        data.retain(|k, _v| {
901
80
            usable_intros
902
80
                .iter()
903
236
                .any(|ipt| RelayIdForExperience::for_lookup(&ipt.intro_target).any(|id| &id == k))
904
80
        });
905

            
906
        // Join with existing state recording our experiences,
907
        // sort by descending goodness, and then randomly
908
        // (so clients without any experience don't all pile onto the same, first, IPT)
909
164
        usable_intros.sort_by_key(|ipt: &UsableIntroPt| {
910
164
            let experience =
911
184
                RelayIdForExperience::for_lookup(&ipt.intro_target).find_map(|id| data.get(&id));
912
164
            IptSortKey {
913
164
                outcome: experience.into(),
914
164
                sort_rand: ipt.sort_rand,
915
164
            }
916
164
        });
917
30
        self.mocks.test_got_ipts(&usable_intros);
918

            
919
30
        let mut intro_attempts = usable_intros.iter().cycle().take(max_total_attempts);
920

            
921
        // We retain a rendezvous we managed to set up in here.  That way if we created it, and
922
        // then failed before we actually needed it, we can reuse it.
923
        // If we exit with an error, we will waste it - but because we isolate things we do
924
        // for different services, it wouldn't be reusable anyway.
925
30
        let mut saved_rendezvous = None;
926

            
927
        // If we are using proof-of-work DoS mitigation, this chooses an
928
        // algorithm and initial effort, and adjusts that effort when we retry.
929
30
        let mut pow_client = HsPowClient::new(&self.hs_blind_id, desc);
930

            
931
        // We might consider making multiple INTRODUCE attempts to different
932
        // IPTs in parallel, and somehow aggregating the errors and
933
        // experiences.
934
        // However our HS experts don't consider that important:
935
        //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914438
936
        // Parallelizing our HsCircPool circuit building would likely have
937
        // greater impact. (See #1149.)
938
        loop {
939
            // When did we start doing things that depended on the IPT?
940
            //
941
            // Used for recording our experience with the selected IPT
942
196
            let mut ipt_use_started = None::<Instant>;
943

            
944
            // Error handling inner async block (analogous to an IEFE):
945
            //  * Ok(Some()) means this attempt succeeded
946
            //  * Ok(None) means all attempts exhausted
947
            //  * Err(error) means this attempt failed
948
            //
949
196
            let outcome = async {
950
                // We establish a rendezvous point first.  Although it appears from reading
951
                // this code that this means we serialise establishment of the rendezvous and
952
                // introduction circuits, this isn't actually the case.  The circmgr maintains
953
                // a pool of circuits.  What actually happens in the "standing start" case is
954
                // that we obtain a circuit for rendezvous from the circmgr's pool, expecting
955
                // one to be available immediately; the circmgr will then start to build a new
956
                // one to replenish its pool, and that happens in parallel with the work we do
957
                // here - but in arrears.  If the circmgr pool is empty, then we must wait.
958
                //
959
                // Perhaps this should be parallelised here.  But that's really what the pool
960
                // is for, since we expect building the rendezvous circuit and building the
961
                // introduction circuit to take about the same length of time.
962
                //
963
                // We *do* serialise the ESTABLISH_RENDEZVOUS exchange, with the
964
                // building of the introduction circuit.  That could be improved, at the cost
965
                // of some additional complexity here.
966
                //
967
                // Our HS experts don't consider it important to increase the parallelism:
968
                //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914444
969
                //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914445
970
196
                if saved_rendezvous.is_none() {
971
196
                    debug!("hs conn to {}: setting up rendezvous point", &self.hsid);
972
                    // Establish a rendezvous circuit.
973
196
                    let Some(_): Option<usize> = rend_attempts.next() else {
974
26
                        return Ok(None);
975
                    };
976

            
977
170
                    saved_rendezvous = Some(self.establish_rendezvous().await?);
978
                }
979

            
980
170
                let Some(ipt) = intro_attempts.next() else {
981
                    return Ok(None);
982
                };
983
170
                let intro_index = ipt.intro_index;
984
170
                let is_single_onion_service = desc.is_single_onion_service();
985

            
986
170
                let proof_of_work = match pow_client.solve().await {
987
170
                    Ok(solution) => solution,
988
                    Err(e) => {
989
                        debug!(
990
                            "failing to compute proof-of-work, trying without. ({:?})",
991
                            e
992
                        );
993
                        None
994
                    }
995
                };
996

            
997
                // We record how long things take, starting from here, as
998
                // as a statistic we'll use for the IPT in future.
999
                // This is stored in a variable outside this async block,
                // so that the outcome handling can use it.
170
                ipt_use_started = Some(self.runtime.now());
                // No `Option::get_or_try_insert_with`, or we'd avoid this expect()
170
                let rend_pt_for_error = rend_pt_identity_for_error(
170
                    &saved_rendezvous
170
                        .as_ref()
170
                        .expect("just made Some")
170
                        .rend_relay,
                );
170
                debug!(
                    "hs conn to {}: RPT {}",
                    &self.hsid,
                    rend_pt_for_error.as_inner()
                );
4
                let (rendezvous, introduced) =
170
                    self.exchange_introduce(desc, ipt, &mut saved_rendezvous, proof_of_work)
170
                    .await
                    // TODO: Maybe try, once, to extend-and-reuse the intro circuit.
                    //
                    // If the introduction fails, the introduction circuit is in principle
                    // still usable.  We believe that in this case, C Tor extends the intro
                    // circuit by one hop to the next IPT to try.  That saves on building a
                    // whole new 3-hop intro circuit.  However, our HS experts tell us that
                    // if introduction fails at one IPT it is likely to fail at the others too,
                    // so that optimisation might reduce our network impact and time to failure,
                    // but isn't likely to improve our chances of success.
                    //
                    // However, it's not clear whether this approach risks contaminating
                    // the 2nd attempt with some fault relating to the introduction point.
                    // The 1st ipt might also gain more knowledge about which HS we're talking to.
                    //
                    // TODO SPEC: Discuss extend-and-reuse HS intro circuit after nack
166
                    ?;
                #[allow(unused_variables)] // it's *supposed* to be unused
4
                let saved_rendezvous = (); // don't use `saved_rendezvous` any more, use rendezvous
4
                let rend_pt = rend_pt_identity_for_error(&rendezvous.rend_relay);
4
                let circ = self.complete_rendezvous(ipt, rendezvous, introduced, is_single_onion_service)
4
                    .await?;
4
                debug!(
                    "hs conn to {}: RPT {} IPT {}: success",
                    &self.hsid,
                    rend_pt.as_inner(),
                    intro_index,
                );
4
                Ok::<_, FAE>(Some((intro_index, circ)))
196
            }
196
            .await;
            // Store the experience `outcome` we had with IPT `intro_index`, in `data`
            #[allow(clippy::unused_unit)] // -> () is here for error handling clarity
196
            let mut store_experience = |intro_index, outcome| -> () {
170
                (|| {
170
                    let ipt = usable_intros
170
                        .iter()
338
                        .find(|ipt| ipt.intro_index == intro_index)
170
                        .ok_or_else(|| internal!("IPT not found by index"))?;
170
                    let id = RelayIdForExperience::for_store(&ipt.intro_target)?;
170
                    let started = ipt_use_started.ok_or_else(|| {
                        internal!("trying to record IPT use but no IPT start time noted")
                    })?;
170
                    let duration = self
170
                        .runtime
170
                        .now()
170
                        .checked_duration_since(started)
170
                        .ok_or_else(|| internal!("clock overflow calculating IPT use duration"))?;
170
                    data.insert(id, IptExperience { duration, outcome });
170
                    Ok::<_, Bug>(())
                })()
170
                .unwrap_or_else(|e| warn_report!(e, "error recording HS IPT use experience"));
170
            };
30
            match outcome {
4
                Ok(Some((intro_index, y))) => {
                    // Record successful outcome in Data
4
                    store_experience(intro_index, Ok(()));
4
                    return Ok(y);
                }
26
                Ok(None) => return Err(CE::Failed(errors)),
166
                Err(error) => {
166
                    debug_report!(&error, "hs conn to {}: attempt failed", &self.hsid);
                    // Record error outcome in Data, if in fact we involved the IPT
                    // at all.  The IPT information is be retrieved from `error`,
                    // since only some of the errors implicate the introduction point.
166
                    if let Some(intro_index) = error.intro_index() {
166
                        store_experience(intro_index, Err(error.retry_time()));
166
                    }
166
                    errors.push_timed(error, self.runtime.now(), Some(self.runtime.wallclock()));
                    // If we are using proof-of-work DoS mitigation, try harder next time
166
                    pow_client.increase_effort();
                }
            }
        }
30
    }
    /// Make one attempt to establish a rendezvous circuit
    ///
    /// This doesn't really depend on anything,
    /// other than (obviously) the isolation implied by our circuit pool.
    /// In particular it doesn't depend on the introduction point.
    ///
    /// Applies timeouts as appropriate.
    #[instrument(level = "trace", skip_all)]
170
    async fn establish_rendezvous(&'c self) -> Result<Rendezvous<'c, R, M>, FAE> {
        let (rend_tunnel, rend_relay) = self
            .circpool
            .m_get_or_launch_client_rend(&self.netdir)
            .await
            .map_err(|error| FAE::RendezvousCircuitObtain { error })?;
        let rend_pt = rend_pt_identity_for_error(&rend_relay);
        let rend_cookie: RendCookie = self.mocks.thread_rng().random();
        let message = EstablishRendezvous::new(rend_cookie);
        let (rend_established_tx, rend_established_rx) = proto_oneshot::channel();
        let (rend2_tx, rend2_rx) = proto_oneshot::channel();
        /// Handler which expects `RENDEZVOUS_ESTABLISHED` and then
        /// `RENDEZVOUS2`.   Returns each message via the corresponding `oneshot`.
        struct Handler {
            /// Sender for a RENDEZVOUS_ESTABLISHED message.
            rend_established_tx: proto_oneshot::Sender<RendezvousEstablished>,
            /// Sender for a RENDEZVOUS2 message.
            rend2_tx: proto_oneshot::Sender<Rendezvous2>,
        }
        impl MsgHandler for Handler {
340
            fn handle_msg(
340
                &mut self,
340
                msg: AnyRelayMsg,
340
            ) -> Result<MetaCellDisposition, tor_proto::Error> {
                // The first message we expect is a RENDEZVOUS_ESTABALISHED.
340
                if self.rend_established_tx.still_expected() {
170
                    self.rend_established_tx
170
                        .deliver_expected_message(msg, MetaCellDisposition::Consumed)
                } else {
170
                    self.rend2_tx
170
                        .deliver_expected_message(msg, MetaCellDisposition::ConversationFinished)
                }
340
            }
        }
        debug!(
            "hs conn to {}: RPT {}: sending ESTABLISH_RENDEZVOUS",
            &self.hsid,
            rend_pt.as_inner(),
        );
        let failed_map_err = |error| FAE::RendezvousEstablish {
            error,
            rend_pt: rend_pt.clone(),
        };
        let handler = Handler {
            rend_established_tx,
            rend2_tx,
        };
        let num_hops = rend_tunnel
            .m_num_own_hops()
            .map_err(|error| FAE::RendezvousCircuitObtain { error })?;
        let timeout_roundtrip =
            self.estimate_timeout(&[(1, TimeoutsAction::RoundTrip { length: num_hops })]);
        // TODO(conflux) This error handling is horrible. Problem is that this Mock system requires
        // to send back a tor_circmgr::Error while our reply handler requires a tor_proto::Error.
        // And unifying both is hard here considering it needs to be converted to yet another Error
        // type "FAE" so we have to do these hoops and jumps.
        rend_tunnel
            .m_start_conversation_last_hop(Some(message.into()), handler)
            .await
            .map_err(|e| {
                let proto_error = match e {
                    tor_circmgr::Error::Protocol { error, .. } => error,
                    _ => tor_proto::Error::CircuitClosed,
                };
                FAE::RendezvousEstablish {
                    error: proto_error,
                    rend_pt: rend_pt.clone(),
                }
            })?;
        // `start_conversation` returns as soon as the control message has been sent.
        // We need to obtain the RENDEZVOUS_ESTABLISHED message, which is "returned" via the oneshot.
        let _: RendezvousEstablished = self
            .runtime
            .timeout(timeout_roundtrip, rend_established_rx.recv(failed_map_err))
            .await
            .map_err(
                |_timeout: tor_rtcompat::TimeoutError| FAE::RendezvousEstablishTimeout {
                    rend_pt: rend_pt.clone(),
                },
            )??;
        debug!(
            "hs conn to {}: RPT {}: got RENDEZVOUS_ESTABLISHED",
            &self.hsid,
            rend_pt.as_inner(),
        );
        Ok(Rendezvous {
            rend_tunnel,
            rend_cookie,
            rend_relay,
            rend2_rx,
            marker: PhantomData,
        })
170
    }
    /// Attempt (once) to send an INTRODUCE1 and wait for the INTRODUCE_ACK
    ///
    /// `take`s the input `rendezvous` (but only takes it if it gets that far)
    /// and, if successful, returns it.
    /// (This arranges that the rendezvous is "used up" precisely if
    /// we sent its secret somewhere.)
    ///
    /// Although this function handles the `Rendezvous`,
    /// nothing in it actually involves the rendezvous point.
    /// So if there's a failure, it's purely to do with the introduction point.
    ///
    /// Applies timeouts as appropriate.
    #[allow(clippy::cognitive_complexity, clippy::type_complexity)] // TODO: Refactor
    #[instrument(level = "trace", skip_all)]
170
    async fn exchange_introduce(
170
        &'c self,
170
        hsdesc: &HsDesc,
170
        ipt: &UsableIntroPt<'_>,
170
        rendezvous: &mut Option<Rendezvous<'c, R, M>>,
170
        proof_of_work: Option<ProofOfWork>,
170
    ) -> Result<(Rendezvous<'c, R, M>, Introduced<R, M>), FAE> {
170
        let intro_index = ipt.intro_index;
        debug!(
            "hs conn to {}: IPT {}: obtaining intro circuit",
            &self.hsid, intro_index,
        );
        let intro_circ = self
            .circpool
            .m_get_or_launch_intro(
                &self.netdir,
                ipt.intro_target.clone(), // &OwnedCircTarget isn't CircTarget apparently
            )
            .await
            .map_err(|error| FAE::IntroductionCircuitObtain { error, intro_index })?;
        let rendezvous = rendezvous.take().ok_or_else(|| internal!("no rend"))?;
        let rend_pt = rend_pt_identity_for_error(&rendezvous.rend_relay);
        debug!(
            "hs conn to {}: RPT {} IPT {}: making introduction",
            &self.hsid,
            rend_pt.as_inner(),
            intro_index,
        );
        // Now we construct an introduce1 message and perform the first part of the
        // rendezvous handshake.
        //
        // This process is tricky because the header of the INTRODUCE1 message
        // -- which depends on the IntroPt configuration -- is authenticated as
        // part of the HsDesc handshake.
        // Construct the header, since we need it as input to our encryption.
        let intro_header = {
            let ipt_sid_key = ipt.intro_desc.ipt_sid_key();
            let intro1 = Introduce1::new(
                AuthKeyType::ED25519_SHA3_256,
                ipt_sid_key.as_bytes().to_vec(),
                vec![],
            );
            let mut header = vec![];
            intro1
                .encode_onto(&mut header)
                .map_err(into_internal!("couldn't encode intro1 header"))?;
            header
        };
        let peer_caps = caps::PeerCaps::new(hsdesc);
        // Construct the introduce payload, which tells the onion service how to find
        // our rendezvous point.  (We could do this earlier if we wanted.)
        let intro_payload = {
            let onion_key =
                intro_payload::OnionKey::NtorOnionKey(*rendezvous.rend_relay.ntor_onion_key());
            let linkspecs = rendezvous
                .rend_relay
                .linkspecs()
                .map_err(into_internal!("Couldn't encode link specifiers"))?;
            #[allow(unused_mut)] // TODO: Remove once negotiate-extensions is always-on.
            let mut payload = IntroduceHandshakePayload::new(
                rendezvous.rend_cookie,
                onion_key,
                linkspecs,
                proof_of_work,
            );
            peer_caps.add_extensions(&mut payload);
            let mut encoded = vec![];
            payload
                .write_onto(&mut encoded)
                .map_err(into_internal!("Couldn't encode introduce1 payload"))?;
            encoded
        };
        // Perform the cryptographic handshake with the onion service.
        let service_info = hs_ntor::HsNtorServiceInfo::new(
            ipt.intro_desc.svc_ntor_key().clone(),
            ipt.intro_desc.ipt_sid_key().clone(),
            self.subcredential,
        );
        let handshake_state =
            hs_ntor::HsNtorClientState::new(&mut self.mocks.thread_rng(), service_info);
        let encrypted_body = handshake_state
            .client_send_intro(&intro_header, &intro_payload)
            .map_err(into_internal!("can't begin hs-ntor handshake"))?;
        // Build our actual INTRODUCE1 message.
        let intro1_real = Introduce1::new(
            AuthKeyType::ED25519_SHA3_256,
            ipt.intro_desc.ipt_sid_key().as_bytes().to_vec(),
            encrypted_body,
        );
        /// Handler which expects just `INTRODUCE_ACK`
        struct Handler {
            /// Sender for `INTRODUCE_ACK`
            intro_ack_tx: proto_oneshot::Sender<IntroduceAck>,
        }
        impl MsgHandler for Handler {
170
            fn handle_msg(
170
                &mut self,
170
                msg: AnyRelayMsg,
170
            ) -> Result<MetaCellDisposition, tor_proto::Error> {
170
                self.intro_ack_tx
170
                    .deliver_expected_message(msg, MetaCellDisposition::ConversationFinished)
170
            }
        }
        let failed_map_err = |error| FAE::IntroductionExchange { error, intro_index };
        let (intro_ack_tx, intro_ack_rx) = proto_oneshot::channel();
        let handler = Handler { intro_ack_tx };
        let num_hops = intro_circ
            .m_num_hops()
            .map_err(|error| FAE::IntroductionCircuitObtain { error, intro_index })?;
        // NOTE: Should we allow this to be longer in case the introduction point is grievously
        // overloaded?
        let timeout_roundtrip =
            self.estimate_timeout(&[(1, TimeoutsAction::RoundTrip { length: num_hops })]);
        debug!(
            "hs conn to {}: RPT {} IPT {}: making introduction - sending INTRODUCE1",
            &self.hsid,
            rend_pt.as_inner(),
            intro_index,
        );
        // TODO(conflux) This error handling is horrible. Problem is that this Mock system requires
        // to send back a tor_circmgr::Error while our reply handler requires a tor_proto::Error.
        // And unifying both is hard here considering it needs to be converted to yet another Error
        // type "FAE" so we have to do these hoops and jumps.
        intro_circ
            .m_start_conversation_last_hop(Some(intro1_real.into()), handler)
            .await
            .map_err(|e| {
                let proto_error = match e {
                    tor_circmgr::Error::Protocol { error, .. } => error,
                    _ => tor_proto::Error::CircuitClosed,
                };
                FAE::IntroductionExchange {
                    error: proto_error,
                    intro_index,
                }
            })?;
        // Status is checked by `.success()`, and we don't look at the extensions;
        // just discard the known-successful `IntroduceAck`
        let _: IntroduceAck = self
            .runtime
            .timeout(timeout_roundtrip, intro_ack_rx.recv(failed_map_err))
            .await
            .map_err(|_timeout: TimeoutError| FAE::IntroductionTimeout { intro_index })??
            .success()
            .map_err(|status| FAE::IntroductionFailed {
166
                status,
166
                intro_index,
166
            })?;
        debug!(
            "hs conn to {}: RPT {} IPT {}: making introduction - success",
            &self.hsid,
            rend_pt.as_inner(),
            intro_index,
        );
        // Having received INTRODUCE_ACK. we can forget about this circuit
        // (and potentially tear it down).
        drop(intro_circ);
        Ok((
            rendezvous,
            Introduced {
                handshake_state,
                marker: PhantomData,
                peer_caps,
            },
        ))
170
    }
    /// Attempt (once) to connect a rendezvous circuit using the given intro pt.
    ///
    /// That is to say, we simply wait for a RENDEZVOUS2 message,
    /// and if we get one, we add a virtual hop.
    ///
    /// Timeouts here might be due to the IPT, RPT, service,
    /// or any of the intermediate relays.
    ///
    /// If, rather than a timeout, we actually encounter some kind of error,
    /// we'll return the appropriate `FailedAttemptError`.
    /// (Who is responsible may vary, so the `FailedAttemptError` variant will reflect that.)
    #[allow(clippy::cognitive_complexity)]
4
    async fn complete_rendezvous(
4
        &'c self,
4
        ipt: &UsableIntroPt<'_>,
4
        rendezvous: Rendezvous<'c, R, M>,
4
        introduced: Introduced<R, M>,
4
        is_single_onion_service: bool,
4
    ) -> Result<DataTunnel!(R, M), FAE> {
        /// Largest number of hops that the onion service must build for _its_
        /// circuits to our rendezvous points.
        ///
        /// This is 4 hops (assuming that it has full vanguards enabled) plus one for the
        /// renedezvous point itself.
        const MAX_PEER_REND_HOPS: usize = 5;
        /// Largest number of retries that we think the peer might make if its
        /// circuits are failing.
        const MAX_PEER_CIRC_RETRIES: u32 = 3;
4
        let rend_pt = rend_pt_identity_for_error(&rendezvous.rend_relay);
4
        let intro_index = ipt.intro_index;
4
        let failed_map_err = |error| FAE::RendezvousCompletionCircuitError {
            error,
            intro_index,
            rend_pt: rend_pt.clone(),
        };
4
        debug!(
            "hs conn to {}: RPT {} IPT {}: awaiting rendezvous completion",
            &self.hsid,
            rend_pt.as_inner(),
            intro_index,
        );
4
        let num_hops = rendezvous
4
            .rend_tunnel
4
            .m_num_own_hops()
            // This is not necessarily the best error, but it isn't totally wrong.
            // We can't wrap the tor_circuit error in anything else that makes sense.
            // See #2513.
4
            .map_err(|error| FAE::RendezvousCircuitObtain { error })?;
        // Maximum length of the circuit that the peer will build to the rendezvous point.
4
        let peer_rend_circ_len = if is_single_onion_service {
            1
        } else {
4
            MAX_PEER_REND_HOPS
        };
        // The total number of hops from the peer to us.
        //
        // We subtract 1 because both circuits terminate at the rendezvous point.
4
        let total_circ_len = peer_rend_circ_len + num_hops - 1;
        // Limit on the duration of each attempt for activities involving both
        // RPT and IPT.
4
        let rpt_ipt_timeout = self.estimate_timeout(&[
4
            // The API requires us to specify a number of circuit builds and round trips.
4
            // So what we tell the estimator is a rather imprecise description.
4
            //
4
            // What we are timing here is:
4
            //
4
            //    INTRODUCE2 goes from IPT to HS.
4
            //    This happens in parallel with our waiting for the INTRODUCE_ACK,
4
            //    and we know that our own introduction circuit is always at least
4
            //    as long as the peer's (even if they are using full vanguards),
4
            //    so we don't need any additional delay here.
4
            //
4
            //    HS builds to our RPT
4
            (
4
                MAX_PEER_CIRC_RETRIES,
4
                TimeoutsAction::BuildCircuit {
4
                    length: peer_rend_circ_len,
4
                },
4
            ),
4
            //
4
            //    RENDEZVOUS1 goes from HS to RPT.  `peer_circ_len`, one-way.
4
            //    RENDEZVOUS2 goes from RPT to us.  `num_hops`, one-way.
4
            (
4
                1,
4
                TimeoutsAction::OneWay {
4
                    length: total_circ_len,
4
                },
4
            ),
4
        ]);
4
        let rend2_msg: Rendezvous2 = self
4
            .runtime
4
            .timeout(rpt_ipt_timeout, rendezvous.rend2_rx.recv(failed_map_err))
4
            .await
4
            .map_err(|_: TimeoutError| FAE::RendezvousCompletionTimeout {
                intro_index,
                rend_pt: rend_pt.clone(),
            })??;
4
        debug!(
            "hs conn to {}: RPT {} IPT {}: received RENDEZVOUS2",
            &self.hsid,
            rend_pt.as_inner(),
            intro_index,
        );
        // In theory would be great if we could have multiple introduction attempts in parallel
        // with similar x,X values but different IPTs.  However, our HS experts don't
        // think increasing parallelism here is important:
        //   https://gitlab.torproject.org/tpo/core/arti/-/issues/913#note_2914438
4
        let handshake_state = introduced.handshake_state;
        // Try to complete the cryptographic handshake.
4
        let keygen =
4
            self.mocks
4
                .rendezvous_handshake(handshake_state, rend2_msg, intro_index, &rend_pt)?;
4
        let mut params = onion_circparams_from_netparams(self.netdir.params())
4
            .map_err(into_internal!("Failed to build CircParameters"))?;
4
        if let Some(inc) = introduced.peer_caps.cc_sendme_inc() {
4
            params.ccontrol.override_sendme_inc(inc);
4
        }
4
        let protocols = introduced.peer_caps.shared_protos();
4
        rendezvous
4
            .rend_tunnel
4
            .m_extend_virtual(
4
                handshake::RelayProtocol::HsV3,
4
                handshake::HandshakeRole::Initiator,
4
                keygen,
4
                params,
4
                &protocols,
4
            )
4
            .await
4
            .map_err(into_internal!(
                "actually this is probably a 'circuit closed' error" // TODO HS
            ))?;
4
        debug!(
            "hs conn to {}: RPT {} IPT {}: HS circuit established",
            &self.hsid,
            rend_pt.as_inner(),
            intro_index,
        );
4
        Ok(rendezvous.rend_tunnel)
4
    }
    /// Helper to estimate a timeout for a complicated operation
    ///
    /// `actions` is a list of `(count, action)`, where each entry
    /// represents doing `action`, `count` times sequentially.
    ///
    /// Combines the timeout estimates and returns an overall timeout.
358
    fn estimate_timeout(&self, actions: &[(u32, TimeoutsAction)]) -> Duration {
        // This algorithm is, perhaps, wrong.  For uncorrelated variables, a particular
        // percentile estimate for a sum of random variables, is not calculated by adding the
        // percentile estimates of the individual variables.
        //
        // But the actual lengths of times of the operations aren't uncorrelated.
        // If they were *perfectly* correlated, then this addition would be correct.
        // It will do for now; it just might be rather longer than it ought to be.
358
        actions
358
            .iter()
362
            .map(|(count, action)| {
362
                self.circpool
362
                    .m_estimate_timeout(action)
362
                    .saturating_mul(*count)
362
            })
358
            .fold(Duration::ZERO, Duration::saturating_add)
358
    }
}
/// Mocks used for testing `connect.rs`
///
/// This is different to `MockableConnectorData`,
/// which is used to *replace* this file, when testing `state.rs`.
///
/// `MocksForConnect` provides mock facilities for *testing* this file.
//
// TODO this should probably live somewhere else, maybe tor-circmgr even?
// TODO this really ought to be made by macros or something
trait MocksForConnect<R>: Clone {
    /// HS circuit pool
    type HsCircPool: MockableCircPool<R>;
    /// A random number generator
    type Rng: rand::Rng + rand::CryptoRng;
    /// Key generator used for generating the keys for the virtual hop.
    type KeyGenerator: tor_proto::client::circuit::handshake::KeyGenerator + Send;
    /// Tell tests we got this descriptor text
    fn test_got_desc(&self, _: &HsDesc) {}
    /// Tell tests we got this data tunnel.
4
    fn test_got_tunnel(&self, _: &DataTunnel!(R, Self)) {}
    /// Tell tests we have obtained and sorted the intros like this
    fn test_got_ipts(&self, _: &[UsableIntroPt]) {}
    /// Return a random number generator
    fn thread_rng(&self) -> Self::Rng;
    /// Complete the rendezvous handshake, returning the resulting keygen
    fn rendezvous_handshake(
        &self,
        handshake_state: hs_ntor::HsNtorClientState,
        rend2_msg: Rendezvous2,
        intro_index: IntroPtIndex,
        rend_pt: &RendPtIdentityForError,
    ) -> Result<Self::KeyGenerator, FAE>;
}
/// Mock for `HsCircPool`
///
/// Methods start with `m_` to avoid the following problem:
/// `ClientCirc::start_conversation` (say) means
/// to use the inherent method if one exists,
/// but will use a trait method if there isn't an inherent method.
///
/// So if the inherent method is renamed, the call in the impl here
/// turns into an always-recursive call.
/// This is not detected by the compiler due to the situation being
/// complicated by futures, `#[async_trait]` etc.
/// <https://github.com/rust-lang/rust/issues/111177>
#[async_trait]
trait MockableCircPool<R> {
    /// Directory tunnel.
    type DirTunnel: MockableClientDir;
    /// Data tunnel.
    type DataTunnel: MockableClientData;
    /// Intro tunnel.
    type IntroTunnel: MockableClientIntro;
    async fn m_get_or_launch_dir(
        &self,
        netdir: &NetDir,
        target: impl CircTarget + Send + Sync + 'async_trait,
    ) -> tor_circmgr::Result<Self::DirTunnel>;
    async fn m_get_or_launch_intro(
        &self,
        netdir: &NetDir,
        target: impl CircTarget + Send + Sync + 'async_trait,
    ) -> tor_circmgr::Result<Self::IntroTunnel>;
    /// Client circuit
    async fn m_get_or_launch_client_rend<'a>(
        &self,
        netdir: &'a NetDir,
    ) -> tor_circmgr::Result<(Self::DataTunnel, Relay<'a>)>;
    /// Estimate timeout
    fn m_estimate_timeout(&self, action: &TimeoutsAction) -> Duration;
}
/// Mock for onion service client directory tunnel.
#[async_trait]
trait MockableClientDir: Debug {
    /// Client circuit
    type DirStream: AsyncRead + AsyncWrite + Send + Unpin;
    async fn m_begin_dir_stream(&self) -> tor_circmgr::Result<Self::DirStream>;
    /// Get a tor_dirclient::SourceInfo for this circuit, if possible.
    fn m_source_info(&self) -> tor_proto::Result<Option<SourceInfo>>;
    /// Return the length of this circuit.
    fn m_num_hops(&self) -> tor_circmgr::Result<usize>;
}
/// Mock for onion service client data tunnel.
#[async_trait]
trait MockableClientData: Debug {
    /// Conversation
    type Conversation<'r>
    where
        Self: 'r;
    /// Converse
    async fn m_start_conversation_last_hop(
        &self,
        msg: Option<AnyRelayMsg>,
        reply_handler: impl MsgHandler + Send + 'static,
    ) -> tor_circmgr::Result<Self::Conversation<'_>>;
    /// Add a virtual hop to the circuit.
    async fn m_extend_virtual(
        &self,
        protocol: handshake::RelayProtocol,
        role: handshake::HandshakeRole,
        handshake: impl handshake::KeyGenerator + Send,
        params: CircParameters,
        capabilities: &tor_protover::Protocols,
    ) -> tor_circmgr::Result<()>;
    /// Return the number of our own hops in this circuit.
    ///
    /// This does not count any hops for the service's rendezvous circuit.
    /// It does count our virtual hop, if we have one.
    /// (That isn't a problem, since we only use this method to calculate
    /// timeouts, and we only calculate timeouts _before_ we establish
    /// the virtual hop.)
    fn m_num_own_hops(&self) -> tor_circmgr::Result<usize>;
}
/// Mock for onion service client introduction tunnel.
#[async_trait]
trait MockableClientIntro: Debug {
    /// Conversation
    type Conversation<'r>
    where
        Self: 'r;
    /// Converse
    async fn m_start_conversation_last_hop(
        &self,
        msg: Option<AnyRelayMsg>,
        reply_handler: impl MsgHandler + Send + 'static,
    ) -> tor_circmgr::Result<Self::Conversation<'_>>;
    /// Return the number of hops in this circuit.
    fn m_num_hops(&self) -> tor_circmgr::Result<usize>;
}
impl<R: Runtime> MocksForConnect<R> for () {
    type HsCircPool = HsCircPool<R>;
    type Rng = rand::rngs::ThreadRng;
    type KeyGenerator = HsNtorHkdfKeyGenerator;
    fn thread_rng(&self) -> Self::Rng {
        rand::rng()
    }
    fn rendezvous_handshake(
        &self,
        handshake_state: hs_ntor::HsNtorClientState,
        rend2_msg: Rendezvous2,
        intro_index: IntroPtIndex,
        rend_pt: &RendPtIdentityForError,
    ) -> Result<Self::KeyGenerator, FAE> {
        // Try to complete the cryptographic handshake.
        handshake_state
            .client_receive_rend(rend2_msg.handshake_info())
            // If this goes wrong. either the onion service has mangled the crypto,
            // or the rendezvous point has misbehaved (that that is possible is a protocol bug),
            // or we have used the wrong handshake_state (let's assume that's not true).
            //
            // If this happens we'll go and try another RPT.
            .map_err(|error| FAE::RendezvousCompletionHandshake {
                error,
                intro_index,
                rend_pt: rend_pt.clone(),
            })
    }
}
#[async_trait]
impl<R: Runtime> MockableCircPool<R> for HsCircPool<R> {
    type DirTunnel = ClientOnionServiceDirTunnel;
    type DataTunnel = ClientOnionServiceDataTunnel;
    type IntroTunnel = ClientOnionServiceIntroTunnel;
    #[instrument(level = "trace", skip_all)]
    async fn m_get_or_launch_dir(
        &self,
        netdir: &NetDir,
        target: impl CircTarget + Send + Sync + 'async_trait,
    ) -> tor_circmgr::Result<Self::DirTunnel> {
        Ok(HsCircPool::get_or_launch_client_dir(self, netdir, target).await?)
    }
    #[instrument(level = "trace", skip_all)]
    async fn m_get_or_launch_intro(
        &self,
        netdir: &NetDir,
        target: impl CircTarget + Send + Sync + 'async_trait,
    ) -> tor_circmgr::Result<Self::IntroTunnel> {
        Ok(HsCircPool::get_or_launch_client_intro(self, netdir, target).await?)
    }
    #[instrument(level = "trace", skip_all)]
    async fn m_get_or_launch_client_rend<'a>(
        &self,
        netdir: &'a NetDir,
    ) -> tor_circmgr::Result<(Self::DataTunnel, Relay<'a>)> {
        HsCircPool::get_or_launch_client_rend(self, netdir).await
    }
    fn m_estimate_timeout(&self, action: &TimeoutsAction) -> Duration {
        HsCircPool::estimate_timeout(self, action)
    }
}
#[async_trait]
impl MockableClientDir for ClientOnionServiceDirTunnel {
    /// Client circuit
    type DirStream = tor_proto::client::stream::DataStream;
    async fn m_begin_dir_stream(&self) -> tor_circmgr::Result<Self::DirStream> {
        Self::begin_dir_stream(self).await
    }
    /// Get a tor_dirclient::SourceInfo for this circuit, if possible.
    fn m_source_info(&self) -> tor_proto::Result<Option<SourceInfo>> {
        SourceInfo::from_tunnel(self)
    }
    fn m_num_hops(&self) -> tor_circmgr::Result<usize> {
        self.n_hops()
    }
}
#[async_trait]
impl MockableClientData for ClientOnionServiceDataTunnel {
    type Conversation<'r> = tor_proto::Conversation<'r>;
    async fn m_start_conversation_last_hop(
        &self,
        msg: Option<AnyRelayMsg>,
        reply_handler: impl MsgHandler + Send + 'static,
    ) -> tor_circmgr::Result<Self::Conversation<'_>> {
        Self::start_conversation(self, msg, reply_handler, TargetHop::LastHop).await
    }
    async fn m_extend_virtual(
        &self,
        protocol: handshake::RelayProtocol,
        role: handshake::HandshakeRole,
        handshake: impl handshake::KeyGenerator + Send,
        params: CircParameters,
        capabilities: &tor_protover::Protocols,
    ) -> tor_circmgr::Result<()> {
        Self::extend_virtual(self, protocol, role, handshake, params, capabilities).await
    }
    fn m_num_own_hops(&self) -> tor_circmgr::Result<usize> {
        self.n_hops()
    }
}
#[async_trait]
impl MockableClientIntro for ClientOnionServiceIntroTunnel {
    type Conversation<'r> = tor_proto::Conversation<'r>;
    async fn m_start_conversation_last_hop(
        &self,
        msg: Option<AnyRelayMsg>,
        reply_handler: impl MsgHandler + Send + 'static,
    ) -> tor_circmgr::Result<Self::Conversation<'_>> {
        Self::start_conversation(self, msg, reply_handler, TargetHop::LastHop).await
    }
    fn m_num_hops(&self) -> tor_circmgr::Result<usize> {
        self.n_hops()
    }
}
#[async_trait]
impl MockableConnectorData for Data {
    type DataTunnel = ClientOnionServiceDataTunnel;
    type MockGlobalState = ();
    async fn connect<R: Runtime>(
        connector: &HsClientConnector<R>,
        netdir: Arc<NetDir>,
        config: Arc<Config>,
        hsid: HsId,
        data: &mut Self,
        secret_keys: HsClientSecretKeys,
    ) -> Result<Self::DataTunnel, ConnError> {
        connect(connector, netdir, config, hsid, data, secret_keys).await
    }
    fn tunnel_is_ok(tunnel: &Self::DataTunnel) -> bool {
        !tunnel.is_closed()
    }
}
/// Return an error if `desc` declares a set of parameters
/// that is too far away from the consensus.
14
fn ensure_descriptor_compatible_with_params(
14
    desc: &HsDesc,
14
    params: &NetParameters,
14
) -> Result<(), DescriptorErrorDetail> {
14
    if let Some((_, desc_inc)) = desc.flow_control() {
14
        let difference = u8::from(desc_inc).abs_diff(params.cc_sendme_inc.into());
14
        if difference > 1 {
            return Err(DescriptorErrorDetail::ParameterMismatch(
                "cc_sendme_inc too far from consensus".into(),
            ));
14
        }
    }
14
    Ok(())
14
}
#[cfg(test)]
mod test {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::mixed_attributes_style)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_time_subtraction)]
    #![allow(clippy::useless_vec)]
    #![allow(clippy::needless_pass_by_value)]
    #![allow(clippy::string_slice)] // See arti#2571
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
    #![allow(dead_code, unused_variables)] // TODO HS TESTS delete, after tests are completed
    use super::*;
    use crate::*;
    use itertools::chain;
    use std::iter;
    use tokio_crate as tokio;
    use tor_async_utils::JoinReadWrite;
    use tor_basic_utils::test_rng::{TestingRng, testing_rng};
    use tor_hscrypto::pk::{HsClientDescEncKey, HsClientDescEncKeypair};
    use tor_llcrypto::pk::curve25519;
    use tor_netdoc::doc::{hsdesc::test_data, netstatus::Lifetime};
    use tor_rtcompat::RuntimeSubstExt as _;
    use tor_rtcompat::tokio::TokioNativeTlsRuntime;
    use tor_rtmock::simple_time::SimpleMockTimeProvider;
    use tracing_test::traced_test;
    #[derive(derive_more::Debug, Default)]
    struct MocksGlobal {
        hsdirs_asked: Vec<OwnedCircTarget>,
        got_desc: Option<HsDesc>,
        #[debug(skip)]
        rendezvous: Option<Box<dyn MsgHandler + Send + 'static>>,
        intro_acks: Vec<(IntroduceAck, MetaCellDisposition)>,
    }
    #[derive(Clone, Debug)]
    struct Mocks<I> {
        mglobal: Arc<Mutex<MocksGlobal>>,
        id: I,
    }
    struct MockKeyGenerator;
    impl handshake::KeyGenerator for MockKeyGenerator {
        fn expand(self, _keylen: usize) -> tor_proto::Result<tor_bytes::SecretBuf> {
            todo!()
        }
    }
    impl<R: Runtime> MocksForConnect<R> for Mocks<()> {
        type HsCircPool = Mocks<()>;
        type Rng = TestingRng;
        type KeyGenerator = MockKeyGenerator;
        fn test_got_desc(&self, desc: &HsDesc) {
            self.mglobal.lock().unwrap().got_desc = Some(desc.clone());
        }
        fn test_got_ipts(&self, desc: &[UsableIntroPt]) {}
        fn thread_rng(&self) -> Self::Rng {
            testing_rng()
        }
        fn rendezvous_handshake(
            &self,
            _handshake_state: hs_ntor::HsNtorClientState,
            _rend2_msg: Rendezvous2,
            _intro_index: IntroPtIndex,
            _rend_pt: &RendPtIdentityForError,
        ) -> Result<Self::KeyGenerator, FAE> {
            Ok(MockKeyGenerator)
        }
    }
    #[async_trait]
    impl<R: Runtime> MockableCircPool<R> for Mocks<()> {
        type DataTunnel = Mocks<()>;
        type DirTunnel = Mocks<()>;
        type IntroTunnel = Mocks<()>;
        async fn m_get_or_launch_dir(
            &self,
            _netdir: &NetDir,
            target: impl CircTarget + Send + Sync + 'async_trait,
        ) -> tor_circmgr::Result<Self::DirTunnel> {
            let target = OwnedCircTarget::from_circ_target(&target);
            self.mglobal.lock().unwrap().hsdirs_asked.push(target);
            Ok(self.clone())
        }
        async fn m_get_or_launch_intro(
            &self,
            _netdir: &NetDir,
            target: impl CircTarget + Send + Sync + 'async_trait,
        ) -> tor_circmgr::Result<Self::IntroTunnel> {
            Ok(self.clone())
        }
        /// Client circuit
        async fn m_get_or_launch_client_rend<'a>(
            &self,
            netdir: &'a NetDir,
        ) -> tor_circmgr::Result<(Self::DataTunnel, Relay<'a>)> {
            // Pick one of the relays we know to be in the test net as our RPT
            let rpt = netdir.by_id(&Ed25519Identity::from([12; 32])).unwrap();
            Ok((self.clone(), rpt))
        }
        fn m_estimate_timeout(&self, action: &TimeoutsAction) -> Duration {
            Duration::from_secs(10)
        }
    }
    #[async_trait]
    impl MockableClientDir for Mocks<()> {
        type DirStream = JoinReadWrite<futures::io::Cursor<Box<[u8]>>, futures::io::Sink>;
        async fn m_begin_dir_stream(&self) -> tor_circmgr::Result<Self::DirStream> {
            let response = format!(
                r#"HTTP/1.1 200 OK
{}"#,
                test_data::TEST_DATA_2
            )
            .into_bytes()
            .into_boxed_slice();
            Ok(JoinReadWrite::new(
                futures::io::Cursor::new(response),
                futures::io::sink(),
            ))
        }
        fn m_source_info(&self) -> tor_proto::Result<Option<SourceInfo>> {
            Ok(None)
        }
        fn m_num_hops(&self) -> tor_circmgr::Result<usize> {
            Ok(4)
        }
    }
    #[async_trait]
    impl MockableClientData for Mocks<()> {
        type Conversation<'r> = &'r ();
        async fn m_start_conversation_last_hop(
            &self,
            msg: Option<AnyRelayMsg>,
            mut reply_handler: impl MsgHandler + Send + 'static,
        ) -> tor_circmgr::Result<Self::Conversation<'_>> {
            match msg {
                Some(AnyRelayMsg::EstablishRendezvous(_)) => {
                    let reply = RendezvousEstablished::default();
                    let disp = reply_handler.handle_msg(reply.into()).unwrap();
                    assert_eq!(disp, MetaCellDisposition::Consumed);
                    // Save this, because we'll need to use it later,
                    // when handling the INTRODUCE1
                    let mut global = self.mglobal.lock().unwrap();
                    global.rendezvous = Some(Box::new(reply_handler));
                }
                _ => panic!("unexpected msg {msg:?}"),
            }
            Ok(&())
        }
        async fn m_extend_virtual(
            &self,
            protocol: handshake::RelayProtocol,
            role: handshake::HandshakeRole,
            handshake: impl handshake::KeyGenerator + Send,
            params: CircParameters,
            capabilities: &tor_protover::Protocols,
        ) -> tor_circmgr::Result<()> {
            Ok(())
        }
        fn m_num_own_hops(&self) -> tor_circmgr::Result<usize> {
            Ok(4)
        }
    }
    #[async_trait]
    impl MockableClientIntro for Mocks<()> {
        type Conversation<'r> = &'r ();
        async fn m_start_conversation_last_hop(
            &self,
            msg: Option<AnyRelayMsg>,
            mut reply_handler: impl MsgHandler + Send + 'static,
        ) -> tor_circmgr::Result<Self::Conversation<'_>> {
            match msg {
                Some(AnyRelayMsg::Introduce1(introduce1)) => {
                    let mut global = self.mglobal.lock().unwrap();
                    let (reply, expected_disp) = global.intro_acks.remove(0);
                    let disp = reply_handler.handle_msg(reply.into()).unwrap();
                    assert_eq!(disp, expected_disp);
                    // Mock the service's response
                    let rendezvous = global
                        .rendezvous
                        .as_mut()
                        .expect("got INTRODUCE1 before ESTABLISH_RENDEZVOUS?!");
                    let reply = Rendezvous2::new(b"dummy handshake info, ignored");
                    let disp = rendezvous.handle_msg(reply.into()).unwrap();
                    assert_eq!(disp, MetaCellDisposition::ConversationFinished);
                }
                _ => panic!("unexpected msg {msg:?}"),
            }
            Ok(&())
        }
        fn m_num_hops(&self) -> tor_circmgr::Result<usize> {
            Ok(4)
        }
    }
    fn ks_hsc_desc_enc() -> HsClientDescEncKeypair {
        let pk: HsClientDescEncKey = curve25519::PublicKey::from(test_data::TEST_PUBKEY_2).into();
        let sk = curve25519::StaticSecret::from(test_data::TEST_SECKEY_2).into();
        HsClientDescEncKeypair::new(pk, sk)
    }
    fn expected_hsdesc(hsid: HsId, netdir: &NetDir, now: SystemTime) -> HsDesc {
        let time_period = netdir.hs_time_period();
        let (hs_blind_id_key, subcredential) = HsIdKey::try_from(hsid)
            .unwrap()
            .compute_blinded_key(time_period)
            .unwrap();
        let hs_blind_id = hs_blind_id_key.id();
        HsDesc::parse_decrypt_validate(
            test_data::TEST_DATA_2,
            &hs_blind_id,
            now,
            &subcredential,
            Some(&ks_hsc_desc_enc()),
        )
        .unwrap()
        .dangerously_assume_timely()
    }
    fn build_test_netdir() -> Arc<NetDir> {
        let valid_after = humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap();
        let fresh_until = valid_after + humantime::parse_duration("1 hours").unwrap();
        let valid_until = valid_after + humantime::parse_duration("24 hours").unwrap();
        let lifetime = Lifetime::new(valid_after, fresh_until, valid_until).unwrap();
        let netdir = tor_netdir::testnet::construct_custom_netdir_with_params(
            tor_netdir::testnet::simple_net_func,
            iter::empty::<(&str, _)>(),
            Some(lifetime),
        )
        .expect("failed to build default testing netdir");
        Arc::new(netdir.unwrap_if_sufficient().unwrap())
    }
    #[traced_test]
    #[tokio::test]
    async fn test_connect() {
        use MetaCellDisposition::*;
        let netdir = build_test_netdir();
        let runtime = TokioNativeTlsRuntime::current().unwrap();
        let now = humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap();
        let mock_sp = SimpleMockTimeProvider::from_wallclock(now);
        let runtime = runtime
            .with_sleep_provider(mock_sp.clone())
            .with_coarse_time_provider(mock_sp.clone());
        let success = (
            IntroduceAck::new(IntroduceAckStatus::SUCCESS),
            ConversationFinished,
        );
        let nack = (
            IntroduceAck::new(IntroduceAckStatus::NOT_RECOGNIZED),
            ConversationFinished,
        );
        // The number of times to make Context:connect() fail due to intro NACK
        //
        // Set to 5 in order to trigger a rate-limit for all 6 HsDirs:
        //
        // there are 6 HsDirs in total, one of which is "used up" by the
        // first (successful) connect() attempt below.
        const INTRO_FAIL_COUNT: usize = 5;
        /// The number of times we expect the client to retry the
        /// introduction per connect() call
        /// (it will essentially try two rounds of `intro_rend_connect()`,
        /// once with the cached descriptor, and once with the potentially
        /// new descriptor).
        const IPT_RETRY_COUNT: usize = 12;
        // The first introduction will succeed
        let intro_acks = chain!(
            [&success],
            // But the next INTRO_FAIL_COUNT connect() will fail
            // (+1 because we want to fail *again*, in order to find
            // that there's now a limit on all our HsDirs)
            [&nack; IPT_RETRY_COUNT * (INTRO_FAIL_COUNT + 1)],
            // One more round of failures, to trigger a refecth after the rate-limit is lifted
            [&nack; IPT_RETRY_COUNT - 1],
            // After refetching the descriptor, the client will retry the introduction,
            // and succeed.
            [&success],
        )
        .cloned()
        .collect();
        let mglobal = Arc::new(Mutex::new(MocksGlobal {
            intro_acks,
            ..Default::default()
        }));
        let mocks = Mocks { mglobal, id: () };
        // From C Tor src/test/test_hs_common.c test_build_address
        let hsid = test_data::TEST_HSID_2.into();
        let mut data = Data::default();
        let mut expected_hsdirs_asked = 1;
        let mut secret_keys_builder = HsClientSecretKeysBuilder::default();
        secret_keys_builder.ks_hsc_desc_enc(ks_hsc_desc_enc());
        let secret_keys = secret_keys_builder.build().unwrap();
        let ctx = Context::new(
            &runtime,
            &mocks,
            Arc::clone(&netdir),
            Default::default(),
            hsid,
            secret_keys,
            mocks.clone(),
        )
        .unwrap();
        let _got = ctx.connect(&mut data).await.unwrap();
        // Our mock IPT hasn't sent any NACKs yet
        assert!(!logs_contain("NACKed, refetching descriptor and retrying"));
        let hsdesc = expected_hsdesc(hsid, &netdir, now);
        {
            let mglobal = mocks.mglobal.lock().unwrap();
            assert_eq!(mglobal.hsdirs_asked.len(), expected_hsdirs_asked);
            // TODO hs: here and in other places, consider implementing PartialEq instead, or creating
            // an assert_dbg_eq macro (which would be part of a test_helpers crate or something)
            assert_eq!(
                format!("{:?}", mglobal.got_desc),
                format!("{:?}", Some(hsdesc.clone()))
            );
        }
        // Check how long the descriptor is valid for
        let (start_time, end_time) = data.desc.as_ref().unwrap().desc.bounds();
        assert_eq!(start_time, None);
        let desc_valid_until = humantime::parse_rfc3339("2023-02-11T20:00:00Z").unwrap();
        assert_eq!(end_time, Some(desc_valid_until));
        // These attempts will all fail due to intro NACK,
        // and trigger a rate-limit for all 6 HsDirs
        for i in 1..=INTRO_FAIL_COUNT + 1 {
            let err = ctx.connect(&mut data).await.unwrap_err();
            let is_intro_nack = |e| matches!(e, FAE::IntroductionFailed { status, .. });
            // All attempts failed because of our repeated intro NACKs
            assert!(matches!(err, CE::Failed(e) if e.clone().into_iter().all(is_intro_nack)));
            {
                assert!(logs_contain("NACKed, refetching descriptor and retrying"));
                let mglobal = mocks.mglobal.lock().unwrap();
                // Because all intro attempts failed with NACK (NOT_RECOGNIZED),
                // the client must've tried to refetch the descriptor
                if i <= INTRO_FAIL_COUNT {
                    // No rate limiting yet, so the client must've tried to fetch a new
                    // descriptor, before failing again.
                    expected_hsdirs_asked += 1;
                    assert!(!logs_contain("but all hsdirs are rate-limited"));
                    assert_eq!(mglobal.hsdirs_asked.len(), expected_hsdirs_asked);
                } else {
                    // The final failure won't lead to an HsDir fetch
                    // because all HsDirs will be rate-limited at that point
                    assert!(logs_contain("but all hsdirs are rate-limited"));
                    assert_eq!(mglobal.hsdirs_asked.len(), expected_hsdirs_asked);
                }
                // Same descriptor each time
                // TODO hs: here and in other places, consider implementing PartialEq instead, or creating
                // an assert_dbg_eq macro (which would be part of a test_helpers crate or something)
                assert_eq!(
                    format!("{:?}", mglobal.got_desc),
                    format!("{:?}", Some(hsdesc.clone()))
                );
            }
            let (start_time, end_time) = data.desc.as_ref().unwrap().desc.bounds();
            assert_eq!(start_time, None);
            let desc_valid_until = humantime::parse_rfc3339("2023-02-11T20:00:00Z").unwrap();
            assert_eq!(end_time, Some(desc_valid_until));
        }
        // By default, the HsDir fetches are rate-limited for 15min
        mock_sp.advance(Duration::from_secs(15 * 60));
        // Finally, we succeed.
        let _got = ctx.connect(&mut data).await.unwrap();
        // And it turns out we did, in fact refetch the descriptor
        // Finally, we try again, but find that all HsDirs are now rate-limited!
        // So now we advance the time to lift the rate limit, and hope that
        //
        // TODO HS TESTS: we could extend our mock infrastructure
        // to support returning a different hsdesc this time,
        // with various revision counters, to check that the client is indeed
        // keeping the newest one.
        {
            assert!(logs_contain("NACKed, refetching descriptor and retrying"));
            let mglobal = mocks.mglobal.lock().unwrap();
            // Because all intro attempts failed with NACK (NOT_RECOGNIZED),
            // the client must've tried to refetch the descriptor
            expected_hsdirs_asked += 1;
            assert_eq!(mglobal.hsdirs_asked.len(), expected_hsdirs_asked);
        }
        // TODO HS TESTS: check the circuit in got is the one we gave out
        // TODO HS TESTS: continue with this
    }
    // TODO HS TESTS: Test IPT state management and expiry:
    //   - obtain a test descriptor with only a broken ipt
    //     (broken in the sense that intro can be attempted, but will fail somehow)
    //   - try to make a connection and expect it to fail
    //   - assert that the ipt data isn't empty
    //   - cause the descriptor to expire (advance clock)
    //   - start using a mocked RNG if we weren't already and pin its seed here
    //   - make a new descriptor with two IPTs: the broken one from earlier, and a new one
    //   - make a new connection
    //   - use test_got_ipts to check that the random numbers
    //     would sort the bad intro first, *and* that the good one is appears first
    //   - assert that connection succeeded
    //   - cause the circuit and descriptor to expire (advance clock)
    //   - go back to the previous descriptor contents, but with a new validity period
    //   - try to make a connection
    //   - use test_got_ipts to check that only the broken ipt is present
    // TODO HS TESTS: test retries (of every retry loop we have here)
    // TODO HS TESTS: test error paths
}