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
use 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)]
30
pub 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

            
41
impl BootstrapStatus {
42
    /// Create a new `BootstrapStatus` from a given `BootstrapSetting`
43
182
    pub(crate) fn from_setting(bootstrap_setting: BootstrapSetting) -> Self {
44
182
        Self {
45
182
            bootstrap_setting,
46
182
            ..Default::default()
47
182
        }
48
182
    }
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)]
145
pub 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

            
152
impl 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]
169
pub 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

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

            
207
impl From<DirBlockage> for BlockageKind {
208
    fn from(_: DirBlockage) -> Self {
209
        BlockageKind::CantBootstrap
210
    }
211
}
212

            
213
impl 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.
248
pub(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)]
298
pub 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

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