tor_guardmgr/sample/
candidate.rs

1//! This module defines and implements traits used to create a guard sample from
2//! either bridges or relays.
3
4use std::{sync::Arc, time::SystemTime};
5
6use tor_linkspec::{ByRelayIds, ChanTarget, HasRelayIds, OwnedChanTarget};
7use tor_netdir::{NetDir, Relay, RelayWeight};
8use tor_relay_selection::{RelayExclusion, RelaySelector, RelayUsage};
9
10use crate::{GuardFilter, GuardParams};
11
12/// A "Universe" is a source from which guard candidates are drawn, and from
13/// which guards are updated.
14pub(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)]
55pub(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)]
67pub(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)]
89pub(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
100impl Universe for NetDir {
101    fn timestamp(&self) -> SystemTime {
102        NetDir::lifetime(self).valid_after()
103    }
104
105    fn contains<T: ChanTarget>(&self, guard: &T) -> Option<bool> {
106        NetDir::ids_listed(self, guard)
107    }
108
109    fn status<T: ChanTarget>(&self, guard: &T) -> CandidateStatus<Candidate> {
110        // TODO #504 - if we make a data extractor for Relays, we'll want
111        // to use it here.
112        match NetDir::by_ids(self, guard) {
113            Some(relay) => CandidateStatus::Present(Candidate {
114                listed_as_guard: relay.low_level_details().is_suitable_as_guard(),
115                is_dir_cache: relay.low_level_details().is_dir_cache(),
116                owned_target: OwnedChanTarget::from_chan_target(&relay),
117                full_dir_info: true,
118                sensitivity: crate::guard::DisplayRule::Sensitive,
119            }),
120            None => match NetDir::ids_listed(self, guard) {
121                Some(true) => panic!("ids_listed said true, but by_ids said none!"),
122                Some(false) => CandidateStatus::Absent,
123                None => CandidateStatus::Uncertain,
124            },
125        }
126    }
127
128    fn weight_threshold<T>(&self, sample: &ByRelayIds<T>, params: &GuardParams) -> WeightThreshold
129    where
130        T: HasRelayIds,
131    {
132        // When adding from a netdir, we impose a limit on the fraction of the
133        // universe we're willing to add.
134        let maximum_weight = {
135            // TODO #504 - to convert this, we need tor_relay_selector to apply
136            // to UncheckedRelay.
137            let total_weight = self.total_weight(tor_netdir::WeightRole::Guard, |r| {
138                let d = r.low_level_details();
139                d.is_suitable_as_guard() && d.is_dir_cache()
140            });
141            total_weight
142                .ratio(params.max_sample_bw_fraction)
143                .unwrap_or(total_weight)
144        };
145
146        let current_weight: tor_netdir::RelayWeight = sample
147            .values()
148            .filter_map(|guard| {
149                self.weight_by_rsa_id(guard.rsa_identity()?, tor_netdir::WeightRole::Guard)
150            })
151            .sum();
152
153        WeightThreshold {
154            current_weight,
155            maximum_weight,
156        }
157    }
158
159    fn sample<T>(
160        &self,
161        pre_existing: &ByRelayIds<T>,
162        filter: &GuardFilter,
163        n: usize,
164    ) -> Vec<(Candidate, RelayWeight)>
165    where
166        T: HasRelayIds,
167    {
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        fn weight(dir: &NetDir, relay: &Relay<'_>) -> Option<RelayWeight> {
173            dir.weight_by_rsa_id(relay.rsa_identity()?, tor_netdir::WeightRole::Guard)
174        }
175
176        let already_selected = pre_existing
177            .values()
178            .flat_map(|item| item.identities())
179            .map(|id| id.to_owned())
180            .collect();
181        let mut sel = RelaySelector::new(
182            RelayUsage::new_guard(),
183            RelayExclusion::exclude_identities(already_selected),
184        );
185        filter.add_to_selector(&mut sel);
186
187        let (relays, _outcome) = sel.select_n_relays(&mut rand::rng(), n, self);
188        // TODO: report _outcome somehow.
189        relays
190            .iter()
191            .map(|relay| {
192                (
193                    Candidate {
194                        listed_as_guard: true,
195                        is_dir_cache: true,
196                        full_dir_info: true,
197                        owned_target: OwnedChanTarget::from_chan_target(relay),
198                        sensitivity: crate::guard::DisplayRule::Sensitive,
199                    },
200                    // TODO: It would be better not to need this function.
201                    weight(self, relay).unwrap_or_else(|| RelayWeight::from(0)),
202                )
203            })
204            .collect()
205    }
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)]
212pub(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
220impl Universe for UniverseRef {
221    fn contains<T: ChanTarget>(&self, guard: &T) -> Option<bool> {
222        match self {
223            UniverseRef::NetDir(r) => r.contains(guard),
224            #[cfg(feature = "bridge-client")]
225            UniverseRef::BridgeSet(r) => r.contains(guard),
226        }
227    }
228
229    fn status<T: ChanTarget>(&self, guard: &T) -> CandidateStatus<Candidate> {
230        match self {
231            UniverseRef::NetDir(r) => r.status(guard),
232            #[cfg(feature = "bridge-client")]
233            UniverseRef::BridgeSet(r) => r.status(guard),
234        }
235    }
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    fn weight_threshold<T>(&self, sample: &ByRelayIds<T>, params: &GuardParams) -> WeightThreshold
246    where
247        T: HasRelayIds,
248    {
249        match self {
250            UniverseRef::NetDir(r) => r.weight_threshold(sample, params),
251            #[cfg(feature = "bridge-client")]
252            UniverseRef::BridgeSet(r) => r.weight_threshold(sample, params),
253        }
254    }
255
256    fn sample<T>(
257        &self,
258        pre_existing: &ByRelayIds<T>,
259        filter: &GuardFilter,
260        n: usize,
261    ) -> Vec<(Candidate, RelayWeight)>
262    where
263        T: HasRelayIds,
264    {
265        match self {
266            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    }
271}