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_config::MutCfg;
31
use tor_dirmgr::DirProvider;
32
use tor_error::{into_internal, warn_report};
33
use tor_netdir::DirEvent;
34
use tor_netdoc::doc::netstatus::{ProtoStatuses, ProtocolSupportError};
35
use tor_protover::Protocols;
36
use tor_rtcompat::{Runtime, SpawnExt as _};
37
use tracing::{debug, error, info, warn};
38

            
39
use crate::{config::SoftwareStatusOverrideConfig, err::ErrorDetail};
40

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

            
62
22
    let initial_evaluated_proto_status = match netdir_provider.protocol_statuses() {
63
        Some((timestamp, recommended)) if timestamp >= software_publication_time => {
64
            // Here we exit if the initial (cached) status is bogus.
65
            evaluate_protocol_status(
66
                timestamp,
67
                &recommended,
68
                &software_protocols,
69
                override_status.get().as_ref(),
70
            )?;
71

            
72
            Some(recommended)
73
        }
74
        Some((_, _)) => {
75
            // In this case, our software is newer than the consensus, so we don't enforce it.
76
            None
77
        }
78
22
        None => None,
79
    };
80

            
81
22
    runtime
82
22
        .spawn(watch_protocol_statuses(
83
22
            netdir_provider,
84
22
            events,
85
22
            initial_evaluated_proto_status,
86
22
            software_publication_time,
87
22
            software_protocols,
88
22
            override_status,
89
22
            on_fatal,
90
        ))
91
22
        .map_err(|e| ErrorDetail::from_spawn("protocol status monitor", e))?;
92

            
93
22
    Ok(())
94
22
}
95

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

            
119
22
    while let Some(e) = events.next().await {
120
        if e != DirEvent::NewProtocolRecommendation {
121
            continue;
122
        }
123

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

            
150
        if let Err(fatal) = evaluate_protocol_status(
151
            timestamp,
152
            &new_status,
153
            &software_protocols,
154
            override_status.get().as_ref(),
155
        ) {
156
            on_fatal(fatal).await;
157
            return;
158
        }
159
        last_evaluated_proto_status = Some(new_status);
160
    }
161

            
162
    // If we reach this point,
163
    // either we failed to upgrade the weak reference (because the netdir provider went away)
164
    // or the event stream was closed.
165
    // Either of these cases implies a clean shutdown.
