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
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}