use crate::TimingConfig;
use crate::probe::{ProbeInfo, ProbeResponse};
use crate::socket::icmpv6;
use crate::socket::traits::{ProbeMode, ProbeSocket};
use crate::traceroute::TracerouteError;
use socket2::{Domain, Protocol, SockAddr, Socket as Socket2, Type};
use std::future::Future;
use std::net::{IpAddr, Ipv6Addr, SocketAddrV6};
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::oneshot;
const RECV_BUFFER_SIZE: usize = 1500;
const RECV_POLL_INTERVAL: Duration = Duration::from_millis(1);
#[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
const ICMP6_FILTER: libc::c_int = 18;
#[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
#[repr(C)]
struct Icmp6Filter {
icmp6_filt: [u32; 8],
}
#[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
impl Icmp6Filter {
fn block_all() -> Self {
Icmp6Filter { icmp6_filt: [0; 8] }
}
fn pass(mut self, ty: u8) -> Self {
self.icmp6_filt[(ty >> 5) as usize] |= 1u32 << (ty & 31);
self
}
}
#[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
fn apply_icmp6_filter(socket: &Socket2) {
use std::os::fd::AsRawFd;
let filter = Icmp6Filter::block_all()
.pass(icmpv6::ICMPV6_DEST_UNREACHABLE)
.pass(icmpv6::ICMPV6_TIME_EXCEEDED)
.pass(icmpv6::ICMPV6_ECHO_REPLY);
let rc = unsafe {
libc::setsockopt(
socket.as_raw_fd(),
libc::IPPROTO_ICMPV6,
ICMP6_FILTER,
(&raw const filter).cast(),
std::mem::size_of::<Icmp6Filter>() as libc::socklen_t,
)
};
let _ = rc;
}
#[cfg(not(any(target_os = "freebsd", target_os = "openbsd")))]
fn apply_icmp6_filter(_socket: &Socket2) {}
pub struct BsdAsyncIcmpV6Socket {
icmp_identifier: u16,
destination_reached: Arc<AtomicBool>,
pending_count: Arc<AtomicUsize>,
timing_config: TimingConfig,
}
impl BsdAsyncIcmpV6Socket {
pub fn new_with_config(timing_config: TimingConfig) -> Result<Self, TracerouteError> {
Socket2::new(Domain::IPV6, Type::RAW, Some(Protocol::ICMPV6)).map_err(|e| {
if e.kind() == std::io::ErrorKind::PermissionDenied {
TracerouteError::InsufficientPermissions {
required: "root".to_string(),
suggestion:
"Run with sudo, or make the binary setuid root (chown root:wheel ftr && chmod u+s ftr)"
.to_string(),
}
} else {
TracerouteError::SocketError(format!("Failed to create raw ICMPv6 socket: {e}"))
}
})?;
Ok(BsdAsyncIcmpV6Socket {
icmp_identifier: std::process::id() as u16,
destination_reached: Arc::new(AtomicBool::new(false)),
pending_count: Arc::new(AtomicUsize::new(0)),
timing_config,
})
}
fn parse_raw_response(
icmp_identifier: u16,
data: &[u8],
from_addr: Ipv6Addr,
sequence: u16,
dest: Ipv6Addr,
) -> Option<(Ipv6Addr, bool)> {
let hdr = icmpv6::parse_icmpv6_header(data)?;
if icmpv6::is_ndp(hdr.icmpv6_type) {
return None; }
match hdr.icmpv6_type {
icmpv6::ICMPV6_ECHO_REPLY => {
let (reply_id, reply_seq) = icmpv6::parse_echo_reply_v6(data)?;
if reply_id == icmp_identifier && reply_seq == sequence {
return Some((from_addr, true));
}
}
icmpv6::ICMPV6_TIME_EXCEEDED | icmpv6::ICMPV6_DEST_UNREACHABLE => {
let embedded = icmpv6::parse_embedded_probe(data)?;
if embedded.identifier == icmp_identifier
&& embedded.sequence == sequence
&& embedded.destination == dest
{
return Some((from_addr, false));
}
}
_ => {}
}
None
}
}
impl ProbeSocket for BsdAsyncIcmpV6Socket {
fn mode(&self) -> ProbeMode {
ProbeMode::RawIcmp
}
fn send_probe_and_recv(
&self,
dest: IpAddr,
probe: ProbeInfo,
) -> Pin<Box<dyn Future<Output = Result<ProbeResponse, TracerouteError>> + Send + '_>> {
Box::pin(async move {
let dest_v6 = match dest {
IpAddr::V6(addr) => addr,
IpAddr::V4(_) => {
return Err(TracerouteError::SocketError(
"ICMPv6 socket cannot probe an IPv4 destination".to_string(),
));
}
};
self.pending_count.fetch_add(1, Ordering::Relaxed);
let result = (|| {
let socket = Socket2::new(Domain::IPV6, Type::RAW, Some(Protocol::ICMPV6))
.map_err(|e| {
TracerouteError::SocketError(format!(
"Failed to create raw ICMPv6 socket: {e}"
))
})?;
socket.set_unicast_hops_v6(probe.ttl as u32).map_err(|e| {
TracerouteError::SocketError(format!("Failed to set IPV6_UNICAST_HOPS: {e}"))
})?;
socket.set_nonblocking(true).map_err(|e| {
TracerouteError::SocketError(format!("Failed to set non-blocking: {e}"))
})?;
apply_icmp6_filter(&socket);
let mut payload = [0u8; 16];
let tag = b"ftr-traceroute";
payload[..tag.len()].copy_from_slice(tag);
let pkt =
icmpv6::build_echo_request_v6(self.icmp_identifier, probe.sequence, &payload);
let dest_sockaddr = SockAddr::from(SocketAddrV6::new(dest_v6, 0, 0, 0));
let sent_at = Instant::now();
socket.send_to(&pkt, &dest_sockaddr).map_err(|e| {
TracerouteError::ProbeSendError(format!("Failed to send ICMPv6 packet: {e}"))
})?;
Ok::<_, TracerouteError>((socket, sent_at))
})();
let (socket, sent_at) = match result {
Ok(pair) => pair,
Err(e) => {
self.pending_count.fetch_sub(1, Ordering::Relaxed);
return Err(e);
}
};
let destination_reached = self.destination_reached.clone();
let pending_count = self.pending_count.clone();
let sequence = probe.sequence;
let ttl = probe.ttl;
let icmp_identifier = self.icmp_identifier;
let timeout = self.timing_config.socket_read_timeout;
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
loop {
let mut buf = [std::mem::MaybeUninit::uninit(); RECV_BUFFER_SIZE];
match socket.recv_from(&mut buf) {
Ok((size, from)) => {
if let Some(from_sa) = from.as_socket_ipv6() {
let data = unsafe {
std::slice::from_raw_parts(buf.as_ptr().cast::<u8>(), size)
};
if let Some((resp_addr, is_destination)) =
BsdAsyncIcmpV6Socket::parse_raw_response(
icmp_identifier,
data,
*from_sa.ip(),
sequence,
dest_v6,
)
{
let rtt = Instant::now().duration_since(sent_at);
if is_destination {
destination_reached.store(true, Ordering::Relaxed);
}
pending_count.fetch_sub(1, Ordering::Relaxed);
let _ = tx.send(ProbeResponse {
from_addr: IpAddr::V6(resp_addr),
sequence,
ttl,
rtt,
received_at: Instant::now(),
is_destination,
is_timeout: false,
});
break;
}
continue;
}
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(_) => {
pending_count.fetch_sub(1, Ordering::Relaxed);
break;
}
}
if sent_at.elapsed() >= timeout {
pending_count.fetch_sub(1, Ordering::Relaxed);
let _ = tx.send(ProbeResponse {
from_addr: dest,
sequence,
ttl,
rtt: timeout,
received_at: Instant::now(),
is_destination: false,
is_timeout: true,
});
break;
}
tokio::time::sleep(RECV_POLL_INTERVAL).await;
}
});
match rx.await {
Ok(response) => Ok(response),
Err(_) => {
self.pending_count.fetch_sub(1, Ordering::Relaxed);
Err(TracerouteError::SocketError(
"Failed to receive response".to_string(),
))
}
}
})
}
fn destination_reached(&self) -> bool {
self.destination_reached.load(Ordering::Relaxed)
}
fn pending_count(&self) -> usize {
self.pending_count.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
const GOOGLE_V6: Ipv6Addr = Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888);
const HOP_ROUTER: Ipv6Addr = Ipv6Addr::new(0x2001, 0x5a8, 0x657, 0x21, 0, 0, 0xf0, 4);
fn build_te(id: u16, seq: u16, embedded_dst: Ipv6Addr) -> Vec<u8> {
let mut buf = vec![0u8; 56];
buf[0] = icmpv6::ICMPV6_TIME_EXCEEDED;
buf[8] = 6 << 4; buf[14] = icmpv6::IPV6_NEXT_HEADER_ICMPV6; buf[32..48].copy_from_slice(&embedded_dst.octets());
buf[48] = icmpv6::ICMPV6_ECHO_REQUEST;
buf[52..54].copy_from_slice(&id.to_be_bytes());
buf[54..56].copy_from_slice(&seq.to_be_bytes());
buf
}
#[test]
fn test_parse_raw_response_echo_reply_demux() {
let id = 0x4242;
let seq = 7;
let mut own = icmpv6::build_echo_request_v6(id, seq, &[]);
own[0] = icmpv6::ICMPV6_ECHO_REPLY;
assert_eq!(
BsdAsyncIcmpV6Socket::parse_raw_response(id, &own, GOOGLE_V6, seq, GOOGLE_V6),
Some((GOOGLE_V6, true))
);
let mut foreign = icmpv6::build_echo_request_v6(0x9999, seq, &[]);
foreign[0] = icmpv6::ICMPV6_ECHO_REPLY;
assert_eq!(
BsdAsyncIcmpV6Socket::parse_raw_response(id, &foreign, GOOGLE_V6, seq, GOOGLE_V6),
None
);
let mut wrong_seq = icmpv6::build_echo_request_v6(id, seq + 1, &[]);
wrong_seq[0] = icmpv6::ICMPV6_ECHO_REPLY;
assert_eq!(
BsdAsyncIcmpV6Socket::parse_raw_response(id, &wrong_seq, GOOGLE_V6, seq, GOOGLE_V6),
None
);
}
#[test]
fn test_parse_raw_response_time_exceeded_demux() {
let id = 0x4242;
let seq = 3;
let te = build_te(id, seq, GOOGLE_V6);
assert_eq!(
BsdAsyncIcmpV6Socket::parse_raw_response(id, &te, HOP_ROUTER, seq, GOOGLE_V6),
Some((HOP_ROUTER, false))
);
let foreign = build_te(0x1111, seq, GOOGLE_V6);
assert_eq!(
BsdAsyncIcmpV6Socket::parse_raw_response(id, &foreign, HOP_ROUTER, seq, GOOGLE_V6),
None
);
let other_dst = Ipv6Addr::new(0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1111);
let wrong_dst = build_te(id, seq, other_dst);
assert_eq!(
BsdAsyncIcmpV6Socket::parse_raw_response(id, &wrong_dst, HOP_ROUTER, seq, GOOGLE_V6),
None
);
}
#[test]
fn test_parse_raw_response_skips_ndp_noise() {
let ra = [134u8, 0, 0, 0, 0x40, 0xc8, 0x07, 0x08];
assert_eq!(
BsdAsyncIcmpV6Socket::parse_raw_response(1, &ra, HOP_ROUTER, 1, GOOGLE_V6),
None
);
}
#[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
#[test]
fn test_icmp6_filter_pass_bits() {
let filter = Icmp6Filter::block_all()
.pass(icmpv6::ICMPV6_DEST_UNREACHABLE)
.pass(icmpv6::ICMPV6_TIME_EXCEEDED)
.pass(icmpv6::ICMPV6_ECHO_REPLY);
let is_set = |ty: u8| filter.icmp6_filt[(ty >> 5) as usize] & (1u32 << (ty & 31)) != 0;
assert!(is_set(1));
assert!(is_set(3));
assert!(is_set(129));
assert!(!is_set(128));
for ty in 133..=137u8 {
assert!(!is_set(ty), "NDP type {ty} must remain blocked");
}
}
#[test]
fn test_socket_initialization_permission_behavior() {
let result = BsdAsyncIcmpV6Socket::new_with_config(TimingConfig::default());
if crate::socket::utils::is_root() {
assert!(result.is_ok(), "root must be able to open raw ICMPv6");
} else {
assert!(
matches!(result, Err(TracerouteError::InsufficientPermissions { .. })),
"non-root must get the typed permission error"
);
}
}
}