1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
|
//! `tor-dirmgr`: Code to fetch, store, and update Tor directory information.
//!
//! # Overview
//!
//! This crate is part of
//! [Arti](https://gitlab.torproject.org/tpo/core/arti/), a project to
//! implement [Tor](https://www.torproject.org/) in Rust.
//!
//! In its current design, Tor requires a set of up-to-date
//! authenticated directory documents in order to build multi-hop
//! anonymized circuits through the network.
//!
//! This directory manager crate is responsible for figuring out which
//! directory information we lack, downloading what we're missing, and
//! keeping a cache of it on disk.
#![deny(missing_docs)]
#![warn(noop_method_call)]
#![deny(unreachable_pub)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![warn(clippy::clone_on_ref_ptr)]
#![warn(clippy::cognitive_complexity)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![warn(clippy::option_option)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::trait_duplication_in_bounds)]
#![warn(clippy::unseparated_literal_suffix)]
pub mod authority;
mod bootstrap;
mod config;
mod docid;
mod docmeta;
mod err;
mod retry;
mod shared_ref;
mod state;
mod storage;
use crate::docid::{CacheUsage, ClientRequest, DocQuery};
use crate::shared_ref::SharedMutArc;
use crate::storage::sqlite::SqliteStore;
use retry::RetryConfig;
use tor_circmgr::CircMgr;
use tor_netdir::NetDir;
use tor_netdoc::doc::netstatus::ConsensusFlavor;
use anyhow::{Context, Result};
use async_trait::async_trait;
use futures::{channel::oneshot, lock::Mutex, task::SpawnExt};
use log::{info, warn};
use tor_rtcompat::{Runtime, SleepProviderExt};
use std::sync::Arc;
use std::{collections::HashMap, sync::Weak};
use std::{fmt::Debug, time::SystemTime};
pub use authority::Authority;
pub use config::{
DirMgrConfig, DirMgrConfigBuilder, DownloadScheduleConfig, DownloadScheduleConfigBuilder,
NetworkConfig, NetworkConfigBuilder,
};
pub use docid::DocId;
pub use err::Error;
pub use storage::DocumentText;
/// A directory manager to download, fetch, and cache a Tor directory.
///
/// A DirMgr can operate in three modes:
/// * In **offline** mode, it only reads from the cache, and can
/// only read once.
/// * In **read-only** mode, it reads from the cache, but checks
/// whether it can acquire an associated lock file. If it can, then
/// it enters read-write mode. If not, it checks the cache
/// periodically for new information.
/// * In **read-write** mode, it knows that no other process will be
/// writing to the cache, and it takes responsibility for fetching
/// data from the network and updating the directory with new
/// directory information.
///
/// # Limitations
///
/// Because of portability issues in [`fslock::LockFile`], you might
/// get weird results if you run two of these in the same process with
/// the same underlying cache.
pub struct DirMgr<R: Runtime> {
/// Configuration information: where to find directories, how to
/// validate them, and so on.
config: DirMgrConfig,
/// Handle to our sqlite cache.
// XXXX I'd like to use an rwlock, but that's not feasible, since
// rusqlite::Connection isn't Sync.
// TODO: Does this have to be a futures::Mutex? I would rather have
// a rule that we never hold the guard for this mutex across an async
// suspend point. But that will be hard to enforce until the
// `must_not_suspend` lint is in stable.
store: Mutex<SqliteStore>,
/// Our latest sufficiently bootstrapped directory, if we have one.
///
/// We use the RwLock so that we can give this out to a bunch of other
/// users, and replace it once a new directory is bootstrapped.
netdir: SharedMutArc<NetDir>,
/// A circuit manager, if this DirMgr supports downloading.
circmgr: Option<Arc<CircMgr<R>>>,
/// Our asynchronous runtime.
runtime: R,
}
impl<R: Runtime> DirMgr<R> {
/// Try to load the directory from disk, without launching any
/// kind of update process.
///
/// This function runs in **offline** mode: it will give an error
/// if the result is not up-to-date, or not fully downloaded.
///
/// In general, you shouldn't use this function in a long-running
/// program; it's only suitable for command-line or batch tools.
// TODO: I wish this function didn't have to be async or take a runtime.
pub async fn load_once(runtime: R, config: DirMgrConfig) -> Result<Arc<NetDir>> {
let dirmgr = Arc::new(Self::from_config(config, runtime, None)?);
// TODO: add some way to return a directory that isn't up-to-date
let _success = dirmgr.load_directory().await?;
dirmgr
.opt_netdir()
.ok_or_else(|| Error::DirectoryNotPresent.into())
}
/// Return a current netdir, either loading it or bootstrapping it
/// as needed.
///
/// Like load_once, but will try to bootstrap (or wait for another
/// process to bootstrap) if we don't have an up-to-date
/// bootstrapped directory.
///
/// In general, you shouldn't use this function in a long-running
/// program; it's only suitable for command-line or batch tools.
pub async fn load_or_bootstrap_once(
config: DirMgrConfig,
runtime: R,
circmgr: Arc<CircMgr<R>>,
) -> Result<Arc<NetDir>> {
let dirmgr = DirMgr::bootstrap_from_config(config, runtime, circmgr).await?;
Ok(dirmgr.netdir())
}
/// Return a new directory manager from a given configuration,
/// bootstrapping from the network as necessary.
///
/// This function will to return until the directory is
/// bootstrapped enough to build circuits. It will also launch a
/// background task that fetches any missing information, and that
/// replaces the directory when a new one is available.
pub async fn bootstrap_from_config(
config: DirMgrConfig,
runtime: R,
circmgr: Arc<CircMgr<R>>,
) -> Result<Arc<Self>> {
let dirmgr = Arc::new(DirMgr::from_config(config, runtime.clone(), Some(circmgr))?);
// Try to load from the cache.
let have_directory = dirmgr
.load_directory()
.await
.context("Error loading cached directory")?;
let (mut sender, receiver) = if have_directory {
info!("Loaded a good directory from cache.");
(None, None)
} else {
info!("Didn't get usable directory from cache.");
let (sender, receiver) = oneshot::channel();
(Some(sender), Some(receiver))
};
// Whether we loaded or not, we now start downloading.
let dirmgr_weak = Arc::downgrade(&dirmgr);
runtime.spawn(async move {
// TODO: don't warn when these are Error::ManagerDropped.
if let Err(e) = Self::reload_until_owner(&dirmgr_weak, &mut sender).await {
warn!("Unrecoverd error while waiting for bootstrap: {}", e);
} else if let Err(e) = Self::download_forever(dirmgr_weak, sender).await {
warn!("Unrecovered error while downloading: {}", e);
}
})?;
if let Some(receiver) = receiver {
let _ = receiver.await;
}
info!("We have enough information to build circuits.");
Ok(dirmgr)
}
/// Try forever to either lock the storage (and thereby become the
/// owner), or to reload the database.
///
/// If we have begin to have a bootstrapped directory, send a
/// message using `on_complete`.
///
/// If we eventually become the owner, return Ok().
async fn reload_until_owner(
weak: &Weak<Self>,
on_complete: &mut Option<oneshot::Sender<()>>,
) -> Result<()> {
let mut logged = false;
let mut bootstrapped = false;
let runtime = upgrade_weak_ref(weak)?.runtime.clone();
loop {
{
let dirmgr = upgrade_weak_ref(weak)?;
if dirmgr.try_upgrade_to_readwrite().await? {
// We now own the lock! (Maybe we owned it before; the
// upgrade_to_readwrite() function is idempotent.) We can
// do our own bootstrapping.
return Ok(());
}
}
if !logged {
logged = true;
info!("Another process is bootstrapping. Waiting till it finishes or exits.");
}
// We don't own the lock. Somebody else owns the cache. They
// should be updating it. Wait a bit, then try again.
let pause = if bootstrapped {
std::time::Duration::new(120, 0)
} else {
std::time::Duration::new(5, 0)
};
runtime.sleep(pause).await;
// TODO: instead of loading the whole thing we should have a
// database entry that says when the last update was, or use
// our state functions.
{
let dirmgr = upgrade_weak_ref(weak)?;
if dirmgr.load_directory().await? {
// Successfully loaded a bootstrapped directory.
if let Some(send_done) = on_complete.take() {
let _ = send_done.send(());
}
bootstrapped = true;
}
}
}
}
/// Try to fetch our directory info and keep it updated, indefinitely.
///
/// If we have begin to have a bootstrapped directory, send a
/// message using `on_complete`.
async fn download_forever(
weak: Weak<Self>,
mut on_complete: Option<oneshot::Sender<()>>,
) -> Result<()> {
let mut state: Box<dyn DirState> = Box::new(state::GetConsensusState::new(
Weak::clone(&weak),
CacheUsage::CacheOkay,
)?);
let (retry_config, runtime) = {
let dirmgr = upgrade_weak_ref(&weak)?;
(
*dirmgr.config.schedule().retry_bootstrap(),
dirmgr.runtime.clone(),
)
};
loop {
let mut usable = false;
let mut retry_delay = retry_config.schedule();
'retry_attempt: for _ in retry_config.attempts() {
let (newstate, recoverable_err) =
bootstrap::download(Weak::clone(&weak), state, on_complete.take()).await?;
state = newstate;
if let Some(err) = recoverable_err {
if state.is_ready(Readiness::Usable) {
usable = true;
info!("Unable to completely download a directory: {}. Nevertheless, the directory is usable, so we'll pause for now.", err);
break 'retry_attempt;
}
let delay = retry_delay.next_delay(&mut rand::thread_rng());
warn!(
"Unable to download a usable directory: {}. We will restart in {:?}.",
err, delay
);
runtime.sleep(delay).await;
state = state.reset()?;
} else {
info!("Directory is complete.");
usable = true;
break 'retry_attempt;
}
}
if !usable {
// we ran out of attempts.
warn!(
"We failed {} times to bootstrap a directory. We're going to give up.",
retry_config.n_attempts()
);
return Err(Error::CantAdvanceState.into());
}
let reset_at = state.reset_time();
match reset_at {
Some(t) => runtime.sleep_until_wallclock(t).await,
None => return Ok(()),
}
state = state.reset()?;
}
}
/// Get a reference to the circuit manager, if we have one.
fn circmgr(&self) -> Result<Arc<CircMgr<R>>> {
self.circmgr
.as_ref()
.map(Arc::clone)
.ok_or_else(|| Error::NoDownloadSupport.into())
}
/// Try to make this a directory manager with read-write access to its
/// storage.
///
/// Return true if we got the lock, or if we already had it.
///
/// Return false if another process has the lock
async fn try_upgrade_to_readwrite(&self) -> Result<bool> {
self.store.lock().await.upgrade_to_readwrite()
}
/// Construct a DirMgr from a DirMgrConfig.
fn from_config(
config: DirMgrConfig,
runtime: R,
circmgr: Option<Arc<CircMgr<R>>>,
) -> Result<Self> {
let readonly = circmgr.is_none();
let store = Mutex::new(config.open_sqlite_store(readonly)?);
let netdir = SharedMutArc::new();
Ok(DirMgr {
config,
store,
netdir,
circmgr,
runtime,
})
}
/// Load the latest non-pending non-expired directory from the
/// cache, if it is newer than the one we have.
///
/// Return false if there is no such consensus.
async fn load_directory(self: &Arc<Self>) -> Result<bool> {
//let store = &self.store;
let state = state::GetConsensusState::new(Arc::downgrade(self), CacheUsage::CacheOnly)?;
let _ = bootstrap::load(Arc::clone(self), Box::new(state)).await?;
Ok(self.netdir.get().is_some())
}
/// Return an Arc handle to our latest directory, if we have one.
///
/// This is a private method, since by the time anybody else has a
/// handle to a DirMgr, the NetDir should definitely be
/// bootstrapped.
fn opt_netdir(&self) -> Option<Arc<NetDir>> {
self.netdir.get()
}
/// Return an Arc handle to our latest directory, if we have one.
// TODO: Add variants of this that make sure that it's up-to-date?
pub fn netdir(&self) -> Arc<NetDir> {
self.opt_netdir().expect("DirMgr was not bootstrapped!")
}
/// Try to load the text of a signle document described by `doc` from
/// storage.
pub async fn text(&self, doc: &DocId) -> Result<Option<DocumentText>> {
let mut result = HashMap::new();
let query = (*doc).into();
self.load_documents_into(&query, &mut result).await?;
if let Some((docid, doctext)) = result.into_iter().next() {
assert_eq!(&docid, doc);
Ok(Some(doctext))
} else {
Ok(None)
}
}
/// Load the text for a collection of documents.
///
/// If many of the documents have the same type, this can be more
/// efficient than calling [`text`](Self::text).
pub async fn texts<T>(&self, docs: T) -> Result<HashMap<DocId, DocumentText>>
where
T: IntoIterator<Item = DocId>,
{
let partitioned = docid::partition_by_type(docs);
let mut result = HashMap::new();
for (_, query) in partitioned.into_iter() {
self.load_documents_into(&query, &mut result).await?
}
Ok(result)
}
/// Load all the documents for a single DocumentQuery from the store.
async fn load_documents_into(
&self,
query: &DocQuery,
result: &mut HashMap<DocId, DocumentText>,
) -> Result<()> {
use DocQuery::*;
let store = self.store.lock().await;
match query {
LatestConsensus {
flavor,
cache_usage,
} => {
if *cache_usage == CacheUsage::MustDownload {
// Do nothing: we don't want a cached consensus.
} else if let Some(c) =
store.latest_consensus(*flavor, cache_usage.pending_requirement())?
{
let id = DocId::LatestConsensus {
flavor: *flavor,
cache_usage: *cache_usage,
};
result.insert(id, c.into());
}
}
AuthCert(ids) => result.extend(
store
.authcerts(ids)?
.into_iter()
.map(|(id, c)| (DocId::AuthCert(id), DocumentText::from_string(c))),
),
Microdesc(digests) => {
result.extend(
store
.microdescs(digests)?
.into_iter()
.map(|(id, md)| (DocId::Microdesc(id), DocumentText::from_string(md))),
);
}
RouterDesc(digests) => result.extend(
store
.routerdescs(digests)?
.into_iter()
.map(|(id, rd)| (DocId::RouterDesc(id), DocumentText::from_string(rd))),
),
}
Ok(())
}
/// Convert a DocQuery into a set of ClientRequests, suitable for sending
/// to a directory cache.
///
/// This conversion has to be a function of the dirmgr, since it may
/// require knowledge about our current state.
async fn query_into_requests(&self, q: DocQuery) -> Result<Vec<ClientRequest>> {
let mut res = Vec::new();
for q in q.split_for_download() {
match q {
DocQuery::LatestConsensus { flavor, .. } => {
res.push(self.make_consensus_request(flavor).await?);
}
DocQuery::AuthCert(ids) => {
res.push(ClientRequest::AuthCert(ids.into_iter().collect()));
}
DocQuery::Microdesc(ids) => {
res.push(ClientRequest::Microdescs(ids.into_iter().collect()));
}
DocQuery::RouterDesc(ids) => {
res.push(ClientRequest::RouterDescs(ids.into_iter().collect()));
}
}
}
Ok(res)
}
/// Construct an appropriate ClientRequest to download a consensus
/// of the given flavor.
async fn make_consensus_request(&self, flavor: ConsensusFlavor) -> Result<ClientRequest> {
let mut request = tor_dirclient::request::ConsensusRequest::new(flavor);
let r = self.store.lock().await;
match r.latest_consensus_meta(flavor) {
Ok(Some(meta)) => {
request.set_last_consensus_date(meta.lifetime().valid_after());
request.push_old_consensus_digest(*meta.sha3_256_of_signed());
}
Ok(None) => {}
Err(e) => {
warn!("Error loading directory metadata: {}", e);
}
}
Ok(ClientRequest::Consensus(request))
}
/// Given a request we sent and the response we got from a
/// directory server, see whether we should expand that response
/// into "something larger".
///
/// Currently, this handles expanding consensus diffs, and nothing
/// else. We do it at this stage of our downloading operation
/// because it requires access to the store.
async fn expand_response_text(&self, req: &ClientRequest, text: String) -> Result<String> {
if let ClientRequest::Consensus(req) = req {
if tor_consdiff::looks_like_diff(&text) {
if let Some(old_d) = req.old_consensus_digests().next() {
let db_val = {
let s = self.store.lock().await;
s.consensus_by_sha3_digest_of_signed_part(old_d)?
};
if let Some((old_consensus, meta)) = db_val {
info!("Applying a consensus diff");
let new_consensus = tor_consdiff::apply_diff(
old_consensus.as_str()?,
&text,
Some(*meta.sha3_256_of_signed()),
)?;
new_consensus.check_digest()?;
return Ok(new_consensus.to_string());
}
}
return Err(Error::Unwanted("Received a consensus diff we did not ask for").into());
}
}
Ok(text)
}
}
/// A degree of readiness for a given directory state object.
#[derive(Debug, Copy, Clone)]
enum Readiness {
/// There is no more information to download.
Complete,
/// There is more information to download, but we don't need to
Usable,
}
/// A "state" object used to represent our progress in downloading a
/// directory.
///
/// These state objects are not meant to know about the network, or
/// how to fetch documents at all. Instead, they keep track of what
/// information they are missing, and what to do when they get that
/// information.
///
/// Every state object has two possible transitions: "resetting", and
/// "advancing". Advancing happens when a state has no more work to
/// do, and needs to transform into a different kind of object.
/// Resetting happens when this state needs to go back to an initial
/// state in order to start over -- either because of an error or
/// because the information it has downloaded is no longer timely.
#[async_trait]
trait DirState: Send {
/// Return a human-readable description of this state.
fn describe(&self) -> String;
/// Return a list of the documents we're missing.
///
/// If every document on this list were to be loaded or downloaded, then
/// the state should either become "ready to advance", or "complete."
///
/// This list should never _grow_ on a given state; only advancing
/// or resetting the state should add new DocIds that weren't
/// there before.
fn missing_docs(&self) -> Vec<DocId>;
/// Describe whether this state has reached `ready` status.
fn is_ready(&self, ready: Readiness) -> bool;
/// Return true if this state can advance to another state via its
/// `advance` method.
fn can_advance(&self) -> bool;
/// Add one or more documents from our cache; returns 'true' if there
/// was any change in this state.
fn add_from_cache(&mut self, docs: HashMap<DocId, DocumentText>) -> Result<bool>;
/// Add information that we have just downloaded to this state; returns
/// 'true' if there as any change in this state.
///
/// This method receives a copy of the original request, and
/// should reject any documents that do not purtain to it.
///
/// If `storage` is provided, then we should write any accepted documents
/// into `storage` so they can be saved in a cache.
// TODO: It might be good to say "there was a change but also an
// error" in this API if possible.
// TODO: It would be better to not have this function be async,
// once the `must_not_suspend` lint is stable.
// TODO: this should take a "DirSource" too.
async fn add_from_download(
&mut self,
text: &str,
request: &ClientRequest,
storage: Option<&Mutex<SqliteStore>>,
) -> Result<bool>;
/// Return the number of attempts that should be made in parallel
/// to attempt downloads for missing documents, and a
/// configuration for retrying downloads.
fn dl_config(&self) -> Result<(usize, RetryConfig)>;
/// If possible, advance to the next state.
fn advance(self: Box<Self>) -> Result<Box<dyn DirState>>;
/// Return a time (if any) when downloaders should stop attempting to
/// advance this state, and should instead reset it and start over.
fn reset_time(&self) -> Option<SystemTime>;
/// Reset this state and start over.
fn reset(self: Box<Self>) -> Result<Box<dyn DirState>>;
}
/// Try to upgrade a weak reference to a DirMgr, and give an error on
/// failure.
fn upgrade_weak_ref<T>(weak: &Weak<T>) -> Result<Arc<T>> {
Weak::upgrade(weak).ok_or_else(|| Error::ManagerDropped.into())
}
|