1
//! The DNS resolver reactor, for resolving DNS queries asynchronously.
2
//!
3
//! The [`DnsResolverReactor`] is meant to be run as a background task.
4
//! It is paired with a [`DnsResolver`], which exposes an async interface
5
//! for sending DNS queries to the reactor.
6

            
7
use std::collections::HashMap;
8
use std::collections::hash_map::Entry;
9
use std::sync::Arc;
10
use std::time::{Duration, Instant};
11

            
12
use async_trait::async_trait;
13
use derive_more::Constructor;
14
use futures::FutureExt as _;
15
use futures::channel::mpsc;
16
use futures::select_biased;
17
use futures::stream::StreamExt as _;
18
use hickory_resolver::lookup::Lookup;
19
use hickory_resolver::net::runtime::TokioRuntimeProvider;
20
use smallvec::SmallVec;
21
use tokio::task::JoinSet;
22

            
23
use tor_async_utils::oneshot_broadcast;
24
use tor_error::{Bug, internal, into_internal};
25
use tor_rtcompat::{DynTimeProvider, SleepProvider as _};
26

            
27
use crate::stream::dns::resolver::{
28
    DnsRequest, DnsResolver, DnsResponseReceiver, LookupAnswers, LookupError, RecordData,
29
};
30

            
31
/// The hickory stub resolver.
32
pub(crate) type HickoryResolver = hickory_resolver::Resolver<TokioRuntimeProvider>;
33

            
34
/// The ASCII-encoded hostname from a RESOLVE, in canonical form.
35
type Query = Arc<str>;
36

            
37
// TODO(relay): check the TTLs against C Tor and the relevant DNS RFCs
38
// to make sure they're fit for purpose
39

            
40
/// The TTL to use for responses that have an empty answer section (not NXDOMAIN).
41
const TTL_FOR_EMPTY_ANS: Duration = Duration::from_mins(5);
42

            
43
/// The TTL for error responses.
44
const TTL_FOR_ERRORS: Duration = Duration::from_mins(5);
45

            
46
/// Our "low" TTL value. Any TTLs below this are clipped to this value.
47
///
48
/// Must be greater than FUZZY_DNS_TTL_SECONDS, otherwise our calculations can underflow.
49
const MIN_DNS_TTL: Duration = Duration::from_mins(5);
50

            
51
/// Our "high" TTL value. Any TTLs above MIN_DNS_TTL are clipped to this value.
52
const MAX_DNS_TTL: Duration = Duration::from_mins(60);
53

            
54
/// The amount we +- to the clipped TTL to make it "fuzzy".
55
///
56
/// Must be less than MIN_DNS_TTL, otherwise our calculations can underflow.
57
///
58
/// See "The effect of DNS on Tor's Anonymity" by Greschbach et al.
59
const FUZZY_DNS_TTL: Duration = Duration::from_mins(4);
60

            
61
/// A reactor that performs DNS lookups on behalf of incoming streams (RESOLVE, BEGIN).
62
///
63
/// De-duplicates queries, and uses hickory-resolver under the hood.
64
///
65
/// Queries are de-duplicated using [`PendingQueries`], which keeps track
66
/// of all the currently running lookup tasks.
67
#[must_use = "the reactor doesn't do anything unless you run it"]
68
pub(crate) struct DnsResolverReactor<M: MockableAsyncResolver = HickoryResolver> {
69
    /// The async stub resolver.
70
    resolver: Arc<M>,
71
    /// The time provider.
72
    runtime: DynTimeProvider,
73
    /// The DNS lookups we have launched that we are still waiting on.
74
    pending: PendingQueries,
75
    /// The DNS cache.
76
    cache: DnsCache,
77
    /// MPSC channel for receiving DNS queries.
78
    query_rx: mpsc::Receiver<DnsRequest>,
79
}
80

            
81
/// A DNS response.
82
///
83
/// We cache the response as a whole, not the individual answers.
84
#[derive(Clone, Debug, Constructor)]
85
pub(crate) struct DnsResponse {
86
    /// The answer records.
87
    pub(crate) answers: Result<LookupAnswers<RecordData>, LookupError>,
88
    /// The timestamp when this response becomes invalid.
89
    ///
90
    /// This is computed by clipping + fuzzing the max TTL of all the answers.
91
    ///
92
    // TODO(relay): This does mean we'll be serving stale Records sometimes,
93
    // but I think that might be fine?
94
    //
95
    // RFC-8767 says resolvers are allowed to serve stale data,
96
    // **if** they're unable to refresh it
97
    // (that's not necessarily the case here,
98
    // although with a flexible enough interpretation of the RFC,
99
    // maybe it could be?):
100
    //
101
    // "If the data is unable to be authoritatively refreshed when the TTL expires,
102
    // the record MAY be used as though it is unexpired"
103
    valid_until: Instant,
104
}
105

            
106
/// An answer record.
107
#[derive(Clone, Debug, Constructor)]
108
pub(crate) struct AnswerRecord {
109
    /// The answer record.
110
    data: RecordData,
111
    /// The TTL, used to compute the valid_until of the corresponding [`DnsResponse`].
112
    ttl: u32,
113
}
114

            
115
/// Type-alias for the hickory record type, for convenience
116
type HickoryRecord = hickory_proto::rr::Record<hickory_proto::rr::RData>;
117

            
118
impl TryFrom<&HickoryRecord> for AnswerRecord {
119
    type Error = LookupError;
120

            
121
    fn try_from(rec: &HickoryRecord) -> Result<Self, Self::Error> {
122
        Ok(Self {
123
            data: RecordData::try_from(&rec.data)?,
124
            ttl: rec.ttl,
125
        })
126
    }
127
}
128

            
129
/// A mockable stub resolver.
130
///
131
/// Used for mocking the hickory resolver in the
132
/// [`DnsResolverReactor`] tests.
133
#[async_trait]
134
pub(crate) trait MockableAsyncResolver: Send + Sync + 'static {
135
    /// A random number generator
