Skip to main content

arti_client/
protostatus.rs

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
24use futures::{Stream, StreamExt as _};
25use std::{
26    future::Future,
27    sync::{Arc, Weak},
28    time::SystemTime,
29};
30use tor_dirmgr::DirProvider;
31use tor_error::{into_internal, warn_report};
32use tor_netdir::DirEvent;
33use tor_netdoc::doc::netstatus::{ProtoStatuses, ProtocolSupportError};
34use tor_protover::Protocols;
35use tor_rtcompat::{Runtime, SpawnExt as _};
36use tracing::{debug, error, info, warn};
37
38use 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`.
45pub(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>
52where
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.
95async 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.
165pub(crate) fn evaluate_protocol_status(
166    recommendation_timestamp: SystemTime,
167    recommendation: &ProtoStatuses,
168    software_protocols: &Protocols,
169) -> Result<(), ErrorDetail> {
170    let result = recommendation.client().check_protocols(software_protocols);
171
172    let rectime = || humantime::format_rfc3339(recommendation_timestamp);
173
174    match &result {
175        Ok(()) => Ok(()),
176        Err(ProtocolSupportError::MissingRecommended(missing))
177            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        Err(ProtocolSupportError::MissingRecommended(missing)) => {
186            info!(
187"At least one protocol not implemented by this version of Arti ({}) is listed as recommended for clients as of {}.
188Please upgrade to a more recent version of Arti.",
189                 missing, rectime());
190
191            Ok(())
192        }
193        Err(e @ ProtocolSupportError::MissingRequired(missing)) => {
194            error!(
195"At least one protocol not implemented by this version of Arti ({}) is listed as required for clients, as of {}.
196This version of Arti may not work correctly on the Tor network; please upgrade.",
197                  &missing, rectime());
198            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}
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.
222fn missing_recommended_ok() -> Protocols {
223    // TODO: Remove this once congestion control is fully implemented.
224    use tor_protover::named as n;
225    [n::FLOWCTRL_CC].into_iter().collect()
226}
227
228#[cfg(test)]
229mod 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}