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_config::MutCfg;
31use tor_dirmgr::DirProvider;
32use tor_error::{into_internal, warn_report};
33use tor_netdir::DirEvent;
34use tor_netdoc::doc::netstatus::{ProtoStatuses, ProtocolSupportError};
35use tor_protover::Protocols;
36use tor_rtcompat::{Runtime, SpawnExt as _};
37use tracing::{debug, error, info, warn};
38
39use 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`.
46pub(crate) fn enforce_protocol_recommendations<R, F, Fut>(
47    runtime: &R,
48    netdir_provider: Arc<dyn DirProvider>,
49    software_publication_time: SystemTime,
50    software_protocols: Protocols,
51    override_status: Arc<MutCfg<SoftwareStatusOverrideConfig>>,
52    on_fatal: F,
53) -> Result<(), ErrorDetail>
54where
55    R: Runtime,
56    F: FnOnce(ErrorDetail) -> Fut + Send + 'static,
57    Fut: Future<Output = ()> + Send + 'static,
58{
59    // We need to get this stream before we check the initial status, to avoid race conditions.
60    let events = netdir_provider.events();
61
62    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        None => None,
79    };
80
81    runtime
82        .spawn(watch_protocol_statuses(
83            netdir_provider,
84            events,
85            initial_evaluated_proto_status,
86            software_publication_time,
87            software_protocols,
88            override_status,
89            on_fatal,
90        ))
91        .map_err(|e| ErrorDetail::from_spawn("protocol status monitor", e))?;
92
93    Ok(())
94}
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.
103async fn watch_protocol_statuses<S, F, Fut>(
104    netdir_provider: Arc<dyn DirProvider>,
105    mut events: S,
106    mut last_evaluated_proto_status: Option<Arc<ProtoStatuses>>,
107    software_publication_time: SystemTime,
108    software_protocols: Protocols,
109    override_status: Arc<MutCfg<SoftwareStatusOverrideConfig>>,
110    on_fatal: F,
111) where
112    S: Stream<Item = DirEvent> + Send + Unpin,
113    F: FnOnce(ErrorDetail) -> Fut + Send,
114    Fut: Future<Output = ()> + Send,
115{
116    let weak_netdir_provider = Arc::downgrade(&netdir_provider);
117    drop(netdir_provider);
118
119    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.
179pub(crate) fn evaluate_protocol_status(
180    recommendation_timestamp: SystemTime,
181    recommendation: &ProtoStatuses,
182    software_protocols: &Protocols,
183    override_status: &SoftwareStatusOverrideConfig,
184) -> Result<(), ErrorDetail> {
185    let result = recommendation.client().check_protocols(software_protocols);
186
187    let rectime = || humantime::format_rfc3339(recommendation_timestamp);
188
189    match &result {
190        Ok(()) => Ok(()),
191        Err(ProtocolSupportError::MissingRecommended(missing))
192            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        Err(ProtocolSupportError::MissingRecommended(missing)) => {
201            info!(
202"At least one protocol not implemented by this version of Arti ({}) is listed as recommended for clients as of {}.
203Please upgrade to a more recent version of Arti.",
204                 missing, rectime());
205
206            Ok(())
207        }
208        Err(e @ ProtocolSupportError::MissingRequired(missing)) => {
209            error!(
210"At least one protocol not implemented by this version of Arti ({}) is listed as required for clients, as of {}.
211This version of Arti may not work correctly on the Tor network; please upgrade.",
212                  &missing, rectime());
213            if missing
214                .difference(&override_status.ignore_missing_required_protocols)
215                .is_empty()
216            {
217                warn!(
218                    "(These protocols are listed in 'ignore_missing_required_protocols', so Arti won't exit now, but you should still upgrade.)"
219                );
220                return Ok(());
221            }
222
223            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}
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.
247fn missing_recommended_ok() -> Protocols {
248    // TODO: Remove this once congestion control is fully implemented.
249    use tor_protover::named as n;
250    [n::FLOWCTRL_CC].into_iter().collect()
251}
252
253#[cfg(test)]
254mod 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}