Initial functions to determine and expose a clock skew estimate.

(This is just a placeholder; I'm going to make the functions
smarter in the next commit.)
This commit is contained in:
Nick Mathewson 2022-04-07 12:16:13 -04:00
parent ae92f626fb
commit eedee51899
8 changed files with 125 additions and 5 deletions

1
Cargo.lock generated
View File

@ -3463,6 +3463,7 @@ dependencies = [
"derive_more", "derive_more",
"educe", "educe",
"futures", "futures",
"humantime 2.1.0",
"humantime-serde", "humantime-serde",
"itertools", "itertools",
"pin-project", "pin-project",

View File

@ -33,6 +33,7 @@ derive_builder = "0.11"
derive_more = "0.99" derive_more = "0.99"
educe = "0.4.6" educe = "0.4.6"
futures = "0.3.14" futures = "0.3.14"
humantime = "2"
humantime-serde = "1.1.1" humantime-serde = "1.1.1"
itertools = "0.10.1" itertools = "0.10.1"
pin-project = "1" pin-project = "1"

View File

@ -215,6 +215,13 @@ impl FallbackState {
entry.status.clock_skew = Some(observation); entry.status.clock_skew = Some(observation);
} }
} }
/// Return an iterator over all the clock skew observations we've made for fallback directories
pub(crate) fn skew_observations(&self) -> impl Iterator<Item = &SkewObservation> {
self.fallbacks
.iter()
.filter_map(|fb| fb.status.clock_skew.as_ref())
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -673,6 +673,12 @@ impl Guard {
self.clock_skew = Some(observation); self.clock_skew = Some(observation);
} }
/// Return the most recent clock skew observation for this guard, if we have
/// made one.
pub(crate) fn skew(&self) -> Option<&SkewObservation> {
self.clock_skew.as_ref()
}
/// Testing only: Return true if this guard was ever contacted successfully. /// Testing only: Return true if this guard was ever contacted successfully.
#[cfg(test)] #[cfg(test)]
pub(crate) fn confirmed(&self) -> bool { pub(crate) fn confirmed(&self) -> bool {

View File

@ -163,6 +163,7 @@ pub use err::{GuardMgrError, PickGuardError};
pub use filter::GuardFilter; pub use filter::GuardFilter;
pub use ids::FirstHopId; pub use ids::FirstHopId;
pub use pending::{GuardMonitor, GuardStatus, GuardUsable}; pub use pending::{GuardMonitor, GuardStatus, GuardUsable};
pub use skew::SkewEstimate;
use pending::{PendingRequest, RequestId}; use pending::{PendingRequest, RequestId};
use sample::GuardSet; use sample::GuardSet;
@ -568,6 +569,14 @@ impl<R: Runtime> GuardMgr<R> {
); );
} }
/// Return our best estimate of our current clock skew, based on reports from the
/// guards and fallbacks we have contacted.
pub fn skew_estimate(&self) -> Option<SkewEstimate> {
let inner = self.inner.lock().expect("Poisoned lock");
let now = self.runtime.now();
SkewEstimate::estimate_skew(inner.skew_observations(), now)
}
/// Ensure that the message queue is flushed before proceeding to /// Ensure that the message queue is flushed before proceeding to
/// the next step. Used for testing. /// the next step. Used for testing.
#[cfg(test)] #[cfg(test)]
@ -835,6 +844,14 @@ impl GuardMgrInner {
} }
} }
/// Return an iterator over all of the clock skew observations we've made
/// for guards or fallbacks.
fn skew_observations(&self) -> impl Iterator<Item = &skew::SkewObservation> {
self.fallbacks
.skew_observations()
.chain(self.guards.active_guards().skew_observations())
}
/// If the circuit built because of a given [`PendingRequest`] may /// If the circuit built because of a given [`PendingRequest`] may
/// now be used (or discarded), return `Some(true)` or /// now be used (or discarded), return `Some(true)` or
/// `Some(false)` respectively. /// `Some(false)` respectively.

