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