1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
//! Core types for multiprobe
use std::net::IpAddr;
use std::time::Duration;
/// Protocol used for probing
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Protocol {
/// ICMP Echo Request/Reply
Icmp,
/// TCP connection probe
Tcp(u16),
/// UDP packet probe
Udp(u16),
}
impl Protocol {
/// Get the port number if applicable
pub fn port(&self) -> Option<u16> {
match self {
Protocol::Icmp => None,
Protocol::Tcp(port) | Protocol::Udp(port) => Some(*port),
}
}
/// Get a human-readable name
pub fn name(&self) -> &'static str {
match self {
Protocol::Icmp => "ICMP",
Protocol::Tcp(_) => "TCP",
Protocol::Udp(_) => "UDP",
}
}
}
impl std::fmt::Display for Protocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Protocol::Icmp => write!(f, "ICMP"),
Protocol::Tcp(port) => write!(f, "TCP/{port}"),
Protocol::Udp(port) => write!(f, "UDP/{port}"),
}
}
}
/// Timing breakdown for a probe operation
#[derive(Debug, Clone, Default)]
pub struct TimingBreakdown {
/// Time spent on DNS resolution (None if IP was provided directly)
pub dns_time: Option<Duration>,
/// Time to establish connection (TCP) or send packet (ICMP/UDP)
pub connect_time: Duration,
/// Time to receive response
pub response_time: Duration,
/// Total round-trip time
pub total_time: Duration,
}
impl TimingBreakdown {
/// Create a new timing breakdown
pub fn new(
dns_time: Option<Duration>,
connect_time: Duration,
response_time: Duration,
) -> Self {
let total_time = dns_time.unwrap_or_default() + connect_time + response_time;
Self {
dns_time,
connect_time,
response_time,
total_time,
}
}
/// Get DNS time in milliseconds
pub fn dns_ms(&self) -> Option<f64> {
self.dns_time.map(|d| d.as_secs_f64() * 1000.0)
}
/// Get connect time in milliseconds
pub fn connect_ms(&self) -> f64 {
self.connect_time.as_secs_f64() * 1000.0
}
/// Get response time in milliseconds
pub fn response_ms(&self) -> f64 {
self.response_time.as_secs_f64() * 1000.0
}
/// Get total time in milliseconds
pub fn total_ms(&self) -> f64 {
self.total_time.as_secs_f64() * 1000.0
}
}
/// Result of a single probe operation
#[derive(Debug, Clone)]
pub struct ProbeResult {
/// Target that was probed
pub target: String,
/// Resolved IP address
pub resolved_ip: IpAddr,
/// Protocol used
pub protocol: Protocol,
/// Whether the probe succeeded
pub success: bool,
/// Timing breakdown
pub timing: TimingBreakdown,
/// TTL/hop count from response (if available)
pub ttl: Option<u8>,
/// Error message if probe failed
pub error: Option<String>,
/// Response data (protocol-specific)
pub response_data: Option<ResponseData>,
}
/// Protocol-specific response data
#[derive(Debug, Clone)]
pub enum ResponseData {
/// ICMP response data
Icmp {
/// ICMP sequence number
sequence: u16,
/// Identifier
identifier: u16,
},
/// TCP response data
Tcp {
/// Whether connection was established
connected: bool,
/// Whether RST was received (port closed)
reset: bool,
},
/// UDP response data
Udp {
/// Bytes received in response
bytes_received: usize,
/// Whether ICMP port unreachable was received
port_unreachable: bool,
},
}
impl ProbeResult {
/// Create a successful probe result
pub fn success(
target: String,
resolved_ip: IpAddr,
protocol: Protocol,
timing: TimingBreakdown,
) -> Self {
Self {
target,
resolved_ip,
protocol,
success: true,
timing,
ttl: None,
error: None,
response_data: None,
}
}
/// Create a failed probe result
pub fn failure(
target: String,
resolved_ip: IpAddr,
protocol: Protocol,
error: String,
timing: TimingBreakdown,
) -> Self {
Self {
target,
resolved_ip,
protocol,
success: false,
timing,
ttl: None,
error: Some(error),
response_data: None,
}
}
/// Set TTL
pub fn with_ttl(mut self, ttl: u8) -> Self {
self.ttl = Some(ttl);
self
}
/// Set response data
pub fn with_response_data(mut self, data: ResponseData) -> Self {
self.response_data = Some(data);
self
}
}
/// Options for probe operations
#[derive(Debug, Clone)]
pub struct ProbeOptions {
/// Timeout for the entire operation
pub timeout: Duration,
/// Number of retries on failure
pub retries: u32,
/// TTL to set on outgoing packets
pub ttl: Option<u8>,
/// Source IP to bind to
pub source_ip: Option<IpAddr>,
/// Source port to bind to (TCP/UDP only)
pub source_port: Option<u16>,
}
impl Default for ProbeOptions {
fn default() -> Self {
Self {
timeout: Duration::from_secs(5),
retries: 0,
ttl: None,
source_ip: None,
source_port: None,
}
}
}
impl ProbeOptions {
/// Create options with specified timeout
pub fn with_timeout(timeout: Duration) -> Self {
Self {
timeout,
..Default::default()
}
}
}
/// Result of multi-protocol probe
#[derive(Debug, Clone)]
pub struct MultiProbeResult {
/// Target that was probed
pub target: String,
/// Individual probe results by protocol
pub results: Vec<ProbeResult>,
/// Path classification based on results
pub classification: Option<PathClassification>,
}
impl MultiProbeResult {
/// Get result for a specific protocol
pub fn get(&self, protocol: &Protocol) -> Option<&ProbeResult> {
self.results.iter().find(|r| &r.protocol == protocol)
}
/// Check if ICMP succeeded
pub fn icmp_success(&self) -> bool {
self.results.iter()
.any(|r| matches!(r.protocol, Protocol::Icmp) && r.success)
}
/// Check if TCP succeeded for a specific port
pub fn tcp_success(&self, port: u16) -> bool {
self.results.iter()
.any(|r| matches!(r.protocol, Protocol::Tcp(p) if p == port) && r.success)
}
/// Check if UDP succeeded for a specific port
pub fn udp_success(&self, port: u16) -> bool {
self.results.iter()
.any(|r| matches!(r.protocol, Protocol::Udp(p) if p == port) && r.success)
}
/// Classify the path based on probe results
pub fn classify(&self) -> String {
if let Some(ref class) = self.classification {
class.to_string()
} else {
"Unknown".to_string()
}
}
}
/// Path classification based on multi-protocol probe results
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathClassification {
/// All protocols succeed - direct/open path
Open,
/// ICMP blocked, TCP/UDP open - firewall with ICMP filter
IcmpFiltered,
/// Only specific ports open - selective firewall
SelectiveFirewall {
open_ports: Vec<u16>,
closed_ports: Vec<u16>,
},
/// All protocols blocked - host down or fully firewalled
Blocked,
/// ICMP succeeds, TCP fails - possible TCP-specific filtering
TcpFiltered,
/// Mixed results indicating NAT or load balancer
NatDetected,
/// Unable to classify
Unknown,
}
impl std::fmt::Display for PathClassification {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PathClassification::Open => write!(f, "Open (all protocols pass)"),
PathClassification::IcmpFiltered => write!(f, "ICMP Filtered (TCP/UDP open)"),
PathClassification::SelectiveFirewall { open_ports, closed_ports } => {
write!(f, "Selective Firewall (open: {open_ports:?}, closed: {closed_ports:?})")
}
PathClassification::Blocked => write!(f, "Blocked (all protocols fail)"),
PathClassification::TcpFiltered => write!(f, "TCP Filtered (ICMP open)"),
PathClassification::NatDetected => write!(f, "NAT/Load Balancer detected"),
PathClassification::Unknown => write!(f, "Unknown"),
}
}
}