1
//! This module defines and implements traits used to create a guard sample from
2
//! either bridges or relays.
3

            
4
use std::{sync::Arc, time::SystemTime};
5

            
6
use tor_linkspec::{ByRelayIds, ChanTarget, HasRelayIds, OwnedChanTarget};
7
use tor_netdir::{NetDir, Relay, RelayWeight};
8
use tor_relay_selection::{RelayExclusion, RelaySelector, RelayUsage};
9

            
10
use crate::{GuardFilter, GuardParams};
11

            
12
/// A "Universe" is a source from which guard candidates are drawn, and from
13
/// which guards are updated.
14
pub(crate) trait Universe {
15
    /// Check whether this universe contains a candidate for the given guard.
16
    ///
17
    /// Return `Some(true)` if it definitely does; `Some(false)` if it
18
    /// definitely does not, and `None` if we cannot tell without downloading
19
    /// more information.
20
    fn contains<T: ChanTarget>(&self, guard: &T) -> Option<bool>;
21

            
22
    /// Return full information about a member of this universe for a given guard.
23
    fn status<T: ChanTarget>(&self, guard: &T) -> CandidateStatus<Candidate>;
24

            
25
    /// Return an (approximate) timestamp describing when this universe was
26
    /// generated.
27
    ///
28
    /// This timestamp is used to determine how long a guard has been listed or
29
    /// unlisted.
30
    fn timestamp(&self) -> SystemTime;
31

            
32
    /// Return information about how much of this universe has been added to
33
    /// `sample`, and how much we're willing to add according to `params`.
34
    fn weight_threshold<T>(&self, sample: &ByRelayIds<T>, params: &GuardParams) -> WeightThreshold
35
    where
36
        T: HasRelayIds;
37

            
38
    /// Return up to `n` of new candidate guards from this Universe.
39
    ///
40
    /// Only return elements that have no conflicts with identities in
41
    /// `pre_existing`, and which obey `filter`.
42
    fn sample<T>(
43
        &self,
44
        pre_existing: &ByRelayIds<T>,
45
        filter: &GuardFilter,
46
        n: usize,
47
    ) -> Vec<(Candidate, RelayWeight)>
48
    where
49
        T: HasRelayIds;
50
}
51

            
52
/// Information about a single guard candidate, as returned by
53
/// [`Universe::status`].
54
#[derive(Clone, Debug)]
55
pub(crate) enum CandidateStatus<T> {
56
    /// The candidate is definitely present in some form.
57
    Present(T),
58
    /// The candidate is definitely not in the [`Universe`].
59
    Absent,
60
    /// We would need to download more directory information to be sure whether
61
    /// this candidate is in the [`Universe`].
62
    Uncertain,
63
}
64

            
65
/// Information about a candidate that we have selected as a guard.
66
#[derive(Clone, Debug)]
67
pub(crate) struct Candidate {
68
    /// True if the candidate is not currently disabled for use as a guard.
69
    ///
70
    /// (To be enabled, it must be in the last directory, with the Fast,
71
    /// Stable, and Guard flags.)
72
    pub(crate) listed_as_guard: bool,
73
    /// True if the candidate can be used as a directory cache.
74
    pub(crate) is_dir_cache: bool,
75
    /// True if we have complete directory information about this candidate.
76
    pub(crate) full_dir_info: bool,
77
    /// Information about connecting to the candidate and using it to build
78
    /// a channel.
79
    pub(crate) owned_target: OwnedChanTarget,
80
    /// How should we display information about this candidate if we select it?
81
    pub(crate) sensitivity: crate::guard::DisplayRule,
82
}
83

            
84
/// Information about how much of the universe we are using in a guard sample,
85
/// and how much we are allowed to use.
86
///
87
/// We use this to avoid adding the whole network to our guard sample.
88
#[derive(Debug, Clone)]
89
pub(crate) struct WeightThreshold {
90
    /// The amount of the universe that we are using, in [`RelayWeight`].
91
    pub(crate) current_weight: RelayWeight,
92
    /// The greatest amount that we are willing to use, in [`RelayWeight`].
93
    ///
94
    /// We can violate this maximum if it's necessary in order to meet our
95
    /// minimum number of guards; otherwise, were're willing to add a _single_
96
    /// guard that exceeds this threshold, but no more.
97
    pub(crate) maximum_weight: RelayWeight,
98
}
99

            
100
impl Universe for NetDir {
101
4
    fn timestamp(&self) -> SystemTime {
102
4
        NetDir::lifetime(self).valid_after()
103
4
    }
104

            
105
42
    fn contains<T: ChanTarget>(&self, guard: &T) -> Option<bool> {
106
42
        NetDir::ids_listed(self, guard)
107
42
    }
108

            
109
48
    fn status<T: ChanTarget>(&self, guard: &T) -> CandidateStatus<Candidate> {
110
48
        // TODO #504 - if we make a data extractor for Relays, we'll want
111
48
        // to use it here.
112
48
        match NetDir::by_ids(self, guard) {
113
42
            Some(relay) => CandidateStatus::Present(Candidate {
114
42
                listed_as_guard: relay.low_level_details().is_suitable_as_guard(),
115
42
                is_dir_cache: relay.low_level_details().is_dir_cache(),
116
42
                owned_target: OwnedChanTarget::from_chan_target(&relay),
117
42
                full_dir_info: true,
118
42
                sensitivity: crate::guard::DisplayRule::Sensitive,
119
42
            }),
120
6
            None => match NetDir::ids_listed(self, guard) {
121
                Some(true) => panic!("ids_listed said true, but by_ids said none!"),
122
4
                Some(false) => CandidateStatus::Absent,
123
2
                None => CandidateStatus::Uncertain,
124
            },
125
        }
126
48
    }
127

            
128
17813
    fn weight_threshold<T>(&self, sample: &ByRelayIds<T>, params: &GuardParams) -> WeightThreshold
129
17813
    where
130
17813
        T: HasRelayIds,
131
17813
    {
132
17813
        // When adding from a netdir, we impose a limit on the fraction of the
133
17813
        // universe we're willing to add.
134
17813
        let maximum_weight = {
135
17813
            // TODO #504 - to convert this, we need tor_relay_selector to apply
136
17813
            // to UncheckedRelay.
137
382031
            let total_weight = self.total_weight(tor_netdir::WeightRole::Guard, |r| {
138
381490
                let d = r.low_level_details();
139
381490
                d.is_suitable_as_guard() && d.is_dir_cache()
140
382031
            });
141
17813
            total_weight
142
17813
                .ratio(params.max_sample_bw_fraction)
143
17813
                .unwrap_or(total_weight)
144
17813
        };
145
17813

            
146
17813
        let current_weight: tor_netdir::RelayWeight = sample
147
17813
            .values()
148
82428
            .filter_map(|guard| {
149
81876
                self.weight_by_rsa_id(guard.rsa_identity()?, tor_netdir::WeightRole::Guard)
150
82428
            })
151
17813
            .sum();
152
17813

            
153
17813
        WeightThreshold {
154
17813
            current_weight,
155
17813
            maximum_weight,
156
17813
        }
157
17813
    }
158

            
159
17813
    fn sample<T>(
160
17813
        &self,
161
17813
        pre_existing: &ByRelayIds<T>,
162
17813
        filter: &GuardFilter,
163
17813
        n: usize,
164
17813
    ) -> Vec<(Candidate, RelayWeight)>
165
17813
    where
166
17813
        T: HasRelayIds,
167
17813
    {
168
        /// Return the weight for this relay, if we can find it.
169
        ///
170
        /// (We should always be able to find it as `NetDir`s are constructed
171
        /// today.)
172
88406
        fn weight(dir: &NetDir, relay: &Relay<'_>) -> Option<RelayWeight> {
173
88406
            dir.weight_by_rsa_id(relay.rsa_identity()?, tor_netdir::WeightRole::Guard)
174
88406
        }
175

            
176
17813
        let already_selected = pre_existing
177
17813
            .values()
178
82428
            .flat_map(|item| item.identities())
179
164293
            .map(|id| id.to_owned())
180
17813
            .collect();
181
17813
        let mut sel = RelaySelector::new(
182
17813
            RelayUsage::new_guard(),
183
17813
            RelayExclusion::exclude_identities(already_selected),
184
17813
        );
185
17813
        filter.add_to_selector(&mut sel);
186
17813

            
187
17813
        let (relays, _outcome) = sel.select_n_relays(&mut rand::thread_rng(), n, self);
188
17813
        // TODO: report _outcome somehow.
189
17813
        relays
190
17813
            .iter()
191
88947
            .map(|relay| {
192
88406
                (
193
88406
                    Candidate {
194
88406
                        listed_as_guard: true,
195
88406
                        is_dir_cache: true,
196
88406
                        full_dir_info: true,
197
88406
                        owned_target: OwnedChanTarget::from_chan_target(relay),
198
88406
                        sensitivity: crate::guard::DisplayRule::Sensitive,
199
88406
                    },
200
88406
                    // TODO: It would be better not to need this function.
201
88406
                    weight(self, relay).unwrap_or_else(|| RelayWeight::from(0)),
202
88406
                )
203
88947
            })
204
17813
            .collect()
205
17813
    }
206
}
207

            
208
/// Reference to a [`Universe`] of one of the types supported by this crate.
209
///
210
/// This enum exists because `Universe` is not dyn-compatible.
211
#[derive(Clone, Debug)]
212
pub(crate) enum UniverseRef {
213
    /// A reference to a netdir.
214
    NetDir(Arc<NetDir>),
215
    /// A BridgeSet (which is always references internally)
216
    #[cfg(feature = "bridge-client")]
217
    BridgeSet(crate::bridge::BridgeSet),
218
}
219

            
220
impl Universe for UniverseRef {
221
24
    fn contains<T: ChanTarget>(&self, guard: &T) -> Option<bool> {
222
24
        match self {
223
24
            UniverseRef::NetDir(r) => r.contains(guard),
224
            #[cfg(feature = "bridge-client")]
225
            UniverseRef::BridgeSet(r) => r.contains(guard),
226
        }
227
24
    }
228

            
229
40
    fn status<T: ChanTarget>(&self, guard: &T) -> CandidateStatus<Candidate> {
230
40
        match self {
231
40
            UniverseRef::NetDir(r) => r.status(guard),
232
            #[cfg(feature = "bridge-client")]
233
            UniverseRef::BridgeSet(r) => r.status(guard),
234
        }
235
40
    }
236

            
237
    fn timestamp(&self) -> SystemTime {
238
        match self {
239
            UniverseRef::NetDir(r) => r.timestamp(),
240
            #[cfg(feature = "bridge-client")]
241
            UniverseRef::BridgeSet(r) => r.timestamp(),
242
        }
243
    }
244

            
245
17785
    fn weight_threshold<T>(&self, sample: &ByRelayIds<T>, params: &GuardParams) -> WeightThreshold
246
17785
    where
247
17785
        T: HasRelayIds,
248
17785
    {
249
17785
        match self {
250
17785
            UniverseRef::NetDir(r) => r.weight_threshold(sample, params),
251
            #[cfg(feature = "bridge-client")]
252
            UniverseRef::BridgeSet(r) => r.weight_threshold(sample, params),
253
        }
254
17785
    }
255

            
256
17785
    fn sample<T>(
257
17785
        &self,
258
17785
        pre_existing: &ByRelayIds<T>,
259
17785
        filter: &GuardFilter,
260
17785
        n: usize,
261
17785
    ) -> Vec<(Candidate, RelayWeight)>
262
17785
    where
263
17785
        T: HasRelayIds,
264
17785
    {
265
17785
        match self {
266
17785
            UniverseRef::NetDir(r) => r.sample(pre_existing, filter, n),
267
            #[cfg(feature = "bridge-client")]
268
            UniverseRef::BridgeSet(r) => r.sample(pre_existing, filter, n),
269
        }
270
17785
    }
271
}