tor_proto/util/
skew.rs

1//! Tools and types for reporting declared clock skew.
2
3use 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)]
14pub 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.)
30const MIN: Duration = Duration::from_secs(2);
31
32impl 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    pub(crate) fn from_handshake_timestamps(
40        ours_at_start: SystemTime,
41        theirs: SystemTime,
42        delay: Duration,
43    ) -> 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        let theirs_at_start_min = theirs - delay;
48        let theirs_at_start_max = theirs;
49
50        if let Ok(skew) = theirs_at_start_min.duration_since(ours_at_start) {
51            ClockSkew::Slow(skew).if_above(MIN)
52        } else if let Ok(skew) = ours_at_start.duration_since(theirs_at_start_max) {
53            ClockSkew::Fast(skew).if_above(MIN)
54        } else {
55            // Either there is no clock skew, or we can't detect any.
56            ClockSkew::None
57        }
58    }
59
60    /// Return the magnitude of this clock skew.
61    pub fn magnitude(&self) -> Duration {
62        match self {
63            ClockSkew::Slow(d) => *d,
64            ClockSkew::None => Duration::from_secs(0),
65            ClockSkew::Fast(d) => *d,
66        }
67    }
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    pub fn as_secs_f64(&self) -> f64 {
72        match self {
73            ClockSkew::Slow(d) => -d.as_secs_f64(),
74            ClockSkew::None => 0.0,
75            ClockSkew::Fast(d) => d.as_secs_f64(),
76        }
77    }
78
79    /// Return a clock skew computed from a signed number of seconds.
80    ///
81    /// Returns None if the value is degenerate.
82    pub fn from_secs_f64(seconds: f64) -> Option<Self> {
83        use std::num::FpCategory;
84        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        match seconds.classify() {
90            FpCategory::Nan => None,
91            FpCategory::Zero | FpCategory::Subnormal => Some(ClockSkew::None),
92            FpCategory::Normal | FpCategory::Infinite => Some(if seconds <= -max_seconds {
93                ClockSkew::Slow(Duration::MAX)
94            } else if seconds < 0.0 {
95                ClockSkew::Slow(Duration::from_secs_f64(-seconds)).if_above(MIN)
96            } else if seconds < max_seconds {
97                ClockSkew::Fast(Duration::from_secs_f64(seconds)).if_above(MIN)
98            } else {
99                ClockSkew::Fast(Duration::MAX)
100            }),
101        }
102    }
103
104    /// Return this value if it is greater than `min`; otherwise return None.
105    pub fn if_above(self, min: Duration) -> Self {
106        if self.magnitude() > min {
107            self
108        } else {
109            ClockSkew::None
110        }
111    }
112
113    /// Return true if we're estimating any skew.
114    pub fn is_skewed(&self) -> bool {
115        !matches!(self, ClockSkew::None)
116    }
117}
118
119impl Ord for ClockSkew {
120    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
121        use std::cmp::Ordering::*;
122        use ClockSkew::*;
123        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            (Slow(a), Slow(b)) => a.cmp(b).reverse(),
128            (Slow(_), _) => Less,
129
130            (None, None) => Equal,
131            (None, Slow(_)) => Greater,
132            (None, Fast(_)) => Less,
133
134            (Fast(a), Fast(b)) => a.cmp(b),
135            (Fast(_), _) => Greater,
136        }
137    }
138}
139
140impl PartialOrd for ClockSkew {
141    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
142        Some(self.cmp(other))
143    }
144}
145
146#[cfg(test)]
147mod 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_duration_subtraction)]
158    #![allow(clippy::useless_vec)]
159    #![allow(clippy::needless_pass_by_value)]
160    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
161
162    use super::*;
163    use tor_basic_utils::test_rng::testing_rng;
164
165    #[test]
166    fn make_skew() {
167        let now = SystemTime::now();
168        let later = now + Duration::from_secs(777);
169        let earlier = now - Duration::from_secs(333);
170        let window = Duration::from_secs(30);
171
172        // Case 1: they say our clock is slow.
173        let skew = ClockSkew::from_handshake_timestamps(now, later, window);
174        // The window is only subtracted in this case, since we're reporting the _minimum_ skew.
175        assert_eq!(skew, ClockSkew::Slow(Duration::from_secs(747)));
176
177        // Case 2: they say our clock is fast.
178        let skew = ClockSkew::from_handshake_timestamps(now, earlier, window);
179        assert_eq!(skew, ClockSkew::Fast(Duration::from_secs(333)));
180
181        // Case 3: The variation in our clock is less than the time window it took them to answer.
182        let skew = ClockSkew::from_handshake_timestamps(now, now + Duration::from_secs(20), window);
183        assert_eq!(skew, ClockSkew::None);
184
185        // Case 4: The variation in our clock is less than the limits of the timer precision.
186        let skew = ClockSkew::from_handshake_timestamps(
187            now,
188            now + Duration::from_millis(500),
189            Duration::from_secs(0),
190        );
191        assert_eq!(skew, ClockSkew::None);
192    }
193
194    #[test]
195    fn from_f64() {
196        use ClockSkew as CS;
197        use Duration as D;
198
199        assert_eq!(CS::from_secs_f64(0.0), Some(CS::None));
200        assert_eq!(CS::from_secs_f64(f64::MIN_POSITIVE / 2.0), Some(CS::None)); // subnormal
201        assert_eq!(CS::from_secs_f64(1.0), Some(CS::None));
202        assert_eq!(CS::from_secs_f64(-1.0), Some(CS::None));
203        assert_eq!(CS::from_secs_f64(3.0), Some(CS::Fast(D::from_secs(3))));
204        assert_eq!(CS::from_secs_f64(-3.0), Some(CS::Slow(D::from_secs(3))));
205
206        assert_eq!(CS::from_secs_f64(1.0e100), Some(CS::Fast(D::MAX)));
207        assert_eq!(CS::from_secs_f64(-1.0e100), Some(CS::Slow(D::MAX)));
208
209        assert_eq!(CS::from_secs_f64(f64::NAN), None);
210        assert_eq!(CS::from_secs_f64(f64::INFINITY), Some(CS::Fast(D::MAX)));
211        assert_eq!(CS::from_secs_f64(f64::NEG_INFINITY), Some(CS::Slow(D::MAX)));
212    }
213
214    #[test]
215    fn order() {
216        use rand::seq::SliceRandom as _;
217        use ClockSkew as CS;
218        let sorted: Vec<ClockSkew> = vec![-10.0, -5.0, 0.0, 0.0, 10.0, 20.0]
219            .into_iter()
220            .map(|n| CS::from_secs_f64(n).unwrap())
221            .collect();
222
223        let mut rng = testing_rng();
224        let mut v = sorted.clone();
225        for _ in 0..100 {
226            v.shuffle(&mut rng);
227            v.sort();
228            assert_eq!(v, sorted);
229        }
230    }
231}