1use 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
12pub(crate) trait Universe {
15 fn contains<T: ChanTarget>(&self, guard: &T) -> Option<bool>;
21
22 fn status<T: ChanTarget>(&self, guard: &T) -> CandidateStatus<Candidate>;
24
25 fn timestamp(&self) -> SystemTime;
31
32 fn weight_threshold<T>(&self, sample: &ByRelayIds<T>, params: &GuardParams) -> WeightThreshold
35 where
36 T: HasRelayIds;
37
38 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#[derive(Clone, Debug)]
55pub(crate) enum CandidateStatus<T> {
56 Present(T),
58 Absent,
60 Uncertain,
63}
64
65#[derive(Clone, Debug)]
67pub(crate) struct Candidate {
68 pub(crate) listed_as_guard: bool,
73 pub(crate) is_dir_cache: bool,
75 pub(crate) full_dir_info: bool,
77 pub(crate) owned_target: OwnedChanTarget,
80 pub(crate) sensitivity: crate::guard::DisplayRule,
82}
83
84#[derive(Debug, Clone)]
89pub(crate) struct WeightThreshold {
90 pub(crate) current_weight: RelayWeight,
92 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 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 let maximum_weight = {
135 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 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 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 weight(self, relay).unwrap_or_else(|| RelayWeight::from(0)),
202 )
203 })
204 .collect()
205 }
206}
207
208#[derive(Clone, Debug)]
212pub(crate) enum UniverseRef {
213 NetDir(Arc<NetDir>),
215 #[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}