tor_hsservice/publish/reactor.rs
1//! The onion service publisher reactor.
2//!
3//! Generates and publishes hidden service descriptors in response to various events.
4//!
5//! [`Reactor::run`] is the entry-point of the reactor. It starts the reactor,
6//! and runs until [`Reactor::run_once`] returns [`ShutdownStatus::Terminate`]
7//! or a fatal error occurs. `ShutdownStatus::Terminate` is returned if
8//! any of the channels the reactor is receiving events from is closed
9//! (i.e. when the senders are dropped).
10//!
11//! ## Publisher status
12//!
13//! The publisher has an internal [`PublishStatus`], distinct from its [`State`],
14//! which is used for onion service status reporting.
15//!
16//! The main loop of the reactor reads the current `PublishStatus` from `publish_status_rx`,
17//! and responds by generating and publishing a new descriptor if needed.
18//!
19//! See [`PublishStatus`] and [`Reactor::publish_status_rx`] for more details.
20//!
21//! ## When do we publish?
22//!
23//! We generate and publish a new descriptor if
24//! * the introduction points have changed
25//! * the onion service configuration has changed in a meaningful way (for example,
26//! if the `restricted_discovery` configuration or its [`Anonymity`](crate::Anonymity)
27//! has changed. See [`OnionServiceConfigPublisherView`]).
28//! * there is a new consensus
29//! * it is time to republish the descriptor (after we upload a descriptor,
30//! we schedule it for republishing at a random time between 60 minutes and 120 minutes
31//! in the future)
32//!
33//! ## Onion service status
34//!
35//! With respect to [`OnionServiceStatus`] reporting,
36//! the following state transitions are possible:
37//!
38//!
39//! ```ignore
40//!
41//! update_publish_status(UploadScheduled|AwaitingIpts|RateLimited)
42//! +---------------------------------------+
43//! | |
44//! | v
45//! | +---------------+
46//! | | Bootstrapping |
47//! | +---------------+
48//! | |
49//! | | uploaded to at least
50//! | not enough HsDir uploads succeeded | some HsDirs from each ring
51//! | +-----------------------------+-----------------------+
52//! | | | |
53//! | | all HsDir uploads succeeded |
54//! | | | |
55//! | v v v
56//! | +---------------------+ +---------+ +---------------------+
57//! | | DegradedUnreachable | | Running | | DegradedReachable |
58//! +----------+ | +---------------------+ +---------+ +---------------------+
59//! | Shutdown |-- | | | |
60//! +----------+ | | | |
61//! | | | |
62//! | | | |
63//! | +---------------------------+------------------------+
64//! | | invalid authorized_clients
65//! | | after handling config change
66//! | |
67//! | v
68//! | run_once() returns an error +--------+
69//! +-------------------------------->| Broken |
70//! +--------+
71//! ```
72//!
73//! We can also transition from `Broken`, `DegradedReachable`, or `DegradedUnreachable`
74//! back to `Bootstrapping` (those transitions were omitted for brevity).
75
76use tor_circmgr::ServiceOnionServiceDirTunnel;
77use tor_config::file_watcher::{
78 self, Event as FileEvent, FileEventReceiver, FileEventSender, FileWatcher, FileWatcherBuilder,
79};
80use tor_config_path::{CfgPath, CfgPathResolver};
81use tor_dirclient::SourceInfo;
82use tor_netdir::{DirEvent, NetDir};
83use tracing::instrument;
84
85use crate::config::OnionServiceConfigPublisherView;
86use crate::config::restricted_discovery::{
87 DirectoryKeyProviderList, RestrictedDiscoveryConfig, RestrictedDiscoveryKeys,
88};
89use crate::status::{DescUploadRetryError, Problem};
90
91use super::*;
92use derive_more::From;
93
94// TODO-CLIENT-AUTH: perhaps we should add a separate CONFIG_CHANGE_REPUBLISH_DEBOUNCE_INTERVAL
95// for rate-limiting the publish jobs triggered by a change in the config?
96//
97// Currently the descriptor publish tasks triggered by changes in the config
98// are rate-limited via the usual rate limiting mechanism
99// (which rate-limits the uploads for 1m).
100//
101// I think this is OK for now, but we might need to rethink this if it becomes problematic
102// (for example, we might want an even longer rate-limit, or to reset any existing rate-limits
103// each time the config is modified).
104
105/// The upload rate-limiting threshold.
106///
107/// Before initiating an upload, the reactor checks if the last upload was at least
108/// `UPLOAD_RATE_LIM_THRESHOLD` seconds ago. If so, it uploads the descriptor to all HsDirs that
109/// need it. If not, it schedules the upload to happen `UPLOAD_RATE_LIM_THRESHOLD` seconds from the
110/// current time.
111//
112// TODO: We may someday need to tune this value; it was chosen more or less arbitrarily.
113const UPLOAD_RATE_LIM_THRESHOLD: Duration = Duration::from_secs(60);
114
115/// The maximum number of concurrent upload tasks per time period.
116//
117// TODO: this value was arbitrarily chosen and may not be optimal. For now, it
118// will have no effect, since the current number of replicas is far less than
119// this value.
120//
121// The uploads for all TPs happen in parallel. As a result, the actual limit for the maximum
122// number of concurrent upload tasks is multiplied by a number which depends on the TP parameters
123// (currently 2, which means the concurrency limit will, in fact, be 32).
124//
125// We should try to decouple this value from the TP parameters.
126const MAX_CONCURRENT_UPLOADS: usize = 16;
127
128/// The maximum time allowed for uploading a descriptor to a single HSDir,
129/// across all attempts.
130pub(crate) const OVERALL_UPLOAD_TIMEOUT: Duration = Duration::from_secs(5 * 60);
131
132/// A reactor for the HsDir [`Publisher`]
133///
134/// The entrypoint is [`Reactor::run`].
135#[must_use = "If you don't call run() on the reactor, it won't publish any descriptors."]
136pub(super) struct Reactor<R: Runtime, M: Mockable> {
137 /// The immutable, shared inner state.
138 imm: Arc<Immutable<R, M>>,
139 /// A source for new network directories that we use to determine
140 /// our HsDirs.
141 dir_provider: Arc<dyn NetDirProvider>,
142 /// The mutable inner state,
143 inner: Arc<Mutex<Inner>>,
144 /// A channel for receiving IPT change notifications.
145 ipt_watcher: IptsPublisherView,
146 /// A channel for receiving onion service config change notifications.
147 config_rx: watch::Receiver<Arc<OnionServiceConfig>>,
148 /// A channel for receiving restricted discovery key_dirs change notifications.
149 key_dirs_rx: FileEventReceiver,
150 /// A channel for sending restricted discovery key_dirs change notifications.
151 ///
152 /// A copy of this sender is handed out to every `FileWatcher` created.
153 key_dirs_tx: FileEventSender,
154 /// A channel for receiving updates regarding our [`PublishStatus`].
155 ///
156 /// The main loop of the reactor watches for updates on this channel.
157 ///
158 /// When the [`PublishStatus`] changes to [`UploadScheduled`](PublishStatus::UploadScheduled),
159 /// we can start publishing descriptors.
160 ///
161 /// If the [`PublishStatus`] is [`AwaitingIpts`](PublishStatus::AwaitingIpts), publishing is
162 /// paused until we receive a notification on `ipt_watcher` telling us the IPT manager has
163 /// established some introduction points.
164 publish_status_rx: watch::Receiver<PublishStatus>,
165 /// A sender for updating our [`PublishStatus`].
166 ///
167 /// When our [`PublishStatus`] changes to [`UploadScheduled`](PublishStatus::UploadScheduled),
168 /// we can start publishing descriptors.
169 publish_status_tx: watch::Sender<PublishStatus>,
170 /// A channel for sending upload completion notifications.
171 ///
172 /// This channel is polled in the main loop of the reactor.
173 upload_task_complete_rx: mpsc::Receiver<TimePeriodUploadResult>,
174 /// A channel for receiving upload completion notifications.
175 ///
176 /// A copy of this sender is handed to each upload task.
177 upload_task_complete_tx: mpsc::Sender<TimePeriodUploadResult>,
178 /// A sender for notifying any pending upload tasks that the reactor is shutting down.
179 ///
180 /// Receivers can use this channel to find out when reactor is dropped.
181 ///
182 /// This is currently only used in [`upload_for_time_period`](Reactor::upload_for_time_period).
183 /// Any future background tasks can also use this channel to detect if the reactor is dropped.
184 ///
185 /// Closing this channel will cause any pending upload tasks to be dropped.
186 shutdown_tx: broadcast::Sender<Void>,
187 /// Path resolver for configuration files.
188 path_resolver: Arc<CfgPathResolver>,
189 /// Queue on which we receive messages from the [`PowManager`] telling us that a seed has
190 /// rotated and thus we need to republish the descriptor for a particular time period.
191 update_from_pow_manager_rx: mpsc::Receiver<TimePeriod>,
192}
193
194/// The immutable, shared state of the descriptor publisher reactor.
195#[derive(Clone)]
196struct Immutable<R: Runtime, M: Mockable> {
197 /// The runtime.
198 runtime: R,
199 /// Mockable state.
200 ///
201 /// This is used for launching circuits and for obtaining random number generators.
202 mockable: M,
203 /// The service for which we're publishing descriptors.
204 nickname: HsNickname,
205 /// The key manager,
206 keymgr: Arc<KeyMgr>,
207 /// A sender for updating the status of the onion service.
208 status_tx: PublisherStatusSender,
209 /// Proof-of-work state.
210 pow_manager: Arc<PowManager<R>>,
211}
212
213impl<R: Runtime, M: Mockable> Immutable<R, M> {
214 /// Create an [`AesOpeKey`] for generating revision counters for the descriptors associated
215 /// with the specified [`TimePeriod`].
216 ///
217 /// If the onion service is not running in offline mode, the key of the returned `AesOpeKey` is
218 /// the private part of the blinded identity key. Otherwise, the key is the private part of the
219 /// descriptor signing key.
220 ///
221 /// Returns an error if the service is running in offline mode and the descriptor signing
222 /// keypair of the specified `period` is not available.
223 //
224 // TODO (#1194): we don't support "offline" mode (yet), so this always returns an AesOpeKey
225 // built from the blinded id key
226 fn create_ope_key(&self, period: TimePeriod) -> Result<AesOpeKey, FatalError> {
227 let ope_key = match read_blind_id_keypair(&self.keymgr, &self.nickname, period)? {
228 Some(key) => {
229 let key: ed25519::ExpandedKeypair = key.into();
230 key.to_secret_key_bytes()[0..32]
231 .try_into()
232 .expect("Wrong length on slice")
233 }
234 None => {
235 // TODO (#1194): we don't support externally provisioned keys (yet), so this branch
236 // is unreachable (for now).
237 let desc_sign_key_spec =
238 DescSigningKeypairSpecifier::new(self.nickname.clone(), period);
239 let key: ed25519::Keypair = self
240 .keymgr
241 .get::<HsDescSigningKeypair>(&desc_sign_key_spec)?
242 // TODO (#1194): internal! is not the right type for this error (we need an
243 // error type for the case where a hidden service running in offline mode has
244 // run out of its pre-previsioned keys).
245 //
246 // This will be addressed when we add support for offline hs_id mode
247 .ok_or_else(|| {
248 internal!(
249 "identity keys are offline, but descriptor signing key is unavailable?!"
250 )
251 })?
252 .into();
253 key.to_bytes()
254 }
255 };
256
257 Ok(AesOpeKey::from_secret(&ope_key))
258 }
259
260 /// Generate a revision counter for a descriptor associated with the specified
261 /// [`TimePeriod`].
262 ///
263 /// Returns a revision counter generated according to the [encrypted time in period] scheme.
264 ///
265 /// [encrypted time in period]: https://spec.torproject.org/rend-spec/revision-counter-mgt.html#encrypted-time
266 fn generate_revision_counter(
267 &self,
268 params: &HsDirParams,
269 now: SystemTime,
270 ) -> Result<RevisionCounter, FatalError> {
271 // TODO: in the future, we might want to compute ope_key once per time period (as oppposed
272 // to each time we generate a new descriptor), for performance reasons.
273 let ope_key = self.create_ope_key(params.time_period())?;
274
275 // TODO: perhaps this should be moved to a new HsDirParams::offset_within_sr() function
276 let srv_start = params.start_of_shard_rand_period();
277 let offset = params.offset_within_srv_period(now).ok_or_else(|| {
278 internal!(
279 "current wallclock time not within SRV range?! (now={:?}, SRV_start={:?})",
280 now,
281 srv_start
282 )
283 })?;
284 let rev = ope_key.encrypt(offset);
285
286 Ok(RevisionCounter::from(rev))
287 }
288}
289
290/// Mockable state for the descriptor publisher reactor.
291///
292/// This enables us to mock parts of the [`Reactor`] for testing purposes.
293#[async_trait]
294pub(crate) trait Mockable: Clone + Send + Sync + Sized + 'static {
295 /// The type of random number generator.
296 type Rng: rand::Rng + rand::CryptoRng;
297
298 /// The type of client circuit.
299 type Tunnel: MockableDirTunnel;
300
301 /// Return a random number generator.
302 fn thread_rng(&self) -> Self::Rng;
303
304 /// Create a circuit of the specified `kind` to `target`.
305 async fn get_or_launch_hs_dir<T>(
306 &self,
307 netdir: &NetDir,
308 target: T,
309 ) -> Result<Self::Tunnel, tor_circmgr::Error>
310 where
311 T: CircTarget + Send + Sync;
312
313 /// Return an estimate-based value for how long we should allow a single
314 /// directory upload operation to complete.
315 ///
316 /// Includes circuit construction, stream opening, upload, and waiting for a
317 /// response.
318 fn estimate_upload_timeout(&self) -> Duration;
319}
320
321/// Mockable client circuit
322#[async_trait]
323pub(crate) trait MockableDirTunnel: Send + Sync {
324 /// The data stream type.
325 type DataStream: AsyncRead + AsyncWrite + Send + Unpin;
326
327 /// Start a new stream to the last relay in the circuit, using
328 /// a BEGIN_DIR cell.
329 async fn begin_dir_stream(&self) -> Result<Self::DataStream, tor_circmgr::Error>;
330
331 /// Try to get a SourceInfo for this circuit, for using it in a directory request.
332 fn source_info(&self) -> tor_proto::Result<Option<SourceInfo>>;
333}
334
335#[async_trait]
336impl MockableDirTunnel for ServiceOnionServiceDirTunnel {
337 type DataStream = tor_proto::client::stream::DataStream;
338
339 async fn begin_dir_stream(&self) -> Result<Self::DataStream, tor_circmgr::Error> {
340 Self::begin_dir_stream(self).await
341 }
342
343 fn source_info(&self) -> tor_proto::Result<Option<SourceInfo>> {
344 SourceInfo::from_tunnel(self)
345 }
346}
347
348/// The real version of the mockable state of the reactor.
349#[derive(Clone, From, Into)]
350pub(crate) struct Real<R: Runtime>(Arc<HsCircPool<R>>);
351
352#[async_trait]
353impl<R: Runtime> Mockable for Real<R> {
354 type Rng = rand::rngs::ThreadRng;
355 type Tunnel = ServiceOnionServiceDirTunnel;
356
357 fn thread_rng(&self) -> Self::Rng {
358 rand::rng()
359 }
360
361 #[instrument(level = "trace", skip_all)]
362 async fn get_or_launch_hs_dir<T>(
363 &self,
364 netdir: &NetDir,
365 target: T,
366 ) -> Result<Self::Tunnel, tor_circmgr::Error>
367 where
368 T: CircTarget + Send + Sync,
369 {
370 self.0.get_or_launch_svc_dir(netdir, target).await
371 }
372
373 fn estimate_upload_timeout(&self) -> Duration {
374 use tor_circmgr::timeouts::Action;
375 let est_build = self.0.estimate_timeout(&Action::BuildCircuit { length: 4 });
376 let est_roundtrip = self.0.estimate_timeout(&Action::RoundTrip { length: 4 });
377 // We assume that in the worst case we'll have to wait for an entire
378 // circuit construction and two round-trips to the hsdir.
379 let est_total = est_build + est_roundtrip * 2;
380 // We always allow _at least_ this much time, in case our estimate is
381 // ridiculously low.
382 let min_timeout = Duration::from_secs(30);
383 max(est_total, min_timeout)
384 }
385}
386
387/// The mutable state of a [`Reactor`].
388struct Inner {
389 /// The onion service config.
390 config: Arc<OnionServiceConfigPublisherView>,
391 /// Watcher for key_dirs.
392 ///
393 /// Set to `None` if the reactor is not running, or if `watch_configuration` is false.
394 ///
395 /// The watcher is recreated whenever the `restricted_discovery.key_dirs` change.
396 file_watcher: Option<FileWatcher>,
397 /// The relevant time periods.
398 ///
399 /// This includes the current time period, as well as any other time periods we need to be
400 /// publishing descriptors for.
401 ///
402 /// This is empty until we fetch our first netdir in [`Reactor::run`].
403 time_periods: Vec<TimePeriodContext>,
404 /// Our most up to date netdir.
405 ///
406 /// This is initialized in [`Reactor::run`].
407 netdir: Option<Arc<NetDir>>,
408 /// The timestamp of our last upload.
409 ///
410 /// This is the time when the last update was _initiated_ (rather than completed), to prevent
411 /// the publisher from spawning multiple upload tasks at once in response to multiple external
412 /// events happening in quick succession, such as the IPT manager sending multiple IPT change
413 /// notifications in a short time frame (#1142), or an IPT change notification that's
414 /// immediately followed by a consensus change. Starting two upload tasks at once is not only
415 /// inefficient, but it also causes the publisher to generate two different descriptors with
416 /// the same revision counter (the revision counter is derived from the current timestamp),
417 /// which ultimately causes the slower upload task to fail (see #1142).
418 ///
419 /// Note: This is only used for deciding when to reschedule a rate-limited upload. It is _not_
420 /// used for retrying failed uploads (these are handled internally by
421 /// [`Reactor::upload_descriptor_with_retries`]).
422 last_uploaded: Option<Instant>,
423 /// A max-heap containing the time periods for which we need to reupload the descriptor.
424 // TODO: we are currently reuploading more than nececessary.
425 // Ideally, this shouldn't contain contain duplicate TimePeriods,
426 // because we only need to retain the latest reupload time for each time period.
427 //
428 // Currently, if, for some reason, we upload the descriptor multiple times for the same TP,
429 // we will end up with multiple ReuploadTimer entries for that TP,
430 // each of which will (eventually) result in a reupload.
431 //
432 // TODO: maybe this should just be a HashMap<TimePeriod, Instant>
433 //
434 // See https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1971#note_2994950
435 reupload_timers: BinaryHeap<ReuploadTimer>,
436 /// The restricted discovery authorized clients.
437 ///
438 /// `None`, unless the service is running in restricted discovery mode.
439 authorized_clients: Option<Arc<RestrictedDiscoveryKeys>>,
440}
441
442/// The part of the reactor state that changes with every time period.
443struct TimePeriodContext {
444 /// The HsDir params.
445 params: HsDirParams,
446 /// The HsDirs to use in this time period.
447 ///
448 // We keep a list of `RelayIds` because we can't store a `Relay<'_>` inside the reactor
449 // (the lifetime of a relay is tied to the lifetime of its corresponding `NetDir`. To
450 // store `Relay<'_>`s in the reactor, we'd need a way of atomically swapping out both the
451 // `NetDir` and the cached relays, and to convince Rust what we're doing is sound)
452 hs_dirs: Vec<(RelayIds, DescriptorStatus)>,
453 /// The revision counter of the last successful upload, if any.
454 last_successful: Option<RevisionCounter>,
455 /// The outcome of the last upload, if any.
456 upload_results: Vec<HsDirUploadStatus>,
457}
458
459impl TimePeriodContext {
460 /// Create a new `TimePeriodContext`.
461 ///
462 /// Any of the specified `old_hsdirs` also present in the new list of HsDirs
463 /// (returned by `NetDir::hs_dirs_upload`) will have their `DescriptorStatus` preserved.
464 fn new<'r>(
465 params: HsDirParams,
466 blind_id: HsBlindId,
467 netdir: &Arc<NetDir>,
468 old_hsdirs: impl Iterator<Item = &'r (RelayIds, DescriptorStatus)>,
469 old_upload_results: Vec<HsDirUploadStatus>,
470 ) -> Result<Self, FatalError> {
471 let period = params.time_period();
472 let hs_dirs = Self::compute_hsdirs(period, blind_id, netdir, old_hsdirs)?;
473 let upload_results = old_upload_results
474 .into_iter()
475 .filter(|res|
476 // Check if the HsDir of this result still exists
477 hs_dirs
478 .iter()
479 .any(|(relay_ids, _status)| relay_ids == &res.relay_ids))
480 .collect();
481
482 Ok(Self {
483 params,
484 hs_dirs,
485 last_successful: None,
486 upload_results,
487 })
488 }
489
490 /// Recompute the HsDirs for this time period.
491 fn compute_hsdirs<'r>(
492 period: TimePeriod,
493 blind_id: HsBlindId,
494 netdir: &Arc<NetDir>,
495 mut old_hsdirs: impl Iterator<Item = &'r (RelayIds, DescriptorStatus)>,
496 ) -> Result<Vec<(RelayIds, DescriptorStatus)>, FatalError> {
497 let hs_dirs = netdir.hs_dirs_upload(blind_id, period)?;
498
499 Ok(hs_dirs
500 .map(|hs_dir| {
501 let mut builder = RelayIds::builder();
502 if let Some(ed_id) = hs_dir.ed_identity() {
503 builder.ed_identity(*ed_id);
504 }
505
506 if let Some(rsa_id) = hs_dir.rsa_identity() {
507 builder.rsa_identity(*rsa_id);
508 }
509
510 let relay_id = builder.build().unwrap_or_else(|_| RelayIds::empty());
511
512 // Have we uploaded the descriptor to thiw relay before? If so, we don't need to
513 // reupload it unless it was already dirty and due for a reupload.
514 let status = match old_hsdirs.find(|(id, _)| *id == relay_id) {
515 Some((_, status)) => *status,
516 None => DescriptorStatus::Dirty,
517 };
518
519 (relay_id, status)
520 })
521 .collect::<Vec<_>>())
522 }
523
524 /// Mark the descriptor dirty for all HSDirs of this time period.
525 fn mark_all_dirty(&mut self) {
526 self.hs_dirs
527 .iter_mut()
528 .for_each(|(_relay_id, status)| *status = DescriptorStatus::Dirty);
529 }
530
531 /// Update the upload result for this time period.
532 fn set_upload_results(&mut self, upload_results: Vec<HsDirUploadStatus>) {
533 self.upload_results = upload_results;
534 }
535}
536
537/// An error that occurs while trying to upload a descriptor.
538#[derive(Clone, Debug, thiserror::Error)]
539#[non_exhaustive]
540pub enum UploadError {
541 /// An error that has occurred after we have contacted a directory cache and made a circuit to it.
542 #[error("descriptor upload request failed: {}", _0.error)]
543 Request(#[from] RequestFailedError),
544
545 /// Failed to establish circuit to hidden service directory
546 #[error("could not build circuit to HsDir")]
547 Circuit(#[from] tor_circmgr::Error),
548
549 /// Failed to establish stream to hidden service directory
550 #[error("failed to establish directory stream to HsDir")]
551 Stream(#[source] tor_circmgr::Error),
552
553 /// An internal error.
554 #[error("Internal error")]
555 Bug(#[from] tor_error::Bug),
556}
557define_asref_dyn_std_error!(UploadError);
558
559impl UploadError {
560 /// Return true if this error is one that we should report as a suspicious event,
561 /// along with the dirserver, and description of the relevant document.
562 pub(crate) fn should_report_as_suspicious(&self) -> bool {
563 match self {
564 UploadError::Request(e) => e.error.should_report_as_suspicious_if_anon(),
565 UploadError::Circuit(_) => false, // TODO prop360
566 UploadError::Stream(_) => false, // TODO prop360
567 UploadError::Bug(_) => false,
568 }
569 }
570}
571
572impl<R: Runtime, M: Mockable> Reactor<R, M> {
573 /// Create a new `Reactor`.
574 #[allow(clippy::too_many_arguments)]
575 pub(super) fn new(
576 runtime: R,
577 nickname: HsNickname,
578 dir_provider: Arc<dyn NetDirProvider>,
579 mockable: M,
580 config: &OnionServiceConfig,
581 ipt_watcher: IptsPublisherView,
582 config_rx: watch::Receiver<Arc<OnionServiceConfig>>,
583 status_tx: PublisherStatusSender,
584 keymgr: Arc<KeyMgr>,
585 path_resolver: Arc<CfgPathResolver>,
586 pow_manager: Arc<PowManager<R>>,
587 update_from_pow_manager_rx: mpsc::Receiver<TimePeriod>,
588 ) -> Self {
589 /// The maximum size of the upload completion notifier channel.
590 ///
591 /// The channel we use this for is a futures::mpsc channel, which has a capacity of
592 /// `UPLOAD_CHAN_BUF_SIZE + num-senders`. We don't need the buffer size to be non-zero, as
593 /// each sender will send exactly one message.
594 const UPLOAD_CHAN_BUF_SIZE: usize = 0;
595
596 // Internally-generated instructions, no need for mq.
597 let (upload_task_complete_tx, upload_task_complete_rx) =
598 mpsc_channel_no_memquota(UPLOAD_CHAN_BUF_SIZE);
599
600 let (publish_status_tx, publish_status_rx) = watch::channel();
601 // Setting the buffer size to zero here is OK,
602 // since we never actually send anything on this channel.
603 let (shutdown_tx, _shutdown_rx) = broadcast::channel(0);
604
605 let authorized_clients =
606 Self::read_authorized_clients(&config.restricted_discovery, &path_resolver);
607
608 // Create a channel for watching for changes in the configured
609 // restricted_discovery.key_dirs.
610 let (key_dirs_tx, key_dirs_rx) = file_watcher::channel();
611
612 let imm = Immutable {
613 runtime,
614 mockable,
615 nickname,
616 keymgr,
617 status_tx,
618 pow_manager,
619 };
620
621 let inner = Inner {
622 time_periods: vec![],
623 config: Arc::new(config.into()),
624 file_watcher: None,
625 netdir: None,
626 last_uploaded: None,
627 reupload_timers: Default::default(),
628 authorized_clients,
629 };
630
631 Self {
632 imm: Arc::new(imm),
633 inner: Arc::new(Mutex::new(inner)),
634 dir_provider,
635 ipt_watcher,
636 config_rx,
637 key_dirs_rx,
638 key_dirs_tx,
639 publish_status_rx,
640 publish_status_tx,
641 upload_task_complete_rx,
642 upload_task_complete_tx,
643 shutdown_tx,
644 path_resolver,
645 update_from_pow_manager_rx,
646 }
647 }
648
649 /// Start the reactor.
650 ///
651 /// Under normal circumstances, this function runs indefinitely.
652 ///
653 /// Note: this also spawns the "reminder task" that we use to reschedule uploads whenever an
654 /// upload fails or is rate-limited.
655 pub(super) async fn run(mut self) -> Result<(), FatalError> {
656 debug!(nickname=%self.imm.nickname, "starting descriptor publisher reactor");
657
658 {
659 let netdir = self
660 .dir_provider
661 .wait_for_netdir(Timeliness::Timely)
662 .await?;
663 let time_periods = self.compute_time_periods(&netdir, &[])?;
664
665 let mut inner = self.inner.lock().expect("poisoned lock");
666
667 inner.netdir = Some(netdir);
668 inner.time_periods = time_periods;
669 }
670
671 // Create the initial key_dirs watcher.
672 self.update_file_watcher();
673
674 loop {
675 match self.run_once().await {
676 Ok(ShutdownStatus::Continue) => continue,
677 Ok(ShutdownStatus::Terminate) => {
678 debug!(nickname=%self.imm.nickname, "descriptor publisher is shutting down!");
679
680 self.imm.status_tx.send_shutdown();
681 return Ok(());
682 }
683 Err(e) => {
684 error_report!(
685 e,
686 "HS service {}: descriptor publisher crashed!",
687 self.imm.nickname
688 );
689
690 self.imm.status_tx.send_broken(e.clone());
691
692 return Err(e);
693 }
694 }
695 }
696 }
697
698 /// Run one iteration of the reactor loop.
699 #[allow(clippy::cognitive_complexity)] // TODO: Refactor
700 async fn run_once(&mut self) -> Result<ShutdownStatus, FatalError> {
701 let mut netdir_events = self.dir_provider.events();
702
703 // Note: TrackingNow tracks the values it is compared with.
704 // This is equivalent to sleeping for (until - now) units of time,
705 let upload_rate_lim: TrackingNow = TrackingNow::now(&self.imm.runtime);
706 if let PublishStatus::RateLimited(until) = self.status() {
707 if upload_rate_lim > until {
708 // We are no longer rate-limited
709 self.expire_rate_limit().await?;
710 }
711 }
712
713 let reupload_tracking = TrackingNow::now(&self.imm.runtime);
714 let mut reupload_periods = vec![];
715 {
716 let mut inner = self.inner.lock().expect("poisoned lock");
717 let inner = &mut *inner;
718 while let Some(reupload) = inner.reupload_timers.peek().copied() {
719 // First, extract all the timeouts that already elapsed.
720 if reupload.when <= reupload_tracking {
721 inner.reupload_timers.pop();
722 reupload_periods.push(reupload.period);
723 } else {
724 // We are not ready to schedule any more reuploads.
725 //
726 // How much we need to sleep is implicitly
727 // tracked in reupload_tracking (through
728 // the TrackingNow implementation)
729 break;
730 }
731 }
732 }
733
734 // Check if it's time to schedule any reuploads.
735 for period in reupload_periods {
736 if self.mark_dirty(&period) {
737 debug!(
738 time_period=?period,
739 "descriptor reupload timer elapsed; scheduling reupload",
740 );
741 self.update_publish_status_unless_rate_lim(PublishStatus::UploadScheduled)
742 .await?;
743 }
744 }
745
746 select_biased! {
747 res = self.upload_task_complete_rx.next().fuse() => {
748 let Some(upload_res) = res else {
749 return Ok(ShutdownStatus::Terminate);
750 };
751
752 self.handle_upload_results(upload_res);
753 self.upload_result_to_svc_status()?;
754 },
755 () = upload_rate_lim.wait_for_earliest(&self.imm.runtime).fuse() => {
756 self.expire_rate_limit().await?;
757 },
758 () = reupload_tracking.wait_for_earliest(&self.imm.runtime).fuse() => {
759 // Run another iteration, executing run_once again. This time, we will remove the
760 // expired reupload from self.reupload_timers, mark the descriptor dirty for all
761 // relevant HsDirs, and schedule the upload by setting our status to
762 // UploadScheduled.
763 return Ok(ShutdownStatus::Continue);
764 },
765 netdir_event = netdir_events.next().fuse() => {
766 let Some(netdir_event) = netdir_event else {
767 debug!("netdir event stream ended");
768 return Ok(ShutdownStatus::Terminate);
769 };
770
771 if !matches!(netdir_event, DirEvent::NewConsensus) {
772 return Ok(ShutdownStatus::Continue);
773 };
774
775 // The consensus changed. Grab a new NetDir.
776 let netdir = match self.dir_provider.netdir(Timeliness::Timely) {
777 Ok(y) => y,
778 Err(e) => {
779 error_report!(e, "HS service {}: netdir unavailable. Retrying...", self.imm.nickname);
780 // Hopefully a netdir will appear in the future.
781 // in the meantime, suspend operations.
782 //
783 // TODO (#1218): there is a bug here: we stop reading on our inputs
784 // including eg publish_status_rx, but it is our job to log some of
785 // these things. While we are waiting for a netdir, all those messages
786 // are "stuck"; they'll appear later, with misleading timestamps.
787 //
788 // Probably this should be fixed by moving the logging
789 // out of the reactor, where it won't be blocked.
790 self.dir_provider.wait_for_netdir(Timeliness::Timely)
791 .await?
792 }
793 };
794 let relevant_periods = netdir.hs_all_time_periods();
795 self.handle_consensus_change(netdir).await?;
796 expire_publisher_keys(
797 &self.imm.keymgr,
798 &self.imm.nickname,
799 &relevant_periods,
800 ).unwrap_or_else(|e| {
801 error_report!(e, "failed to remove expired keys");
802 });
803 }
804 update = self.ipt_watcher.await_update().fuse() => {
805 if self.handle_ipt_change(update).await? == ShutdownStatus::Terminate {
806 return Ok(ShutdownStatus::Terminate);
807 }
808 },
809 config = self.config_rx.next().fuse() => {
810 let Some(config) = config else {
811 return Ok(ShutdownStatus::Terminate);
812 };
813
814 self.handle_svc_config_change(&config).await?;
815 },
816 res = self.key_dirs_rx.next().fuse() => {
817 let Some(event) = res else {
818 return Ok(ShutdownStatus::Terminate);
819 };
820
821 while let Some(_ignore) = self.key_dirs_rx.try_recv() {
822 // Discard other events, so that we only reload once.
823 }
824
825 self.handle_key_dirs_change(event).await?;
826 }
827 should_upload = self.publish_status_rx.next().fuse() => {
828 let Some(should_upload) = should_upload else {
829 return Ok(ShutdownStatus::Terminate);
830 };
831
832 // Our PublishStatus changed -- are we ready to publish?
833 if should_upload == PublishStatus::UploadScheduled {
834 self.update_publish_status_unless_waiting(PublishStatus::Idle).await?;
835 self.upload_all().await?;
836 }
837 }
838 update_tp_pow_seed = self.update_from_pow_manager_rx.next().fuse() => {
839 debug!("Update PoW seed for TP!");
840 let Some(time_period) = update_tp_pow_seed else {
841 return Ok(ShutdownStatus::Terminate);
842 };
843 self.mark_dirty(&time_period);
844 self.upload_all().await?;
845 }
846 }
847
848 Ok(ShutdownStatus::Continue)
849 }
850
851 /// Returns the current status of the publisher
852 fn status(&self) -> PublishStatus {
853 *self.publish_status_rx.borrow()
854 }
855
856 /// Handle a batch of upload outcomes,
857 /// possibly updating the status of the descriptor for the corresponding HSDirs.
858 fn handle_upload_results(&self, results: TimePeriodUploadResult) {
859 let mut inner = self.inner.lock().expect("poisoned lock");
860 let inner = &mut *inner;
861
862 // Check which time period these uploads pertain to.
863 let period = inner
864 .time_periods
865 .iter_mut()
866 .find(|ctx| ctx.params.time_period() == results.time_period);
867
868 let Some(period) = period else {
869 // The uploads were for a time period that is no longer relevant, so we
870 // can ignore the result.
871 return;
872 };
873
874 // We will need to reupload this descriptor at some point, so we pick
875 // a random time between 60 minutes and 120 minutes in the future.
876 //
877 // See https://spec.torproject.org/rend-spec/deriving-keys.html#WHEN-HSDESC
878 let mut rng = self.imm.mockable.thread_rng();
879 // TODO SPEC: Control republish period using a consensus parameter?
880 let minutes = rng.gen_range_checked(60..=120).expect("low > high?!");
881 let duration = Duration::from_secs(minutes * 60);
882 let reupload_when = self.imm.runtime.now() + duration;
883 let time_period = period.params.time_period();
884
885 info!(
886 time_period=?time_period,
887 "reuploading descriptor in {}",
888 humantime::format_duration(duration),
889 );
890
891 inner.reupload_timers.push(ReuploadTimer {
892 period: time_period,
893 when: reupload_when,
894 });
895
896 let mut upload_results = vec![];
897 for upload_res in results.hsdir_result {
898 let relay = period
899 .hs_dirs
900 .iter_mut()
901 .find(|(relay_ids, _status)| relay_ids == &upload_res.relay_ids);
902
903 let Some((_relay, status)): Option<&mut (RelayIds, _)> = relay else {
904 // This HSDir went away, so the result doesn't matter.
905 // Continue processing the rest of the results
906 continue;
907 };
908
909 if upload_res.upload_res.is_ok() {
910 let update_last_successful = match period.last_successful {
911 None => true,
912 Some(counter) => counter <= upload_res.revision_counter,
913 };
914
915 if update_last_successful {
916 period.last_successful = Some(upload_res.revision_counter);
917 // TODO (#1098): Is it possible that this won't update the statuses promptly
918 // enough. For example, it's possible for the reactor to see a Dirty descriptor
919 // and start an upload task for a descriptor has already been uploaded (or is
920 // being uploaded) in another task, but whose upload results have not yet been
921 // processed.
922 //
923 // This is probably made worse by the fact that the statuses are updated in
924 // batches (grouped by time period), rather than one by one as the upload tasks
925 // complete (updating the status involves locking the inner mutex, and I wanted
926 // to minimize the locking/unlocking overheads). I'm not sure handling the
927 // updates in batches was the correct decision here.
928 *status = DescriptorStatus::Clean;
929 }
930 }
931
932 upload_results.push(upload_res);
933 }
934
935 period.set_upload_results(upload_results);
936 }
937
938 /// Maybe update our list of HsDirs.
939 async fn handle_consensus_change(&mut self, netdir: Arc<NetDir>) -> Result<(), FatalError> {
940 trace!("the consensus has changed; recomputing HSDirs");
941
942 let _old: Option<Arc<NetDir>> = self.replace_netdir(netdir);
943
944 self.recompute_hs_dirs()?;
945 self.update_publish_status_unless_waiting(PublishStatus::UploadScheduled)
946 .await?;
947
948 // If the time period has changed, some of our upload results may now be irrelevant,
949 // so we might need to update our status (for example, if our uploads are
950 // for a no-longer-relevant time period, it means we might be able to update
951 // out status from "degraded" to "running")
952 self.upload_result_to_svc_status()?;
953
954 Ok(())
955 }
956
957 /// Recompute the HsDirs for all relevant time periods.
958 fn recompute_hs_dirs(&self) -> Result<(), FatalError> {
959 let mut inner = self.inner.lock().expect("poisoned lock");
960 let inner = &mut *inner;
961
962 let netdir = Arc::clone(
963 inner
964 .netdir
965 .as_ref()
966 .ok_or_else(|| internal!("started upload task without a netdir"))?,
967 );
968
969 // Update our list of relevant time periods.
970 let new_time_periods = self.compute_time_periods(&netdir, &inner.time_periods)?;
971 inner.time_periods = new_time_periods;
972
973 Ok(())
974 }
975
976 /// Compute the [`TimePeriodContext`]s for the time periods from the specified [`NetDir`].
977 ///
978 /// The specified `time_periods` are used to preserve the `DescriptorStatus` of the
979 /// HsDirs where possible.
980 fn compute_time_periods(
981 &self,
982 netdir: &Arc<NetDir>,
983 time_periods: &[TimePeriodContext],
984 ) -> Result<Vec<TimePeriodContext>, FatalError> {
985 netdir
986 .hs_all_time_periods()
987 .iter()
988 .map(|params| {
989 let period = params.time_period();
990 let blind_id_kp =
991 read_blind_id_keypair(&self.imm.keymgr, &self.imm.nickname, period)?
992 // Note: for now, read_blind_id_keypair cannot return Ok(None).
993 // It's supposed to return Ok(None) if we're in offline hsid mode,
994 // but that might change when we do #1194
995 .ok_or_else(|| internal!("offline hsid mode not supported"))?;
996
997 let blind_id: HsBlindIdKey = (&blind_id_kp).into();
998
999 // If our previous `TimePeriodContext`s also had an entry for `period`, we need to
1000 // preserve the `DescriptorStatus` of its HsDirs. This helps prevent unnecessarily
1001 // publishing the descriptor to the HsDirs that already have it (the ones that are
1002 // marked with DescriptorStatus::Clean).
1003 //
1004 // In other words, we only want to publish to those HsDirs that
1005 // * are part of a new time period (which we have never published the descriptor
1006 // for), or
1007 // * have just been added to the ring of a time period we already knew about
1008 if let Some(ctx) = time_periods
1009 .iter()
1010 .find(|ctx| ctx.params.time_period() == period)
1011 {
1012 TimePeriodContext::new(
1013 params.clone(),
1014 blind_id.into(),
1015 netdir,
1016 ctx.hs_dirs.iter(),
1017 ctx.upload_results.clone(),
1018 )
1019 } else {
1020 // Passing an empty iterator here means all HsDirs in this TimePeriodContext
1021 // will be marked as dirty, meaning we will need to upload our descriptor to them.
1022 TimePeriodContext::new(
1023 params.clone(),
1024 blind_id.into(),
1025 netdir,
1026 iter::empty(),
1027 vec![],
1028 )
1029 }
1030 })
1031 .collect::<Result<Vec<TimePeriodContext>, FatalError>>()
1032 }
1033
1034 /// Replace the old netdir with the new, returning the old.
1035 fn replace_netdir(&self, new_netdir: Arc<NetDir>) -> Option<Arc<NetDir>> {
1036 self.inner
1037 .lock()
1038 .expect("poisoned lock")
1039 .netdir
1040 .replace(new_netdir)
1041 }
1042
1043 /// Replace our view of the service config with `new_config` if `new_config` contains changes
1044 /// that would cause us to generate a new descriptor.
1045 fn replace_config_if_changed(&self, new_config: Arc<OnionServiceConfigPublisherView>) -> bool {
1046 let mut inner = self.inner.lock().expect("poisoned lock");
1047 let old_config = &mut inner.config;
1048
1049 // The fields we're interested in haven't changed, so there's no need to update
1050 // `inner.config`.
1051 if *old_config == new_config {
1052 return false;
1053 }
1054
1055 let log_change = match (
1056 old_config.restricted_discovery.enabled,
1057 new_config.restricted_discovery.enabled,
1058 ) {
1059 (true, false) => Some("Disabling restricted discovery mode"),
1060 (false, true) => Some("Enabling restricted discovery mode"),
1061 _ => None,
1062 };
1063
1064 if let Some(msg) = log_change {
1065 info!(nickname=%self.imm.nickname, "{}", msg);
1066 }
1067
1068 let _old: Arc<OnionServiceConfigPublisherView> = std::mem::replace(old_config, new_config);
1069
1070 true
1071 }
1072
1073 /// Recreate the FileWatcher for watching the restricted discovery key_dirs.
1074 fn update_file_watcher(&self) {
1075 let mut inner = self.inner.lock().expect("poisoned lock");
1076 if inner.config.restricted_discovery.watch_configuration() {
1077 debug!("The restricted_discovery.key_dirs have changed, updating file watcher");
1078 let mut watcher = FileWatcher::builder(self.imm.runtime.clone());
1079
1080 let dirs = inner.config.restricted_discovery.key_dirs().clone();
1081
1082 watch_dirs(&mut watcher, &dirs, &self.path_resolver);
1083
1084 let watcher = watcher
1085 .start_watching(self.key_dirs_tx.clone())
1086 .map_err(|e| {
1087 // TODO: update the publish status (see also the module-level TODO about this).
1088 error_report!(e, "Cannot set file watcher");
1089 })
1090 .ok();
1091 inner.file_watcher = watcher;
1092 } else {
1093 if inner.file_watcher.is_some() {
1094 debug!("removing key_dirs watcher");
1095 }
1096 inner.file_watcher = None;
1097 }
1098 }
1099
1100 /// Read the intro points from `ipt_watcher`, and decide whether we're ready to start
1101 /// uploading.
1102 fn note_ipt_change(&self) -> PublishStatus {
1103 let mut ipts = self.ipt_watcher.borrow_for_publish();
1104 match ipts.ipts.as_mut() {
1105 Some(_ipts) => PublishStatus::UploadScheduled,
1106 None => PublishStatus::AwaitingIpts,
1107 }
1108 }
1109
1110 /// Update our list of introduction points.
1111 async fn handle_ipt_change(
1112 &mut self,
1113 update: Option<Result<(), crate::FatalError>>,
1114 ) -> Result<ShutdownStatus, FatalError> {
1115 trace!(nickname=%self.imm.nickname, "received IPT change notification from IPT manager");
1116 match update {
1117 Some(Ok(())) => {
1118 let should_upload = self.note_ipt_change();
1119 debug!(nickname=%self.imm.nickname, "the introduction points have changed");
1120
1121 self.mark_all_dirty();
1122 self.update_publish_status_unless_rate_lim(should_upload)
1123 .await?;
1124 Ok(ShutdownStatus::Continue)
1125 }
1126 Some(Err(e)) => Err(e),
1127 None => {
1128 debug!(nickname=%self.imm.nickname, "received shut down signal from IPT manager");
1129 Ok(ShutdownStatus::Terminate)
1130 }
1131 }
1132 }
1133
1134 /// Update the `PublishStatus` of the reactor with `new_state`,
1135 /// unless the current state is `AwaitingIpts`.
1136 async fn update_publish_status_unless_waiting(
1137 &mut self,
1138 new_state: PublishStatus,
1139 ) -> Result<(), FatalError> {
1140 // Only update the state if we're not waiting for intro points.
1141 if self.status() != PublishStatus::AwaitingIpts {
1142 self.update_publish_status(new_state).await?;
1143 }
1144
1145 Ok(())
1146 }
1147
1148 /// Update the `PublishStatus` of the reactor with `new_state`,
1149 /// unless the current state is `RateLimited`.
1150 async fn update_publish_status_unless_rate_lim(
1151 &mut self,
1152 new_state: PublishStatus,
1153 ) -> Result<(), FatalError> {
1154 // We can't exit this state until the rate-limit expires.
1155 if !matches!(self.status(), PublishStatus::RateLimited(_)) {
1156 self.update_publish_status(new_state).await?;
1157 }
1158
1159 Ok(())
1160 }
1161
1162 /// Unconditionally update the `PublishStatus` of the reactor with `new_state`.
1163 async fn update_publish_status(&mut self, new_state: PublishStatus) -> Result<(), Bug> {
1164 let onion_status = match new_state {
1165 PublishStatus::Idle => None,
1166 PublishStatus::UploadScheduled
1167 | PublishStatus::AwaitingIpts
1168 | PublishStatus::RateLimited(_) => Some(State::Bootstrapping),
1169 };
1170
1171 if let Some(onion_status) = onion_status {
1172 self.imm.status_tx.send(onion_status, None);
1173 }
1174
1175 trace!(
1176 "publisher reactor status change: {:?} -> {:?}",
1177 self.status(),
1178 new_state
1179 );
1180
1181 self.publish_status_tx.send(new_state).await.map_err(
1182 |_: postage::sink::SendError<_>| internal!("failed to send upload notification?!"),
1183 )?;
1184
1185 Ok(())
1186 }
1187
1188 /// Update the onion svc status based on the results of the last descriptor uploads.
1189 fn upload_result_to_svc_status(&self) -> Result<(), FatalError> {
1190 let inner = self.inner.lock().expect("poisoned lock");
1191 let netdir = inner
1192 .netdir
1193 .as_ref()
1194 .ok_or_else(|| internal!("handling upload results without netdir?!"))?;
1195
1196 let (state, err) = upload_result_state(netdir, &inner.time_periods);
1197 self.imm.status_tx.send(state, err);
1198
1199 Ok(())
1200 }
1201
1202 /// Update the descriptors based on the config change.
1203 async fn handle_svc_config_change(
1204 &mut self,
1205 config: &OnionServiceConfig,
1206 ) -> Result<(), FatalError> {
1207 let new_config = Arc::new(config.into());
1208 if self.replace_config_if_changed(Arc::clone(&new_config)) {
1209 self.update_file_watcher();
1210 self.update_authorized_clients_if_changed();
1211
1212 info!(nickname=%self.imm.nickname, "Config has changed, generating a new descriptor");
1213 self.mark_all_dirty();
1214
1215 // Schedule an upload, unless we're still waiting for IPTs.
1216 self.update_publish_status_unless_waiting(PublishStatus::UploadScheduled)
1217 .await?;
1218 }
1219
1220 Ok(())
1221 }
1222
1223 /// Update the descriptors based on a restricted discovery key_dirs change.
1224 ///
1225 /// If the authorized clients from the [`RestrictedDiscoveryConfig`] have changed,
1226 /// this marks the descriptor as dirty for all time periods,
1227 /// and schedules a reupload.
1228 async fn handle_key_dirs_change(&mut self, event: FileEvent) -> Result<(), FatalError> {
1229 debug!("The configured key_dirs have changed");
1230 match event {
1231 FileEvent::Rescan | FileEvent::FileChanged => {
1232 // These events are handled in the same way, by re-reading the keys from disk
1233 // and republishing the descriptor if necessary
1234 }
1235 _ => return Err(internal!("file watcher event {event:?}").into()),
1236 };
1237
1238 // Update the file watcher, in case the change was triggered by a key_dir move.
1239 self.update_file_watcher();
1240
1241 if self.update_authorized_clients_if_changed() {
1242 self.mark_all_dirty();
1243
1244 // Schedule an upload, unless we're still waiting for IPTs.
1245 self.update_publish_status_unless_waiting(PublishStatus::UploadScheduled)
1246 .await?;
1247 }
1248
1249 Ok(())
1250 }
1251
1252 /// Recreate the authorized_clients based on the current config.
1253 ///
1254 /// Returns `true` if the authorized clients have changed.
1255 fn update_authorized_clients_if_changed(&mut self) -> bool {
1256 let mut inner = self.inner.lock().expect("poisoned lock");
1257 let authorized_clients =
1258 Self::read_authorized_clients(&inner.config.restricted_discovery, &self.path_resolver);
1259
1260 let clients = &mut inner.authorized_clients;
1261 let changed = clients.as_ref() != authorized_clients.as_ref();
1262
1263 if changed {
1264 info!("The restricted discovery mode authorized clients have changed");
1265 *clients = authorized_clients;
1266 }
1267
1268 changed
1269 }
1270
1271 /// Read the authorized `RestrictedDiscoveryKeys` from `config`.
1272 fn read_authorized_clients(
1273 config: &RestrictedDiscoveryConfig,
1274 path_resolver: &CfgPathResolver,
1275 ) -> Option<Arc<RestrictedDiscoveryKeys>> {
1276 let authorized_clients = config.read_keys(path_resolver);
1277
1278 if matches!(authorized_clients.as_ref(), Some(c) if c.is_empty()) {
1279 warn!(
1280 "Running in restricted discovery mode, but we have no authorized clients. Service will be unreachable"
1281 );
1282 }
1283
1284 authorized_clients.map(Arc::new)
1285 }
1286
1287 /// Mark the descriptor dirty for all time periods.
1288 fn mark_all_dirty(&self) {
1289 trace!("marking the descriptor dirty for all time periods");
1290
1291 self.inner
1292 .lock()
1293 .expect("poisoned lock")
1294 .time_periods
1295 .iter_mut()
1296 .for_each(|tp| tp.mark_all_dirty());
1297 }
1298
1299 /// Mark the descriptor dirty for the specified time period.
1300 ///
1301 /// Returns `true` if the specified period is still relevant, and `false` otherwise.
1302 fn mark_dirty(&self, period: &TimePeriod) -> bool {
1303 let mut inner = self.inner.lock().expect("poisoned lock");
1304 let period_ctx = inner
1305 .time_periods
1306 .iter_mut()
1307 .find(|tp| tp.params.time_period() == *period);
1308
1309 match period_ctx {
1310 Some(ctx) => {
1311 trace!(time_period=?period, "marking the descriptor dirty");
1312 ctx.mark_all_dirty();
1313 true
1314 }
1315 None => false,
1316 }
1317 }
1318
1319 /// Try to upload our descriptor to the HsDirs that need it.
1320 ///
1321 /// If we've recently uploaded some descriptors, we return immediately and schedule the upload
1322 /// to happen after [`UPLOAD_RATE_LIM_THRESHOLD`].
1323 ///
1324 /// Failed uploads are retried
1325 /// (see [`upload_descriptor_with_retries`](Reactor::upload_descriptor_with_retries)).
1326 ///
1327 /// If restricted discovery mode is enabled and there are no authorized clients,
1328 /// we abort the upload and set our status to [`State::Broken`].
1329 //
1330 // Note: a broken restricted discovery config won't prevent future uploads from being scheduled
1331 // (for example if the IPTs change),
1332 // which can can cause the publisher's status to oscillate between `Bootstrapping` and `Broken`.
1333 // TODO: we might wish to refactor the publisher to be more sophisticated about this.
1334 //
1335 /// For each current time period, we spawn a task that uploads the descriptor to
1336 /// all the HsDirs on the HsDir ring of that time period.
1337 /// Each task shuts down on completion, or when the reactor is dropped.
1338 ///
1339 /// Each task reports its upload results (`TimePeriodUploadResult`)
1340 /// via the `upload_task_complete_tx` channel.
1341 /// The results are received and processed in the main loop of the reactor.
1342 ///
1343 /// Returns an error if it fails to spawn a task, or if an internal error occurs.
1344 #[allow(clippy::cognitive_complexity)] // TODO #2010: Refactor
1345 async fn upload_all(&mut self) -> Result<(), FatalError> {
1346 trace!("starting descriptor upload task...");
1347
1348 // Abort the upload entirely if we have an empty list of authorized clients
1349 let authorized_clients = match self.authorized_clients() {
1350 Ok(authorized_clients) => authorized_clients,
1351 Err(e) => {
1352 error_report!(e, "aborting upload");
1353 self.imm.status_tx.send_broken(e.clone());
1354
1355 // Returning an error would shut down the reactor, so we have to return Ok here.
1356 return Ok(());
1357 }
1358 };
1359
1360 let last_uploaded = self.inner.lock().expect("poisoned lock").last_uploaded;
1361 let now = self.imm.runtime.now();
1362 // Check if we should rate-limit this upload.
1363 if let Some(ts) = last_uploaded {
1364 let duration_since_upload = now.duration_since(ts);
1365
1366 if duration_since_upload < UPLOAD_RATE_LIM_THRESHOLD {
1367 return Ok(self.start_rate_limit(UPLOAD_RATE_LIM_THRESHOLD).await?);
1368 }
1369 }
1370
1371 let mut inner = self.inner.lock().expect("poisoned lock");
1372 let inner = &mut *inner;
1373
1374 let _ = inner.last_uploaded.insert(now);
1375
1376 for period_ctx in inner.time_periods.iter_mut() {
1377 let upload_task_complete_tx = self.upload_task_complete_tx.clone();
1378
1379 // Figure out which HsDirs we need to upload the descriptor to (some of them might already
1380 // have our latest descriptor, so we filter them out).
1381 let hs_dirs = period_ctx
1382 .hs_dirs
1383 .iter()
1384 .filter_map(|(relay_id, status)| {
1385 if *status == DescriptorStatus::Dirty {
1386 Some(relay_id.clone())
1387 } else {
1388 None
1389 }
1390 })
1391 .collect::<Vec<_>>();
1392
1393 if hs_dirs.is_empty() {
1394 trace!("the descriptor is clean for all HSDirs. Nothing to do");
1395 return Ok(());
1396 }
1397
1398 let time_period = period_ctx.params.time_period();
1399 // This scope exists because rng is not Send, so it needs to fall out of scope before we
1400 // await anything.
1401 let netdir = Arc::clone(
1402 inner
1403 .netdir
1404 .as_ref()
1405 .ok_or_else(|| internal!("started upload task without a netdir"))?,
1406 );
1407
1408 let imm = Arc::clone(&self.imm);
1409 let ipt_upload_view = self.ipt_watcher.upload_view();
1410 let config = Arc::clone(&inner.config);
1411 let authorized_clients = authorized_clients.clone();
1412
1413 trace!(nickname=%self.imm.nickname, time_period=?time_period,
1414 "spawning upload task"
1415 );
1416
1417 let params = period_ctx.params.clone();
1418 let shutdown_rx = self.shutdown_tx.subscribe();
1419
1420 // Spawn a task to upload the descriptor to all HsDirs of this time period.
1421 //
1422 // This task will shut down when the reactor is dropped (i.e. when shutdown_rx is
1423 // dropped).
1424 let _handle: () = self
1425 .imm
1426 .runtime
1427 .spawn(async move {
1428 if let Err(e) = Self::upload_for_time_period(
1429 hs_dirs,
1430 &netdir,
1431 config,
1432 params,
1433 Arc::clone(&imm),
1434 ipt_upload_view.clone(),
1435 authorized_clients.clone(),
1436 upload_task_complete_tx,
1437 shutdown_rx,
1438 )
1439 .await
1440 {
1441 error_report!(
1442 e,
1443 "descriptor upload failed for HS service {} and time period {:?}",
1444 imm.nickname,
1445 time_period
1446 );
1447 }
1448 })
1449 .map_err(|e| FatalError::from_spawn("upload_for_time_period task", e))?;
1450 }
1451
1452 Ok(())
1453 }
1454
1455 /// Upload the descriptor for the time period specified in `params`.
1456 ///
1457 /// Failed uploads are retried
1458 /// (see [`upload_descriptor_with_retries`](Reactor::upload_descriptor_with_retries)).
1459 #[allow(clippy::too_many_arguments)] // TODO: refactor
1460 #[allow(clippy::cognitive_complexity)] // TODO: Refactor
1461 async fn upload_for_time_period(
1462 hs_dirs: Vec<RelayIds>,
1463 netdir: &Arc<NetDir>,
1464 config: Arc<OnionServiceConfigPublisherView>,
1465 params: HsDirParams,
1466 imm: Arc<Immutable<R, M>>,
1467 ipt_upload_view: IptsPublisherUploadView,
1468 authorized_clients: Option<Arc<RestrictedDiscoveryKeys>>,
1469 mut upload_task_complete_tx: mpsc::Sender<TimePeriodUploadResult>,
1470 shutdown_rx: broadcast::Receiver<Void>,
1471 ) -> Result<(), FatalError> {
1472 let time_period = params.time_period();
1473 trace!(time_period=?time_period, "uploading descriptor to all HSDirs for this time period");
1474
1475 let hsdir_count = hs_dirs.len();
1476
1477 /// An error returned from an upload future.
1478 //
1479 // Exhaustive, because this is a private type.
1480 #[derive(Clone, Debug, thiserror::Error)]
1481 enum PublishError {
1482 /// The upload was aborted because there are no IPTs.
1483 ///
1484 /// This happens because of an inevitable TOCTOU race, where after being notified by
1485 /// the IPT manager that the IPTs have changed (via `self.ipt_watcher.await_update`),
1486 /// we find out there actually are no IPTs, so we can't build the descriptor.
1487 ///
1488 /// This is a special kind of error that interrupts the current upload task, and is
1489 /// logged at `debug!` level rather than `warn!` or `error!`.
1490 ///
1491 /// Ideally, this shouldn't happen very often (if at all).
1492 #[error("No IPTs")]
1493 NoIpts,
1494
1495 /// The reactor has shut down
1496 #[error("The reactor has shut down")]
1497 Shutdown,
1498
1499 /// An fatal error.
1500 #[error("{0}")]
1501 Fatal(#[from] FatalError),
1502 }
1503
1504 let max_hsdesc_len: usize = netdir
1505 .params()
1506 .hsdir_max_desc_size
1507 .try_into()
1508 .expect("Unable to convert positive int32 to usize!?");
1509
1510 let upload_results = futures::stream::iter(hs_dirs)
1511 .map(|relay_ids| {
1512 let netdir = netdir.clone();
1513 let config = Arc::clone(&config);
1514 let imm = Arc::clone(&imm);
1515 let ipt_upload_view = ipt_upload_view.clone();
1516 let authorized_clients = authorized_clients.clone();
1517 let params = params.clone();
1518 let mut shutdown_rx = shutdown_rx.clone();
1519
1520 let ed_id = relay_ids
1521 .rsa_identity()
1522 .map(|id| id.to_string())
1523 .unwrap_or_else(|| "unknown".into());
1524 let rsa_id = relay_ids
1525 .rsa_identity()
1526 .map(|id| id.to_string())
1527 .unwrap_or_else(|| "unknown".into());
1528
1529 async move {
1530 let run_upload = |desc| async {
1531 let Some(hsdir) = netdir.by_ids(&relay_ids) else {
1532 // This should never happen (all of our relay_ids are from the stored
1533 // netdir).
1534 let err =
1535 "tried to upload descriptor to relay not found in consensus?!";
1536 warn!(
1537 nickname=%imm.nickname, hsdir_id=%ed_id, hsdir_rsa_id=%rsa_id,
1538 "{err}"
1539 );
1540 return Err(internal!("{err}").into());
1541 };
1542
1543 Self::upload_descriptor_with_retries(
1544 desc,
1545 &netdir,
1546 &hsdir,
1547 &ed_id,
1548 &rsa_id,
1549 Arc::clone(&imm),
1550 )
1551 .await
1552 };
1553
1554 // How long until we're supposed to time out?
1555 let worst_case_end = imm.runtime.now() + OVERALL_UPLOAD_TIMEOUT;
1556 // We generate a new descriptor before _each_ HsDir upload. This means each
1557 // HsDir could, in theory, receive a different descriptor (not just in terms of
1558 // revision-counters, but also with a different set of IPTs). It may seem like
1559 // this could lead to some HsDirs being left with an outdated descriptor, but
1560 // that's not the case: after the upload completes, the publisher will be
1561 // notified by the ipt_watcher of the IPT change event (if there was one to
1562 // begin with), which will trigger another upload job.
1563 let hsdesc = {
1564 // This scope is needed because the ipt_set MutexGuard is not Send, so it
1565 // needs to fall out of scope before the await point below
1566 let mut ipt_set = ipt_upload_view.borrow_for_publish();
1567
1568 // If there are no IPTs, we abort the upload. At this point, we might have
1569 // uploaded the descriptor to some, but not all, HSDirs from the specified
1570 // time period.
1571 //
1572 // Returning an error here means the upload completion task is never
1573 // notified of the outcome of any of these uploads (which means the
1574 // descriptor is not marked clean). This is OK, because if we suddenly find
1575 // out we have no IPTs, it means our built `hsdesc` has an outdated set of
1576 // IPTs, so we need to go back to the main loop to wait for IPT changes,
1577 // and generate a fresh descriptor anyway.
1578 //
1579 // Ideally, this shouldn't happen very often (if at all).
1580 let Some(ipts) = ipt_set.ipts.as_mut() else {
1581 return Err(PublishError::NoIpts);
1582 };
1583
1584 let hsdesc = {
1585 trace!(
1586 nickname=%imm.nickname, time_period=?time_period,
1587 "building descriptor"
1588 );
1589 let mut rng = imm.mockable.thread_rng();
1590 let mut key_rng = tor_llcrypto::rng::CautiousRng;
1591
1592 // We're about to generate a new version of the descriptor,
1593 // so let's generate a new revision counter.
1594 let now = imm.runtime.wallclock();
1595 let revision_counter = imm.generate_revision_counter(¶ms, now)?;
1596
1597 build_sign(
1598 &imm.keymgr,
1599 &imm.pow_manager,
1600 &config,
1601 authorized_clients.as_deref(),
1602 ipts,
1603 time_period,
1604 revision_counter,
1605 &mut rng,
1606 &mut key_rng,
1607 imm.runtime.wallclock(),
1608 max_hsdesc_len,
1609 )?
1610 };
1611
1612 if let Err(e) =
1613 ipt_set.note_publication_attempt(&imm.runtime, worst_case_end)
1614 {
1615 let wait = e.log_retry_max(&imm.nickname)?;
1616 // TODO (#1226): retry instead of this
1617 return Err(FatalError::Bug(internal!(
1618 "ought to retry after {wait:?}, crashing instead"
1619 ))
1620 .into());
1621 }
1622
1623 hsdesc
1624 };
1625
1626 let VersionedDescriptor {
1627 desc,
1628 revision_counter,
1629 } = hsdesc;
1630 let desc: Arc<str> = desc.into();
1631
1632 trace!(
1633 nickname=%imm.nickname, time_period=?time_period,
1634 revision_counter=?revision_counter,
1635 "generated new descriptor for time period",
1636 );
1637
1638 // (Actually launch the upload attempt. No timeout is needed
1639 // here, since the backoff::Runner code will handle that for us.)
1640 let upload_res: UploadResult = select_biased! {
1641 shutdown = shutdown_rx.next().fuse() => {
1642 // This will always be None, since Void is uninhabited.
1643 let _: Option<Void> = shutdown;
1644
1645 // It looks like the reactor has shut down,
1646 // so there is no point in uploading the descriptor anymore.
1647 //
1648 // Let's shut down the upload task too.
1649 trace!(
1650 nickname=%imm.nickname, time_period=?time_period,
1651 "upload task received shutdown signal"
1652 );
1653
1654 return Err(PublishError::Shutdown);
1655 },
1656 res = run_upload(desc.clone()).fuse() => res,
1657 };
1658
1659 // Note: UploadResult::Failure is only returned when
1660 // upload_descriptor_with_retries fails, i.e. if all our retry
1661 // attempts have failed
1662 Ok(HsDirUploadStatus {
1663 relay_ids,
1664 upload_res,
1665 revision_counter,
1666 })
1667 }
1668 })
1669 // This fails to compile unless the stream is boxed. See https://github.com/rust-lang/rust/issues/104382
1670 .boxed()
1671 .buffer_unordered(MAX_CONCURRENT_UPLOADS)
1672 .try_collect::<Vec<_>>()
1673 .await;
1674
1675 let upload_results = match upload_results {
1676 Ok(v) => v,
1677 Err(PublishError::Fatal(e)) => return Err(e),
1678 Err(PublishError::NoIpts) => {
1679 debug!(
1680 nickname=%imm.nickname, time_period=?time_period,
1681 "no introduction points; skipping upload"
1682 );
1683
1684 return Ok(());
1685 }
1686 Err(PublishError::Shutdown) => {
1687 debug!(
1688 nickname=%imm.nickname, time_period=?time_period,
1689 "the reactor has shut down; aborting upload"
1690 );
1691
1692 return Ok(());
1693 }
1694 };
1695
1696 let (succeeded, _failed): (Vec<_>, Vec<_>) = upload_results
1697 .iter()
1698 .partition(|res| res.upload_res.is_ok());
1699
1700 debug!(
1701 nickname=%imm.nickname, time_period=?time_period,
1702 "descriptor uploaded successfully to {}/{} HSDirs",
1703 succeeded.len(), hsdir_count
1704 );
1705
1706 if upload_task_complete_tx
1707 .send(TimePeriodUploadResult {
1708 time_period,
1709 hsdir_result: upload_results,
1710 })
1711 .await
1712 .is_err()
1713 {
1714 return Err(internal!(
1715 "failed to notify reactor of upload completion (reactor shut down)"
1716 )
1717 .into());
1718 }
1719
1720 Ok(())
1721 }
1722
1723 /// Upload a descriptor to the specified HSDir.
1724 ///
1725 /// If an upload fails, this returns an `Err`. This function does not handle retries. It is up
1726 /// to the caller to retry on failure.
1727 ///
1728 /// This function does not handle timeouts.
1729 async fn upload_descriptor(
1730 hsdesc: Arc<str>,
1731 netdir: &Arc<NetDir>,
1732 hsdir: &Relay<'_>,
1733 imm: Arc<Immutable<R, M>>,
1734 ) -> Result<(), UploadError> {
1735 let request = HsDescUploadRequest::new(hsdesc);
1736
1737 trace!(nickname=%imm.nickname, hsdir_id=%hsdir.id(), hsdir_rsa_id=%hsdir.rsa_id(),
1738 "starting descriptor upload",
1739 );
1740
1741 let tunnel = imm
1742 .mockable
1743 .get_or_launch_hs_dir(netdir, OwnedCircTarget::from_circ_target(hsdir))
1744 .await?;
1745 let source: Option<SourceInfo> = tunnel
1746 .source_info()
1747 .map_err(into_internal!("Couldn't get SourceInfo for circuit"))?;
1748
1749 let mut stream = tunnel
1750 .begin_dir_stream()
1751 .await
1752 .map_err(UploadError::Stream)?;
1753
1754 let _response: String = send_request(&imm.runtime, &request, &mut stream, source)
1755 .await
1756 .map_err(|dir_error| -> UploadError {
1757 match dir_error {
1758 DirClientError::RequestFailed(e) => e.into(),
1759 DirClientError::CircMgr(e) => into_internal!(
1760 "tor-dirclient complains about circmgr going wrong but we gave it a stream"
1761 )(e)
1762 .into(),
1763 e => into_internal!("unexpected error")(e).into(),
1764 }
1765 })?
1766 .into_output_string()?; // This returns an error if we received an error response
1767
1768 Ok(())
1769 }
1770
1771 /// Upload a descriptor to the specified HSDir, retrying if appropriate.
1772 ///
1773 /// Any failed uploads are retried according to a [`PublisherBackoffSchedule`].
1774 /// Each failed upload is retried until it succeeds, or until the overall timeout specified
1775 /// by [`BackoffSchedule::overall_timeout`] elapses. Individual attempts are timed out
1776 /// according to the [`BackoffSchedule::single_attempt_timeout`].
1777 /// This function gives up after the overall timeout elapses,
1778 /// declaring the upload a failure, and never retrying it again.
1779 ///
1780 /// See also [`BackoffSchedule`].
1781 async fn upload_descriptor_with_retries(
1782 hsdesc: Arc<str>,
1783 netdir: &Arc<NetDir>,
1784 hsdir: &Relay<'_>,
1785 ed_id: &str,
1786 rsa_id: &str,
1787 imm: Arc<Immutable<R, M>>,
1788 ) -> UploadResult {
1789 /// The base delay to use for the backoff schedule.
1790 const BASE_DELAY_MSEC: u32 = 1000;
1791 let schedule = PublisherBackoffSchedule {
1792 retry_delay: RetryDelay::from_msec(BASE_DELAY_MSEC),
1793 mockable: imm.mockable.clone(),
1794 };
1795
1796 let runner = Runner::new(
1797 "upload a hidden service descriptor".into(),
1798 schedule.clone(),
1799 imm.runtime.clone(),
1800 );
1801
1802 let fallible_op = || async {
1803 let r = Self::upload_descriptor(hsdesc.clone(), netdir, hsdir, Arc::clone(&imm)).await;
1804
1805 if let Err(e) = &r {
1806 if e.should_report_as_suspicious() {
1807 // Note that not every protocol violation is suspicious:
1808 // we only warn on the protocol violations that look like attempts
1809 // to do a traffic tagging attack via hsdir inflation.
1810 // (See proposal 360.)
1811 warn_report!(
1812 e,
1813 "Suspicious error while uploading descriptor to {}/{}",
1814 ed_id,
1815 rsa_id
1816 );
1817 }
1818 }
1819 r
1820 };
1821
1822 let outcome: Result<(), BackoffError<UploadError>> = runner.run(fallible_op).await;
1823 match outcome {
1824 Ok(()) => {
1825 debug!(
1826 nickname=%imm.nickname, hsdir_id=%ed_id, hsdir_rsa_id=%rsa_id,
1827 "successfully uploaded descriptor to HSDir",
1828 );
1829
1830 Ok(())
1831 }
1832 Err(e) => {
1833 warn_report!(
1834 e,
1835 "failed to upload descriptor for service {} (hsdir_id={}, hsdir_rsa_id={})",
1836 imm.nickname,
1837 ed_id,
1838 rsa_id
1839 );
1840
1841 Err(e.into())
1842 }
1843 }
1844 }
1845
1846 /// Stop publishing descriptors until the specified delay elapses.
1847 async fn start_rate_limit(&mut self, delay: Duration) -> Result<(), Bug> {
1848 if !matches!(self.status(), PublishStatus::RateLimited(_)) {
1849 debug!(
1850 "We are rate-limited for {}; pausing descriptor publication",
1851 humantime::format_duration(delay)
1852 );
1853 let until = self.imm.runtime.now() + delay;
1854 self.update_publish_status(PublishStatus::RateLimited(until))
1855 .await?;
1856 }
1857
1858 Ok(())
1859 }
1860
1861 /// Handle the upload rate-limit being lifted.
1862 async fn expire_rate_limit(&mut self) -> Result<(), Bug> {
1863 debug!("We are no longer rate-limited; resuming descriptor publication");
1864 self.update_publish_status(PublishStatus::UploadScheduled)
1865 .await?;
1866 Ok(())
1867 }
1868
1869 /// Return the authorized clients, if restricted mode is enabled.
1870 ///
1871 /// Returns `Ok(None)` if restricted discovery mode is disabled.
1872 ///
1873 /// Returns an error if restricted discovery mode is enabled, but the client list is empty.
1874 #[cfg_attr(
1875 not(feature = "restricted-discovery"),
1876 allow(clippy::unnecessary_wraps)
1877 )]
1878 fn authorized_clients(&self) -> Result<Option<Arc<RestrictedDiscoveryKeys>>, FatalError> {
1879 cfg_if::cfg_if! {
1880 if #[cfg(feature = "restricted-discovery")] {
1881 let authorized_clients = self
1882 .inner
1883 .lock()
1884 .expect("poisoned lock")
1885 .authorized_clients
1886 .clone();
1887
1888 if authorized_clients.as_ref().as_ref().map(|v| v.is_empty()).unwrap_or_default() {
1889 return Err(FatalError::RestrictedDiscoveryNoClients);
1890 }
1891
1892 Ok(authorized_clients)
1893 } else {
1894 Ok(None)
1895 }
1896 }
1897 }
1898}
1899
1900/// Try to expand a path, logging a warning on failure.
1901fn maybe_expand_path(p: &CfgPath, r: &CfgPathResolver) -> Option<PathBuf> {
1902 // map_err returns unit for clarity
1903 #[allow(clippy::unused_unit, clippy::semicolon_if_nothing_returned)]
1904 p.path(r)
1905 .map_err(|e| {
1906 tor_error::warn_report!(e, "invalid path");
1907 ()
1908 })
1909 .ok()
1910}
1911
1912/// Add `path` to the specified `watcher`.
1913macro_rules! watch_path {
1914 ($watcher:expr, $path:expr, $watch_fn:ident, $($watch_fn_args:expr,)*) => {{
1915 if let Err(e) = $watcher.$watch_fn(&$path, $($watch_fn_args)*) {
1916 warn_report!(e, "failed to watch path {:?}", $path);
1917 } else {
1918 debug!("watching path {:?}", $path);
1919 }
1920 }}
1921}
1922
1923/// Add the specified directories to the watcher.
1924#[allow(clippy::cognitive_complexity)]
1925fn watch_dirs<R: Runtime>(
1926 watcher: &mut FileWatcherBuilder<R>,
1927 dirs: &DirectoryKeyProviderList,
1928 path_resolver: &CfgPathResolver,
1929) {
1930 for path in dirs {
1931 let path = path.path();
1932 let Some(path) = maybe_expand_path(path, path_resolver) else {
1933 warn!("failed to expand key_dir path {:?}", path);
1934 continue;
1935 };
1936
1937 // If the path doesn't exist, the notify watcher will return an error if we attempt to watch it,
1938 // so we skip over paths that don't exist at this time
1939 // (this obviously suffers from a TOCTOU race, but most of the time,
1940 // it is good enough at preventing the watcher from failing to watch.
1941 // If the race *does* happen it is not disastrous, i.e. the reactor won't crash,
1942 // but it will fail to set the watcher).
1943 if matches!(path.try_exists(), Ok(true)) {
1944 watch_path!(watcher, &path, watch_dir, "auth",);
1945 }
1946 // FileWatcher::watch_path causes the parent dir of the path to be watched.
1947 if matches!(path.parent().map(|p| p.try_exists()), Some(Ok(true))) {
1948 watch_path!(watcher, &path, watch_path,);
1949 }
1950 }
1951}
1952
1953/// Try to read the blinded identity key for a given `TimePeriod`.
1954///
1955/// Returns `None` if the service is running in "offline" mode.
1956///
1957// TODO (#1194): we don't currently have support for "offline" mode so this can never return
1958// `Ok(None)`.
1959pub(super) fn read_blind_id_keypair(
1960 keymgr: &Arc<KeyMgr>,
1961 nickname: &HsNickname,
1962 period: TimePeriod,
1963) -> Result<Option<HsBlindIdKeypair>, FatalError> {
1964 let svc_key_spec = HsIdKeypairSpecifier::new(nickname.clone());
1965 let hsid_kp = keymgr
1966 .get::<HsIdKeypair>(&svc_key_spec)?
1967 .ok_or_else(|| FatalError::MissingHsIdKeypair(nickname.clone()))?;
1968
1969 let blind_id_key_spec = BlindIdKeypairSpecifier::new(nickname.clone(), period);
1970
1971 // TODO: make the keystore selector configurable
1972 let keystore_selector = Default::default();
1973 match keymgr.get::<HsBlindIdKeypair>(&blind_id_key_spec)? {
1974 Some(kp) => Ok(Some(kp)),
1975 None => {
1976 let (_hs_blind_id_key, hs_blind_id_kp, _subcredential) = hsid_kp
1977 .compute_blinded_key(period)
1978 .map_err(|_| internal!("failed to compute blinded key"))?;
1979
1980 // Note: we can't use KeyMgr::generate because this key is derived from the HsId
1981 // (KeyMgr::generate uses the tor_keymgr::Keygen trait under the hood,
1982 // which assumes keys are randomly generated, rather than derived from existing keys).
1983
1984 keymgr.insert(hs_blind_id_kp, &blind_id_key_spec, keystore_selector, true)?;
1985
1986 let arti_path = |spec: &dyn KeySpecifier| {
1987 spec.arti_path()
1988 .map_err(into_internal!("invalid key specifier?!"))
1989 };
1990
1991 Ok(Some(
1992 keymgr.get::<HsBlindIdKeypair>(&blind_id_key_spec)?.ok_or(
1993 FatalError::KeystoreRace {
1994 action: "read",
1995 path: arti_path(&blind_id_key_spec)?,
1996 },
1997 )?,
1998 ))
1999 }
2000 }
2001}
2002
2003/// Determine the [`State`] of the publisher based on the upload results
2004/// from the current `time_periods`.
2005fn upload_result_state(
2006 netdir: &NetDir,
2007 time_periods: &[TimePeriodContext],
2008) -> (State, Option<Problem>) {
2009 let current_period = netdir.hs_time_period();
2010 let current_period_res = time_periods
2011 .iter()
2012 .find(|ctx| ctx.params.time_period() == current_period);
2013
2014 let succeeded_current_tp = current_period_res
2015 .iter()
2016 .flat_map(|res| &res.upload_results)
2017 .filter(|res| res.upload_res.is_ok())
2018 .collect_vec();
2019
2020 let secondary_tp_res = time_periods
2021 .iter()
2022 .filter(|ctx| ctx.params.time_period() != current_period)
2023 .collect_vec();
2024
2025 let succeeded_secondary_tp = secondary_tp_res
2026 .iter()
2027 .flat_map(|res| &res.upload_results)
2028 .filter(|res| res.upload_res.is_ok())
2029 .collect_vec();
2030
2031 // All of the failed uploads (for all TPs)
2032 let failed = time_periods
2033 .iter()
2034 .flat_map(|res| &res.upload_results)
2035 .filter(|res| res.upload_res.is_err())
2036 .collect_vec();
2037 let problems: Vec<DescUploadRetryError> = failed
2038 .iter()
2039 .flat_map(|e| e.upload_res.as_ref().map_err(|e| e.clone()).err())
2040 .collect();
2041
2042 let err = match problems.as_slice() {
2043 [_, ..] => Some(problems.into()),
2044 [] => None,
2045 };
2046
2047 if time_periods.len() < 2 {
2048 // We need at least TP contexts (one for the primary TP,
2049 // and another for the secondary one).
2050 //
2051 // If either is missing, we are unreachable for some or all clients.
2052 return (State::DegradedUnreachable, err);
2053 }
2054
2055 let state = match (
2056 succeeded_current_tp.as_slice(),
2057 succeeded_secondary_tp.as_slice(),
2058 ) {
2059 (&[], &[..]) | (&[..], &[]) if failed.is_empty() => {
2060 // We don't have any upload results for one or both TPs.
2061 // We are still bootstrapping.
2062 State::Bootstrapping
2063 }
2064 (&[_, ..], &[_, ..]) if failed.is_empty() => {
2065 // We have uploaded the descriptor to one or more HsDirs from both
2066 // HsDir rings (primary and secondary), and none of the uploads failed.
2067 // We are fully reachable.
2068 State::Running
2069 }
2070 (&[_, ..], &[_, ..]) => {
2071 // We have uploaded the descriptor to one or more HsDirs from both
2072 // HsDir rings (primary and secondary), but some of the uploads failed.
2073 // We are reachable, but we failed to upload the descriptor to all the HsDirs
2074 // that were supposed to have it.
2075 State::DegradedReachable
2076 }
2077 (&[..], &[]) | (&[], &[..]) => {
2078 // We have either
2079 // * uploaded the descriptor to some of the HsDirs from one of the rings,
2080 // but haven't managed to upload it to any of the HsDirs on the other ring, or
2081 // * all of the uploads failed
2082 //
2083 // Either way, we are definitely not reachable by all clients.
2084 State::DegradedUnreachable
2085 }
2086 };
2087
2088 (state, err)
2089}
2090
2091/// Whether the reactor should initiate an upload.
2092#[derive(Copy, Clone, Debug, Default, PartialEq)]
2093enum PublishStatus {
2094 /// We need to call upload_all.
2095 UploadScheduled,
2096 /// We are rate-limited until the specified [`Instant`].
2097 ///
2098 /// We have tried to schedule multiple uploads in a short time span,
2099 /// and we are rate-limited. We are waiting for a signal from the schedule_upload_tx
2100 /// channel to unblock us.
2101 RateLimited(Instant),
2102 /// We are idle and waiting for external events.
2103 ///
2104 /// We have enough information to build the descriptor, but since we have already called
2105 /// upload_all to upload it to all relevant HSDirs, there is nothing for us to do right nbow.
2106 Idle,
2107 /// We are waiting for the IPT manager to establish some introduction points.
2108 ///
2109 /// No descriptors will be published until the `PublishStatus` of the reactor is changed to
2110 /// `UploadScheduled`.
2111 #[default]
2112 AwaitingIpts,
2113}
2114
2115/// The backoff schedule for the task that publishes descriptors.
2116#[derive(Clone, Debug)]
2117struct PublisherBackoffSchedule<M: Mockable> {
2118 /// The delays
2119 retry_delay: RetryDelay,
2120 /// The mockable reactor state, needed for obtaining an rng.
2121 mockable: M,
2122}
2123
2124impl<M: Mockable> BackoffSchedule for PublisherBackoffSchedule<M> {
2125 fn max_retries(&self) -> Option<usize> {
2126 None
2127 }
2128
2129 fn overall_timeout(&self) -> Option<Duration> {
2130 Some(OVERALL_UPLOAD_TIMEOUT)
2131 }
2132
2133 fn single_attempt_timeout(&self) -> Option<Duration> {
2134 Some(self.mockable.estimate_upload_timeout())
2135 }
2136
2137 fn next_delay<E: RetriableError>(&mut self, _error: &E) -> Option<Duration> {
2138 Some(self.retry_delay.next_delay(&mut self.mockable.thread_rng()))
2139 }
2140}
2141
2142impl RetriableError for UploadError {
2143 fn should_retry(&self) -> bool {
2144 match self {
2145 UploadError::Request(_) | UploadError::Circuit(_) | UploadError::Stream(_) => true,
2146 UploadError::Bug(_) => false,
2147 }
2148 }
2149}
2150
2151/// The outcome of uploading a descriptor to the HSDirs from a particular time period.
2152#[derive(Debug, Clone)]
2153struct TimePeriodUploadResult {
2154 /// The time period.
2155 time_period: TimePeriod,
2156 /// The upload results.
2157 hsdir_result: Vec<HsDirUploadStatus>,
2158}
2159
2160/// The outcome of uploading a descriptor to a particular HsDir.
2161#[derive(Clone, Debug)]
2162struct HsDirUploadStatus {
2163 /// The identity of the HsDir we attempted to upload the descriptor to.
2164 relay_ids: RelayIds,
2165 /// The outcome of this attempt.
2166 upload_res: UploadResult,
2167 /// The revision counter of the descriptor we tried to upload.
2168 revision_counter: RevisionCounter,
2169}
2170
2171/// The outcome of uploading a descriptor.
2172type UploadResult = Result<(), DescUploadRetryError>;
2173
2174impl From<BackoffError<UploadError>> for DescUploadRetryError {
2175 fn from(e: BackoffError<UploadError>) -> Self {
2176 use BackoffError as BE;
2177 use DescUploadRetryError as DURE;
2178
2179 match e {
2180 BE::FatalError(e) => DURE::FatalError(e),
2181 BE::MaxRetryCountExceeded(e) => DURE::MaxRetryCountExceeded(e),
2182 BE::Timeout(e) => DURE::Timeout(e),
2183 BE::ExplicitStop(_) => {
2184 DURE::Bug(internal!("explicit stop in publisher backoff schedule?!"))
2185 }
2186 }
2187 }
2188}
2189
2190// NOTE: the rest of the publisher tests live in publish.rs
2191#[cfg(test)]
2192mod test {
2193 // @@ begin test lint list maintained by maint/add_warning @@
2194 #![allow(clippy::bool_assert_comparison)]
2195 #![allow(clippy::clone_on_copy)]
2196 #![allow(clippy::dbg_macro)]
2197 #![allow(clippy::mixed_attributes_style)]
2198 #![allow(clippy::print_stderr)]
2199 #![allow(clippy::print_stdout)]
2200 #![allow(clippy::single_char_pattern)]
2201 #![allow(clippy::unwrap_used)]
2202 #![allow(clippy::unchecked_time_subtraction)]
2203 #![allow(clippy::useless_vec)]
2204 #![allow(clippy::needless_pass_by_value)]
2205 #![allow(clippy::string_slice)] // See arti#2571
2206 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
2207 use super::*;
2208 use tor_netdir::testnet;
2209
2210 /// Create a `TimePeriodContext` from the specified upload results.
2211 fn create_time_period_ctx(
2212 params: &HsDirParams,
2213 upload_results: Vec<HsDirUploadStatus>,
2214 ) -> TimePeriodContext {
2215 TimePeriodContext {
2216 params: params.clone(),
2217 hs_dirs: vec![],
2218 last_successful: None,
2219 upload_results,
2220 }
2221 }
2222
2223 /// Create a single `HsDirUploadStatus`
2224 fn create_upload_status(upload_res: UploadResult) -> HsDirUploadStatus {
2225 HsDirUploadStatus {
2226 relay_ids: RelayIds::empty(),
2227 upload_res,
2228 revision_counter: RevisionCounter::from(13),
2229 }
2230 }
2231
2232 /// Create a bunch of results, all with the specified `upload_res`.
2233 fn create_upload_results(upload_res: UploadResult) -> Vec<HsDirUploadStatus> {
2234 std::iter::repeat_with(|| create_upload_status(upload_res.clone()))
2235 .take(10)
2236 .collect()
2237 }
2238
2239 fn construct_netdir() -> NetDir {
2240 const SRV1: [u8; 32] = *b"The door refused to open. ";
2241 const SRV2: [u8; 32] = *b"It said, 'Five cents, please.' ";
2242
2243 let dir = testnet::construct_custom_netdir(|_, _, bld| {
2244 bld.shared_rand_cur(7, SRV1.into(), None)
2245 .shared_rand_prev(7, SRV2.into(), None);
2246 })
2247 .unwrap();
2248
2249 dir.unwrap_if_sufficient().unwrap()
2250 }
2251
2252 #[test]
2253 fn upload_result_status_bootstrapping() {
2254 let netdir = construct_netdir();
2255 let all_params = netdir.hs_all_time_periods();
2256 let current_period = netdir.hs_time_period();
2257 let primary_params = all_params
2258 .iter()
2259 .find(|param| param.time_period() == current_period)
2260 .unwrap();
2261 let results = [
2262 (vec![], vec![]),
2263 (vec![], create_upload_results(Ok(()))),
2264 (create_upload_results(Ok(())), vec![]),
2265 ];
2266
2267 for (primary_result, secondary_result) in results {
2268 let primary_ctx = create_time_period_ctx(primary_params, primary_result);
2269
2270 let secondary_params = all_params
2271 .iter()
2272 .find(|param| param.time_period() != current_period)
2273 .unwrap();
2274 let secondary_ctx = create_time_period_ctx(secondary_params, secondary_result.clone());
2275
2276 let (status, err) = upload_result_state(&netdir, &[primary_ctx, secondary_ctx]);
2277 assert_eq!(status, State::Bootstrapping);
2278 assert!(err.is_none());
2279 }
2280 }
2281
2282 #[test]
2283 fn upload_result_status_running() {
2284 let netdir = construct_netdir();
2285 let all_params = netdir.hs_all_time_periods();
2286 let current_period = netdir.hs_time_period();
2287 let primary_params = all_params
2288 .iter()
2289 .find(|param| param.time_period() == current_period)
2290 .unwrap();
2291
2292 let secondary_result = create_upload_results(Ok(()));
2293 let secondary_params = all_params
2294 .iter()
2295 .find(|param| param.time_period() != current_period)
2296 .unwrap();
2297 let secondary_ctx = create_time_period_ctx(secondary_params, secondary_result.clone());
2298
2299 let primary_result = create_upload_results(Ok(()));
2300 let primary_ctx = create_time_period_ctx(primary_params, primary_result);
2301 let (status, err) = upload_result_state(&netdir, &[primary_ctx, secondary_ctx]);
2302 assert_eq!(status, State::Running);
2303 assert!(err.is_none());
2304 }
2305
2306 #[test]
2307 fn upload_result_status_reachable() {
2308 let netdir = construct_netdir();
2309 let all_params = netdir.hs_all_time_periods();
2310 let current_period = netdir.hs_time_period();
2311 let primary_params = all_params
2312 .iter()
2313 .find(|param| param.time_period() == current_period)
2314 .unwrap();
2315
2316 let primary_result = create_upload_results(Ok(()));
2317 let primary_ctx = create_time_period_ctx(primary_params, primary_result.clone());
2318 let failed_res = create_upload_results(Err(DescUploadRetryError::Bug(internal!("test"))));
2319 let secondary_result = create_upload_results(Ok(()))
2320 .into_iter()
2321 .chain(failed_res.iter().cloned())
2322 .collect();
2323 let secondary_params = all_params
2324 .iter()
2325 .find(|param| param.time_period() != current_period)
2326 .unwrap();
2327 let secondary_ctx = create_time_period_ctx(secondary_params, secondary_result);
2328 let (status, err) = upload_result_state(&netdir, &[primary_ctx, secondary_ctx]);
2329
2330 // Degraded but reachable (because some of the secondary HsDir uploads failed).
2331 assert_eq!(status, State::DegradedReachable);
2332 assert!(matches!(err, Some(Problem::DescriptorUpload(_))));
2333 }
2334
2335 #[test]
2336 fn upload_result_status_unreachable() {
2337 let netdir = construct_netdir();
2338 let all_params = netdir.hs_all_time_periods();
2339 let current_period = netdir.hs_time_period();
2340 let primary_params = all_params
2341 .iter()
2342 .find(|param| param.time_period() == current_period)
2343 .unwrap();
2344 let mut primary_result =
2345 create_upload_results(Err(DescUploadRetryError::Bug(internal!("test"))));
2346 let primary_ctx = create_time_period_ctx(primary_params, primary_result.clone());
2347 // No secondary TP (we are unreachable).
2348 let (status, err) = upload_result_state(&netdir, &[primary_ctx]);
2349 assert_eq!(status, State::DegradedUnreachable);
2350 assert!(matches!(err, Some(Problem::DescriptorUpload(_))));
2351
2352 // Add a successful result
2353 primary_result.push(create_upload_status(Ok(())));
2354 let primary_ctx = create_time_period_ctx(primary_params, primary_result.clone());
2355 let (status, err) = upload_result_state(&netdir, &[primary_ctx]);
2356 // Still degraded, and unreachable (because we don't have a TimePeriodContext
2357 // for the secondary TP)
2358 assert_eq!(status, State::DegradedUnreachable);
2359 assert!(matches!(err, Some(Problem::DescriptorUpload(_))));
2360
2361 // If we add another time period where none of the uploads were successful,
2362 // we're *still* unreachable
2363 let secondary_result =
2364 create_upload_results(Err(DescUploadRetryError::Bug(internal!("test"))));
2365 let secondary_params = all_params
2366 .iter()
2367 .find(|param| param.time_period() != current_period)
2368 .unwrap();
2369 let secondary_ctx = create_time_period_ctx(secondary_params, secondary_result.clone());
2370 let primary_ctx = create_time_period_ctx(primary_params, primary_result.clone());
2371 let (status, err) = upload_result_state(&netdir, &[primary_ctx, secondary_ctx]);
2372 assert_eq!(status, State::DegradedUnreachable);
2373 assert!(matches!(err, Some(Problem::DescriptorUpload(_))));
2374 }
2375}