1
//! Declare the [`FallbackState`] type, which is used to store a set of FallbackDir.
2

            
3
use crate::skew::SkewObservation;
4
use rand::seq::IteratorRandom;
5
use std::time::{Duration, Instant};
6
use tor_linkspec::HasRelayIds;
7

            
8
use super::{DirStatus, FallbackDir, FallbackDirBuilder};
9
use crate::fallback::default_fallbacks;
10
use crate::{ids::FallbackId, PickGuardError};
11
use tor_basic_utils::iter::{FilterCount, IteratorExt as _};
12
use tor_config::define_list_builder_helper;
13

            
14
/// A list of fallback directories.
15
///
16
/// Fallback directories (represented by [`FallbackDir`]) are used by Tor
17
/// clients when they don't already have enough other directory information to
18
/// contact the network.
19
#[derive(Debug, Clone, Default, PartialEq, Eq)]
20
pub struct FallbackList {
21
    /// The underlying fallbacks in this set.
22
    fallbacks: Vec<FallbackDir>,
23
}
24

            
25
impl<T: IntoIterator<Item = FallbackDir>> From<T> for FallbackList {
26
16
    fn from(fallbacks: T) -> Self {
27
16
        FallbackList {
28
16
            fallbacks: fallbacks.into_iter().collect(),
29
16
        }
30
16
    }
31
}
32

            
33
define_list_builder_helper! {
34
    // pub because tor-dirmgr needs it for NetworkConfig.fallback_caches
35
    pub struct FallbackListBuilder {
36
        pub(crate) fallbacks: [FallbackDirBuilder],
37
    }
38
    built: FallbackList = FallbackList { fallbacks };
39
    default = default_fallbacks();
40
}
41

            
42
impl FallbackList {
43
    /// Return the number of fallbacks in this list.
44
142
    pub fn len(&self) -> usize {
45
142
        self.fallbacks.len()
46
142
    }
47
    /// Return true if there are no fallbacks in this list.
48
2
    pub fn is_empty(&self) -> bool {
49
2
        self.fallbacks.is_empty()
50
2
    }
51
    /// Return a random member of this list.
52
    pub fn choose<R: rand::Rng>(&self, rng: &mut R) -> Result<&FallbackDir, PickGuardError> {
53
        self.fallbacks
54
            .iter()
55
            .choose(rng)
56
            .ok_or(PickGuardError::NoCandidatesAvailable)
57
    }
58
}
59

            
60
/// A set of fallback directories, in usable form.
61
#[derive(Debug, Clone)]
62
pub(crate) struct FallbackState {
63
    /// The list of fallbacks in the set.
64
    ///
65
    /// We require that these are sorted and unique by (ED,RSA) keys.
66
    fallbacks: Vec<Entry>,
67
}
68

            
69
/// Wrapper type for FallbackDir converted into crate::Guard, and the status
70
/// information that we store about it.
71
///
72
/// Defines a sort order to ensure that we can look up fallback directories by
73
/// binary search on keys.
74
#[derive(Debug, Clone)]
75
pub(super) struct Entry {
76
    /// The inner fallback directory.
77
    fallback: FallbackDir,
78

            
79
    /// Whether the directory is currently usable, and if not, when we can retry
80
    /// it.
81
    status: DirStatus,
82
    /// The latest clock skew observation we have from this fallback directory
83
    /// (if any).
84
    clock_skew: Option<SkewObservation>,
85
}
86

            
87
/// Least amount of time we'll wait before retrying a fallback cache.
88
//
89
// TODO: we may want to make this configurable to a smaller value for chutney networks.
90
const FALLBACK_RETRY_FLOOR: Duration = Duration::from_secs(150);
91

            
92
impl From<FallbackDir> for Entry {
93
28056
    fn from(fallback: FallbackDir) -> Self {
94
28056
        let status = DirStatus::new(FALLBACK_RETRY_FLOOR);
95
28056
        Entry {
96
28056
            fallback,
97
28056
            status,
98
28056
            clock_skew: None,
99
28056
        }
100
28056
    }
101
}
102

            
103
impl HasRelayIds for Entry {
104
504848
    fn identity(
105
504848
        &self,
106
504848
        key_type: tor_linkspec::RelayIdType,
107
504848
    ) -> Option<tor_linkspec::RelayIdRef<'_>> {
108
504848
        self.fallback.identity(key_type)
109
504848
    }
