1use 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
41pub(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 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 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 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
96async 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 if timestamp < software_publication_time {
143 continue;
144 }
145 if last_evaluated_proto_status.as_ref() == Some(&new_status) {
146 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 }
167
168#[allow(clippy::cognitive_complexity)] pub(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 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
241fn missing_recommended_ok() -> Protocols {
248 use tor_protover::named as n;
250 [n::FLOWCTRL_CC].into_iter().collect()
251}
252
253#[cfg(test)]
254mod test {
255 #![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 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 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 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 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 let r = evaluate_protocol_status(rec_date, &rec, &"Relay=1".parse().unwrap(), &no_override);
318 assert!(r.is_err());
319 }
320}