1
//! Module exposing a [`DnsResolver`] and [`DnsResolverReactor`].
2

            
3
mod err;
4
mod reactor;
5

            
6
use std::net::IpAddr;
7

            
8
use either::Either;
9
use futures::SinkExt as _;
10
use futures::channel::mpsc;
11
use hickory_proto::rr::RData as HickoryRecordData;
12
use smallvec::SmallVec;
13

            
14
use oneshot_fused_workaround as oneshot;
15
use tor_async_utils::oneshot_broadcast;
16
use tor_cell::relaycell::msg::ResolvedVal;
17
use tor_error::{Bug, internal, into_internal};
18

            
19
pub(crate) use err::LookupError;
20
pub(crate) use reactor::{DnsResolverReactor, DnsResponse};
21

            
22
/// Type-alias for the DNS answer list.
23
pub(crate) type LookupAnswers<T> = SmallVec<[T; 10]>;
24

            
25
/// The record data types currently supported by RESOLVE,
26
/// for easy conversion to ResolvedVal.
27
///
28
/// Note: we need our own record type here, because the hickory Record type
29
/// is quite big (272 bytes) and includes various fields we don't actually use
30
/// (such as the domain name that was looked up and the DNS record class).
31
#[derive(Clone, Debug)]
32
pub(crate) enum RecordData {
33
    /// An A or AAAA record.
34
    Ip(IpAddr),
35
    /// A PTR record
36
    Hostname(String),
37
}
38

            
39
impl TryFrom<&HickoryRecordData> for RecordData {
40
    type Error = LookupError;
41

            
42
    fn try_from(record: &HickoryRecordData) -> Result<Self, Self::Error> {
43
        let rec = match record {
44
            HickoryRecordData::A(a) => RecordData::Ip(a.0.into()),
45
            HickoryRecordData::AAAA(aaaa) => RecordData::Ip(aaaa.0.into()),
46
            HickoryRecordData::PTR(hostname) => RecordData::Hostname(hostname.to_ascii()),
47
            _ => return Err(internal!("unexpected answer record {record:?}").into()),
48
        };
49

            
50
        Ok(rec)
51
    }
52
}
53

            
54
impl From<RecordData> for ResolvedVal {
55
    fn from(record: RecordData) -> ResolvedVal {
56
        match record {
57
            RecordData::Ip(addr) => ResolvedVal::Ip(addr),
58
            RecordData::Hostname(addr) => ResolvedVal::Hostname(addr.into()),
59
        }
60
    }
61
}
62

            
63
/// An incoming DNS query and the channel where to send back the response receiver.
64
struct DnsRequest {
65
    /// The ASCII-encoded hostname to lookup
66
    query: String,
67
    /// The channel where to send the response receiver.
68
    tx: oneshot::Sender<DnsResponseReceiver>,
69
}
70

            
71
/// The response is either from cache (Left),
72
/// or from a lookup that needs to be .awaited (Right).
73
struct DnsResponseReceiver(Either<DnsResponse, oneshot_broadcast::Receiver<DnsResponse>>);
74

            
75
impl From<DnsResponse> for DnsResponseReceiver {
76
    fn from(res: DnsResponse) -> Self {
77
        Self(Either::Left(res))
78
    }
79
}
80

            
81
impl From<oneshot_broadcast::Receiver<DnsResponse>> for DnsResponseReceiver {
82
    fn from(res: oneshot_broadcast::Receiver<DnsResponse>) -> Self {
83
        Self(Either::Right(res))
84
    }
85
}
86

            
87
impl DnsResponseReceiver {
88
    /// Wait for the [`DnsResponse`]
89
    async fn recv(self) -> Result<DnsResponse, Bug> {
90
        match self.0 {
91
            Either::Left(res) => {
92
                // TODO(relay): TIMELESS-SIDE-CHANNEL-MITIGATION:
93
                //
94
                // Insert a randomized delay here to create FP for attackers
95
                // probing for cached domains.
96
                //
97
                // This delay will need to be based on past DNS lookup timings,
98
                // so as not to make the cached responses stand out too much.
99
                // But we don't want to make this delay too high,
100
                // because we'd lose out on any UX/perf improvement that
101
                // we were hoping to get out of caching.
102
                //
103
                // Question: how to pick an appropriate delay here?
104
                // One possibility would be to sample non-uniformly
105
                // from [0, rolling_avg_lookup_time].
106
                Ok(res)
107
            }
108
            Either::Right(watch_rx) => watch_rx
109
                .await
110
                .map_err(into_internal!("DNS reactor task exited?!")),
111
        }
112
    }
113
}
114

            
115
/// A handle to the [`DnsResolverReactor`].
116
#[derive(Clone)]
117
pub(crate) struct DnsResolver {
118
    /// Sender for sending DNS queries to the reactor
119
    ///
120
    /// The reactor sends back each response over a DnsResponseReceiver.
121
    query_tx: mpsc::Sender<DnsRequest>,
122
}
123

            
124
impl DnsResolver {
125
    /// Create a new [`DnsResolver`] for use with the `query_tx` reactor MPSC channel.
126
    fn new(query_tx: mpsc::Sender<DnsRequest>) -> Self {
127
        Self { query_tx }
128
    }
129

            
130
    /// Asynchronously resolve the specified query
131
    pub(crate) async fn resolve(
132
        &mut self,
133
        query: &[u8],
134
    ) -> Result<LookupAnswers<RecordData>, LookupError> {
135
        let (tx, rx) = oneshot::channel();
136

            
137
        let query = String::from_utf8(query.to_vec()).map_err(|_| LookupError::NonAsciiHostname)?;
138

            
139
        if !query.is_ascii() {
140
            return Err(LookupError::NonAsciiHostname);
141
        }
142

            
143
        let req = DnsRequest { query, tx };
144
        // Send the query to the reactor,
145
        // which handles the actual DNS resolution,
146
        // and then wait for it to respond
147
        self.query_tx
148
            .send(req)
149
            .await
150
            .map_err(into_internal!("DNS resolver reactor shut down?!"))?;
151

            
152
        let response_rx = rx
153
            .await
154
            .map_err(into_internal!("DNS resolver reactor shut down?!"))?;
155

            
156
        response_rx.recv().await?.answers
157
    }
158
}