Skip to main content

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}