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