1
//! Observe and enforce lists of recommended and required subprotocols.
2
//!
3
//! To prevent insecure clients from exposing themselves to attacks,
4
//! and to prevent obsolete clients from [inadvertently DoSing the network][fast-zombies]
5
//! by looking for relays with functionality that no longer exists,
6
//! we have a mechanism for ["recommended" and "required" subprotocols][recommended].
7
//!
8
//! When a subprotocol is recommended, we issue a warning whenever it is absent.
9
//! When a subprotocol is required, we (typically) shut down Arti whenever it is absent.
10
//!
11
//! While Arti is running, we check our subprotocols
12
//! whenever we find a new timely well-signed consensus.
13
//!
14
//! Additionally, we check our subprotocols at startup before any directory is received,
15
//! to ensure that we don't touch the network with invalid software.
16
//!
17
//! We ignore any list of required/recommended protocol
18
//! that is [older than the release date of this software].
19
//!
20
//! [fast-zombies]: https://spec.torproject.org/proposals/266-removing-current-obsolete-clients.html
21
//! [recommended]: https://spec.torproject.org/tor-spec/subprotocol-versioning.html#required-recommended
22
//! [older]: https://spec.torproject.org/proposals/297-safer-protover-shutdowns.html
23

            
24
use futures::{Stream, StreamExt as _};
25
use std::{
26
    future::Future,
27
    sync::{Arc, Weak},
28
    time::SystemTime,
29
};
30
use tor_dirmgr::DirProvider;
31
use tor_error::{into_internal, warn_report};
32
use tor_netdir::DirEvent;
33
use tor_netdoc::doc::netstatus::{ProtoStatuses, ProtocolSupportError};
34
use tor_protover::Protocols;
35
use tor_rtcompat::{Runtime, SpawnExt as _};
36
use tracing::{debug, error, info, warn};
37

            
38
use crate::err::ErrorDetail;
39

            
40
/// Check whether we have any cached protocol recommendations,
41
/// and report about them or enforce them immediately.
42
///
43
/// Then, launch a task to run indefinitely, and continue to enforce protocol recommendations.
44
/// If that task encounters a fatal error, it should invoke `on_fatal`.
45
pub(crate) fn enforce_protocol_recommendations<R, F, Fut>(
46
    runtime: &R,
47
    netdir_provider: Arc<dyn DirProvider>,
48
    software_publication_time: SystemTime,
49
    software_protocols: Protocols,
50
    on_fatal: F,
51
) -> Result<(), ErrorDetail>
52
where
53
    R: Runtime,
54
    F: FnOnce(ErrorDetail) -> Fut + Send + 'static,
55
    Fut: Future<Output = ()> + Send + 'static,
56
{
57
    // We need to get this stream before we check the initial status, to avoid race conditions.
58
    let events = netdir_provider.events();
59

            
60
    let initial_evaluated_proto_status = match netdir_provider.protocol_statuses() {
61
        Some((timestamp, recommended)) if timestamp >= software_publication_time => {
62
            // Here we exit if the initial (cached) status is bogus.
63
            evaluate_protocol_status(timestamp, &recommended, &software_protocols)?;
64

            
65
            Some(recommended)
66
        }
67
        Some((_, _)) => {
68
            // In this case, our software is newer than the consensus, so we don't enforce it.
69
            None
70
        }
71
        None => None,
72
    };
73

            
74
    runtime
75
        .spawn(watch_protocol_statuses(
76
            netdir_provider,
77
            events,
78
            initial_evaluated_proto_status,
79
            software_publication_time,
80
            software_protocols,
81
            on_fatal,
82
        ))
83
        .map_err(|e| ErrorDetail::from_spawn("protocol status monitor", e))?;
84

            
85
    Ok(())
86
}
87

            
88
/// Run indefinitely, checking for any protocol-recommendation issues.
89
///
90
/// In addition to the arguments of `enforce_protocol_recommendations,`
91
/// this function expects `events` (a stream of DirEvent),
92
/// and `last_evaluated_proto_status` (the last protocol status that we passed to evaluate_protocol_status).
93
///
94
/// On a fatal error, invoke `on_fatal` and return.
95
async fn watch_protocol_statuses<S, F, Fut>(
96
    netdir_provider: Arc<dyn DirProvider>,
97
    mut events: S,
98
    mut last_evaluated_proto_status: Option<Arc<ProtoStatuses>>,
99
    software_publication_time: SystemTime,
100
    software_protocols: Protocols,
101
    on_fatal: F,
102
) where
103
    S: Stream<Item = DirEvent> + Send + Unpin,
