Skip to main content

manycastr/worker/outbound/
trace.rs

1use crate::custom_module::manycastr::{ProtocolType, Trace};
2use crate::net::packet::{
3    ProbePayload, TraceDnsId, create_icmp, create_tcp_trace, create_udp_trace,
4};
5use crate::worker::outbound::{OutboundConfig, send_packet};
6use crate::worker::trace_codec::TraceTag;
7use log::warn;
8use socket2::Socket;
9use std::time::{SystemTime, UNIX_EPOCH};
10
11/// Sends a traceroute probe based on the provided trace task, protocol, and configuration.
12///
13/// Supports ICMP, UDP (Paris), and TCP (Paris) traceroute.
14///
15/// - **ICMP**: identifier (worker_hi + timestamp) + sequence (TTL + worker_lo)
16/// - **UDP (Paris)**: IP identification/flow label (worker_hi + timestamp) + UDP checksum (TTL + worker_lo)
17/// - **TCP (Paris)**: seq number (worker_id + TTL + timestamp)
18///
19/// # Arguments
20/// * `config` - The outbound configuration (worker, measurement, origin, and protocol details).
21/// * `trace_task` - The traceroute task containing destination and TTL information.
22/// * `socket` - The socket to send the packet on.
23pub fn send_trace(config: &OutboundConfig, trace_task: &Trace, socket: &Socket) -> (u32, u32) {
24    let worker_id = config.worker_id as u32;
25    let p_type = config.p_type;
26    let src = &config.src;
27    let info_url = config.info_url.as_deref();
28    let target = &trace_task.dst.unwrap();
29    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
30    let tx_micros = now.as_micros() as u64;
31    let tag = TraceTag {
32        worker_id,
33        ttl: trace_task.ttl as u8,
34        ts14: (now.as_millis() & 0x3FFF) as u16,
35    };
36    let ttl = tag.ttl;
37    // No probe ID for trace tasks (no space in probe encoding)
38    let probe_id = crate::probe_id(config.m_id, 0);
39
40    let packet = match p_type {
41        ProtocolType::Icmp => {
42            // ICMP: encode in identifier + sequence number
43            let (identifier, sequence_number) = tag.encode_split();
44
45            let payload_fields = ProbePayload {
46                worker_id,
47                probe_id,
48                trace_ttl: Some(ttl),
49                info_url,
50            };
51
52            create_icmp(
53                src,
54                target,
55                identifier,
56                sequence_number,
57                &payload_fields,
58                ttl,
59            )
60        }
61
62        ProtocolType::ADns | ProtocolType::ChaosDns => {
63            // UDP (Paris): IP identification/flow label + UDP checksum carry the identity.
64            let (identifier, desired_checksum) = tag.encode_split();
65
66            create_udp_trace(
67                src,
68                target,
69                config.sport,
70                config.dport,
71                identifier,
72                desired_checksum,
73                &TraceDnsId {
74                    tx_id: worker_id,
75                    probe_id,
76                    tx_micros,
77                    ttl,
78                    qname: config.qname.as_deref().unwrap_or("example.org"),
79                },
80            )
81        }
82
83        ProtocolType::Tcp => {
84            // TCP (Paris): the whole identity is packed into the 32-bit sequence number.
85            let seq = tag.encode_tcp_seq();
86
87            create_tcp_trace(src, target, config.sport, config.dport, seq, ttl, info_url)
88        }
89    };
90
91    // For ICMP on IPv6, the kernel writes the header, so we set hop limit via socket option.
92    // For UDP/TCP on IPv6, we include the IPv6 header (header_included_v6), so TTL is in the packet.
93    // TODO header included does not work for IPv6?
94    if src.is_v6()
95        && p_type == ProtocolType::Icmp
96        && let Err(e) = socket.set_unicast_hops_v6(trace_task.ttl)
97    {
98        warn!(
99            "[Worker outbound] Failed to set IPv6 hop limit to {}: {e}",
100            trace_task.ttl
101        );
102    }
103
104    let result = match send_packet(
105        socket,
106        &packet,
107        &trace_task.dst.expect("invalid destination"),
108    ) {
109        Ok(()) => (1, 0),
110        Err(e) => {
111            warn!("[Worker outbound] Failed to send {p_type} traceroute packet: {e}");
112            (0, 1)
113        }
114    };
115
116    // Restore the default hop limit for ICMP IPv6
117    if src.is_v6() && p_type == ProtocolType::Icmp {
118        let _ = socket.set_unicast_hops_v6(255);
119    }
120
121    result
122}