136
    type Rng: rand::Rng + rand::CryptoRng;
137

            
138
    /// Performs a dual-stack DNS lookup for the IP for the given hostname.
139
    async fn lookup_ip(&self, query: &str) -> Result<LookupAnswers<AnswerRecord>, LookupError>;
140

            
141
    /// Performs a lookup for the associated type.
142
    async fn reverse_lookup(&self, query: &str)
143
    -> Result<LookupAnswers<AnswerRecord>, LookupError>;
144

            
145
    /// Return a random number generator
146
    fn thread_rng(&self) -> Self::Rng;
147
}
148

            
149
// TODO(relay): do we want hickory_resolver::ConnectionProvider to be a supertrait of tor_rtcompat::Runtime?
150
// It would enable us to write some unit tests involving the hickory resolver,
151
// without having to mock the whole thing like we do today.
152
//
153
// For now, this seems more trouble than it's worth.
154

            
155
#[async_trait]
156
impl MockableAsyncResolver for HickoryResolver {
157
    type Rng = rand::rngs::ThreadRng;
158

            
159
    async fn lookup_ip(&self, query: &str) -> Result<LookupAnswers<AnswerRecord>, LookupError> {
160
        let lookup = self.lookup_ip(query).await?;
161

            
162
        extract_hickory_answers(lookup.as_lookup())
163
    }
164

            
165
    async fn reverse_lookup(
166
        &self,
167
        query: &str,
168
    ) -> Result<LookupAnswers<AnswerRecord>, LookupError> {
169
        let lookup = self.reverse_lookup(query).await?;
170

            
171
        extract_hickory_answers(&lookup)
172
    }
173

            
174
    fn thread_rng(&self) -> Self::Rng {
175
        rand::rng()
176
    }
177
}
178

            
179
/// Convert a hickory [`Lookup`] into a collection of [`AnswerRecord`]s.
180
///
181
/// Note: we need our own AnswerRecord type here, because the hickory Record type
182
/// is quite big (272 bytes) and includes various fields we don't actually use
183
/// (such as the domain name that was looked up and the DNS record class).
184
fn extract_hickory_answers(lookup: &Lookup) -> Result<LookupAnswers<AnswerRecord>, LookupError> {
185
    // Note on TTLs: you might think we could use the valid_until timestamp
186
    // from the Lookup object here. However, that wouldn't give us
187
    // the real TTL, because hickory-resolver always sets this to the maximum
188
    // allowed TTL of 1 day (see the Future impl for LookupIpFuture in hickory_resolver).
189
    lookup
190
        .answers()
191
        .iter()
192
        .map(AnswerRecord::try_from)
193
        .collect::<Result<_, LookupError>>()
194
}
195

            
196
/// A response cache for the [`DnsResolverReactor`].
197
///
198
/// Maps [`Query`]s to [`DnsResponse`]s. In other words, we cache whole responses,
199
/// which may contain multiple answer records, each with its own TTL.
200
/// The TTL of a *cache entry* is calculated using the TTLs of the answers it contains:
201
///
202
///   * if the lookup failed, we use [`TTL_FOR_ERRORS`]
203
///   * if the lookup succeeded, but the answer section is empty,
204
///     we use [`TTL_FOR_EMPTY_ANS`]
205
///   * if the lookup succeded, and we got some answers, we calculate the TTL of the response
206
///     by taking the maximum TTL of the answers it contains
207
///
208
/// Finally, we clip and fuzz the resulting TTL before using it to calculate the
209
/// `valid_until` expiration timestamp of the cache entry (see [`clip_dns_fuzzy_ttl`]).
210
#[derive(Default)]
211
struct DnsCache {
212
    /// Our DNS cache, keyed by the ASCII encoded address extracted
213
    /// from RESOLVE queries.
214
    //
215
    // TODO(relay): make this memquota-aware, and have memquota
216
    // evict entries using an LRU policy if we're under
217
    // memory pressure
218
    inner: HashMap<Query, DnsResponse>,
219
}
220

            
221
impl DnsCache {
222
    /// Return the cached response for `query` if it still fresh at `now`,
223
    /// or `None` otherwise.
224
    ///
225
    /// If the query response is cached, but is expired at `now`,
226
    /// this will remove it from the cache.
227
    fn get_or_remove_expired(&mut self, query: &Query, now: Instant) -> Option<DnsResponse> {
228
        match self.inner.entry(Arc::clone(query)) {
229
            Entry::Occupied(e) => {
230
                let entry = e.get();
231
                if entry.valid_until > now {
232
                    Some(entry.clone())
233
                } else {
234
                    // The entry is expired, we should remove it from the cache
235
                    let _ = e.remove();
236
                    None
237
                }
238
            }
239
            Entry::Vacant(_) => None,
240
        }
241
    }
242

            
243
    /// Insert the specified response into the cache.
244
    fn insert(&mut self, query: Query, res: DnsResponse) {
245
        self.inner.insert(query, res);
246
    }
247
}
248

            
249
/// A collection of in-flight DNS queries.
250
///
251
/// Used by the [`DnsResolverReactor`] to prevent launching multiple lookups for the same query.
252
struct PendingQueries {
253
    // TODO(relay): investigate if it would make sense to use the InternCache
254
    // from tor-basic-utils for the for the hostnames
255
    /// A list of streams that are waiting on responses to the DNS queries.
256
    /// Each of these has an entry in self.inflight.
257
    ///
258
    /// Keyed by the ASCII-encoded hostname from the RESOLVE, in canonical form.
259
    pending_response: HashMap<Query, oneshot_broadcast::Sender<DnsResponse>>,
260
    /// DNS queries we're currently waiting a response for.
261
    //
262
    // TODO(relay): this uses a tokio JoinSet, as opposed to a runtime-agnotic
263
    // tor-rtcompat thing. Eventually, we might want to eventually replace this
264
    // with an equivalent tor-rtcompat type, so that we can control the execution
265
    // of these tasks in the tests.
266
    inflight: JoinSet<(Query, DnsResponse)>,
267
}
268

            
269
impl PendingQueries {
270
    /// Returns a [`DnsResponseReceiver`] for the specified `query`
271
    /// if there is currently an in-flight lookup for it,
272
    /// or spawn a new lookup, returning the [`DnsResponseReceiver`] for awaiting the response.
273
    fn get_or_launch_lookup<M: MockableAsyncResolver>(
274
        &mut self,
275
        query: Query,
276
        resolver: &Arc<M>,
277
        now: Instant,
278
    ) -> DnsResponseReceiver {
279
        let should_spawn_lookup =
280
            if let Entry::Occupied(e) = self.pending_response.entry(Arc::clone(&query)) {
281
                // If we already have an inflight query for the same request,
282
                // we just return a new watcher for that particular query
283
                if let Ok(rx) = e.get().subscribe() {
284
                    return DnsResponseReceiver::from(rx);
285
                } else {
286
                    // All the other receivers that were waiting on this query were dropped!
287
                    // This can happen, for example, if all the other pending DnsResolver::resolve()
288
                    // futures for this `guery` are canceled, for whatever reason.
289
                    //
290
                    // In practice, however, this won't happen, because in arti-relay,
291
                    // each RESOLVE is handled in a separate task,
292
                    // but it's better to handle it gracefully, rather than by returning an error,
293
                    // because returning an error here will cause the DnsResolverReactor to shut down.
294

            
295
                    // The existing pending_response entry is unusable,
296
                    // because its broadcast channel has closed.
297
                    e.remove();
298

            
299
                    // We will insert a new pending_response entry below
300
                    false
301
                }
302
            } else {
303
                // If we *don't* already have an in-flight lookup,
304
                // we need to launch one.
305
                true
306
            };
307

            
308
        // The channel on which to broadcast the lookup result
309
        // to all the tasks waiting on it
310
        // (used for deduplicating queries)
311
        let (res_tx, res_rx) = oneshot_broadcast::channel();
312

            
313
        let previous = self.pending_response.insert(Arc::clone(&query), res_tx);
314

            
315
        // This should be impossible, because we remove any dangling channels above
316
        debug_assert!(previous.is_none());
317

            
318
        if should_spawn_lookup {
319
            let resolver = Arc::clone(resolver);
320

            
321
            // Time to launch a new lookup
322
            self.inflight.spawn(async move {
323
                let res = resolve_query(Arc::clone(&query), resolver, now).await;
324
                (query, res)
325
            });
326
        }
327

            
328
        DnsResponseReceiver::from(res_rx)
329
    }
330

            
331
    /// Remove the pending entry that was waiting for an answer to the specified `query`.
332
    fn remove(&mut self, query: &str) -> Result<oneshot_broadcast::Sender<DnsResponse>, Bug> {
333
        self.pending_response
334
            .remove(query)
335
            .ok_or_else(|| internal!("got response for a query we didn't ask for?!"))
336
    }
337
}
338

            
339
/// Try to resolve the specified `host`,
340
/// returning a `DnsResponse` to send back to the client.
341
///
342
/// The `host` must be a hostname.
343
/// If it ends in `in-addr.arpa.` or `ip6.arpa.`, this function will perform a reverse lookup.
344
async fn resolve_query<M: MockableAsyncResolver>(
345
    host: Query,
346
    resolver: Arc<M>,
347
    now: Instant,
348
) -> DnsResponse {
349
    // Note the trailing dot (this expects canonicalize_hostname() to turn
350
    // the hostname into a FQDN)
351
    let lookup = if host.ends_with(".in-addr.arpa.") || host.ends_with(".ip6.arpa.") {
352
        resolver.reverse_lookup(&host)
353
    } else {
354
        resolver.lookup_ip(&host)
355
    };
356

            
357
    let (answers, ttl) = match lookup.await {
358
        Ok(res) => {
359
            let answers: SmallVec<_> = res;
360
            let ttl = if answers.is_empty() {
361
                TTL_FOR_EMPTY_ANS
362
            } else {
363
                // Use the max TTL of all the answers
364
                let ttl = answers
365
                    .iter()
366
                    .map(|ans: &AnswerRecord| ans.ttl)
367
                    .max()
368
                    .expect("answers cannot be empty");
369

            
370
                Duration::from_secs(ttl.into())
371
            };
372
            let answers = answers.into_iter().map(|res| res.data).collect();
373

            
374
            (Ok(answers), ttl)
375
        }
376
        Err(e) => (Err(e), TTL_FOR_ERRORS),
377
    };
378

            
379
    let valid_until = now + clip_dns_fuzzy_ttl(ttl, &mut resolver.thread_rng());
380
    DnsResponse::new(answers, valid_until)
381
}
382

            
383
/// Clip the `ttl` to be either 5 minutes or 60 minutes.
384
///
385
/// If `ttl` is below 5 minutes, returns a TTL of 5 minutes.
386
/// If `ttl` is 5 minutes or above, returns a TTL of 60 minutes.
387
///
388
/// Like in C Tor, this logic is a defense against [DefectTor]
389
/// DNS-based traffic confirmation attacks.
390
///
391
/// [DefectTor]: https://nymity.ch/tor-dns/tor-dns.pdf
392
fn clip_dns_ttl(ttl: Duration) -> Duration {
393
    if ttl < MIN_DNS_TTL {
394
        MIN_DNS_TTL
395
    } else {
396
        MAX_DNS_TTL
397
    }
398
}
399

            
400
/// Clip and randomize the specified `ttl`.
401
///
402
/// This clips the `ttl` (see [`clip_dns_ttl`]),
403
/// and adds a random duration (sampled uniformly from [-4 min, 4min]),
404
/// in order to make it harder to infer when the corresponding domain
405
/// was visited from this exit relay (see "Website Fingerprinting with Website Oracles").
406
fn clip_dns_fuzzy_ttl<R: rand::Rng + rand::CryptoRng>(ttl: Duration, rng: &mut R) -> Duration {
407
    use tor_basic_utils::RngExt;
408

            
409
    let fuzzy_ttl_seconds = FUZZY_DNS_TTL.as_secs() as i32;
410

            
411
    let fuzz: i32 = rng
412
        .gen_range_checked(-fuzzy_ttl_seconds..=fuzzy_ttl_seconds)
413
        .expect("low as not < high");
414

            
415
    let ttl_secs: i32 = clip_dns_ttl(ttl)
416
        .as_secs()
417
        .try_into()
418
        .expect("clip_dns_ttl returned a humonguous TTL?!");
419

            
420
    debug_assert!(ttl_secs > fuzz, "FUZZY_DNS_TTL > MIN_DNS_TTL?!");
421

            
422
    let fuzzed_ttl_secs: u64 = (ttl_secs + fuzz)
423
        .try_into()
424
        .expect("a TTL of minutes does not fit in a u64?!");
425

            
426
    Duration::from_secs(fuzzed_ttl_secs)
427
}
428

            
429
impl<M: MockableAsyncResolver> DnsResolverReactor<M> {
430
    /// Build a new reactor that uses the specified mockable resolver.
431
    ///
432
    /// Note: for per-circuit caches (option 3),
433
    /// there will be one DnsResolverReactor per circuit,
434
    /// all of them sharing an Arc::clone of the same underlying
435
    /// hickory Resolver
436
    ///
437
    // Question: should we try to make to make this generic over
438
    // the runtime? If so, we will need to add a (hickory) RuntimeProvider
439
    // trait bound to tor_rtcompat::Runtime.
440
    pub(crate) fn new(runtime: DynTimeProvider, resolver: Arc<M>) -> (Self, DnsResolver) {
441
        // TODO(relay-tuning): pick an appropriate buffer size here
442
        //
443
        // Alternatively, we might want to rework this so that this cannot becomes a bottleneck
444
        // (maybe it wants to be an unbounded channel? Or maybe we can somehow make it
445
        // memquota-aware?)
446
        const DNS_QUERY_BUF_SIZE: usize = 512;
447

            
448
        #[allow(clippy::disallowed_methods)]
449
        let (query_tx, query_rx) = mpsc::channel(DNS_QUERY_BUF_SIZE);
450

            
451
        let mut inflight = JoinSet::new();
452
        // The inflight list has an always-pending task to prevent it from
453
        // yielding None when empty
454
        inflight.spawn(std::future::pending());
455

            
456
        let pending = PendingQueries {
457
            pending_response: Default::default(),
458
            inflight,
459
        };
460

            
461
        let reactor = Self {
462
            resolver,
463
            runtime,
464
            pending,
465
            cache: Default::default(),
466
            query_rx,
467
        };
468

            
469
        let handle = DnsResolver::new(query_tx);
470

            
471
        (reactor, handle)
472
    }
473

            
474
    /// Run the reactor.
475
    pub(crate) async fn run(mut self) -> Result<(), Bug> {
476
        loop {
477
            select_biased! {
478
                res = self.pending.inflight.join_next().fuse() => {
479
                    // inflight can never yield None because
480
                    // we pushed a forever pending future in it
481
                    let res = res.expect("pending future resolvewd?!");
482

            
483
                    // This can only happen if a task panics, or is aborted.
484
                    // We never abort tasks, so this can only happen on panic.
485
                    let (query, response) = res.map_err(into_internal!("A DNS lookup panicked?!"))?;
486

            
487
                    self.handle_response(query, response)?;
488
                },
489
                query = self.query_rx.next() => {
490
                    let Some(query) = query else {
491
                        // All DnsResolvers were dropped
492
                        return Ok(());
493
                    };
494
                    self.handle_request(query);
495
                }
496

            
497
                // TODO: periodically garbage-collect the expired cache entries?
498
                // get_or_remove_expired() helps a little bit, but
499
                // we will still accumulate expired entries unless
500
                // we remove them periodically
501
                //
502
                // Alternatively, we can make DnsCache a fixed-sized LRU cache,
503
                // and let the LRU policy take care of any stale records
504
            }
505
        }
506
    }
507

            
508
    /// Handles new queries coming from DnsResolver::resolve()
509
    fn handle_request(&mut self, req: DnsRequest) {
510
        let DnsRequest { query, tx } = req;
511

            
512
        let query = canonicalize_hostname(&query).into();
513
        let now = self.runtime.now();
514

            
515
        let response_rx = if let Some(res) = self.cache.get_or_remove_expired(&query, now) {
516
            // First, if the query is already cached,
517
            // we can respond immediately
518
            DnsResponseReceiver::from(res)
519
        } else {
520
            // If we don't have a cached answer,
521
            // we need to launch a new lookup,
522
            // unless we already have a pending one for this query
523
            self.pending
524
                .get_or_launch_lookup(query, &self.resolver, now)
525
        };
526

            
527
        let _ = tx.send(response_rx);
528
    }
529

            
530
    /// Called when one of our pending queries completes.
531
    fn handle_response(&mut self, query: Query, res: DnsResponse) -> Result<(), Bug> {
532
        let response_tx = self.pending.remove(&query)?;
533

            
534
        // Update the cache
535
        self.cache.insert(query, res.clone());
536

            
537
        // Notify all the tasks waiting on this response.
538
        // If all the receivers have gone away, this will be ignored
539
        // (and the lookup response will be dropped).
540
        response_tx.send(res);
541

            
542
        Ok(())
543
    }
544
}
545

            
546
/// Return the hostname in canonical form.
547
///
548
/// Converts `hostname` to lowercase, and trims any leading or trailing whitespace.
549
/// The returned hostname is in FQDN form (it ends with '.').
550
///
551
/// This function assumes that `hostname` is an ASCII string.
552
fn canonicalize_hostname(hostname: &str) -> String {
553
    let mut hostname = hostname.trim().to_lowercase();
554
    if !hostname.ends_with('.') {
555
        hostname.push('.');
556
    }
557

            
558
    hostname
559
}
560

            
561
// TODO(relay): tests!