110
}
111

            
112
impl From<&FallbackList> for FallbackState {
113
10552
    fn from(list: &FallbackList) -> Self {
114
28382
        let mut fallbacks: Vec<Entry> = list.fallbacks.iter().map(|fb| fb.clone().into()).collect();
115
224692
        fallbacks.sort_by(|x, y| x.cmp_by_relay_ids(y));
116
28235
        fallbacks.dedup_by(|x, y| x.same_relay_ids(y));
117
10552
        FallbackState { fallbacks }
118
10552
    }
119
}
120

            
121
impl FallbackState {
122
    /// Return a random member of this FallbackSet that's usable at `now`.
123
404
    pub(crate) fn choose<R: rand::Rng>(
124
404
        &self,
125
404
        rng: &mut R,
126
404
        now: Instant,
127
404
        filter: &crate::GuardFilter,
128
404
    ) -> Result<&FallbackDir, PickGuardError> {
129
404
        if self.fallbacks.is_empty() {
130
2
            return Err(PickGuardError::NoCandidatesAvailable);
131
402
        }
132
402

            
133
402
        let mut running = FilterCount::default();
134
402
        let mut filtered = FilterCount::default();
135
402

            
136
402
        self.fallbacks
137
402
            .iter()
138
1608
            .filter_cnt(&mut running, |ent| ent.status.usable_at(now))
139
1400
            .filter_cnt(&mut filtered, |ent| filter.permits(&ent.fallback))
140
402
            .choose(rng)
141
402
            .map(|ent| &ent.fallback)
142
402
            .ok_or_else(|| PickGuardError::AllFallbacksDown {
143
2
                retry_at: self.next_retry(),
144
2
                running,
145
2
                filtered,
146
402
            })
147
404
    }
148

            
149
    /// Return the next time at which any member of this set will become ready.
150
    ///
151
    /// Returns None if no elements are failing.
152
8
    fn next_retry(&self) -> Option<Instant> {
153
8
        self.fallbacks
154
8
            .iter()
155
36
            .filter_map(|ent| ent.status.next_retriable())
156
8
            .min()
157
8
    }
158

            
159
    /// Return a reference to the entry whose identity is `id`, if there is one.
160
16
    fn get(&self, id: &FallbackId) -> Option<&Entry> {
161
16
        match self.fallbacks.binary_search_by(|e| e.cmp_by_relay_ids(id)) {
162
            Ok(idx) => Some(&self.fallbacks[idx]),
163
16
            Err(_) => None,
164
        }
165
16
    }
166

            
167
    /// Return a mutable reference to the entry whose identity is `id`, if there is one.
168
40
    fn get_mut(&mut self, id: &FallbackId) -> Option<&mut Entry> {
169
154
        match self.fallbacks.binary_search_by(|e| e.cmp_by_relay_ids(id)) {
170
36
            Ok(idx) => Some(&mut self.fallbacks[idx]),
171
4
            Err(_) => None,
172
        }
173
40
    }
174

            
175
    /// Return true if this set contains some entry with the given `id`.
176
16
    pub(crate) fn contains(&self, id: &FallbackId) -> bool {
177
16
        self.get(id).is_some()
178
16
    }
179

            
180
    /// Record that a success has occurred for the fallback with the given
181
    /// identity.
182
    ///
183
    /// Be aware that for fallbacks, we only count a successful directory
184
    /// operation as a success: a circuit success is not enough.
185
2
    pub(crate) fn note_success(&mut self, id: &FallbackId) {
186
2
        if let Some(entry) = self.get_mut(id) {
187
2
            entry.status.note_success();
188
2
        }
189
2
    }
190

            
191
    /// Record that a failure has occurred for the fallback with the given
192
    /// identity.
193
14
    pub(crate) fn note_failure(&mut self, id: &FallbackId, now: Instant) {
194
14
        if let Some(entry) = self.get_mut(id) {
195
14
            entry.status.note_failure(now);
196
14
        }
197
14
    }
198

            
199
    /// Consume `other` and copy all of its fallback status entries into the corresponding entries for `self`.
200
37
    pub(crate) fn take_status_from(&mut self, other: FallbackState) {
201
        use itertools::EitherOrBoth::Both;
202

            
203
7016
        itertools::merge_join_by(self.fallbacks.iter_mut(), other.fallbacks, |a, b| {
204
7014
            a.fallback.cmp_by_relay_ids(&b.fallback)
205
7016
        })
206
7016
        .for_each(|entry| {
207
7014
            if let Both(entry, other) = entry {
208
7006
                debug_assert!(entry.fallback.same_relay_ids(&other.fallback));
209
7006
                entry.status = other.status;
210
8
            }
211
7016
        });
212
37
    }
213

            
214
    /// Record that a given fallback has told us about clock skew.
215
    pub(crate) fn note_skew(&mut self, id: &FallbackId, observation: SkewObservation) {
216
        if let Some(entry) = self.get_mut(id) {
217
            entry.clock_skew = Some(observation);
218
        }
219
    }
220

            
221
    /// Return an iterator over all the clock skew observations we've made for fallback directories
222
    pub(crate) fn skew_observations(&self) -> impl Iterator<Item = &SkewObservation> {
223
        self.fallbacks
224
            .iter()
225
            .filter_map(|fb| fb.clock_skew.as_ref())
226
    }
227
}
228

            
229
#[cfg(test)]
230
mod test {
231
    // @@ begin test lint list maintained by maint/add_warning @@
232
    #![allow(clippy::bool_assert_comparison)]
233
    #![allow(clippy::clone_on_copy)]
234
    #![allow(clippy::dbg_macro)]
235
    #![allow(clippy::mixed_attributes_style)]
236
    #![allow(clippy::print_stderr)]
237
    #![allow(clippy::print_stdout)]
238
    #![allow(clippy::single_char_pattern)]
239
    #![allow(clippy::unwrap_used)]
240
    #![allow(clippy::unchecked_duration_subtraction)]
241
    #![allow(clippy::useless_vec)]
242
    #![allow(clippy::needless_pass_by_value)]
243
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
244

            
245
    use super::*;
246
    use rand::Rng;
247
    use tor_basic_utils::test_rng::testing_rng;
248

            
249
    /// Construct a `FallbackDir` with random identity keys and addresses.
250
    ///
251
    /// Since there are 416 bits of random id here, the risk of collision is
252
    /// negligible.
253
    fn rand_fb<R: Rng>(rng: &mut R) -> FallbackDir {
254
        let ed: [u8; 32] = rng.gen();
255
        let rsa: [u8; 20] = rng.gen();
256
        let ip: u32 = rng.gen();
257
        let mut bld = FallbackDir::builder();
258
        bld.ed_identity(ed.into())
259
            .rsa_identity(rsa.into())
260
            .orports()
261
            .push(std::net::SocketAddrV4::new(ip.into(), 9090).into());
262
        bld.build().unwrap()
263
    }
264

            
265
    #[test]
266
    fn construct_fallback_set() {
267
        use rand::seq::SliceRandom;
268
        use std::cmp::Ordering as O;
269

            
270
        // fabricate some fallbacks.
271
        let mut rng = testing_rng();
272
        let fbs = vec![
273
            rand_fb(&mut rng),
274
            rand_fb(&mut rng),
275
            rand_fb(&mut rng),
276
            rand_fb(&mut rng),
277
        ];
278
        let fb_other = rand_fb(&mut rng);
279
        let id_other = FallbackId::from_relay_ids(&fb_other);
280

            
281
        // basic case: construct a set
282
        let list: FallbackList = fbs.clone().into();
283
        assert!(!list.is_empty());
284
        assert_eq!(list.len(), 4);
285
        let mut set: FallbackState = (&list).into();
286

            
287
        // inspect the generated set
288
        assert_eq!(set.fallbacks.len(), 4);
289
        assert_eq!(
290
            set.fallbacks[0].cmp_by_relay_ids(&set.fallbacks[1]),
291
            O::Less
292
        );
293
        assert_eq!(
294
            set.fallbacks[1].cmp_by_relay_ids(&set.fallbacks[2]),
295
            O::Less
296
        );
297
        assert_eq!(
298
            set.fallbacks[2].cmp_by_relay_ids(&set.fallbacks[3]),
299
            O::Less
300
        );
301

            
302
        // use the constructed set a little.
303
        for fb in fbs.iter() {
304
            let id = FallbackId::from_relay_ids(fb);
305
            assert_eq!(set.get_mut(&id).unwrap().cmp_by_relay_ids(&id), O::Equal);
306
        }
307
        assert!(set.get_mut(&id_other).is_none());
308

            
309
        // Now try an input set with duplicates.
310
        let mut redundant_fbs = fbs.clone();
311
        redundant_fbs.extend(fbs.clone());
312
        redundant_fbs.extend(fbs[0..2].iter().map(Clone::clone));
313
        redundant_fbs[..].shuffle(&mut testing_rng());
314
        let list2 = redundant_fbs.into();
315
        assert_ne!(&list, &list2);
316
        let set2: FallbackState = (&list2).into();
317

            
318
        // It should have the same elements, in the same order.
319
        assert_eq!(set.fallbacks.len(), set2.fallbacks.len());
320
        assert!(set
321
            .fallbacks
322
            .iter()
323
            .zip(set2.fallbacks.iter())
324
            .all(|(ent1, ent2)| ent1.same_relay_ids(ent2)));
325
    }
326

            
327
    #[test]
328
    fn set_choose() {
329
        dbg!("X");
330

            
331
        let mut rng = testing_rng();
332
        let fbs = vec![
333
            rand_fb(&mut rng),
334
            rand_fb(&mut rng),
335
            rand_fb(&mut rng),
336
            rand_fb(&mut rng),
337
        ];
338
        let list: FallbackList = fbs.into();
339
        let mut set: FallbackState = (&list).into();
340
        let filter = crate::GuardFilter::unfiltered();
341

            
342
        let mut counts = [0_usize; 4];
343
        let now = Instant::now();
344
        dbg!("A");
345
        fn lookup_idx(set: &FallbackState, id: &impl HasRelayIds) -> Option<usize> {
346
            set.fallbacks
347
                .binary_search_by(|ent| ent.fallback.cmp_by_relay_ids(id))
348
                .ok()
349
        }
350
        // Basic case: everybody is up.
351
        for _ in 0..100 {
352
            let fb = set.choose(&mut rng, now, &filter).unwrap();
353
            let idx = lookup_idx(&set, fb).unwrap();
354
            counts[idx] += 1;
355
        }
356
        dbg!("B");
357
        assert!(counts.iter().all(|v| *v > 0));
358

            
359
        // Mark somebody down and make sure they don't get chosen.
360
        let ids: Vec<_> = set
361
            .fallbacks
362
            .iter()
363
            .map(|ent| FallbackId::from_relay_ids(&ent.fallback))
364
            .collect();
365
        set.note_failure(&ids[2], now);
366
        counts = [0; 4];
367
        for _ in 0..100 {
368
            let fb = set.choose(&mut rng, now, &filter).unwrap();
369
            let idx = lookup_idx(&set, fb).unwrap();
370
            counts[idx] += 1;
371
        }
372
        assert_eq!(counts.iter().filter(|v| **v > 0).count(), 3);
373
        assert_eq!(counts[2], 0);
374

            
375
        // Mark everybody down; make sure we get the right error.
376
        for id in ids.iter() {
377
            set.note_failure(id, now);
378
        }
379
        assert!(matches!(
380
            set.choose(&mut rng, now, &filter),
381
            Err(PickGuardError::AllFallbacksDown { .. })
382
        ));
383

            
384
        // Construct an empty set; make sure we get the right error.
385
        let empty_set = FallbackState::from(&FallbackList::from(vec![]));
386
        assert!(matches!(
387
            empty_set.choose(&mut rng, now, &filter),
388
            Err(PickGuardError::NoCandidatesAvailable)
389
        ));
390

            
391
        // TODO: test restrictions and filters once they're implemented.
392
    }
393

            
394
    #[test]
395
    fn test_status() {
396
        let mut rng = testing_rng();
397
        let fbs = vec![
398
            rand_fb(&mut rng),
399
            rand_fb(&mut rng),
400
            rand_fb(&mut rng),
401
            rand_fb(&mut rng),
402
        ];
403
        let list: FallbackList = fbs.clone().into();
404
        let mut set: FallbackState = (&list).into();
405
        let ids: Vec<_> = set
406
            .fallbacks
407
            .iter()
408
            .map(|ent| FallbackId::from_relay_ids(&ent.fallback))
409
            .collect();
410

            
411
        let now = Instant::now();
412

            
413
        // There's no "next retry time" when everybody's up.
414
        assert!(set.next_retry().is_none());
415

            
416
        // Mark somebody down; try accessors.
417
        set.note_failure(&ids[3], now);
418
        assert!(set.fallbacks[3].status.next_retriable().unwrap() > now);
419
        assert!(!set.fallbacks[3].status.usable_at(now));
420
        assert_eq!(set.next_retry(), set.fallbacks[3].status.next_retriable());
421

            
422
        // Mark somebody else down; try accessors.
423
        set.note_failure(&ids[0], now);
424
        assert!(set.fallbacks[0].status.next_retriable().unwrap() > now);
425
        assert!(!set.fallbacks[0].status.usable_at(now));
426
        assert_eq!(
427
            set.next_retry().unwrap(),
428
            std::cmp::min(
429
                set.fallbacks[0].status.next_retriable().unwrap(),
430
                set.fallbacks[3].status.next_retriable().unwrap()
431
            )
432
        );
433

            
434
        // Mark somebody as running; try accessors.
435
        set.note_success(&ids[0]);
436
        assert!(set.fallbacks[0].status.next_retriable().is_none());
437
        assert!(set.fallbacks[0].status.usable_at(now));
438

            
439
        // Make a new set with slightly different members; make sure that we can copy stuff successfully.
440
        let mut fbs2: Vec<_> = fbs
441
            .into_iter()
442
            // (Remove the fallback with id==ids[2])
443
            .filter(|fb| FallbackId::from_relay_ids(fb) != ids[2])
444
            .collect();
445
        // add 2 new ones.
446
        let fbs_new = vec![rand_fb(&mut rng), rand_fb(&mut rng), rand_fb(&mut rng)];
447
        fbs2.extend(fbs_new.clone());
448

            
449
        let mut set2 = FallbackState::from(&FallbackList::from(fbs2.clone()));
450
        set2.take_status_from(set); // consumes set.
451
        assert_eq!(set2.fallbacks.len(), 6); // Started with 4, added 3, removed 1.
452

            
453
        // Make sure that the status entries  are correctly copied.
454
        assert!(set2.get_mut(&ids[0]).unwrap().status.usable_at(now));
455
        assert!(set2.get_mut(&ids[1]).unwrap().status.usable_at(now));
456
        assert!(set2.get_mut(&ids[2]).is_none());
457
        assert!(!set2.get_mut(&ids[3]).unwrap().status.usable_at(now));
458

            
459
        // Make sure that the new fbs are there.
460
        for new_fb in fbs_new {
461
            assert!(set2
462
                .get_mut(&FallbackId::from_relay_ids(&new_fb))
463
                .unwrap()
464
                .status
465
                .usable_at(now));
466
        }
467
    }
468
}