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

            
4
use std::{borrow::Cow, fmt, fmt::Display, time::SystemTime};
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

            
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)]
27
pub 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

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

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

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

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

            
127
impl 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]
144
pub 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

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

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

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

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

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