View File

@ -669,6 +669,11 @@ impl GuardSet {
} }
} }
/// Return an iterator over all stored clock skew observations.
pub(crate) fn skew_observations(&self) -> impl Iterator<Item = &SkewObservation> {
self.guards.values().filter_map(|g| g.skew())
}
/// Return whether the circuit manager can be allowed to use a /// Return whether the circuit manager can be allowed to use a
/// circuit with the `guard_id`. /// circuit with the `guard_id`.
/// ///

View File

@ -1,6 +1,6 @@
//! Code for creating and manipulating observations about clock skew. //! Code for creating and manipulating observations about clock skew.
use std::time::Instant; use std::time::{Duration, Instant};
use tor_proto::ClockSkew; use tor_proto::ClockSkew;
@ -13,3 +13,77 @@ pub(crate) struct SkewObservation {
/// The time when we added this observation. /// The time when we added this observation.
pub(crate) when: Instant, pub(crate) when: Instant,
} }
impl SkewObservation {
/// Return true if this observation has been made more recently than
/// `cutoff`.
pub(crate) fn more_recent_than(&self, cutoff: Instant) -> bool {
self.when > cutoff
}
}
/// An estimate of how skewed our clock is, plus a summary of why we think so.
//
// XXXX This is a placeholder for now.
#[derive(Clone, Debug)]
pub struct SkewEstimate {
/// Our best guess for the magnitude of the skew.
estimate: ClockSkew,
/// The number of observations leading to this estimate.
n_observations: usize,
}
impl std::fmt::Display for SkewEstimate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use humantime::format_duration;
match self.estimate {
ClockSkew::Slow(d) => write!(f, "slow by {}", format_duration(d)),
ClockSkew::None => write!(f, "not skewed"),
ClockSkew::Fast(d) => write!(f, "fast by {}", format_duration(d)),
}?;
write!(f, " (based on {} recent observations)", self.n_observations)
}
}
impl SkewEstimate {
/// Return our best estimate for the current clock skew.
pub fn skew(&self) -> ClockSkew {
self.estimate
}
/// Compute an estimate of how skewed we think our clock is, based on the
/// reports in `skews`.
pub(crate) fn estimate_skew<'a>(
skews: impl Iterator<Item = &'a SkewObservation>,
now: Instant,
) -> Option<Self> {
// Only consider skew observations reported at least this recently.
let cutoff = now - Duration::from_secs(3600);
// Don't believe observations unless we have at least this many. (This
// value is chosen somewhat arbitrarily.)
//
// Note that under normal client operation, we won't connect to this
// many guards or fallbacks. That's fine: clock skew is only a problem
// when it keeps us from bootstrapping, and when we are having
// bootstrapping problems, we _will_ connect to many guards or
// fallbacks.
let min_observations = 8;
let mut skews: Vec<_> = skews
.filter_map(|obs| obs.more_recent_than(cutoff).then(|| obs.skew))
.collect();
let n_observations = skews.len();
if n_observations < min_observations {
return None;
}
let (_, median, _) = skews.select_nth_unstable(n_observations / 2);
// TODO: Consider the quartiles as well, as a rough estimate of confidence.
Some(SkewEstimate {
estimate: *median,
n_observations,
})
}
}

View File

@ -57,12 +57,21 @@ impl ClockSkew {
} }
} }
/// 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 value if it is greater than `min`; otherwise return None. /// Return this value if it is greater than `min`; otherwise return None.
pub fn if_above(self, min: Duration) -> Self { pub fn if_above(self, min: Duration) -> Self {
match self { if self.magnitude() > min {
ClockSkew::Slow(d) if d > min => ClockSkew::Slow(d), self
ClockSkew::Fast(d) if d > min => ClockSkew::Fast(d), } else {
_ => ClockSkew::None, ClockSkew::None
} }
} }
} }