manycastr/orchestrator/result_handler.rs
1use crate::custom_module::manycastr::{DiscoveryReply, Probe, Task, Trace, TraceReply, task};
2pub(crate) use crate::orchestrator::trace::{
3 SessionTracker, TraceIdentifier, TraceProgress, TraceSession, ttl_midpoint,
4};
5use crate::orchestrator::{TracerouteConfig, wire_nprobes};
6use std::collections::{HashMap, VecDeque};
7use std::time::{Duration, Instant};
8
9/// Takes a TaskResult containing discovery probe replies for --responsive or --latency probes.
10///
11/// # Arguments
12/// * `discovery_results` - List of discovery results
13/// * `worker_id` - worker that will perform the follow-up tasks
14/// * `worker_stacks` - shared stack to put worker tasks in
15/// * `origin_id` - Origin for which these replies are received
16/// * `nprobes` - number of times the worker sends each follow-up probe
17pub fn discovery_handler(
18 discovery_results: Vec<DiscoveryReply>,
19 worker_id: u32,
20 worker_stacks: &mut HashMap<u32, VecDeque<Task>>,
21 origin_id: u32,
22 nprobes: u32,
23) {
24 // Get the discovery results as a vector of tasks
25 let responsive_targets: Vec<Task> = discovery_results
26 .iter()
27 .map(|result| Task {
28 task_type: Some(task::TaskType::Probe(Probe { dst: result.src })),
29 origin_id,
30 nprobes: wire_nprobes(nprobes),
31 session_id: 0, // sessions are feed only (not hitlist-based)
32 })
33 .collect();
34
35 // Assign follow-up probes to the 'catcher' stack
36 worker_stacks
37 .entry(worker_id)
38 .or_default()
39 .extend(responsive_targets);
40}
41
42/// Handles discovery replies for traceroute measurements.
43/// Initializes a `TraceSession` for a traceroute from the catching worker to the target.
44/// Also instruct the catching Worker to send a `Trace` with TTL = 1.
45///
46/// # Arguments
47/// * `discovery_results` - List of discovery results
48/// * `worker_id` - Worker that received the discovery results and will perform the traceroute
49/// * `worker_stacks` - Shared stack to put follow-up tasks into
50/// * `traceroute_config` - Traceroute parameters
51/// * `origin_id` - Origin for which these replies are received
52pub fn trace_discovery_handler(
53 discovery_results: Vec<DiscoveryReply>,
54 catcher_id: u32,
55 worker_stacks: &mut HashMap<u32, VecDeque<Task>>,
56 traceroute_config: &mut TracerouteConfig,
57 origin_id: u32,
58) {
59 let stack = worker_stacks.entry(catcher_id).or_default();
60
61 // Discovery replies
62 for result in discovery_results {
63 // Create an ongoing TraceSession for each discovery reply
64 let target = result.src;
65
66 // Create Trace identifier
67 let identifier = TraceIdentifier {
68 worker_id: catcher_id,
69 target: target.unwrap(),
70 origin_id,
71 };
72
73 // Init Trace session
74 let session = TraceSession {
75 worker_id: catcher_id,
76 target,
77 origin_id,
78 progress: TraceProgress::Linear {
79 current_ttl: traceroute_config.initial_hop as u8,
80 consecutive_failures: 0,
81 },
82 last_updated: Instant::now(),
83 };
84
85 traceroute_config
86 .session_tracker
87 .sessions
88 .insert(identifier.clone(), session);
89 // Add deadline
90 let deadline = Instant::now() + Duration::from_secs(traceroute_config.timeout);
91 traceroute_config
92 .session_tracker
93 .expiration_queue
94 .push_back((identifier, deadline));
95
96 stack.push_back(Task {
97 task_type: Some(task::TaskType::Trace(Trace {
98 dst: target,
99 ttl: traceroute_config.initial_hop,
100 })),
101 origin_id,
102 nprobes: 0, // single send
103 session_id: 0, // trace probes carry no session
104 });
105 }
106}
107
108/// Awaits `Trace` replies (i.e., ICMP Time Exceeded).
109/// Updates the corresponding `TraceSession`, including the timeout, and follows
110/// up with the next `Trace` task according to the session's progress strategy:
111///
112/// - **Linear** (anycast-traceroute): probe TTL + 1
113/// - **Binary** (tracemap): the responding TTL becomes the new lower search bound;
114/// probe the midpoint of the remaining range
115///
116/// If a regular reply (from the target) is received, it closes the `TraceSession`.
117///
118/// # Arguments
119/// * `trace_replies` - A list of traceroute results
120/// * `worker_stacks` - Stacks for workers to put follow-up trace tasks into
121/// * `traceroute_config` - Configuration and state for the ongoing traceroute measurement
122///
123/// # Returns
124/// The replies that matched an active trace session (to be forwarded to the CLI).
125/// Replies without a matching session (stray/foreign packets that passed the
126/// worker's filters, or replies arriving after their session closed) are dropped.
127pub fn trace_replies_handler(
128 trace_replies: Vec<TraceReply>,
129 worker_stacks: &mut HashMap<u32, VecDeque<Task>>,
130 traceroute_config: &mut TracerouteConfig,
131 origin_id: u32,
132) -> Vec<TraceReply> {
133 let max_hops = traceroute_config.max_hops;
134 let max_failures = traceroute_config.max_failures;
135 let session_tracker = &mut traceroute_config.session_tracker;
136 let mut matched = Vec::with_capacity(trace_replies.len());
137
138 for trace_reply in trace_replies {
139 // Get identifier of corresponding trace
140 let identifier = TraceIdentifier {
141 worker_id: trace_reply.tx_id,
142 target: trace_reply.trace_dst.unwrap(),
143 origin_id,
144 };
145
146 // Find session of corresponding trace (drop replies without one)
147 let Some(session) = session_tracker.sessions.get_mut(&identifier) else {
148 continue;
149 };
150
151 let dest_reached = trace_reply.hop_addr.unwrap() == trace_reply.trace_dst.unwrap();
152 let target = session.target;
153
154 // Advance the session; None means it is finished
155 let next_ttl = match &mut session.progress {
156 TraceProgress::Linear {
157 current_ttl,
158 consecutive_failures,
159 } => {
160 *current_ttl += 1;
161 *consecutive_failures = 0;
162
163 if *current_ttl > max_hops as u8 || dest_reached {
164 // Routing loop or destination reached -> close session
165 None
166 } else {
167 Some(*current_ttl)
168 }
169 }
170 TraceProgress::Binary {
171 lo,
172 hi,
173 mid,
174 probing_ttl,
175 window_left,
176 } => {
177 let answered = trace_reply.hop_count as u8;
178 if answered < *lo {
179 // Duplicate reply for an already-measured TTL
180 continue;
181 }
182
183 if dest_reached {
184 None
185 } else {
186 // Deepest responder so far -> search the deeper half
187 *lo = answered + 1;
188 if *lo > *hi {
189 None // Search converged: deepest responder found
190 } else {
191 *mid = ttl_midpoint(*lo, *hi);
192 *probing_ttl = *mid;
193 *window_left = max_failures as u8;
194 Some(*probing_ttl)
195 }
196 }
197 }
198 };
199
200 session.last_updated = Instant::now();
201
202 if let Some(next_ttl) = next_ttl {
203 // Send trace task for the next hop
204 worker_stacks
205 .entry(trace_reply.tx_id)
206 .or_default()
207 .push_back(Task {
208 task_type: Some(task::TaskType::Trace(Trace {
209 dst: target,
210 ttl: next_ttl as u32,
211 })),
212 origin_id,
213 nprobes: 0, // single send
214 session_id: 0, // trace probes carry no session
215 });
216 } else {
217 session_tracker.sessions.remove(&identifier);
218 }
219
220 matched.push(trace_reply);
221 }
222
223 matched
224}