1
//! Tools and types for reporting declared clock skew.
2

            
3
use std::time::{Duration, SystemTime};
4

            
5
/// A reported amount of clock skew from a relay or other source.
6
///
7
/// Note that this information may not be accurate or trustworthy: the relay
8
/// could be wrong, or lying.
9
///
10
/// The skews reported here are _minimum_ amounts; the actual skew may
11
/// be a little higher, depending on latency.
12
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
13
#[allow(clippy::exhaustive_enums)]
14
pub enum ClockSkew {
15
    /// Our own clock is "running slow": the relay's clock is at least this far
16
    /// ahead of ours.
17
    Slow(Duration),
18
    /// Our own clock is not necessarily inconsistent with the relay's clock.
19
    None,
20
    /// Own own clock is "running fast": the relay's clock is at least this far
21
    /// behind ours.
22
    Fast(Duration),
23
}
24

            
25
/// We treat clock skew as "zero" if it less than this long.
26
///
27
/// (Since the relay only reports its time to the nearest second, we
28
/// can't reasonably infer that differences less than this much reflect
29
/// accurate differences in our clocks.)
30
const MIN: Duration = Duration::from_secs(2);
31

            
32
impl ClockSkew {
33
    /// Construct a ClockSkew from a set of channel handshake timestamps.
34
    ///
35
    /// Requires that `ours_at_start` is the timestamp at the point when we
36
    /// started the handshake, `theirs` is the timestamp the relay reported in
37
    /// its NETINFO cell, and `delay` is the total amount of time between when
38
    /// we started the handshake and when we received the NETINFO cell.
39
58
    pub(crate) fn from_handshake_timestamps(
40
58
        ours_at_start: SystemTime,
41
58
        theirs: SystemTime,
42
58
        delay: Duration,
43
58
    ) -> Self {
44
        // The relay may have generated its own timestamp any time between when
45
        // we sent the handshake, and when we got the reply.  Therefore, at the
46
        // time we started, it was between these values.
47
58
        let theirs_at_start_min = theirs - delay;
48
58
        let theirs_at_start_max = theirs;
49

            
50
58
        if let Ok(skew) = theirs_at_start_min.duration_since(ours_at_start) {
51
3
            ClockSkew::Slow(skew).if_above(MIN)
52
55
        } else if let Ok(skew) = ours_at_start.duration_since(theirs_at_start_max) {
53
54
            ClockSkew::Fast(skew).if_above(MIN)
54
        } else {
55
            // Either there is no clock skew, or we can't detect any.
56
1
            ClockSkew::None
57
        }
58
58
    }
59

            
60
    /// Return the magnitude of this clock skew.
61
1265
    pub fn magnitude(&self) -> Duration {
62
1265
        match self {
63
707
            ClockSkew::Slow(d) => *d,
64
50
            ClockSkew::None => Duration::from_secs(0),
65
508
            ClockSkew::Fast(d) => *d,
66
        }
67
1265
    }
68

            
69
    /// Return this clock skew as a signed number of seconds, with slow values
70
    /// treated as negative and fast values treated as positive.
71
3050
    pub fn as_secs_f64(&self) -> f64 {
72
3050
        match self {
73
1550
            ClockSkew::Slow(d) => -d.as_secs_f64(),
74
300
            ClockSkew::None => 0.0,
75
1200
            ClockSkew::Fast(d) => d.as_secs_f64(),
76
        }
77
3050
    }
78

            
79
    /// Return a clock skew computed from a signed number of seconds.
80
    ///
81
    /// Returns None if the value is degenerate.
82
1067
    pub fn from_secs_f64(seconds: f64) -> Option<Self> {
83
        use std::num::FpCategory;
84
1067
        let max_seconds = Duration::MAX.as_secs_f64();
85

            
86
        // I dislike working with floating point, and I dislike the current lack
87
        // of Duration::try_from_secs_f64() in stable Rust.  Look what they made
88
        // me do!
89
1067
        match seconds.classify() {
90
1
            FpCategory::Nan => None,
91
104
            FpCategory::Zero | FpCategory::Subnormal => Some(ClockSkew::None),
92
962
            FpCategory::Normal | FpCategory::Infinite => Some(if seconds <= -max_seconds {
93
2
                ClockSkew::Slow(Duration::MAX)
94
960
            } else if seconds < 0.0 {
95
554
                ClockSkew::Slow(Duration::from_secs_f64(-seconds)).if_above(MIN)
96
406
            } else if seconds < max_seconds {
97
404
                ClockSkew::Fast(Duration::from_secs_f64(seconds)).if_above(MIN)
98
            } else {
99
2
                ClockSkew::Fast(Duration::MAX)
100
            }),
101
        }
102
1067
    }
103

            
104
    /// Return this value if it is greater than `min`; otherwise return None.
105
1215
    pub fn if_above(self, min: Duration) -> Self {
106
1215
        if self.magnitude() > min {
107
1110
            self
108
        } else {
109
105
            ClockSkew::None
110
        }
111
1215
    }
112

            
113
    /// Return true if we're estimating any skew.
114
100
    pub fn is_skewed(&self) -> bool {
115
100
        !matches!(self, ClockSkew::None)
116
100
    }
117
}
118

            
119
impl Ord for ClockSkew {
120
5051
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
121
        use ClockSkew::*;
122
        use std::cmp::Ordering::*;
123
5051
        match (self, other) {
124
            // This is the reason we need to define this ordering rather than
125
            // deriving it: we want clock skews to sort by their signed distance
126
            // from the current time.
127
1800
            (Slow(a), Slow(b)) => a.cmp(b).reverse(),
128
660
            (Slow(_), _) => Less,
129

            
130
100
            (None, None) => Equal,
131
350
            (None, Slow(_)) => Greater,
132
262
            (None, Fast(_)) => Less,
133

            
134
1450
            (Fast(a), Fast(b)) => a.cmp(b),
135
429
            (Fast(_), _) => Greater,
136
        }
137
5051
    }