104
    F: FnOnce(ErrorDetail) -> Fut + Send,
105
    Fut: Future<Output = ()> + Send,
106
{
107
    let weak_netdir_provider = Arc::downgrade(&netdir_provider);
108
    drop(netdir_provider);
109

            
110
    while let Some(e) = events.next().await {
111
        if e != DirEvent::NewProtocolRecommendation {
112
            continue;
113
        }
114

            
115
        let new_status = {
116
            let Some(provider) = Weak::upgrade(&weak_netdir_provider) else {
117
                break;
118
            };
119
            provider.protocol_statuses()
120
        };
121
        let Some((timestamp, new_status)) = new_status else {
122
            warn!(
123
                "Bug: Got DirEvent::NewProtocolRecommendation, but protocol_statuses() returned None."
124
            );
125
            continue;
126
        };
127
        // It information is older than this software, there is a good chance
128
        // that it has come from an invalid piece of data that somebody has cached.
129
        // We'll ignore it.
130
        //
131
        // For more information about this behavior, see:
132
        // https://spec.torproject.org/tor-spec/subprotocol-versioning.html#required-recommended
133
        if timestamp < software_publication_time {
134
            continue;
135
        }
136
        if last_evaluated_proto_status.as_ref() == Some(&new_status) {
137
            // We've already acted on this status information.
138
            continue;
139
        }
140

            
141
        if let Err(fatal) = evaluate_protocol_status(timestamp, &new_status, &software_protocols) {
142
            on_fatal(fatal).await;
143
            return;
144
        }
145
        last_evaluated_proto_status = Some(new_status);
146
    }
147

            
148
    // If we reach this point,
149
    // either we failed to upgrade the weak reference (because the netdir provider went away)
150
    // or the event stream was closed.
151
    // Either of these cases implies a clean shutdown.
152
}
153

            
154
/// Check whether we should take action based on the protocol `recommendation`
155
/// from `recommendation_timestamp`,
156
/// given that our own supported subprotocols are `software_protocols`.
157
///
158
/// - If any required protocols are missing, log and return an error.
159
/// - If no required protocols are missing, but some recommended protocols are missing,
160
///   log and return `Ok(())`.
161
/// - If no protocols are missing, return `Ok(())`.
162
///
163
/// Note: This function should ONLY return an error when the error is fatal.
164
#[allow(clippy::cognitive_complexity)] // complexity caused by trace macros.
165
6
pub(crate) fn evaluate_protocol_status(
166
6
    recommendation_timestamp: SystemTime,
167
6
    recommendation: &ProtoStatuses,
168
6
    software_protocols: &Protocols,
169
6
) -> Result<(), ErrorDetail> {
170
6
    let result = recommendation.client().check_protocols(software_protocols);
171

            
172
8
    let rectime = || humantime::format_rfc3339(recommendation_timestamp);
173

            
174
2
    match &result {
175
2
        Ok(()) => Ok(()),
176
2
        Err(ProtocolSupportError::MissingRecommended(missing))
177
2
            if missing.difference(&missing_recommended_ok()).is_empty() =>
178
        {
179
            debug!(
180
                "Recommended protocols ({}) are missing, but that's expected: we haven't built them yet in Arti.",
181
                missing
182
            );
183
            Ok(())
184
        }
185
2
        Err(ProtocolSupportError::MissingRecommended(missing)) => {
186
2
            info!(
187
"At least one protocol not implemented by this version of Arti ({}) is listed as recommended for clients as of {}.
188
Please upgrade to a more recent version of Arti.",
189
                 missing, rectime());
190

            
191
2
            Ok(())
192
        }
193
2
        Err(e @ ProtocolSupportError::MissingRequired(missing)) => {
194
2
            error!(
195
"At least one protocol not implemented by this version of Arti ({}) is listed as required for clients, as of {}.
196
This version of Arti may not work correctly on the Tor network; please upgrade.",
197
                  &missing, rectime());
198
2
            Err(ErrorDetail::MissingProtocol(e.clone()))
199
        }
200
        Err(e) => {
201
            // Because ProtocolSupportError is non-exhaustive, we need this case.
202
            warn_report!(
203
                e,
204
                "Unexpected problem while examining protocol recommendations"
205
            );
206
            if e.should_shutdown() {
207
                return Err(ErrorDetail::Bug(into_internal!(
208
                    "Unexpected fatal protocol error"
209
                )(e.clone())));
210
            }
211
            Ok(())
212
        }
213
    }
214
6
}
215

            
216
/// Return a list of the protocols which may be recommended,
217
/// and which we know are missing in Arti.
218
///
219
/// This function should go away in the future:
220
/// we use it to generate a slightly less alarming warning
221
/// when we have an _expected_ missing recommended protocol.
222
2
fn missing_recommended_ok() -> Protocols {
223
    // TODO: Remove this once congestion control is fully implemented.
224
    use tor_protover::named as n;
225
2
    [n::FLOWCTRL_CC].into_iter().collect()
226
2
}
227

            
228
#[cfg(test)]
229
mod test {
230
    // @@ begin test lint list maintained by maint/add_warning @@
231
    #![allow(clippy::bool_assert_comparison)]
232
    #![allow(clippy::clone_on_copy)]
233
    #![allow(clippy::dbg_macro)]
234
    #![allow(clippy::mixed_attributes_style)]
235
    #![allow(clippy::print_stderr)]
236
    #![allow(clippy::print_stdout)]
237
    #![allow(clippy::single_char_pattern)]
238
    #![allow(clippy::unwrap_used)]
239
    #![allow(clippy::unchecked_time_subtraction)]
240
    #![allow(clippy::useless_vec)]
241
    #![allow(clippy::needless_pass_by_value)]
242
    #![allow(clippy::string_slice)] // See arti#2571
243
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
244

            
245
    use tracing_test::traced_test;
246

            
247
    use super::*;
248

            
249
    #[test]
250
    #[traced_test]
251
    fn evaluate() {
252
        let rec: ProtoStatuses = serde_json::from_str(
253
            r#"{
254
                "client": { "recommended" : "Relay=1-5", "required" : "Relay=3" },
255
                "relay": { "recommended": "", "required" : ""}
256
            }"#,
257
        )
258
        .unwrap();
259
        let rec_date = humantime::parse_rfc3339("2025-03-08T10:16:00Z").unwrap();
260

            
261
        // nothing missing.
262
        let r = evaluate_protocol_status(rec_date, &rec, &"Relay=1-10".parse().unwrap());
263
        assert!(r.is_ok());
264
        assert!(!logs_contain("listed as required"));
265
        assert!(!logs_contain("listed as recommended"));
266

            
267
        // Missing recommended.
268
        let r = evaluate_protocol_status(rec_date, &rec, &"Relay=1-4".parse().unwrap());
269
        assert!(r.is_ok());
270
        assert!(!logs_contain("listed as required"));
271
        assert!(logs_contain("listed as recommended"));
272

            
273
        // Missing required, no override.
274
        let r = evaluate_protocol_status(rec_date, &rec, &"Relay=1".parse().unwrap());
275
        assert!(r.is_err());
276
    }
277
}