use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::{Duration, Instant};
use std::mem::MaybeUninit;
use socket2::{Domain, Protocol, Socket, Type};
use crate::dns;
use crate::error::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FlowId {
pub src_port: u16,
pub dst_port: u16,
pub protocol: u8,
}
impl FlowId {
pub fn udp(src_port: u16, dst_port: u16) -> Self {
Self {
src_port,
dst_port,
protocol: 17,
}
}
pub fn tcp(src_port: u16, dst_port: u16) -> Self {
Self {
src_port,
dst_port,
protocol: 6,
}
}
pub fn icmp(identifier: u16) -> Self {
Self {
src_port: identifier,
dst_port: 0,
protocol: 1,
}
}
pub fn default_paris() -> Self {
Self::udp(33434, 33434)
}
}
impl Default for FlowId {
fn default() -> Self {
Self::default_paris()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParisMode {
Udp,
Icmp,
Tcp,
}
impl Default for ParisMode {
fn default() -> Self {
Self::Udp
}
}
#[derive(Debug, Clone)]
pub struct ParisHop {
pub ttl: u8,
pub addr: Option<IpAddr>,
pub rtt: Duration,
pub responded: bool,
pub icmp_type: Option<u8>,
pub icmp_code: Option<u8>,
pub flow_id: FlowId,
pub mpls_labels: Vec<MplsLabel>,
}
#[derive(Debug, Clone)]
pub struct MplsLabel {
pub label: u32,
pub experimental: u8,
pub bottom_of_stack: bool,
pub ttl: u8,
}
impl ParisHop {
fn timeout(ttl: u8, flow_id: FlowId, timeout: Duration) -> Self {
Self {
ttl,
addr: None,
rtt: timeout,
responded: false,
icmp_type: None,
icmp_code: None,
flow_id,
mpls_labels: Vec::new(),
}
}
fn success(ttl: u8, addr: IpAddr, rtt: Duration, icmp_type: u8, icmp_code: u8, flow_id: FlowId) -> Self {
Self {
ttl,
addr: Some(addr),
rtt,
responded: true,
icmp_type: Some(icmp_type),
icmp_code: Some(icmp_code),
flow_id,
mpls_labels: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct ParisTraceResult {
pub target: String,
pub target_ip: IpAddr,
pub hops: Vec<ParisHop>,
pub reached_destination: bool,
pub flow_id: FlowId,
pub mode: ParisMode,
pub total_time: Duration,
pub load_balancing: LoadBalancingType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadBalancingType {
None,
PerFlow,
PerPacket,
Unknown,
}
impl std::fmt::Display for LoadBalancingType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LoadBalancingType::None => write!(f, "None"),
LoadBalancingType::PerFlow => write!(f, "Per-flow ECMP"),
LoadBalancingType::PerPacket => write!(f, "Per-packet ECMP"),
LoadBalancingType::Unknown => write!(f, "Unknown"),
}
}
}
impl ParisTraceResult {
pub fn responsive_hops(&self) -> Vec<&ParisHop> {
self.hops.iter().filter(|h| h.responded).collect()
}
pub fn path(&self) -> Vec<IpAddr> {
self.hops.iter().filter_map(|h| h.addr).collect()
}
pub fn contains_ip(&self, ip: IpAddr) -> bool {
self.hops.iter().any(|h| h.addr == Some(ip))
}
pub fn hop_count(&self) -> usize {
self.responsive_hops().len()
}
pub fn has_asymmetry(&self) -> bool {
let responsive: Vec<_> = self.responsive_hops();
if responsive.len() < 3 {
return false;
}
for i in 1..responsive.len() {
if responsive[i].rtt < responsive[i - 1].rtt {
let decrease = responsive[i - 1].rtt.as_micros() - responsive[i].rtt.as_micros();
if decrease > 5000 {
return true;
}
}
}
false
}
}
#[derive(Debug, Clone)]
pub struct ParisOptions {
pub max_hops: u8,
pub timeout_per_hop: Duration,
pub probes_per_hop: u8,
pub mode: ParisMode,
pub flow_id: FlowId,
pub detect_load_balancing: bool,
pub lb_detection_flows: u8,
}
impl Default for ParisOptions {
fn default() -> Self {
Self {
max_hops: 30,
timeout_per_hop: Duration::from_secs(2),
probes_per_hop: 1,
mode: ParisMode::Udp,
flow_id: FlowId::default_paris(),
detect_load_balancing: false,
lb_detection_flows: 6,
}
}
}
pub async fn paris_traceroute(target: &str, options: &ParisOptions) -> crate::Result<ParisTraceResult> {
let start_time = Instant::now();
let dns_result = dns::resolve_ipv4(target).await?;
let target_ip = dns_result.ip;
let target_ipv4 = match target_ip {
IpAddr::V4(ipv4) => ipv4,
IpAddr::V6(_) => return Err(Error::InvalidTarget("IPv6 not yet supported".to_string())),
};
let mut hops = Vec::new();
let mut reached_destination = false;
for ttl in 1..=options.max_hops {
let hop = match options.mode {
ParisMode::Udp => probe_udp_paris(target_ipv4, ttl, options).await,
ParisMode::Icmp => probe_icmp_paris(target_ipv4, ttl, options).await,
ParisMode::Tcp => probe_tcp_paris(target_ipv4, ttl, options).await,
};
if let Some(addr) = hop.addr {
if addr == target_ip {
reached_destination = true;
}
}
if hop.icmp_type == Some(0) || hop.icmp_type == Some(3) {
reached_destination = true;
}
hops.push(hop);
if reached_destination {
break;
}
}
let load_balancing = if options.detect_load_balancing {
detect_load_balancing(target_ipv4, &hops, options).await
} else {
LoadBalancingType::Unknown
};
Ok(ParisTraceResult {
target: target.to_string(),
target_ip,
hops,
reached_destination,
flow_id: options.flow_id,
mode: options.mode,
total_time: start_time.elapsed(),
load_balancing,
})
}
async fn probe_udp_paris(target: Ipv4Addr, ttl: u8, options: &ParisOptions) -> ParisHop {
let timeout = options.timeout_per_hop;
let flow_id = options.flow_id;
let result = tokio::task::spawn_blocking(move || {
probe_udp_paris_sync(target, ttl, timeout, flow_id)
}).await;
match result {
Ok(Ok(hop)) => hop,
Ok(Err(_)) => ParisHop::timeout(ttl, flow_id, timeout),
Err(_) => ParisHop::timeout(ttl, flow_id, timeout),
}
}
fn probe_udp_paris_sync(target: Ipv4Addr, ttl: u8, timeout: Duration, flow_id: FlowId) -> Result<ParisHop, Error> {
let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
let src_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), flow_id.src_port);
socket.set_reuse_address(true)?;
if socket.bind(&src_addr.into()).is_err() {
socket.bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).into())?;
}
socket.set_ttl(ttl as u32)?;
socket.set_read_timeout(Some(timeout))?;
let payload = build_paris_payload(ttl);
let dest = SocketAddr::new(IpAddr::V4(target), flow_id.dst_port);
let start = Instant::now();
socket.send_to(&payload, &dest.into())?;
let icmp_socket = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))?;
icmp_socket.set_read_timeout(Some(timeout))?;
let mut recv_buf: [MaybeUninit<u8>; 1024] = unsafe { MaybeUninit::uninit().assume_init() };
match icmp_socket.recv_from(&mut recv_buf) {
Ok((len, from_addr)) => {
let rtt = start.elapsed();
let buf: &[u8] = unsafe {
std::slice::from_raw_parts(recv_buf.as_ptr() as *const u8, len)
};
if len >= 28 {
let ip_header_len = ((buf[0] & 0x0F) * 4) as usize;
if len > ip_header_len + 1 {
let icmp_type = buf[ip_header_len];
let icmp_code = buf[ip_header_len + 1];
let from_ip = from_addr.as_socket_ipv4()
.map(|a| IpAddr::V4(*a.ip()))
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
return Ok(ParisHop::success(ttl, from_ip, rtt, icmp_type, icmp_code, flow_id));
}
}
Err(Error::Icmp("Invalid response".to_string()))
}
Err(_) => Ok(ParisHop::timeout(ttl, flow_id, timeout)),
}
}
async fn probe_icmp_paris(target: Ipv4Addr, ttl: u8, options: &ParisOptions) -> ParisHop {
let timeout = options.timeout_per_hop;
let flow_id = options.flow_id;
let result = tokio::task::spawn_blocking(move || {
probe_icmp_paris_sync(target, ttl, timeout, flow_id)
}).await;
match result {
Ok(Ok(hop)) => hop,
Ok(Err(_)) => ParisHop::timeout(ttl, flow_id, timeout),
Err(_) => ParisHop::timeout(ttl, flow_id, timeout),
}
}
fn probe_icmp_paris_sync(target: Ipv4Addr, ttl: u8, timeout: Duration, flow_id: FlowId) -> Result<ParisHop, Error> {
let socket = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))?;
socket.set_ttl(ttl as u32)?;
socket.set_read_timeout(Some(timeout))?;
let identifier = flow_id.src_port;
let sequence = ttl as u16;
let packet = build_paris_icmp_packet(identifier, sequence);
let dest = SocketAddr::new(IpAddr::V4(target), 0);
let start = Instant::now();
socket.send_to(&packet, &dest.into())?;
let mut recv_buf: [MaybeUninit<u8>; 1024] = unsafe { MaybeUninit::uninit().assume_init() };
match socket.recv_from(&mut recv_buf) {
Ok((len, from_addr)) => {
let rtt = start.elapsed();
let buf: &[u8] = unsafe {
std::slice::from_raw_parts(recv_buf.as_ptr() as *const u8, len)
};
if len >= 28 {
let ip_header_len = ((buf[0] & 0x0F) * 4) as usize;
if len > ip_header_len + 1 {
let icmp_type = buf[ip_header_len];
let icmp_code = buf[ip_header_len + 1];
let from_ip = from_addr.as_socket_ipv4()
.map(|a| IpAddr::V4(*a.ip()))
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
return Ok(ParisHop::success(ttl, from_ip, rtt, icmp_type, icmp_code, flow_id));
}
}
Err(Error::Icmp("Invalid response".to_string()))
}
Err(_) => Ok(ParisHop::timeout(ttl, flow_id, timeout)),
}
}
async fn probe_tcp_paris(target: Ipv4Addr, ttl: u8, options: &ParisOptions) -> ParisHop {
let timeout = options.timeout_per_hop;
let flow_id = options.flow_id;
let result = tokio::task::spawn_blocking(move || {
probe_tcp_paris_sync(target, ttl, timeout, flow_id)
}).await;
match result {
Ok(Ok(hop)) => hop,
Ok(Err(_)) => ParisHop::timeout(ttl, flow_id, timeout),
Err(_) => ParisHop::timeout(ttl, flow_id, timeout),
}
}
fn probe_tcp_paris_sync(target: Ipv4Addr, ttl: u8, timeout: Duration, flow_id: FlowId) -> Result<ParisHop, Error> {
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?;
socket.set_ttl(ttl as u32)?;
socket.set_nonblocking(true)?;
let src_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), flow_id.src_port);
socket.set_reuse_address(true)?;
let _ = socket.bind(&src_addr.into());
let dest = SocketAddr::new(IpAddr::V4(target), flow_id.dst_port);
let start = Instant::now();
let _ = socket.connect(&dest.into());
std::thread::sleep(std::cmp::min(timeout, Duration::from_millis(100)));
let rtt = start.elapsed();
match socket.take_error() {
Ok(None) => {
Ok(ParisHop::success(ttl, IpAddr::V4(target), rtt, 0, 0, flow_id))
}
Ok(Some(e)) => {
if e.raw_os_error() == Some(libc::ECONNREFUSED) {
Ok(ParisHop::success(ttl, IpAddr::V4(target), rtt, 3, 3, flow_id))
} else {
Ok(ParisHop::timeout(ttl, flow_id, timeout))
}
}
Err(_) => Ok(ParisHop::timeout(ttl, flow_id, timeout)),
}
}
fn build_paris_payload(ttl: u8) -> Vec<u8> {
let mut payload = vec![0u8; 32];
payload[0] = 0x50; payload[1] = 0x41; payload[2] = 0x52; payload[3] = 0x49; payload[4] = 0x53;
payload[5] = ttl;
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u32;
payload[6..10].copy_from_slice(&ts.to_be_bytes());
payload
}
fn build_paris_icmp_packet(identifier: u16, sequence: u16) -> Vec<u8> {
let mut packet = vec![0u8; 64];
packet[0] = 8;
packet[1] = 0;
packet[2] = 0;
packet[3] = 0;
packet[4] = (identifier >> 8) as u8;
packet[5] = identifier as u8;
packet[6] = (sequence >> 8) as u8;
packet[7] = sequence as u8;
packet[8] = 0x50; packet[9] = 0x41; packet[10] = 0x52; packet[11] = 0x49; packet[12] = 0x53;
let checksum = compute_icmp_checksum(&packet);
packet[2] = (checksum >> 8) as u8;
packet[3] = checksum as u8;
packet
}
fn compute_icmp_checksum(data: &[u8]) -> u16 {
let mut sum: u32 = 0;
let mut i = 0;
while i < data.len() {
let word = if i + 1 < data.len() {
((data[i] as u32) << 8) | (data[i + 1] as u32)
} else {
(data[i] as u32) << 8
};
sum = sum.wrapping_add(word);
i += 2;
}
while sum >> 16 != 0 {
sum = (sum & 0xFFFF) + (sum >> 16);
}
!sum as u16
}
async fn detect_load_balancing(
target: Ipv4Addr,
reference_hops: &[ParisHop],
options: &ParisOptions,
) -> LoadBalancingType {
if reference_hops.is_empty() {
return LoadBalancingType::Unknown;
}
let test_ttl = std::cmp::min(reference_hops.len() as u8, 10);
if test_ttl < 3 {
return LoadBalancingType::Unknown;
}
let reference_addr = reference_hops.get(test_ttl as usize - 1)
.and_then(|h| h.addr);
if reference_addr.is_none() {
return LoadBalancingType::Unknown;
}
let mut results: HashMap<Option<IpAddr>, u32> = HashMap::new();
results.insert(reference_addr, 1);
for i in 0..options.lb_detection_flows {
let test_flow = FlowId::udp(33434 + i as u16, 33434);
let test_options = ParisOptions {
flow_id: test_flow,
max_hops: test_ttl,
probes_per_hop: 1,
detect_load_balancing: false,
..options.clone()
};
let hop = probe_udp_paris(target, test_ttl, &test_options).await;
*results.entry(hop.addr).or_insert(0) += 1;
}
let unique_addrs = results.len();
if unique_addrs == 1 {
LoadBalancingType::None
} else if unique_addrs > 1 {
LoadBalancingType::PerFlow
} else {
LoadBalancingType::Unknown
}
}
pub async fn discover_paths(
target: &str,
num_flows: u8,
options: &ParisOptions,
) -> crate::Result<Vec<ParisTraceResult>> {
let mut paths = Vec::new();
for i in 0..num_flows {
let flow_id = FlowId::udp(33434 + i as u16, 33434);
let path_options = ParisOptions {
flow_id,
detect_load_balancing: false,
..options.clone()
};
let trace = paris_traceroute(target, &path_options).await?;
paths.push(trace);
}
Ok(paths)
}
pub fn paths_equal(path1: &ParisTraceResult, path2: &ParisTraceResult) -> bool {
let p1: Vec<_> = path1.path();
let p2: Vec<_> = path2.path();
if p1.len() != p2.len() {
return false;
}
p1.iter().zip(p2.iter()).all(|(a, b)| a == b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_flow_id() {
let udp = FlowId::udp(33434, 33434);
assert_eq!(udp.protocol, 17);
let tcp = FlowId::tcp(80, 443);
assert_eq!(tcp.protocol, 6);
let icmp = FlowId::icmp(12345);
assert_eq!(icmp.protocol, 1);
}
#[test]
fn test_paris_payload() {
let payload = build_paris_payload(5);
assert_eq!(payload[0..5], [0x50, 0x41, 0x52, 0x49, 0x53]); assert_eq!(payload[5], 5); }
#[test]
fn test_paris_icmp_packet() {
let packet = build_paris_icmp_packet(1234, 5);
assert_eq!(packet[0], 8); assert_eq!(packet[1], 0);
let id = ((packet[4] as u16) << 8) | packet[5] as u16;
assert_eq!(id, 1234);
}
#[tokio::test]
async fn test_paris_options_default() {
let opts = ParisOptions::default();
assert_eq!(opts.max_hops, 30);
assert_eq!(opts.mode, ParisMode::Udp);
assert_eq!(opts.flow_id.src_port, 33434);
}
}