diff --git a/crates/tor-cell/src/chancell/msg.rs b/crates/tor-cell/src/chancell/msg.rs index 77b905f20feb1d9c9458af004e00c7ce69334c9f..777d527e3a072b6220a9f61fb6ee2dfdb743394c 100644 --- a/crates/tor-cell/src/chancell/msg.rs +++ b/crates/tor-cell/src/chancell/msg.rs @@ -587,8 +587,11 @@ impl DestroyReason { /// channel and sending data. #[derive(Clone, Debug)] pub struct Netinfo { - /// Time when this cell was sent, or 0 if this cell is sent by - /// a client. + /// Time when this cell was sent, or 0 if this cell is sent by a client. + /// + /// TODO-SPEC(nickm): Y2038 issue here. Better add a new handshake version + /// to solve it. See + /// [torspec#80](https://gitlab.torproject.org/tpo/core/torspec/-/issues/80). timestamp: u32, /// Observed address for party that did not send the netinfo cell. their_addr: Option<IpAddr>, @@ -652,6 +655,15 @@ impl Netinfo { my_addr, } } + /// Return the time reported in this NETINFO cell. + pub fn timestamp(&self) -> Option<std::time::SystemTime> { + use std::time::{Duration, SystemTime}; + if self.timestamp == 0 { + None + } else { + Some(SystemTime::UNIX_EPOCH + Duration::from_secs(self.timestamp.into())) + } + } } impl Body for Netinfo { fn into_message(self) -> ChanMsg { diff --git a/crates/tor-chanmgr/src/builder.rs b/crates/tor-chanmgr/src/builder.rs index cabbb4b56f665d1efb8a4fc098e2f63a232fe8d7..adca41dfb3130ad84eb6d50ccd32721fd40d2823 100644 --- a/crates/tor-chanmgr/src/builder.rs +++ b/crates/tor-chanmgr/src/builder.rs @@ -181,7 +181,10 @@ impl<R: Runtime> ChanBuilder<R> { // 2. Set up the channel. let mut builder = ChannelBuilder::new(); builder.set_declared_addr(addr); - let chan = builder.launch(tls).connect().await?; + let chan = builder + .launch(tls) + .connect(|| self.runtime.wallclock()) + .await?; let now = self.runtime.wallclock(); let chan = chan.check(target, &peer_cert, Some(now))?; let (chan, reactor) = chan.finish().await?; diff --git a/crates/tor-proto/src/channel/handshake.rs b/crates/tor-proto/src/channel/handshake.rs index 577211cbf9a37eac9ce1d627cdfdbb001335a69a..111b660df7cd16f627e99e2d3be139cd97a4fd88 100644 --- a/crates/tor-proto/src/channel/handshake.rs +++ b/crates/tor-proto/src/channel/handshake.rs @@ -9,11 +9,13 @@ use tor_error::internal; use crate::channel::codec::{ChannelCodec, CodecError}; use crate::channel::UniqId; +use crate::util::skew::ClockSkew; use crate::{Error, Result}; use tor_cell::chancell::{msg, ChanCmd}; use std::net::SocketAddr; use std::sync::Arc; +use std::time::SystemTime; use tor_bytes::Reader; use tor_linkspec::ChanTarget; @@ -61,6 +63,8 @@ pub struct UnverifiedChannel<T: AsyncRead + AsyncWrite + Send + Unpin + 'static> /// The netinfo cell that we got from the relay. #[allow(dead_code)] // Relays will need this. netinfo_cell: msg::Netinfo, + /// How much clock skew did we detect in this handshake? + clock_skew: ClockSkew, /// Logging identifier for this stream. (Used for logging only.) unique_id: UniqId, } @@ -108,7 +112,13 @@ impl<T: AsyncRead + AsyncWrite + Send + Unpin + 'static> OutboundClientHandshake /// Negotiate a link protocol version with the relay, and read /// the relay's handshake information. - pub async fn connect(mut self) -> Result<UnverifiedChannel<T>> { + /// + /// Takes a function that reports the current time. In theory, this can just be + /// `SystemTime::now()`. + pub async fn connect<F>(mut self, now_fn: F) -> Result<UnverifiedChannel<T>> + where + F: FnOnce() -> SystemTime, + { /// Helper: wrap an IoError as a HandshakeIoErr. fn io_err_to_handshake(err: std::io::Error) -> Error { Error::HandshakeIoErr(Arc::new(err)) @@ -128,6 +138,8 @@ impl<T: AsyncRead + AsyncWrite + Send + Unpin + 'static> OutboundClientHandshake .map_err(io_err_to_handshake)?; self.tls.flush().await.map_err(io_err_to_handshake)?; } + let versions_flushed_at = coarsetime::Instant::now(); + let versions_flushed_wallclock = now_fn(); // Get versions cell. trace!("{}: waiting for versions", self.unique_id); @@ -172,7 +184,7 @@ impl<T: AsyncRead + AsyncWrite + Send + Unpin + 'static> OutboundClientHandshake // Read until we have the netinfo cells. let mut certs: Option<msg::Certs> = None; - let mut netinfo: Option<msg::Netinfo> = None; + let mut netinfo: Option<(msg::Netinfo, coarsetime::Instant)> = None; let mut seen_authchallenge = false; // Loop: reject duplicate and unexpected cells @@ -207,7 +219,7 @@ impl<T: AsyncRead + AsyncWrite + Send + Unpin + 'static> OutboundClientHandshake "Somehow tried to record a duplicate NETINFO cell" ))); } - netinfo = Some(n); + netinfo = Some((n, coarsetime::Instant::now())); break; } // No other cell types are allowed. @@ -226,13 +238,24 @@ impl<T: AsyncRead + AsyncWrite + Send + Unpin + 'static> OutboundClientHandshake "Missing netinfo or closed stream".into(), )), (None, _) => Err(Error::HandshakeProto("Missing certs cell".into())), - (Some(certs_cell), Some(netinfo_cell)) => { + (Some(certs_cell), Some((netinfo_cell, netinfo_rcvd_at))) => { trace!("{}: received handshake, ready to verify.", self.unique_id); + let clock_skew = if let Some(netinfo_timestamp) = netinfo_cell.timestamp() { + let delay = netinfo_rcvd_at - versions_flushed_at; + ClockSkew::from_handshake_timestamps( + versions_flushed_wallclock, + netinfo_timestamp, + delay.into(), + ) + } else { + ClockSkew::None + }; Ok(UnverifiedChannel { link_protocol, tls, certs_cell, netinfo_cell, + clock_skew, target_addr: self.target_addr, unique_id: self.unique_id, }) @@ -242,6 +265,14 @@ impl<T: AsyncRead + AsyncWrite + Send + Unpin + 'static> OutboundClientHandshake } impl<T: AsyncRead + AsyncWrite + Send + Unpin + 'static> UnverifiedChannel<T> { + /// Return the reported clock skew from this handshake. + /// + /// Note that the skew reported by this function might not be "true": the + /// relay might have its clock set wrong, or it might be lying to us. + pub fn clock_skew(&self) -> ClockSkew { + self.clock_skew + } + /// Validate the certificates and keys in the relay's handshake. /// /// 'peer' is the peer that we want to make sure we're connecting to. @@ -479,7 +510,13 @@ pub(super) mod test { // no certificates in this cell, but connect() doesn't care. const NOCERTS: &[u8] = &hex!("00000000 81 0001 00"); const NETINFO_PREFIX: &[u8] = &hex!( - "00000000 08 085F9067F7 + "00000000 08 00000000 + 04 04 7f 00 00 02 + 01 + 04 04 7f 00 00 03" + ); + const NETINFO_PREFIX_WITH_TIME: &[u8] = &hex!( + "00000000 08 48949290 04 04 7f 00 00 02 01 04 04 7f 00 00 03" @@ -505,18 +542,21 @@ pub(super) mod test { #[test] fn connect_ok() -> Result<()> { tor_rtcompat::test_with_one_runtime!(|_rt| async move { + let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1217696400); let mut buf = Vec::new(); // versions cell buf.extend_from_slice(VERSIONS); // certs cell -- no certs in it, but this function doesn't care. buf.extend_from_slice(NOCERTS); // netinfo cell -- quite minimal. - add_netinfo(&mut buf); + add_padded(&mut buf, NETINFO_PREFIX); let mb = MsgBuf::new(&buf[..]); let handshake = OutboundClientHandshake::new(mb, None); - let unverified = handshake.connect().await?; + let unverified = handshake.connect(|| now).await?; assert_eq!(unverified.link_protocol, 4); + // No timestamp in the NETINFO, so no skew. + assert_eq!(unverified.clock_skew(), ClockSkew::None); // Try again with an authchallenge cell and some padding. let mut buf = Vec::new(); @@ -525,10 +565,22 @@ pub(super) mod test { buf.extend_from_slice(VPADDING); buf.extend_from_slice(AUTHCHALLENGE); buf.extend_from_slice(VPADDING); - add_netinfo(&mut buf); + add_padded(&mut buf, NETINFO_PREFIX_WITH_TIME); let mb = MsgBuf::new(&buf[..]); let handshake = OutboundClientHandshake::new(mb, None); - let _unverified = handshake.connect().await?; + let unverified = handshake.connect(|| now).await?; + // Correct timestamp in the NETINFO, so no skew. + assert_eq!(unverified.clock_skew(), ClockSkew::None); + + // Now pretend our clock is fast. + let now2 = now + Duration::from_secs(3600); + let mb = MsgBuf::new(&buf[..]); + let handshake = OutboundClientHandshake::new(mb, None); + let unverified = handshake.connect(|| now2).await?; + assert_eq!( + unverified.clock_skew(), + ClockSkew::Fast(Duration::from_secs(3600)) + ); Ok(()) }) @@ -537,7 +589,7 @@ pub(super) mod test { async fn connect_err<T: Into<Vec<u8>>>(input: T) -> Error { let mb = MsgBuf::new(input); let handshake = OutboundClientHandshake::new(mb, None); - handshake.connect().await.err().unwrap() + handshake.connect(SystemTime::now).await.err().unwrap() } #[test] @@ -635,11 +687,13 @@ pub(super) mod test { fn make_unverified(certs: msg::Certs) -> UnverifiedChannel<MsgBuf> { let localhost = std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST); let netinfo_cell = msg::Netinfo::for_client(Some(localhost)); + let clock_skew = ClockSkew::None; UnverifiedChannel { link_protocol: 4, tls: futures_codec::Framed::new(MsgBuf::new(&b""[..]), ChannelCodec::new(4)), certs_cell: certs, netinfo_cell, + clock_skew, target_addr: None, unique_id: UniqId::new(), } diff --git a/crates/tor-proto/src/lib.rs b/crates/tor-proto/src/lib.rs index 196f85552ab34abb100669d270563a590d71b7ba..24a34c47f3b846494686192b7769aef2785bbc63 100644 --- a/crates/tor-proto/src/lib.rs +++ b/crates/tor-proto/src/lib.rs @@ -119,6 +119,7 @@ pub mod stream; mod util; pub use util::err::Error; +pub use util::skew::ClockSkew; /// A vector of bytes that gets cleared when it's dropped. type SecretBytes = zeroize::Zeroizing<Vec<u8>>; diff --git a/crates/tor-proto/src/util.rs b/crates/tor-proto/src/util.rs index 515ef5a26b77e2ecd66a5d4e1dae0ea5a3534629..043dbad57469424167eb418fa7f98221f5bf4aea 100644 --- a/crates/tor-proto/src/util.rs +++ b/crates/tor-proto/src/util.rs @@ -2,4 +2,5 @@ pub(crate) mod ct; pub(crate) mod err; +pub(crate) mod skew; pub(crate) mod ts; diff --git a/crates/tor-proto/src/util/skew.rs b/crates/tor-proto/src/util/skew.rs new file mode 100644 index 0000000000000000000000000000000000000000..c85c86a2a56bc8fb3f2cba9ec73b19f0a568c34f --- /dev/null +++ b/crates/tor-proto/src/util/skew.rs @@ -0,0 +1,102 @@ +//! 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, Ord, PartialOrd)] +#[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), +} + +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 { + /// 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); + + // 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 this value if it is greater than `min`; otherwise return None. + pub fn if_above(self, min: Duration) -> Self { + match self { + ClockSkew::Slow(d) if d > min => ClockSkew::Slow(d), + ClockSkew::Fast(d) if d > min => ClockSkew::Fast(d), + _ => ClockSkew::None, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[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); + } +} diff --git a/doc/semver_status.md b/doc/semver_status.md index 07dbb24ba79fcc594bed1ed5c4230884cd7ce827..1f4f86d220732999968e900a7c867dfb098c4690 100644 --- a/doc/semver_status.md +++ b/doc/semver_status.md @@ -58,6 +58,12 @@ tor-netdoc: tor-protover: new-api: Protocols now implements Eq, PartialEq, and Hash. +tor-proto: + api-break: OutboundClientHandshake::connect() now takes now_fn. + +tor-cell: + new-api: Netinfo message now has a timestamp() accessor. + tor-basic-utils: Remove `humantime_serde_option` module.