1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
//! Tools and types for reporting declared clock skew.
use std::time::{Duration, SystemTime};
/// A reported amount of clock skew from a relay or other source.
///
/// Note that this information may not be accurate or trustworthy: the relay
/// could be wrong, or lying.
///
/// The skews reported here are _minimum_ amounts; the actual skew may
/// be a little higher, depending on latency.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[allow(clippy::exhaustive_enums)]
pub enum ClockSkew {
/// Our own clock is "running slow": the relay's clock is at least this far
/// ahead of ours.
Slow(Duration),
/// Our own clock is not necessarily inconsistent with the relay's clock.
None,
/// Own own clock is "running fast": the relay's clock is at least this far
/// behind ours.
Fast(Duration),
}
/// We treat clock skew as "zero" if it less than this long.
///
/// (Since the relay only reports its time to the nearest second, we
/// can't reasonably infer that differences less than this much reflect
/// accurate differences in our clocks.)
const MIN: Duration = Duration::from_secs(2);
impl ClockSkew {
/// Construct a ClockSkew from a set of channel handshake timestamps.
///
/// Requires that `ours_at_start` is the timestamp at the point when we
/// started the handshake, `theirs` is the timestamp the relay reported in
/// its NETINFO cell, and `delay` is the total amount of time between when
/// we started the handshake and when we received the NETINFO cell.
pub(crate) fn from_handshake_timestamps(
ours_at_start: SystemTime,
theirs: SystemTime,
delay: Duration,
) -> Self {
// The relay may have generated its own timestamp any time between when
// we sent the handshake, and when we got the reply. Therefore, at the
// time we started, it was between these values.
let theirs_at_start_min = theirs - delay;
let theirs_at_start_max = theirs;
if let Ok(skew) = theirs_at_start_min.duration_since(ours_at_start) {
ClockSkew::Slow(skew).if_above(MIN)
} else if let Ok(skew) = ours_at_start.duration_since(theirs_at_start_max) {
ClockSkew::Fast(skew).if_above(MIN)
} else {
// Either there is no clock skew, or we can't detect any.
ClockSkew::None
}
}
/// Return the magnitude of this clock skew.
pub fn magnitude(&self) -> Duration {
match self {
ClockSkew::Slow(d) => *d,
ClockSkew::None => Duration::from_secs(0),
ClockSkew::Fast(d) => *d,
}
}
/// Return this clock skew as a signed number of seconds, with slow values
/// treated as negative and fast values treated as positive.
pub fn as_secs_f64(&self) -> f64 {
match self {
ClockSkew::Slow(d) => -d.as_secs_f64(),
ClockSkew::None => 0.0,
ClockSkew::Fast(d) => d.as_secs_f64(),
}
}
/// Return a clock skew computed from a signed number of seconds.
///
/// Returns None if the value is degenerate.
pub fn from_secs_f64(seconds: f64) -> Option<Self> {
use std::num::FpCategory;
let max_seconds = Duration::MAX.as_secs_f64();
// I dislike working with floating point, and I dislike the current lack
// of Duration::try_from_secs_f64() in stable Rust. Look what they made
// me do!
match seconds.classify() {
FpCategory::Nan => None,
FpCategory::Zero | FpCategory::Subnormal => Some(ClockSkew::None),
FpCategory::Normal | FpCategory::Infinite => Some(if seconds <= -max_seconds {
ClockSkew::Slow(Duration::MAX)
} else if seconds < 0.0 {
ClockSkew::Slow(Duration::from_secs_f64(-seconds)).if_above(MIN)
} else if seconds < max_seconds {
ClockSkew::Fast(Duration::from_secs_f64(seconds)).if_above(MIN)
} else {
ClockSkew::Fast(Duration::MAX)
}),
}
}
/// Return this value if it is greater than `min`; otherwise return None.
pub fn if_above(self, min: Duration) -> Self {
if self.magnitude() > min {
self
} else {
ClockSkew::None
}
}
/// Return true if we're estimating any skew.
pub fn is_skewed(&self) -> bool {
!matches!(self, ClockSkew::None)
}
}
impl Ord for ClockSkew {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
use std::cmp::Ordering::*;
use ClockSkew::*;
match (self, other) {
// This is the reason we need to define this ordering rather than
// deriving it: we want clock skews to sort by their signed distance
// from the current time.
(Slow(a), Slow(b)) => a.cmp(b).reverse(),
(Slow(_), _) => Less,
(None, None) => Equal,
(None, Slow(_)) => Greater,
(None, Fast(_)) => Less,
(Fast(a), Fast(b)) => a.cmp(b),
(Fast(_), _) => Greater,
}
}
}
impl PartialOrd for ClockSkew {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
mod test {
// @@ begin test lint list maintained by maint/add_warning @@
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use super::*;
use tor_basic_utils::test_rng::testing_rng;
#[test]
fn make_skew() {
let now = SystemTime::now();
let later = now + Duration::from_secs(777);
let earlier = now - Duration::from_secs(333);
let window = Duration::from_secs(30);
// Case 1: they say our clock is slow.
let skew = ClockSkew::from_handshake_timestamps(now, later, window);
// The window is only subtracted in this case, since we're reporting the _minimum_ skew.
assert_eq!(skew, ClockSkew::Slow(Duration::from_secs(747)));
// Case 2: they say our clock is fast.
let skew = ClockSkew::from_handshake_timestamps(now, earlier, window);
assert_eq!(skew, ClockSkew::Fast(Duration::from_secs(333)));
// Case 3: The variation in our clock is less than the time window it took them to answer.
let skew = ClockSkew::from_handshake_timestamps(now, now + Duration::from_secs(20), window);
assert_eq!(skew, ClockSkew::None);
// Case 4: The variation in our clock is less than the limits of the timer precision.
let skew = ClockSkew::from_handshake_timestamps(
now,
now + Duration::from_millis(500),
Duration::from_secs(0),
);
assert_eq!(skew, ClockSkew::None);
}
#[test]
fn from_f64() {
use ClockSkew as CS;
use Duration as D;
assert_eq!(CS::from_secs_f64(0.0), Some(CS::None));
assert_eq!(CS::from_secs_f64(f64::MIN_POSITIVE / 2.0), Some(CS::None)); // subnormal
assert_eq!(CS::from_secs_f64(1.0), Some(CS::None));
assert_eq!(CS::from_secs_f64(-1.0), Some(CS::None));
assert_eq!(CS::from_secs_f64(3.0), Some(CS::Fast(D::from_secs(3))));
assert_eq!(CS::from_secs_f64(-3.0), Some(CS::Slow(D::from_secs(3))));
assert_eq!(CS::from_secs_f64(1.0e100), Some(CS::Fast(D::MAX)));
assert_eq!(CS::from_secs_f64(-1.0e100), Some(CS::Slow(D::MAX)));
assert_eq!(CS::from_secs_f64(f64::NAN), None);
assert_eq!(CS::from_secs_f64(f64::INFINITY), Some(CS::Fast(D::MAX)));
assert_eq!(CS::from_secs_f64(f64::NEG_INFINITY), Some(CS::Slow(D::MAX)));
}
#[test]
fn order() {
use rand::seq::SliceRandom as _;
use ClockSkew as CS;
let sorted: Vec<ClockSkew> = vec![-10.0, -5.0, 0.0, 0.0, 10.0, 20.0]
.into_iter()
.map(|n| CS::from_secs_f64(n).unwrap())
.collect();
let mut rng = testing_rng();
let mut v = sorted.clone();
for _ in 0..100 {
v.shuffle(&mut rng);
v.sort();
assert_eq!(v, sorted);
}
}
}