tor_circmgr/
preemptive.rs1use crate::{PathConfig, PreemptiveCircuitConfig, TargetPort, TargetTunnelUsage};
4use std::collections::HashMap;
5use std::sync::Arc;
6use tracing::warn;
7use web_time_compat::{Instant, InstantExt};
8
9pub(crate) struct PreemptiveCircuitPredictor {
12 usages: HashMap<Option<TargetPort>, Instant>,
17
18 config: tor_config::MutCfg<PreemptiveCircuitConfig>,
20}
21
22impl PreemptiveCircuitPredictor {
23 pub(crate) fn new(config: PreemptiveCircuitConfig) -> Self {
25 let mut usages = HashMap::new();
26 for port in &config.initial_predicted_ports {
27 usages.insert(Some(TargetPort::ipv4(*port)), Instant::get());
29 }
30
31 usages.insert(None, Instant::get());
33
34 Self {
35 usages,
36 config: config.into(),
37 }
38 }
39
40 pub(crate) fn config(&self) -> Arc<PreemptiveCircuitConfig> {
42 self.config.get()
43 }
44
45 pub(crate) fn set_config(&self, mut new_config: PreemptiveCircuitConfig) {
48 self.config.map_and_replace(|cfg| {
49 new_config
51 .initial_predicted_ports
52 .clone_from(&cfg.initial_predicted_ports);
53 new_config
54 });
55 }
56
57 pub(crate) fn predict(&self, path_config: &PathConfig) -> Vec<TargetTunnelUsage> {
59 let config = self.config();
60 let now = Instant::get();
61 let circs = config.min_exit_circs_for_port;
62 self.usages
63 .iter()
64 .filter(|&(_, &time)| {
65 time.checked_add(config.prediction_lifetime)
66 .map(|t| t > now)
67 .unwrap_or_else(|| {
68 warn!("failed to represent preemptive circuit prediction lifetime as an Instant");
71 false
72 })
73 })
74 .map(|(&port, _)| {
75 let require_stability = port.is_some_and(|p| path_config.long_lived_ports.contains(&p.port));
76 TargetTunnelUsage::Preemptive {
77 port, circs, require_stability,
78 }
79 })
80 .collect()
81 }
82
83 pub(crate) fn note_usage(&mut self, port: Option<TargetPort>, time: Instant) {
90 self.usages.insert(port, time);
91 }
92}
93
94#[cfg(test)]
95mod test {
96 #![allow(clippy::bool_assert_comparison)]
98 #![allow(clippy::clone_on_copy)]
99 #![allow(clippy::dbg_macro)]
100 #![allow(clippy::mixed_attributes_style)]
101 #![allow(clippy::print_stderr)]
102 #![allow(clippy::print_stdout)]
103 #![allow(clippy::single_char_pattern)]
104 #![allow(clippy::unwrap_used)]
105 #![allow(clippy::unchecked_time_subtraction)]
106 #![allow(clippy::useless_vec)]
107 #![allow(clippy::needless_pass_by_value)]
108 #![allow(clippy::string_slice)] use crate::{
111 PathConfig, PreemptiveCircuitConfig, PreemptiveCircuitPredictor, TargetPort,
112 TargetTunnelUsage,
113 };
114 use web_time_compat::{Duration, Instant, InstantExt};
115
116 use crate::isolation::test::{IsolationTokenEq, assert_isoleq};
117
118 #[test]
119 fn predicts_starting_ports() {
120 let path_config = PathConfig::default();
121 let mut cfg = PreemptiveCircuitConfig::builder();
122 cfg.set_initial_predicted_ports(vec![]);
123 cfg.prediction_lifetime(Duration::from_secs(2));
124 let predictor = PreemptiveCircuitPredictor::new(cfg.build().unwrap());
125
126 assert_isoleq!(
127 predictor.predict(&path_config),
128 vec![TargetTunnelUsage::Preemptive {
129 port: None,
130 circs: 2,
131 require_stability: false,
132 }]
133 );
134
135 let mut cfg = PreemptiveCircuitConfig::builder();
136 cfg.set_initial_predicted_ports(vec![80]);
137 cfg.prediction_lifetime(Duration::from_secs(2));
138 let predictor = PreemptiveCircuitPredictor::new(cfg.build().unwrap());
139
140 let results = predictor.predict(&path_config);
141 assert_eq!(results.len(), 2);
142 assert!(
143 results
144 .iter()
145 .any(|r| r.isol_eq(&TargetTunnelUsage::Preemptive {
146 port: None,
147 circs: 2,
148 require_stability: false,
149 }))
150 );
151 assert!(
152 results
153 .iter()
154 .any(|r| r.isol_eq(&TargetTunnelUsage::Preemptive {
155 port: Some(TargetPort::ipv4(80)),
156 circs: 2,
157 require_stability: false,
158 }))
159 );
160 }
161
162 #[test]
163 fn predicts_used_ports() {
164 let path_config = PathConfig::default();
165 let mut cfg = PreemptiveCircuitConfig::builder();
166 cfg.set_initial_predicted_ports(vec![]);
167 cfg.prediction_lifetime(Duration::from_secs(2));
168 let mut predictor = PreemptiveCircuitPredictor::new(cfg.build().unwrap());
169
170 assert_isoleq!(
171 predictor.predict(&path_config),
172 vec![TargetTunnelUsage::Preemptive {
173 port: None,
174 circs: 2,
175 require_stability: false,
176 }]
177 );
178
179 predictor.note_usage(Some(TargetPort::ipv4(1234)), Instant::get());
180
181 let results = predictor.predict(&path_config);
182 assert_eq!(results.len(), 2);
183 assert!(
184 results
185 .iter()
186 .any(|r| r.isol_eq(&TargetTunnelUsage::Preemptive {
187 port: None,
188 circs: 2,
189 require_stability: false,
190 }))
191 );
192 assert!(
193 results
194 .iter()
195 .any(|r| r.isol_eq(&TargetTunnelUsage::Preemptive {
196 port: Some(TargetPort::ipv4(1234)),
197 circs: 2,
198 require_stability: false,
199 }))
200 );
201 }
202
203 #[test]
204 fn does_not_predict_old_ports() {
205 let path_config = PathConfig::default();
206 let mut cfg = PreemptiveCircuitConfig::builder();
207 cfg.set_initial_predicted_ports(vec![]);
208 cfg.prediction_lifetime(Duration::from_secs(2));
209 let mut predictor = PreemptiveCircuitPredictor::new(cfg.build().unwrap());
210 let now = Instant::get();
211 let three_seconds_ago = now - Duration::from_secs(2 + 1);
212
213 predictor.note_usage(Some(TargetPort::ipv4(2345)), three_seconds_ago);
214
215 assert_isoleq!(
216 predictor.predict(&path_config),
217 vec![TargetTunnelUsage::Preemptive {
218 port: None,
219 circs: 2,
220 require_stability: false,
221 }]
222 );
223 }
224}