Skip to main content

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}