166
}
167

            
168
/// Check whether we should take action based on the protocol `recommendation`
169
/// from `recommendation_timestamp`,
170
/// given that our own supported subprotocols are `software_protocols`.
171
///
172
/// - If any required protocols are missing, log and return an error.
173
/// - If no required protocols are missing, but some recommended protocols are missing,
174
///   log and return `Ok(())`.
175
/// - If no protocols are missing, return `Ok(())`.
176
///
177
/// Note: This function should ONLY return an error when the error is fatal.
178
#[allow(clippy::cognitive_complexity)] // complexity caused by trace macros.
179
8
pub(crate) fn evaluate_protocol_status(
180
8
    recommendation_timestamp: SystemTime,
181
8
    recommendation: &ProtoStatuses,
182
8
    software_protocols: &Protocols,
183
8
    override_status: &SoftwareStatusOverrideConfig,
184
8
) -> Result<(), ErrorDetail> {
185
8
    let result = recommendation.client().check_protocols(software_protocols);
186

            
187
11
    let rectime = || humantime::format_rfc3339(recommendation_timestamp);
188

            
189
2
    match &result {
190
2
        Ok(()) => Ok(()),
191
2
        Err(ProtocolSupportError::MissingRecommended(missing))
192
2
            if missing.difference(&missing_recommended_ok()).is_empty() =>
193
        {
194
            debug!(
195
                "Recommended protocols ({}) are missing, but that's expected: we haven't built them yet in Arti.",
196
                missing
197
            );
198
            Ok(())
199
        }
200
2
        Err(ProtocolSupportError::MissingRecommended(missing)) => {
201
2
            info!(
202
"At least one protocol not implemented by this version of Arti ({}) is listed as recommended for clients as of {}.
203
Please upgrade to a more recent version of Arti.",
204
                 missing, rectime());
205

            
206
2
            Ok(())
207
        }
208
4
        Err(e @ ProtocolSupportError::MissingRequired(missing)) => {
209
4
            error!(
210
"At least one protocol not implemented by this version of Arti ({}) is listed as required for clients, as of {}.
211
This version of Arti may not work correctly on the Tor network; please upgrade.",
212
                  &missing, rectime());
213
4
            if missing
214
4
                .difference(&override_status.ignore_missing_required_protocols)
215
4
                .is_empty()
216
            {
217
2
                warn!(
218
                    "(These protocols are listed in 'ignore_missing_required_protocols', so Arti won't exit now, but you should still upgrade.)"
219
                );
220
2
                return Ok(());
221
2
            }
222

            
223
2
            Err(ErrorDetail::MissingProtocol(e.clone()))
224
        }
225
        Err(e) => {
226
            // Because ProtocolSupportError is non-exhaustive, we need this case.
227
            warn_report!(
228
                e,
229
                "Unexpected problem while examining protocol recommendations"
230
            );
231
            if e.should_shutdown() {
232
                return Err(ErrorDetail::Bug(into_internal!(
233
                    "Unexpected fatal protocol error"
234
                )(e.clone())));
235
            }
236
            Ok(())
237
        }
238
    }
239
8
}
240

            
241
/// Return a list of the protocols which may be recommended,
242
/// and which we know are missing in Arti.
243
///
244
/// This function should go away in the future:
245
/// we use it to generate a slightly less alarming warning
246
/// when we have an _expected_ missing recommended protocol.
247
2
fn missing_recommended_ok() -> Protocols {
248
    // TODO: Remove this once congestion control is fully implemented.
249
    use tor_protover::named as n;
250
2
    [n::FLOWCTRL_CC].into_iter().collect()
251
2
}
252

            
253
#[cfg(test)]
254
mod test {
255
    // @@ begin test lint list maintained by maint/add_warning @@
256
    #![allow(clippy::bool_assert_comparison)]
257
    #![allow(clippy::clone_on_copy)]
258
    #![allow(clippy::dbg_macro)]
259
    #![allow(clippy::mixed_attributes_style)]
260
    #![allow(clippy::print_stderr)]
261
    #![allow(clippy::print_stdout)]
262
    #![allow(clippy::single_char_pattern)]
263
    #![allow(clippy::unwrap_used)]
264
    #![allow(clippy::unchecked_time_subtraction)]
265
    #![allow(clippy::useless_vec)]
266
    #![allow(clippy::needless_pass_by_value)]
267
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
268

            
269
    use tracing_test::traced_test;
270

            
271
    use super::*;
272

            
273
    #[test]
274
    #[traced_test]
275
    fn evaluate() {
276
        let rec: ProtoStatuses = serde_json::from_str(
277
            r#"{
278
                "client": { "recommended" : "Relay=1-5", "required" : "Relay=3" },
279
                "relay": { "recommended": "", "required" : ""}
280
            }"#,
281
        )
282
        .unwrap();
283
        let rec_date = humantime::parse_rfc3339("2025-03-08T10:16:00Z").unwrap();
284
        let no_override = SoftwareStatusOverrideConfig {
285
            ignore_missing_required_protocols: Protocols::default(),
286
        };
287
        let override_relay_3_4 = SoftwareStatusOverrideConfig {
288
            ignore_missing_required_protocols: "Relay=3-4".parse().unwrap(),
289
        };
290

            
291
        // nothing missing.
292
        let r =
293
            evaluate_protocol_status(rec_date, &rec, &"Relay=1-10".parse().unwrap(), &no_override);
294
        assert!(r.is_ok());
295
        assert!(!logs_contain("listed as required"));
296
        assert!(!logs_contain("listed as recommended"));
297

            
298
        // Missing recommended.
299
        let r =
300
            evaluate_protocol_status(rec_date, &rec, &"Relay=1-4".parse().unwrap(), &no_override);
301
        assert!(r.is_ok());
302
        assert!(!logs_contain("listed as required"));
303
        assert!(logs_contain("listed as recommended"));
304

            
305
        // Missing required, but override is there.
306
        let r = evaluate_protocol_status(
307
            rec_date,
308
            &rec,
309
            &"Relay=1".parse().unwrap(),
310
            &override_relay_3_4,
311
        );
312
        assert!(r.is_ok());
313
        assert!(logs_contain("listed as required"));
314
        assert!(logs_contain("but you should still upgrade"));
315

            
316
        // Missing required, no override.
317
        let r = evaluate_protocol_status(rec_date, &rec, &"Relay=1".parse().unwrap(), &no_override);
318
        assert!(r.is_err());
319
    }
320
}