arti_client/
protostatus.rs1use 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
40pub(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 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 evaluate_protocol_status(timestamp, &recommended, &software_protocols)?;
64
65 Some(recommended)
66 }
67 Some((_, _)) => {
68 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
88async 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 if timestamp < software_publication_time {
134 continue;
135 }
136 if last_evaluated_proto_status.as_ref() == Some(&new_status) {
137 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 }
153
154#[allow(clippy::cognitive_complexity)] pub(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 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
216fn missing_recommended_ok() -> Protocols {
223 use tor_protover::named as n;
225 [n::FLOWCTRL_CC].into_iter().collect()
226}
227
228#[cfg(test)]
229mod test {
230 #![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)] 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 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 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 let r = evaluate_protocol_status(rec_date, &rec, &"Relay=1".parse().unwrap());
275 assert!(r.is_err());
276 }
277}