138
}
139

            
140
impl PartialOrd for ClockSkew {
141
5051
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
142
5051
        Some(self.cmp(other))
143
5051
    }
144
}
145

            
146
#[cfg(test)]
147
mod test {
148
    // @@ begin test lint list maintained by maint/add_warning @@
149
    #![allow(clippy::bool_assert_comparison)]
150
    #![allow(clippy::clone_on_copy)]
151
    #![allow(clippy::dbg_macro)]
152
    #![allow(clippy::mixed_attributes_style)]
153
    #![allow(clippy::print_stderr)]
154
    #![allow(clippy::print_stdout)]
155
    #![allow(clippy::single_char_pattern)]
156
    #![allow(clippy::unwrap_used)]
157
    #![allow(clippy::unchecked_time_subtraction)]
158
    #![allow(clippy::useless_vec)]
159
    #![allow(clippy::needless_pass_by_value)]
160
    #![allow(clippy::string_slice)] // See arti#2571
161
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
162

            
163
    use super::*;
164
    use tor_basic_utils::test_rng::testing_rng;
165
    use web_time_compat::SystemTimeExt;
166

            
167
    #[test]
168
    fn make_skew() {
169
        let now = SystemTime::get();
170
        let later = now + Duration::from_secs(777);
171
        let earlier = now - Duration::from_secs(333);
172
        let window = Duration::from_secs(30);
173

            
174
        // Case 1: they say our clock is slow.
175
        let skew = ClockSkew::from_handshake_timestamps(now, later, window);
176
        // The window is only subtracted in this case, since we're reporting the _minimum_ skew.
177
        assert_eq!(skew, ClockSkew::Slow(Duration::from_secs(747)));
178

            
179
        // Case 2: they say our clock is fast.
180
        let skew = ClockSkew::from_handshake_timestamps(now, earlier, window);
181
        assert_eq!(skew, ClockSkew::Fast(Duration::from_secs(333)));
182

            
183
        // Case 3: The variation in our clock is less than the time window it took them to answer.
184
        let skew = ClockSkew::from_handshake_timestamps(now, now + Duration::from_secs(20), window);
185
        assert_eq!(skew, ClockSkew::None);
186

            
187
        // Case 4: The variation in our clock is less than the limits of the timer precision.
188
        let skew = ClockSkew::from_handshake_timestamps(
189
            now,
190
            now + Duration::from_millis(500),
191
            Duration::from_secs(0),
192
        );
193
        assert_eq!(skew, ClockSkew::None);
194
    }
195

            
196
    #[test]
197
    fn from_f64() {
198
        use ClockSkew as CS;
199
        use Duration as D;
200

            
201
        assert_eq!(CS::from_secs_f64(0.0), Some(CS::None));
202
        assert_eq!(CS::from_secs_f64(f64::MIN_POSITIVE / 2.0), Some(CS::None)); // subnormal
203
        assert_eq!(CS::from_secs_f64(1.0), Some(CS::None));
204
        assert_eq!(CS::from_secs_f64(-1.0), Some(CS::None));
205
        assert_eq!(CS::from_secs_f64(3.0), Some(CS::Fast(D::from_secs(3))));
206
        assert_eq!(CS::from_secs_f64(-3.0), Some(CS::Slow(D::from_secs(3))));
207

            
208
        assert_eq!(CS::from_secs_f64(1.0e100), Some(CS::Fast(D::MAX)));
209
        assert_eq!(CS::from_secs_f64(-1.0e100), Some(CS::Slow(D::MAX)));
210

            
211
        assert_eq!(CS::from_secs_f64(f64::NAN), None);
212
        assert_eq!(CS::from_secs_f64(f64::INFINITY), Some(CS::Fast(D::MAX)));
213
        assert_eq!(CS::from_secs_f64(f64::NEG_INFINITY), Some(CS::Slow(D::MAX)));
214
    }
215

            
216
    #[test]
217
    fn order() {
218
        use ClockSkew as CS;
219
        use rand::seq::SliceRandom as _;
220
        let sorted: Vec<ClockSkew> = vec![-10.0, -5.0, 0.0, 0.0, 10.0, 20.0]
221
            .into_iter()
222
            .map(|n| CS::from_secs_f64(n).unwrap())
223
            .collect();
224

            
225
        let mut rng = testing_rng();
226
        let mut v = sorted.clone();
227
        for _ in 0..100 {
228
            v.shuffle(&mut rng);
229
            v.sort();
230
            assert_eq!(v, sorted);
231
        }
232
    }
233
}