arti_client/status.rs
1//! Code to collect and publish information about a client's bootstrapping
2//! status.
3
4use std::{borrow::Cow, fmt, fmt::Display};
5
6use educe::Educe;
7use futures::{Stream, StreamExt};
8use tor_basic_utils::skip_fmt;
9use tor_chanmgr::{ConnBlockage, ConnStatus, ConnStatusEvents};
10use tor_circmgr::{ClockSkewEvents, SkewEstimate};
11use tor_dirmgr::{DirBlockage, DirBootstrapStatus};
12use tracing::debug;
13use web_time_compat::{SystemTime, SystemTimeExt};
14
15/// Information about how ready a [`crate::TorClient`] is to handle requests.
16///
17/// Note that this status does not change monotonically: a `TorClient` can
18/// become more _or less_ bootstrapped over time. (For example, a client can
19/// become less bootstrapped if it loses its internet connectivity, or if its
20/// directory information expires before it's able to replace it.)
21//
22// # Note
23//
24// We need to keep this type fairly small, since it will get cloned whenever
25// it's observed on a stream. If it grows large, we can add an Arc<> around
26// its data.
27#[derive(Debug, Clone, Default)]
28pub struct BootstrapStatus {
29 /// Status for our connection to the tor network
30 conn_status: ConnStatus,
31 /// Status for our directory information.
32 dir_status: DirBootstrapStatus,
33 /// Current estimate of our clock skew.
34 skew: Option<SkewEstimate>,
35}
36
37impl BootstrapStatus {
38 /// Return a rough fraction (from 0.0 to 1.0) representing how far along
39 /// the client's bootstrapping efforts are.
40 ///
41 /// 0 is defined as "just started"; 1 is defined as "ready to use."
42 pub fn as_frac(&self) -> f32 {
43 // Coefficients chosen arbitrarily.
44 self.conn_status.frac() * 0.15 + self.dir_status.frac_at(SystemTime::get()) * 0.85
45 }
46
47 /// Return true if the status indicates that the client is ready for
48 /// traffic.
49 ///
50 /// For the purposes of this function, the client is "ready for traffic" if,
51 /// as far as we know, we can start acting on a new client request immediately.
52 pub fn ready_for_traffic(&self) -> bool {
53 let now = SystemTime::get();
54 self.conn_status.usable() && self.dir_status.usable_at(now)
55 }
56
57 /// If the client is unable to make forward progress for some reason, return
58 /// that reason.
59 ///
60 /// (Returns None if the client doesn't seem to be stuck.)
61 ///
62 /// # Caveats
63 ///
64 /// This function provides a "best effort" diagnostic: there
65 /// will always be some blockage types that it can't diagnose
66 /// correctly. It may declare that Arti is stuck for reasons that
67 /// are incorrect; or it may declare that the client is not stuck
68 /// when in fact no progress is being made.
69 ///
70 /// Therefore, the caller should always use a certain amount of
71 /// modesty when reporting these values to the user. For example,
72 /// it's probably better to say "Arti says it's stuck because it
73 /// can't make connections to the internet" rather than "You are
74 /// not on the internet."
75 pub fn blocked(&self) -> Option<Blockage> {
76 if let Some(b) = self.conn_status.blockage() {
77 let message = b.to_string().into();
78 let kind = b.into();
79 if matches!(kind, BlockageKind::ClockSkewed) && self.skew_is_noteworthy() {
80 Some(Blockage {
81 kind,
82 message: format!("Clock is {}", self.skew.as_ref().expect("logic error"))
83 .into(),
84 })
85 } else {
86 Some(Blockage { kind, message })
87 }
88 } else if let Some(b) = self.dir_status.blockage(SystemTime::get()) {
89 let message = b.to_string().into();
90 let kind = b.into();
91 Some(Blockage { kind, message })
92 } else {
93 None
94 }
95 }
96
97 /// Adjust this status based on new connection-status information.
98 fn apply_conn_status(&mut self, status: ConnStatus) {
99 self.conn_status = status;
100 }
101
102 /// Adjust this status based on new directory-status information.
103 fn apply_dir_status(&mut self, status: DirBootstrapStatus) {
104 self.dir_status = status;
105 }
106
107 /// Adjust this status based on new estimated clock skew information.
108 fn apply_skew_estimate(&mut self, status: Option<SkewEstimate>) {
109 self.skew = status;
110 }
111
112 /// Return true if our current clock skew estimate is considered noteworthy.
113 fn skew_is_noteworthy(&self) -> bool {
114 matches!(&self.skew, Some(s) if s.noteworthy())
115 }
116}
117
118/// A reason why a client believes it is stuck.
119#[derive(Clone, Debug, derive_more::Display)]
120#[display("{} ({})", kind, message)]
121pub struct Blockage {
122 /// Why do we think we're blocked?
123 kind: BlockageKind,
124 /// A human-readable message about the blockage.
125 message: Cow<'static, str>,
126}
127
128impl Blockage {
129 /// Get a programmatic indication of the kind of blockage this is.
130 pub fn kind(&self) -> BlockageKind {
131 self.kind.clone()
132 }
133
134 /// Get a human-readable message about the blockage.
135 pub fn message(&self) -> impl Display + '_ {
136 &self.message
137 }
138}
139
140/// A specific type of blockage that a client believes it is experiencing.
141///
142/// Used to distinguish among instances of [`Blockage`].
143#[derive(Clone, Debug, derive_more::Display)]
144#[non_exhaustive]
145pub enum BlockageKind {
146 /// There is some kind of problem with connecting to the network.
147 #[display("We seem to be offline")]
148 Offline,
149 /// We can connect, but our connections seem to be filtered.
150 #[display("Our internet connection seems filtered")]
151 Filtering,
152 /// We have some other kind of problem connecting to Tor
153 #[display("Can't reach the Tor network")]
154 CantReachTor,
155 /// We believe our clock is set incorrectly, and that's preventing us from
156 /// successfully with relays and/or from finding a directory that we trust.
157 #[display("Clock is skewed.")]
158 ClockSkewed,
159 /// We've encountered some kind of problem downloading directory
160 /// information, and it doesn't seem to be caused by any particular
161 /// connection problem.
162 #[display("Can't bootstrap a Tor directory.")]
163 CantBootstrap,
164}
165
166impl From<ConnBlockage> for BlockageKind {
167 fn from(b: ConnBlockage) -> BlockageKind {
168 match b {
169 ConnBlockage::NoTcp => BlockageKind::Offline,
170 ConnBlockage::NoHandshake => BlockageKind::Filtering,
171 ConnBlockage::CertsExpired => BlockageKind::ClockSkewed,
172 _ => BlockageKind::CantReachTor,
173 }
174 }
175}
176
177impl From<DirBlockage> for BlockageKind {
178 fn from(_: DirBlockage) -> Self {
179 BlockageKind::CantBootstrap
180 }
181}
182
183impl fmt::Display for BootstrapStatus {
184 /// Format this [`BootstrapStatus`].
185 ///
186 /// Note that the string returned by this function is designed for human
187 /// readability, not for machine parsing. Other code *should not* depend
188 /// on particular elements of this string.
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 let percent = (self.as_frac() * 100.0).round() as u32;
191 if let Some(problem) = self.blocked() {
192 write!(f, "Stuck at {}%: {}", percent, problem)?;
193 } else {
194 write!(
195 f,
196 "{}%: {}; {}",
197 percent, &self.conn_status, &self.dir_status
198 )?;
199 }
200 if let Some(skew) = &self.skew {
201 if skew.noteworthy() {
202 write!(f, ". Clock is {}", skew)?;
203 }
204 }
205 Ok(())
206 }
207}
208
209/// Task that runs forever, updating a client's status via the provided
210/// `sender`.
211///
212/// TODO(nickm): Eventually this will use real stream of events to see when we
213/// are bootstrapped or not. For now, it just says that we're not-ready until
214/// the given Receiver fires.
215///
216/// TODO(nickm): This should eventually close the stream when the client is
217/// dropped.
218pub(crate) async fn report_status(
219 mut sender: postage::watch::Sender<BootstrapStatus>,
220 conn_status: ConnStatusEvents,
221 dir_status: impl Stream<Item = DirBootstrapStatus> + Send + Unpin,
222 skew_status: ClockSkewEvents,
223) {
224 /// Internal enumeration to combine incoming status changes.
225 #[allow(clippy::large_enum_variant)]
226 enum Event {
227 /// A connection status change
228 Conn(ConnStatus),
229 /// A directory status change
230 Dir(DirBootstrapStatus),
231 /// A clock skew change
232 Skew(Option<SkewEstimate>),
233 }
234 let mut stream = futures::stream::select_all(vec![
235 conn_status.map(Event::Conn).boxed(),
236 dir_status.map(Event::Dir).boxed(),
237 skew_status.map(Event::Skew).boxed(),
238 ]);
239
240 while let Some(event) = stream.next().await {
241 let mut b = sender.borrow_mut();
242 match event {
243 Event::Conn(e) => b.apply_conn_status(e),
244 Event::Dir(e) => b.apply_dir_status(e),
245 Event::Skew(e) => b.apply_skew_estimate(e),
246 }
247 debug!("{}", *b);
248 }
249}
250
251/// A [`Stream`] of [`BootstrapStatus`] events.
252///
253/// This stream isn't guaranteed to receive every change in bootstrap status; if
254/// changes happen more frequently than the receiver can observe, some of them
255/// will be dropped.
256//
257// Note: We use a wrapper type around watch::Receiver here, in order to hide its
258// implementation type. We do that because we might want to change the type in
259// the future, and because some of the functionality exposed by Receiver (like
260// `borrow()` and the postage::Stream trait) are extraneous to the API we want.
261#[derive(Clone, Educe)]
262#[educe(Debug)]
263pub struct BootstrapEvents {
264 /// The receiver that implements this stream.
265 #[educe(Debug(method = "skip_fmt"))]
266 pub(crate) inner: postage::watch::Receiver<BootstrapStatus>,
267}
268
269impl Stream for BootstrapEvents {
270 type Item = BootstrapStatus;
271
272 fn poll_next(
273 mut self: std::pin::Pin<&mut Self>,
274 cx: &mut std::task::Context<'_>,
275 ) -> std::task::Poll<Option<Self::Item>> {
276 self.inner.poll_next_unpin(cx)
277 }
278}