1use futures::{task::SpawnExt as _, 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;
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!("Bug: Got DirEvent::NewProtocolRecommendation, but protocol_statuses() returned None.");
132 continue;
133 };
134 if timestamp < software_publication_time {
141 continue;
142 }
143 if last_evaluated_proto_status.as_ref() == Some(&new_status) {
144 continue;
146 }
147
148 if let Err(fatal) = evaluate_protocol_status(
149 timestamp,
150 &new_status,
151 &software_protocols,
152 override_status.get().as_ref(),
153 ) {
154 on_fatal(fatal).await;
155 return;
156 }
157 last_evaluated_proto_status = Some(new_status);
158 }
159
160 }
165
166#[allow(clippy::cognitive_complexity)] pub(crate) fn evaluate_protocol_status(
178 recommendation_timestamp: SystemTime,
179 recommendation: &ProtoStatuses,
180 software_protocols: &Protocols,
181 override_status: &SoftwareStatusOverrideConfig,
182) -> Result<(), ErrorDetail> {
183 let result = recommendation.client().check_protocols(software_protocols);
184
185 let rectime = || humantime::format_rfc3339(recommendation_timestamp);
186
187 match &result {
188 Ok(()) => Ok(()),
189 Err(ProtocolSupportError::MissingRecommended(missing))
190 if missing.difference(&missing_recommended_ok()).is_empty() =>
191 {
192 debug!("Recommended protocols ({}) are missing, but that's expected: we haven't built them them yet in Arti.",
193 missing);
194 Ok(())
195 }
196 Err(ProtocolSupportError::MissingRecommended(missing)) => {
197 info!(
198"At least one protocol not implemented by this version of Arti ({}) is listed as recommended for clients as of {}.
199Please upgrade to a more recent version of Arti.",
200 missing, rectime());
201
202 Ok(())
203 }
204 Err(e @ ProtocolSupportError::MissingRequired(missing)) => {
205 error!(
206"At least one protocol not implemented by this version of Arti ({}) is listed as required for clients, as of {}.
207This version of Arti may not work correctly on the Tor network; please upgrade.",
208 &missing, rectime());
209 if missing
210 .difference(&override_status.ignore_missing_required_protocols)
211 .is_empty()
212 {
213 warn!(
214"(These protocols are listed in 'ignore_missing_required_protocols', so Arti won't exit now, but you should still upgrade.)");
215 return Ok(());
216 }
217
218 Err(ErrorDetail::MissingProtocol(e.clone()))
219 }
220 Err(e) => {
221 warn_report!(
223 e,
224 "Unexpected problem while examining protocol recommendations"
225 );
226 if e.should_shutdown() {
227 return Err(ErrorDetail::Bug(into_internal!(
228 "Unexpected fatal protocol error"
229 )(e.clone())));
230 }
231 Ok(())
232 }
233 }
234}
235
236fn missing_recommended_ok() -> Protocols {
243 use tor_protover::named as n;
245 [n::FLOWCTRL_CC].into_iter().collect()
246}
247
248#[cfg(test)]
249mod test {
250 #![allow(clippy::bool_assert_comparison)]
252 #![allow(clippy::clone_on_copy)]
253 #![allow(clippy::dbg_macro)]
254 #![allow(clippy::mixed_attributes_style)]
255 #![allow(clippy::print_stderr)]
256 #![allow(clippy::print_stdout)]
257 #![allow(clippy::single_char_pattern)]
258 #![allow(clippy::unwrap_used)]
259 #![allow(clippy::unchecked_duration_subtraction)]
260 #![allow(clippy::useless_vec)]
261 #![allow(clippy::needless_pass_by_value)]
262 use tracing_test::traced_test;
265
266 use super::*;
267
268 #[test]
269 #[traced_test]
270 fn evaluate() {
271 let rec: ProtoStatuses = serde_json::from_str(
272 r#"{
273 "client": { "recommended" : "Relay=1-5", "required" : "Relay=3" },
274 "relay": { "recommended": "", "required" : ""}
275 }"#,
276 )
277 .unwrap();
278 let rec_date = humantime::parse_rfc3339("2025-03-08T10:16:00Z").unwrap();
279 let no_override = SoftwareStatusOverrideConfig {
280 ignore_missing_required_protocols: Protocols::default(),
281 };
282 let override_relay_3_4 = SoftwareStatusOverrideConfig {
283 ignore_missing_required_protocols: "Relay=3-4".parse().unwrap(),
284 };
285
286 let r =
288 evaluate_protocol_status(rec_date, &rec, &"Relay=1-10".parse().unwrap(), &no_override);
289 assert!(r.is_ok());
290 assert!(!logs_contain("listed as required"));
291 assert!(!logs_contain("listed as recommended"));
292
293 let r =
295 evaluate_protocol_status(rec_date, &rec, &"Relay=1-4".parse().unwrap(), &no_override);
296 assert!(r.is_ok());
297 assert!(!logs_contain("listed as required"));
298 assert!(logs_contain("listed as recommended"));
299
300 let r = evaluate_protocol_status(
302 rec_date,
303 &rec,
304 &"Relay=1".parse().unwrap(),
305 &override_relay_3_4,
306 );
307 assert!(r.is_ok());
308 assert!(logs_contain("listed as required"));
309 assert!(logs_contain("but you should still upgrade"));
310
311 let r = evaluate_protocol_status(rec_date, &rec, &"Relay=1".parse().unwrap(), &no_override);
313 assert!(r.is_err());
314 }
315}