From 84b4f18ccfebc47ad55d714ab5b76b7481436e22 Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Fri, 10 Jan 2025 17:08:43 +0100 Subject: [PATCH] fixup: Less unused code --- leak-checker/src/traceroute.rs | 18 +- leak-checker/src/traceroute/platform/mod.rs | 7 - .../src/traceroute/platform/unix/common.rs | 137 --------- .../src/traceroute/platform/unix/macos.rs | 88 ------ .../traceroute/{platform => }/unix/android.rs | 0 leak-checker/src/traceroute/unix/common.rs | 52 ++++ .../traceroute/{platform => }/unix/linux.rs | 2 +- leak-checker/src/traceroute/unix/macos.rs | 285 ++++++++++++++++++ .../src/traceroute/{platform => }/unix/mod.rs | 152 +--------- .../src/traceroute/{platform => }/windows.rs | 0 10 files changed, 356 insertions(+), 385 deletions(-) delete mode 100644 leak-checker/src/traceroute/platform/mod.rs delete mode 100644 leak-checker/src/traceroute/platform/unix/common.rs delete mode 100644 leak-checker/src/traceroute/platform/unix/macos.rs rename leak-checker/src/traceroute/{platform => }/unix/android.rs (100%) create mode 100644 leak-checker/src/traceroute/unix/common.rs rename leak-checker/src/traceroute/{platform => }/unix/linux.rs (99%) create mode 100644 leak-checker/src/traceroute/unix/macos.rs rename leak-checker/src/traceroute/{platform => }/unix/mod.rs (67%) rename leak-checker/src/traceroute/{platform => }/windows.rs (100%) diff --git a/leak-checker/src/traceroute.rs b/leak-checker/src/traceroute.rs index 3a1f2b59dbd7..45a58164fc70 100644 --- a/leak-checker/src/traceroute.rs +++ b/leak-checker/src/traceroute.rs @@ -2,7 +2,13 @@ use std::{net::IpAddr, ops::Range, time::Duration}; use crate::{Interface, LeakStatus}; -mod platform; +/// Traceroute implementation for windows. +#[cfg(target_os = "windows")] +mod windows; + +/// Traceroute implementation for unix. +#[cfg(unix)] +mod unix; #[derive(Clone, clap::Args)] pub struct TracerouteOpt { @@ -69,17 +75,17 @@ pub async fn try_run_leak_test(opt: &TracerouteOpt) -> anyhow::Result(opt).await + unix::try_run_leak_test::(opt).await }; #[cfg(target_os = "windows")] - return platform::windows::traceroute_using_ping(opt).await; + return windows::traceroute_using_ping(opt).await; } /// IP version, v4 or v6, with some associated data. diff --git a/leak-checker/src/traceroute/platform/mod.rs b/leak-checker/src/traceroute/platform/mod.rs deleted file mode 100644 index d158e20f729b..000000000000 --- a/leak-checker/src/traceroute/platform/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -/// Traceroute implementation for windows. -#[cfg(target_os = "windows")] -pub mod windows; - -/// Traceroute implementation for unix. -#[cfg(unix)] -pub mod unix; diff --git a/leak-checker/src/traceroute/platform/unix/common.rs b/leak-checker/src/traceroute/platform/unix/common.rs deleted file mode 100644 index 50c96c519755..000000000000 --- a/leak-checker/src/traceroute/platform/unix/common.rs +++ /dev/null @@ -1,137 +0,0 @@ -#![allow(dead_code)] // some code here is not used on some targets. - -use std::{ - future::pending, - net::{IpAddr, SocketAddr}, -}; - -use anyhow::{anyhow, Context}; -use socket2::Socket; -use tokio::{ - select, - time::{sleep_until, Instant}, -}; - -use crate::{ - traceroute::{ - platform::unix::{ - parse_icmp4_time_exceeded, parse_icmp6_time_exceeded, parse_ipv4, parse_ipv6, - }, - Ip, RECV_GRACE_TIME, - }, - Interface, LeakInfo, LeakStatus, -}; - -use super::AsyncIcmpSocket; - -pub fn get_interface_ip(interface: &Interface, ip_version: Ip) -> anyhow::Result { - let Interface::Name(interface) = interface; - - for interface_address in nix::ifaddrs::getifaddrs()? { - if &interface_address.interface_name != interface { - continue; - }; - let Some(address) = interface_address.address else { - continue; - }; - - match ip_version { - Ip::V4(()) => { - if let Some(address) = address.as_sockaddr_in() { - return Ok(IpAddr::V4(address.ip())); - }; - } - Ip::V6(()) => { - if let Some(address) = address.as_sockaddr_in6() { - return Ok(IpAddr::V6(address.ip())); - }; - } - } - } - - anyhow::bail!("Interface {interface:?} has no valid IP to bind to"); -} - -pub fn bind_socket_to_interface( - socket: &Socket, - interface: &Interface, - ip_version: Ip, -) -> anyhow::Result<()> { - let interface_ip = get_interface_ip(interface, ip_version)?; - - log::info!("Binding socket to {interface_ip} ({interface:?})"); - - socket - .bind(&SocketAddr::new(interface_ip, 0).into()) - .context("Failed to bind socket to interface address")?; - - Ok(()) -} - -pub(crate) async fn recv_ttl_responses( - socket: &impl AsyncIcmpSocket, - interface: &Interface, -) -> anyhow::Result { - // the list of node IP addresses from which we received a response to our probe packets. - let mut reachable_nodes = vec![]; - - // A time at which this function should exit. This is set when we receive the first probe - // response, and allows us to wait a while to collect any additional probe responses before - // returning. - let mut timeout_at = None; - - let mut read_buf = vec![0u8; usize::from(u16::MAX)].into_boxed_slice(); - loop { - let timer = async { - match timeout_at { - // resolve future at the timeout, if it's set - Some(time) => sleep_until(time).await, - - // otherwise, never resolve - None => pending().await, - } - }; - - log::debug!("Reading from ICMP socket"); - - let (n, source) = select! { - result = socket.recv_from(&mut read_buf[..]) => result - .context("Failed to read from raw socket")?, - - _timeout = timer => { - return Ok(LeakStatus::LeakDetected(LeakInfo::NodeReachableOnInterface { - reachable_nodes, - interface: interface.clone(), - })); - } - }; - - let packet = &read_buf[..n]; - - let parsed = match source { - IpAddr::V4(..) => parse_ipv4(packet) - .and_then(|ip_packet| parse_icmp4_time_exceeded(&ip_packet)) - .map(IpAddr::from), - IpAddr::V6(..) => parse_ipv6(packet) - .and_then(|ip_packet| parse_icmp6_time_exceeded(&ip_packet)) - .map(IpAddr::from), - }; - - let result = parsed.map_err(|e| { - anyhow!("Ignoring packet: (len={n}, ip.src={source}) {e} ({packet:02x?})") - }); - - match result { - Ok(ip) => { - log::debug!("Got a probe response, we are leaking!"); - timeout_at.get_or_insert_with(|| Instant::now() + RECV_GRACE_TIME); - if !reachable_nodes.contains(&ip) { - reachable_nodes.push(ip); - } - } - - // an error means the packet wasn't the ICMP/TimeExceeded we're listening for. - Err(e) => log::debug!("{e}"), - } - } -} diff --git a/leak-checker/src/traceroute/platform/unix/macos.rs b/leak-checker/src/traceroute/platform/unix/macos.rs deleted file mode 100644 index db1ba1422379..000000000000 --- a/leak-checker/src/traceroute/platform/unix/macos.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::io; -use std::net::IpAddr; -use std::num::NonZero; -use std::os::fd::{FromRawFd, IntoRawFd}; - -use anyhow::{anyhow, Context}; -use nix::net::if_::if_nametoindex; -use socket2::Socket; - -use crate::traceroute::{Ip, TracerouteOpt}; -use crate::{Interface, LeakStatus}; - -use super::{AsyncIcmpSocket, Traceroute, common::recv_ttl_responses}; - -pub struct TracerouteMacos; - -pub struct AsyncIcmpSocketImpl(tokio::net::UdpSocket); - -impl Traceroute for TracerouteMacos { - type AsyncIcmpSocket = AsyncIcmpSocketImpl; - - fn bind_socket_to_interface( - socket: &Socket, - interface: &Interface, - ip_version: Ip, - ) -> anyhow::Result<()> { - // can't use the same method as desktop-linux here beacuse reasons - bind_socket_to_interface(socket, interface, ip_version) - } - - fn configure_icmp_socket( - _socket: &socket2::Socket, - _opt: &TracerouteOpt, - ) -> anyhow::Result<()> { - Ok(()) - } -} - -impl AsyncIcmpSocket for AsyncIcmpSocketImpl { - fn from_socket2(socket: Socket) -> Self { - let raw_socket = socket.into_raw_fd(); - let std_socket = unsafe { std::net::UdpSocket::from_raw_fd(raw_socket) }; - let tokio_socket = tokio::net::UdpSocket::from_std(std_socket).unwrap(); - AsyncIcmpSocketImpl(tokio_socket) - } - - fn set_ttl(&self, ttl: u32) -> anyhow::Result<()> { - self.0 - .set_ttl(ttl) - .context("Failed to set TTL value for socket") - } - - async fn send_to(&self, packet: &[u8], destination: impl Into) -> io::Result { - self.0.send_to(packet, (destination.into(), 0)).await - } - - async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, IpAddr)> { - self.0 - .recv_from(buf) - .await - .map(|(n, source)| (n, source.ip())) - } - - async fn recv_ttl_responses(&self, opt: &TracerouteOpt) -> anyhow::Result { - recv_ttl_responses(self, &opt.interface).await - } -} - -pub fn bind_socket_to_interface( - socket: &Socket, - interface: &Interface, - ip_version: Ip, -) -> anyhow::Result<()> { - let Interface::Name(interface) = interface; - - log::info!("Binding socket to {interface:?}"); - - let interface_index = if_nametoindex(interface.as_str()) - .map_err(anyhow::Error::from) - .and_then(|code| NonZero::new(code).ok_or(anyhow!("Non-zero error code"))) - .context("Failed to get interface index")?; - - match ip_version { - Ip::V4(..) => socket.bind_device_by_index_v4(Some(interface_index))?, - Ip::V6(..) => socket.bind_device_by_index_v6(Some(interface_index))?, - } - Ok(()) -} diff --git a/leak-checker/src/traceroute/platform/unix/android.rs b/leak-checker/src/traceroute/unix/android.rs similarity index 100% rename from leak-checker/src/traceroute/platform/unix/android.rs rename to leak-checker/src/traceroute/unix/android.rs diff --git a/leak-checker/src/traceroute/unix/common.rs b/leak-checker/src/traceroute/unix/common.rs new file mode 100644 index 000000000000..e7810c7fb407 --- /dev/null +++ b/leak-checker/src/traceroute/unix/common.rs @@ -0,0 +1,52 @@ +#![allow(dead_code)] // some code here is not used on some targets. + +use std::net::{IpAddr, SocketAddr}; + +use anyhow::Context; +use socket2::Socket; + +use crate::{traceroute::Ip, Interface}; + +pub(crate) fn get_interface_ip(interface: &Interface, ip_version: Ip) -> anyhow::Result { + let Interface::Name(interface) = interface; + + for interface_address in nix::ifaddrs::getifaddrs()? { + if &interface_address.interface_name != interface { + continue; + }; + let Some(address) = interface_address.address else { + continue; + }; + + match ip_version { + Ip::V4(()) => { + if let Some(address) = address.as_sockaddr_in() { + return Ok(IpAddr::V4(address.ip())); + }; + } + Ip::V6(()) => { + if let Some(address) = address.as_sockaddr_in6() { + return Ok(IpAddr::V6(address.ip())); + }; + } + } + } + + anyhow::bail!("Interface {interface:?} has no valid IP to bind to"); +} + +pub(crate) fn bind_socket_to_interface( + socket: &Socket, + interface: &Interface, + ip_version: Ip, +) -> anyhow::Result<()> { + let interface_ip = get_interface_ip(interface, ip_version)?; + + log::info!("Binding socket to {interface_ip} ({interface:?})"); + + socket + .bind(&SocketAddr::new(interface_ip, 0).into()) + .context("Failed to bind socket to interface address")?; + + Ok(()) +} diff --git a/leak-checker/src/traceroute/platform/unix/linux.rs b/leak-checker/src/traceroute/unix/linux.rs similarity index 99% rename from leak-checker/src/traceroute/platform/unix/linux.rs rename to leak-checker/src/traceroute/unix/linux.rs index 0630deb7fa8a..def0f698485b 100644 --- a/leak-checker/src/traceroute/platform/unix/linux.rs +++ b/leak-checker/src/traceroute/unix/linux.rs @@ -15,7 +15,7 @@ use pnet_packet::icmpv6::{Icmpv6Code, Icmpv6Type, Icmpv6Types}; use socket2::Socket; use tokio::time::{sleep, Instant}; -use crate::traceroute::platform::unix::parse_icmp_probe; +use crate::traceroute::unix::parse_icmp_probe; use crate::traceroute::{Ip, TracerouteOpt, RECV_GRACE_TIME}; use crate::{Interface, LeakInfo, LeakStatus}; diff --git a/leak-checker/src/traceroute/unix/macos.rs b/leak-checker/src/traceroute/unix/macos.rs new file mode 100644 index 000000000000..1fd6e31d41d1 --- /dev/null +++ b/leak-checker/src/traceroute/unix/macos.rs @@ -0,0 +1,285 @@ +use std::ascii::escape_default; +use std::future::pending; +use std::io; +use std::net::IpAddr; +use std::num::NonZero; +use std::os::fd::{FromRawFd, IntoRawFd}; + +use anyhow::{anyhow, bail, ensure, Context}; +use nix::net::if_::if_nametoindex; +use pnet_packet::{ + icmp::{self, time_exceeded::TimeExceededPacket, IcmpPacket, IcmpTypes}, + icmpv6::{Icmpv6Packet, Icmpv6Types}, + ip::IpNextHeaderProtocols, + ipv4::Ipv4Packet, + ipv6::Ipv6Packet, + udp::UdpPacket, + Packet, +}; +use socket2::Socket; +use tokio::{ + select, + time::{sleep_until, Instant}, +}; + +use crate::traceroute::{Ip, TracerouteOpt, RECV_GRACE_TIME}; +use crate::{Interface, LeakInfo, LeakStatus}; + +use super::{parse_icmp_probe, too_small, AsyncIcmpSocket, Traceroute, PROBE_PAYLOAD}; + +pub struct TracerouteMacos; + +pub struct AsyncIcmpSocketImpl(tokio::net::UdpSocket); + +impl Traceroute for TracerouteMacos { + type AsyncIcmpSocket = AsyncIcmpSocketImpl; + + fn bind_socket_to_interface( + socket: &Socket, + interface: &Interface, + ip_version: Ip, + ) -> anyhow::Result<()> { + // can't use the same method as desktop-linux here beacuse reasons + bind_socket_to_interface(socket, interface, ip_version) + } + + fn configure_icmp_socket( + _socket: &socket2::Socket, + _opt: &TracerouteOpt, + ) -> anyhow::Result<()> { + Ok(()) + } +} + +impl AsyncIcmpSocket for AsyncIcmpSocketImpl { + fn from_socket2(socket: Socket) -> Self { + let raw_socket = socket.into_raw_fd(); + let std_socket = unsafe { std::net::UdpSocket::from_raw_fd(raw_socket) }; + let tokio_socket = tokio::net::UdpSocket::from_std(std_socket).unwrap(); + AsyncIcmpSocketImpl(tokio_socket) + } + + fn set_ttl(&self, ttl: u32) -> anyhow::Result<()> { + self.0 + .set_ttl(ttl) + .context("Failed to set TTL value for socket") + } + + async fn send_to(&self, packet: &[u8], destination: impl Into) -> io::Result { + self.0.send_to(packet, (destination.into(), 0)).await + } + + async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, IpAddr)> { + self.0 + .recv_from(buf) + .await + .map(|(n, source)| (n, source.ip())) + } + + async fn recv_ttl_responses(&self, opt: &TracerouteOpt) -> anyhow::Result { + recv_ttl_responses(self, &opt.interface).await + } +} + +fn bind_socket_to_interface( + socket: &Socket, + interface: &Interface, + ip_version: Ip, +) -> anyhow::Result<()> { + let Interface::Name(interface) = interface; + + log::info!("Binding socket to {interface:?}"); + + let interface_index = if_nametoindex(interface.as_str()) + .map_err(anyhow::Error::from) + .and_then(|code| NonZero::new(code).ok_or(anyhow!("Non-zero error code"))) + .context("Failed to get interface index")?; + + match ip_version { + Ip::V4(..) => socket.bind_device_by_index_v4(Some(interface_index))?, + Ip::V6(..) => socket.bind_device_by_index_v6(Some(interface_index))?, + } + Ok(()) +} + +async fn recv_ttl_responses( + socket: &impl AsyncIcmpSocket, + interface: &Interface, +) -> anyhow::Result { + // the list of node IP addresses from which we received a response to our probe packets. + let mut reachable_nodes = vec![]; + + // A time at which this function should exit. This is set when we receive the first probe + // response, and allows us to wait a while to collect any additional probe responses before + // returning. + let mut timeout_at = None; + + let mut read_buf = vec![0u8; usize::from(u16::MAX)].into_boxed_slice(); + loop { + let timer = async { + match timeout_at { + // resolve future at the timeout, if it's set + Some(time) => sleep_until(time).await, + + // otherwise, never resolve + None => pending().await, + } + }; + + log::debug!("Reading from ICMP socket"); + + let (n, source) = select! { + result = socket.recv_from(&mut read_buf[..]) => result + .context("Failed to read from raw socket")?, + + _timeout = timer => { + return Ok(LeakStatus::LeakDetected(LeakInfo::NodeReachableOnInterface { + reachable_nodes, + interface: interface.clone(), + })); + } + }; + + let packet = &read_buf[..n]; + let result = parse_ip(packet) + .and_then(|packet| match packet { + Ip::V4(ip_packet) => parse_icmp4_time_exceeded(&ip_packet), + Ip::V6(ip_packet) => parse_icmp6_time_exceeded(&ip_packet), + }) + .map_err(|e| { + anyhow!("Ignoring packet: (len={n}, ip.src={source}) {e} ({packet:02x?})") + }); + + match result { + Ok(ip) => { + log::debug!("Got a probe response, we are leaking!"); + timeout_at.get_or_insert_with(|| Instant::now() + RECV_GRACE_TIME); + if !reachable_nodes.contains(&ip) { + reachable_nodes.push(ip); + } + } + + // an error means the packet wasn't the ICMP/TimeExceeded we're listening for. + Err(e) => log::debug!("{e}"), + } + } +} + +/// Try to parse the bytes as an IPv4 or IPv6 packet. +/// +/// This only valdiates the IP header, not the payload. +fn parse_ip(packet: &[u8]) -> anyhow::Result, Ipv6Packet<'_>>> { + let ipv4_packet = Ipv4Packet::new(packet).ok_or_else(too_small)?; + + // ipv4-packets are smaller than ipv6, so we use an Ipv4Packet to check the version. + Ok(match ipv4_packet.get_version() { + 4 => Ip::V4(ipv4_packet), + 6 => { + let ipv6_packet = Ipv6Packet::new(packet).ok_or_else(too_small)?; + Ip::V6(ipv6_packet) + } + _ => bail!("Not a valid IP header"), + }) +} + +/// Try to parse an [Ipv4Packet] as an ICMP/TimeExceeded response to a packet sent by +/// [send_udp_probes] or [send_icmp_probes]. If successful, returns the [Ipv4Addr] of the packet +/// source. +/// +/// If the packet fails to parse, or is not a reply to a packet sent by us, this function returns +/// an error. +fn parse_icmp4_time_exceeded(ip_packet: &Ipv4Packet<'_>) -> anyhow::Result { + let ip_protocol = ip_packet.get_next_level_protocol(); + ensure!(ip_protocol == IpNextHeaderProtocols::Icmp, "Not ICMP"); + parse_icmp_time_exceeded_raw(Ip::V4(ip_packet.payload()))?; + Ok(ip_packet.get_source().into()) +} + +/// Try to parse an [Ipv6Packet] as an ICMP6/TimeExceeded response to a packet sent by +/// [send_udp_probes] or [send_icmp_probes]. If successful, returns the [Ipv6Addr] of the packet +/// source. +/// +/// If the packet fails to parse, or is not a reply to a packet sent by us, this function returns +/// an error. +fn parse_icmp6_time_exceeded(ip_packet: &Ipv6Packet<'_>) -> anyhow::Result { + let ip_protocol = ip_packet.get_next_header(); + ensure!(ip_protocol == IpNextHeaderProtocols::Icmpv6, "Not ICMP6"); + parse_icmp_time_exceeded_raw(Ip::V6(ip_packet.payload()))?; + Ok(ip_packet.get_source().into()) +} + +/// Try to parse some bytes into an ICMP or ICMP6 TimeExceeded response to a probe packet sent by +/// [send_udp_probes] or [send_icmp_probes]. +/// +/// If the packet fails to parse, or is not a reply to a packet sent by us, this function returns +/// an error. +fn parse_icmp_time_exceeded_raw(ip_payload: Ip<&[u8], &[u8]>) -> anyhow::Result<()> { + let icmpv4_packet; + let icmpv6_packet; + let icmp_packet: &[u8] = match ip_payload { + Ip::V4(ipv4_payload) => { + icmpv4_packet = IcmpPacket::new(ipv4_payload).ok_or(anyhow!("Too small"))?; + + let correct_type = icmpv4_packet.get_icmp_type() == IcmpTypes::TimeExceeded; + ensure!(correct_type, "Not ICMP/TimeExceeded"); + + icmpv4_packet.packet() + } + Ip::V6(ipv6_payload) => { + icmpv6_packet = Icmpv6Packet::new(ipv6_payload).ok_or(anyhow!("Too small"))?; + + let correct_type = icmpv6_packet.get_icmpv6_type() == Icmpv6Types::TimeExceeded; + ensure!(correct_type, "Not ICMP6/TimeExceeded"); + + icmpv6_packet.packet() + } + }; + + // TimeExceededPacket looks the same for both ICMP and ICMP6. + let time_exceeded = TimeExceededPacket::new(icmp_packet).ok_or_else(too_small)?; + ensure!( + time_exceeded.get_icmp_code() + == icmp::time_exceeded::IcmpCodes::TimeToLiveExceededInTransit, + "Not TTL Exceeded", + ); + + let original_ip_packet = parse_ip(time_exceeded.payload()).context("ICMP-wrapped IP packet")?; + + let (original_ip_protocol, original_ip_payload) = match &original_ip_packet { + Ip::V4(ipv4_packet) => (ipv4_packet.get_next_level_protocol(), ipv4_packet.payload()), + Ip::V6(ipv6_packet) => (ipv6_packet.get_next_header(), ipv6_packet.payload()), + }; + + match original_ip_protocol { + IpNextHeaderProtocols::Udp => { + let original_udp_packet = UdpPacket::new(original_ip_payload).ok_or_else(too_small)?; + + // check if payload looks right + // some network nodes will strip the payload, that's fine. + if !original_udp_packet.payload().is_empty() { + let udp_len = usize::from(original_udp_packet.get_length()); + let udp_payload = udp_len + .checked_sub(UdpPacket::minimum_packet_size()) + .and_then(|len| original_udp_packet.payload().get(..len)) + .ok_or(anyhow!("Invalid UDP length"))?; + if udp_payload != PROBE_PAYLOAD { + let udp_payload: String = udp_payload + .iter() + .copied() + .flat_map(escape_default) + .map(char::from) + .collect(); + bail!("Wrong UDP payload: {udp_payload:?}"); + } + } + + Ok(()) + } + + IpNextHeaderProtocols::Icmp => parse_icmp_probe(Ip::V4(original_ip_payload)), + + IpNextHeaderProtocols::Icmpv6 => parse_icmp_probe(Ip::V6(original_ip_payload)), + + _ => bail!("Not UDP/ICMP"), + } +} diff --git a/leak-checker/src/traceroute/platform/unix/mod.rs b/leak-checker/src/traceroute/unix/mod.rs similarity index 67% rename from leak-checker/src/traceroute/platform/unix/mod.rs rename to leak-checker/src/traceroute/unix/mod.rs index 05c3feb23f26..8ab877dbd781 100644 --- a/leak-checker/src/traceroute/platform/unix/mod.rs +++ b/leak-checker/src/traceroute/unix/mod.rs @@ -2,7 +2,7 @@ use std::{ ascii::escape_default, convert::Infallible, io, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + net::{IpAddr, SocketAddr}, ops::RangeFrom, os::fd::{FromRawFd, IntoRawFd}, }; @@ -16,12 +16,8 @@ use anyhow::{anyhow, bail, ensure, Context}; use common::get_interface_ip; use futures::{future::pending, select, stream, FutureExt, StreamExt, TryStreamExt}; use pnet_packet::{ - icmp::{self, time_exceeded::TimeExceededPacket, IcmpCode, IcmpPacket, IcmpTypes}, - icmpv6::{self, Icmpv6Code, Icmpv6Packet, Icmpv6Types}, - ip::IpNextHeaderProtocols as IpProtocol, - ipv4::Ipv4Packet, - ipv6::Ipv6Packet, - udp::UdpPacket, + icmp::{self, IcmpCode, IcmpTypes}, + icmpv6::{self, Icmpv6Code, Icmpv6Types}, Packet, }; use socket2::{Domain, Protocol, Socket, Type}; @@ -49,7 +45,7 @@ const DEFAULT_PORT_RANGE: RangeFrom = 33434..; /// Private trait that let's us define the platform-specific implementations and types required for /// tracerouting. -pub(crate) trait Traceroute { +pub trait Traceroute { type AsyncIcmpSocket: AsyncIcmpSocket; fn bind_socket_to_interface( @@ -63,7 +59,7 @@ pub(crate) trait Traceroute { fn configure_icmp_socket(socket: &socket2::Socket, opt: &TracerouteOpt) -> anyhow::Result<()>; } -pub(crate) trait AsyncIcmpSocket { +pub trait AsyncIcmpSocket { fn from_socket2(socket: socket2::Socket) -> Self; fn set_ttl(&self, ttl: u32) -> anyhow::Result<()>; @@ -81,6 +77,7 @@ pub(crate) trait AsyncIcmpSocket { } struct AsyncUdpSocket(tokio::net::UdpSocket); + pub async fn try_run_leak_test( opt: &TracerouteOpt, ) -> anyhow::Result { @@ -308,143 +305,6 @@ async fn send_udp_probes(opt: &TracerouteOpt, socket: &mut AsyncUdpSocket) -> an Ok(()) } -/// Try to parse the bytes as an IPv4 or IPv6 packet. -/// -/// This only valdiates the IP header, not the payload. -fn parse_ip(packet: &[u8]) -> anyhow::Result, Ipv6Packet<'_>>> { - let ipv4_packet = Ipv4Packet::new(packet).ok_or_else(too_small)?; - - // ipv4-packets are smaller than ipv6, so we use an Ipv4Packet to check the version. - Ok(match ipv4_packet.get_version() { - 4 => Ip::V4(ipv4_packet), - 6 => { - let ipv6_packet = Ipv6Packet::new(packet).ok_or_else(too_small)?; - Ip::V6(ipv6_packet) - } - _ => bail!("Not a valid IP header"), - }) -} - -/// Try to parse the bytes as an IPv4 packet. -/// -/// This only valdiates the IPv4 header, not the payload. -fn parse_ipv4(packet: &[u8]) -> anyhow::Result> { - let ip_packet = Ipv4Packet::new(packet).ok_or_else(too_small)?; - ensure!(ip_packet.get_version() == 4, "Not IPv4"); - anyhow::Ok(ip_packet) -} - -/// Try to parse the bytes as an IPv6 packet. -/// -/// This only valdiates the IPv6 header, not the payload. -fn parse_ipv6(packet: &[u8]) -> anyhow::Result> { - let ip_packet = Ipv6Packet::new(packet).ok_or_else(too_small)?; - ensure!(ip_packet.get_version() == 6, "Not IPv6"); - anyhow::Ok(ip_packet) -} - -/// Try to parse an [Ipv4Packet] as an ICMP/TimeExceeded response to a packet sent by -/// [send_udp_probes] or [send_icmp_probes]. If successful, returns the [Ipv4Addr] of the packet -/// source. -/// -/// If the packet fails to parse, or is not a reply to a packet sent by us, this function returns -/// an error. -fn parse_icmp4_time_exceeded(ip_packet: &Ipv4Packet<'_>) -> anyhow::Result { - let ip_protocol = ip_packet.get_next_level_protocol(); - ensure!(ip_protocol == IpProtocol::Icmp, "Not ICMP"); - parse_icmp_time_exceeded_raw(Ip::V4(ip_packet.payload()))?; - Ok(ip_packet.get_source()) -} - -/// Try to parse an [Ipv6Packet] as an ICMP6/TimeExceeded response to a packet sent by -/// [send_udp_probes] or [send_icmp_probes]. If successful, returns the [Ipv6Addr] of the packet -/// source. -/// -/// If the packet fails to parse, or is not a reply to a packet sent by us, this function returns -/// an error. -fn parse_icmp6_time_exceeded(ip_packet: &Ipv6Packet<'_>) -> anyhow::Result { - let ip_protocol = ip_packet.get_next_header(); - ensure!(ip_protocol == IpProtocol::Icmpv6, "Not ICMP6"); - parse_icmp_time_exceeded_raw(Ip::V6(ip_packet.payload()))?; - Ok(ip_packet.get_source()) -} - -/// Try to parse some bytes into an ICMP or ICMP6 TimeExceeded response to a probe packet sent by -/// [send_udp_probes] or [send_icmp_probes]. -/// -/// If the packet fails to parse, or is not a reply to a packet sent by us, this function returns -/// an error. -fn parse_icmp_time_exceeded_raw(ip_payload: Ip<&[u8], &[u8]>) -> anyhow::Result<()> { - let icmpv4_packet; - let icmpv6_packet; - let icmp_packet: &[u8] = match ip_payload { - Ip::V4(ipv4_payload) => { - icmpv4_packet = IcmpPacket::new(ipv4_payload).ok_or(anyhow!("Too small"))?; - - let correct_type = icmpv4_packet.get_icmp_type() == IcmpTypes::TimeExceeded; - ensure!(correct_type, "Not ICMP/TimeExceeded"); - - icmpv4_packet.packet() - } - Ip::V6(ipv6_payload) => { - icmpv6_packet = Icmpv6Packet::new(ipv6_payload).ok_or(anyhow!("Too small"))?; - - let correct_type = icmpv6_packet.get_icmpv6_type() == Icmpv6Types::TimeExceeded; - ensure!(correct_type, "Not ICMP6/TimeExceeded"); - - icmpv6_packet.packet() - } - }; - - // TimeExceededPacket looks the same for both ICMP and ICMP6. - let time_exceeded = TimeExceededPacket::new(icmp_packet).ok_or_else(too_small)?; - ensure!( - time_exceeded.get_icmp_code() - == icmp::time_exceeded::IcmpCodes::TimeToLiveExceededInTransit, - "Not TTL Exceeded", - ); - - let original_ip_packet = parse_ip(time_exceeded.payload()).context("ICMP-wrapped IP packet")?; - - let (original_ip_protocol, original_ip_payload) = match &original_ip_packet { - Ip::V4(ipv4_packet) => (ipv4_packet.get_next_level_protocol(), ipv4_packet.payload()), - Ip::V6(ipv6_packet) => (ipv6_packet.get_next_header(), ipv6_packet.payload()), - }; - - match original_ip_protocol { - IpProtocol::Udp => { - let original_udp_packet = UdpPacket::new(original_ip_payload).ok_or_else(too_small)?; - - // check if payload looks right - // some network nodes will strip the payload, that's fine. - if !original_udp_packet.payload().is_empty() { - let udp_len = usize::from(original_udp_packet.get_length()); - let udp_payload = udp_len - .checked_sub(UdpPacket::minimum_packet_size()) - .and_then(|len| original_udp_packet.payload().get(..len)) - .ok_or(anyhow!("Invalid UDP length"))?; - if udp_payload != PROBE_PAYLOAD { - let udp_payload: String = udp_payload - .iter() - .copied() - .flat_map(escape_default) - .map(char::from) - .collect(); - bail!("Wrong UDP payload: {udp_payload:?}"); - } - } - - Ok(()) - } - - IpProtocol::Icmp => parse_icmp_probe(Ip::V4(original_ip_payload)), - - IpProtocol::Icmpv6 => parse_icmp_probe(Ip::V6(original_ip_payload)), - - _ => bail!("Not UDP/ICMP"), - } -} - /// Try to parse bytes as an ICMP/ICMP6 Echo Request matching the probe packets send by /// [send_icmp_probes]. fn parse_icmp_probe(icmp_bytes: Ip<&[u8], &[u8]>) -> anyhow::Result<()> { diff --git a/leak-checker/src/traceroute/platform/windows.rs b/leak-checker/src/traceroute/windows.rs similarity index 100% rename from leak-checker/src/traceroute/platform/windows.rs rename to leak-checker/src/traceroute/windows.rs