1
//! Code to collect and publish information about a client's bootstrapping
2
//! status.
3

            
4
use std::{borrow::Cow, fmt, fmt::Display};
5

            
6
use educe::Educe;
7
use futures::{Stream, StreamExt};
8
use tor_basic_utils::skip_fmt;
9
use tor_chanmgr::{ConnBlockage, ConnStatus, ConnStatusEvents};
10
use tor_circmgr::{ClockSkewEvents, SkewEstimate};
11
use tor_dirmgr::{DirBlockage, DirBootstrapStatus};
12
use tracing::debug;
13
use 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)]
28
pub 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

            
37
impl 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
36
    pub fn as_frac(&self) -> f32 {
43
        // Coefficients chosen arbitrarily.
44
36
        self.conn_status.frac() * 0.15 + self.dir_status.frac_at(SystemTime::get()) * 0.85
45
36
    }
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
36
    pub fn blocked(&self) -> Option<Blockage> {
76
36
        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
36
        } 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
36
            None
94
        }
95
36
    }
96

            
97
    /// Adjust this status based on new connection-status information.
98
182
    fn apply_conn_status(&mut self, status: ConnStatus) {
99
182
        self.conn_status = status;
100
182
    }
101

            
102
    /// Adjust this status based on new directory-status information.
103
182
    fn apply_dir_status(&mut self, status: DirBootstrapStatus) {
104
182
        self.dir_status = status;
105
182
    }
106

            
107
    /// Adjust this status based on new estimated clock skew information.
108
182
    fn apply_skew_estimate(&mut self, status: Option<SkewEstimate>) {
109
182
        self.skew = status;
110
182
    }
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)]
121
pub 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

            
128
impl 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]
145
pub 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

            
166
impl 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

            
177
impl From<DirBlockage> for BlockageKind {
178
    fn from(_: DirBlockage) -> Self {
179
        BlockageKind::CantBootstrap
180
    }
181
}
182

            
183
impl 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
36
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190
36
        let percent = (self.as_frac() * 100.0).round() as u32;
191
36
        if let Some(problem) = self.blocked() {
192
            write!(f, "Stuck at {}%: {}", percent, problem)?;
193
        } else {
194
36
            write!(
195
36
                f,
196
36
                "{}%: {}; {}",
197
36
                percent, &self.conn_status, &self.dir_status
198
            )?;
199
        }
200
36
        if let Some(skew) = &self.skew {
201
            if skew.noteworthy() {
202
                write!(f, ". Clock is {}", skew)?;
203
            }
204
36
        }
205
36
        Ok(())
206
36
    }
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.
218
22
pub(crate) async fn report_status(
219
22
    mut sender: postage::watch::Sender<BootstrapStatus>,
220
22
    conn_status: ConnStatusEvents,
221
22
    dir_status: impl Stream<Item = DirBootstrapStatus> + Send + Unpin,
222
22
    skew_status: ClockSkewEvents,
223
22
) {
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
22
    let mut stream = futures::stream::select_all(vec![
235
22
        conn_status.map(Event::Conn).boxed(),
236
22
        dir_status.map(Event::Dir).boxed(),
237
22
        skew_status.map(Event::Skew).boxed(),
238
    ]);
239

            
240
88
    while let Some(event) = stream.next().await {
241
66
        let mut b = sender.borrow_mut();
242
66
        match event {
243
22
            Event::Conn(e) => b.apply_conn_status(e),
244
22
            Event::Dir(e) => b.apply_dir_status(e),
245
22
            Event::Skew(e) => b.apply_skew_estimate(e),
246
        }
247
66
        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)]
263
pub 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

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