diff --git a/leak-checker/src/traceroute.rs b/leak-checker/src/traceroute.rs index b77fff18ccdc..a40eaca131c8 100644 --- a/leak-checker/src/traceroute.rs +++ b/leak-checker/src/traceroute.rs @@ -2,7 +2,7 @@ use std::{ ascii::escape_default, convert::Infallible, io, - net::{IpAddr, Ipv4Addr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, ops::{Range, RangeFrom}, time::Duration, }; @@ -10,11 +10,11 @@ use std::{ use anyhow::{anyhow, bail, ensure, Context}; use futures::{future::pending, select, stream, FutureExt, StreamExt, TryStreamExt}; use pnet_packet::{ - icmp::{ - echo_request::EchoRequestPacket, time_exceeded::TimeExceededPacket, IcmpPacket, IcmpTypes, - }, + icmp::{self, time_exceeded::TimeExceededPacket, IcmpCode, IcmpPacket, IcmpTypes}, + icmpv6::{self, Icmpv6Code, Icmpv6Packet, Icmpv6Types}, ip::IpNextHeaderProtocols as IpProtocol, ipv4::Ipv4Packet, + ipv6::Ipv6Packet, udp::UdpPacket, Packet, }; @@ -111,7 +111,14 @@ pub async fn try_run_leak_test(opt: &TracerouteOpt) -> anyhow::Result( +/// IP version, v4 or v6, with some associated data. +#[derive(Clone, Copy)] +enum Ip { + V4(V4), + V6(V6), +} + +async fn try_run_leak_test_impl( opt: &TracerouteOpt, ) -> anyhow::Result { // create the socket used for receiving the ICMP/TimeExceeded responses @@ -123,27 +130,32 @@ pub async fn try_run_leak_test_impl( Type::DGRAM }; - let icmp_socket = Socket::new(Domain::IPV4, icmp_socket_type, Some(Protocol::ICMPV4)) + let (ip_version, domain, icmp_protocol) = match opt.destination { + IpAddr::V4(..) => (Ip::V4(()), Domain::IPV4, Protocol::ICMPV4), + IpAddr::V6(..) => (Ip::V6(()), Domain::IPV6, Protocol::ICMPV6), + }; + + let icmp_socket = Socket::new(domain, icmp_socket_type, Some(icmp_protocol)) .context("Failed to open ICMP socket")?; icmp_socket .set_nonblocking(true) .context("Failed to set icmp_socket to nonblocking")?; - Impl::bind_socket_to_interface(&icmp_socket, &opt.interface)?; + Impl::bind_socket_to_interface(&icmp_socket, &opt.interface, ip_version)?; Impl::configure_icmp_socket(&icmp_socket, opt)?; let icmp_socket = Impl::AsyncIcmpSocket::from_socket2(icmp_socket); let send_probes = async { if opt.icmp { - send_icmp_probes(opt, &icmp_socket).await?; + send_icmp_probes::(opt, &icmp_socket).await?; } else { // create the socket used for sending the UDP probing packets - let udp_socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)) + let udp_socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP)) .context("Failed to open UDP socket")?; - Impl::bind_socket_to_interface(&udp_socket, &opt.interface) + Impl::bind_socket_to_interface(&udp_socket, &opt.interface, ip_version) .context("Failed to bind UDP socket to interface")?; udp_socket @@ -181,12 +193,10 @@ pub async fn try_run_leak_test_impl( /// Send ICMP/Echo packets with a very low TTL to `opt.destination`. /// /// Use [AsyncIcmpSocket::recv_ttl_responses] to receive replies. -async fn send_icmp_probes( +async fn send_icmp_probes( opt: &TracerouteOpt, socket: &impl AsyncIcmpSocket, ) -> anyhow::Result<()> { - use pnet_packet::icmp::{echo_request::*, *}; - for ttl in DEFAULT_TTL_RANGE { log::debug!("sending probe packet (ttl={ttl})"); @@ -197,22 +207,61 @@ async fn send_icmp_probes( // the first packet will sometimes get dropped on MacOS, thus we send two packets let number_of_sends = if cfg!(target_os = "macos") { 2 } else { 1 }; - let echo = EchoRequest { - icmp_type: IcmpTypes::EchoRequest, - icmp_code: IcmpCode(0), - checksum: 0, - identifier: 1, - sequence_number: 1, - payload: PROBE_PAYLOAD.to_vec(), - }; - let mut packet = - MutableEchoRequestPacket::owned(vec![0u8; 8 + PROBE_PAYLOAD.len()]).unwrap(); - packet.populate(&echo); - packet.set_checksum(checksum(&IcmpPacket::new(packet.packet()).unwrap())); + // construct ICMP/ICMP6 echo request packet + let mut packet_v4; + let mut packet_v6; + let packet_bytes; + const ECHO_REQUEST_HEADER_LEN: usize = 8; + match opt.destination { + IpAddr::V4(..) => { + let echo = icmp::echo_request::EchoRequest { + icmp_type: IcmpTypes::EchoRequest, + icmp_code: IcmpCode(0), + checksum: 0, + identifier: 1, + sequence_number: 1, + payload: PROBE_PAYLOAD.to_vec(), + }; + + let len = ECHO_REQUEST_HEADER_LEN + PROBE_PAYLOAD.len(); + packet_v4 = + icmp::echo_request::MutableEchoRequestPacket::owned(vec![0u8; len]).unwrap(); + packet_v4.populate(&echo); + packet_v4.set_checksum(icmp::checksum( + &icmp::IcmpPacket::new(packet_v4.packet()).unwrap(), + )); + packet_bytes = packet_v4.packet(); + } + IpAddr::V6(destination) => { + let IpAddr::V6(source) = Impl::get_interface_ip(&opt.interface, Ip::V6(()))? else { + bail!("Tried to send IPv6 on IPv4 interface"); + }; + + let echo = icmpv6::echo_request::EchoRequest { + icmpv6_type: Icmpv6Types::EchoRequest, + icmpv6_code: Icmpv6Code(0), + checksum: 0, + identifier: 1, + sequence_number: 1, + payload: PROBE_PAYLOAD.to_vec(), + }; + + let len = ECHO_REQUEST_HEADER_LEN + PROBE_PAYLOAD.len(); + packet_v6 = + icmpv6::echo_request::MutableEchoRequestPacket::owned(vec![0u8; len]).unwrap(); + packet_v6.populate(&echo); + packet_v6.set_checksum(icmpv6::checksum( + &icmpv6::Icmpv6Packet::new(packet_v6.packet()).unwrap(), + &source, + &destination, + )); + packet_bytes = packet_v6.packet(); + } + } let result: io::Result<()> = stream::iter(0..number_of_sends) // call `send_to` `number_of_sends` times - .then(|_| socket.send_to(packet.packet(), opt.destination)) + .then(|_| socket.send_to(packet_bytes, opt.destination)) .map_ok(drop) .try_collect() // abort on the first error .await; @@ -279,6 +328,23 @@ async fn send_udp_probes( 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. @@ -288,40 +354,86 @@ fn parse_ipv4(packet: &[u8]) -> anyhow::Result> { 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_icmp_time_exceeded(ip_packet: &Ipv4Packet<'_>) -> anyhow::Result { +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_packet.payload())?; + parse_icmp_time_exceeded_raw(Ip::V4(ip_packet.payload()))?; Ok(ip_packet.get_source()) } -/// Try to parse some bytes into an ICMP/TimeExceeded response to a probe packet sent by +/// 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(bytes: &[u8]) -> anyhow::Result<()> { - let icmp_packet = IcmpPacket::new(bytes).ok_or(anyhow!("Too small"))?; +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 = icmp_packet.get_icmp_type() == IcmpTypes::TimeExceeded; - ensure!(correct_type, "Not ICMP/TimeExceeded"); + 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 time_exceeeded = TimeExceededPacket::new(icmp_packet.packet()).ok_or_else(too_small)?; + let correct_type = icmpv6_packet.get_icmpv6_type() == Icmpv6Types::TimeExceeded; + ensure!(correct_type, "Not ICMP6/TimeExceeded"); - let original_ip_packet = Ipv4Packet::new(time_exceeeded.payload()).ok_or_else(too_small)?; - let original_ip_protocol = original_ip_packet.get_next_level_protocol(); - ensure!(original_ip_packet.get_version() == 4, "Not IPv4"); + 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_packet.payload()).ok_or_else(too_small)?; + 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. @@ -345,9 +457,36 @@ fn parse_icmp_time_exceeded_raw(bytes: &[u8]) -> anyhow::Result<()> { Ok(()) } + IpProtocol::Icmpv6 => { + let original_icmp_packet = + icmpv6::echo_request::EchoRequestPacket::new(original_ip_payload) + .ok_or_else(too_small)?; + + ensure!( + original_icmp_packet.get_icmpv6_type() == Icmpv6Types::EchoRequest, + "Not ICMP6/EchoRequest" + ); + + // check if payload looks right + // some network nodes will strip the payload, that's fine. + let echo_payload = original_icmp_packet.payload(); + if !echo_payload.is_empty() && !echo_payload.starts_with(&PROBE_PAYLOAD) { + let echo_payload: String = echo_payload + .iter() + .copied() + .flat_map(escape_default) + .map(char::from) + .collect(); + bail!("Wrong ICMP6/Echo payload: {echo_payload:?}"); + } + + Ok(()) + } + IpProtocol::Icmp => { let original_icmp_packet = - EchoRequestPacket::new(original_ip_packet.payload()).ok_or_else(too_small)?; + icmp::echo_request::EchoRequestPacket::new(original_ip_payload) + .ok_or_else(too_small)?; ensure!( original_icmp_packet.get_icmp_type() == IcmpTypes::EchoRequest, @@ -374,18 +513,37 @@ fn parse_icmp_time_exceeded_raw(bytes: &[u8]) -> anyhow::Result<()> { } } -fn parse_icmp_echo_raw(icmp_bytes: &[u8]) -> anyhow::Result<()> { - let echo_packet = EchoRequestPacket::new(icmp_bytes).ok_or_else(too_small)?; +fn parse_icmp_echo_raw(icmp_bytes: Ip<&[u8], &[u8]>) -> anyhow::Result<()> { + let echo_packet_v4; + let echo_packet_v6; + let echo_payload = match icmp_bytes { + Ip::V4(icmpv4_bytes) => { + echo_packet_v4 = + icmp::echo_request::EchoRequestPacket::new(icmpv4_bytes).ok_or_else(too_small)?; - ensure!( - echo_packet.get_icmp_type() == IcmpTypes::EchoRequest, - "Not ICMP/EchoRequest" - ); + ensure!( + echo_packet_v4.get_icmp_type() == IcmpTypes::EchoRequest, + "Not ICMP/EchoRequest" + ); + + echo_packet_v4.payload() + } + Ip::V6(icmpv6_bytes) => { + echo_packet_v6 = + icmpv6::echo_request::EchoRequestPacket::new(icmpv6_bytes).ok_or_else(too_small)?; + + ensure!( + echo_packet_v6.get_icmpv6_type() == Icmpv6Types::EchoRequest, + "Not ICMP6/EchoRequest" + ); + + echo_packet_v6.payload() + } + }; // check if payload looks right // some network nodes will strip the payload. // some network nodes will add a bunch of zeros at the end. - let echo_payload = echo_packet.payload(); if !echo_payload.is_empty() && !echo_payload.starts_with(&PROBE_PAYLOAD) { let echo_payload: String = echo_payload .iter() @@ -393,7 +551,7 @@ fn parse_icmp_echo_raw(icmp_bytes: &[u8]) -> anyhow::Result<()> { .flat_map(escape_default) .map(char::from) .collect(); - bail!("Wrong ICMP/Echo payload: {echo_payload:?}"); + bail!("Wrong ICMP6/Echo payload: {echo_payload:?}"); } Ok(()) diff --git a/leak-checker/src/traceroute/platform/android.rs b/leak-checker/src/traceroute/platform/android.rs index c561d74e43f1..ee8539ec3d77 100644 --- a/leak-checker/src/traceroute/platform/android.rs +++ b/leak-checker/src/traceroute/platform/android.rs @@ -2,7 +2,10 @@ use std::net::IpAddr; use socket2::Socket; -use crate::{traceroute::TracerouteOpt, Interface}; +use crate::{ + traceroute::{Ip, TracerouteOpt}, + Interface, +}; use super::{linux, linux::TracerouteLinux, unix, Traceroute}; @@ -12,13 +15,17 @@ impl Traceroute for TracerouteAndroid { type AsyncIcmpSocket = linux::AsyncIcmpSocketImpl; type AsyncUdpSocket = unix::AsyncUdpSocketUnix; - fn bind_socket_to_interface(socket: &Socket, interface: &Interface) -> anyhow::Result<()> { + 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 - super::common::bind_socket_to_interface::(socket, interface) + super::common::bind_socket_to_interface::(socket, interface, ip_version) } - fn get_interface_ip(interface: &Interface) -> anyhow::Result { - super::unix::get_interface_ip(interface) + fn get_interface_ip(interface: &Interface, ip_version: Ip) -> anyhow::Result { + super::unix::get_interface_ip(interface, ip_version) } fn configure_icmp_socket(socket: &socket2::Socket, opt: &TracerouteOpt) -> anyhow::Result<()> { diff --git a/leak-checker/src/traceroute/platform/common.rs b/leak-checker/src/traceroute/platform/common.rs index 438f1b97e2cc..54df8599bb00 100644 --- a/leak-checker/src/traceroute/platform/common.rs +++ b/leak-checker/src/traceroute/platform/common.rs @@ -13,7 +13,10 @@ use tokio::{ }; use crate::{ - traceroute::{parse_icmp_time_exceeded, parse_ipv4, RECV_GRACE_TIME}, + traceroute::{ + parse_icmp4_time_exceeded, parse_icmp6_time_exceeded, parse_ipv4, parse_ipv6, Ip, + RECV_GRACE_TIME, + }, Interface, LeakInfo, LeakStatus, }; @@ -22,8 +25,9 @@ use super::{AsyncIcmpSocket, Traceroute}; pub fn bind_socket_to_interface( socket: &Socket, interface: &Interface, + ip_version: Ip, ) -> anyhow::Result<()> { - let interface_ip = Impl::get_interface_ip(interface)?; + let interface_ip = Impl::get_interface_ip(interface, ip_version)?; log::info!("Binding socket to {interface_ip} ({interface:?})"); @@ -60,10 +64,6 @@ pub async fn recv_ttl_responses( log::debug!("Reading from ICMP socket"); - // let n = socket - // .recv(unsafe { &mut *(&mut read_buf[..] as *mut [u8] as *mut [MaybeUninit]) }) - // .context("Failed to read from raw socket")?; - let (n, source) = select! { result = socket.recv_from(&mut read_buf[..]) => result .context("Failed to read from raw socket")?, @@ -77,23 +77,24 @@ pub async fn recv_ttl_responses( }; let packet = &read_buf[..n]; - // TODO: ipv6 - let result = parse_ipv4(packet) - .map_err(|e| anyhow!("Ignoring packet: (len={n}, ip.src={source}) {e} ({packet:02x?})")) - .and_then(|ip_packet| { - parse_icmp_time_exceeded(&ip_packet).map_err(|e| { - anyhow!( - "Ignoring packet (len={n}, ip.src={source}, ip.dest={}): {e}", - ip_packet.get_destination(), - ) - }) - }); + + 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); - let ip = IpAddr::from(ip); if !reachable_nodes.contains(&ip) { reachable_nodes.push(ip); } diff --git a/leak-checker/src/traceroute/platform/linux.rs b/leak-checker/src/traceroute/platform/linux.rs index f534c442512d..3c5012298b14 100644 --- a/leak-checker/src/traceroute/platform/linux.rs +++ b/leak-checker/src/traceroute/platform/linux.rs @@ -4,19 +4,18 @@ use std::{net::IpAddr, time::Duration}; use anyhow::{anyhow, bail, Context}; use nix::errno::Errno; -use nix::sys::socket::sockopt::Ipv4RecvErr; use nix::sys::socket::{ - recvmsg, setsockopt, ControlMessageOwned, MsgFlags, SockaddrIn, SockaddrIn6, SockaddrLike, + recvmsg, setsockopt, sockopt::Ipv4RecvErr, ControlMessageOwned, MsgFlags, SockaddrIn, + SockaddrIn6, SockaddrLike, }; use nix::{cmsg_space, libc}; use pnet_packet::icmp::time_exceeded::IcmpCodes; -use pnet_packet::icmp::IcmpTypes; -use pnet_packet::icmp::{IcmpCode, IcmpType}; +use pnet_packet::icmp::{IcmpCode, IcmpType, IcmpTypes}; use pnet_packet::icmpv6::{Icmpv6Code, Icmpv6Type, Icmpv6Types}; use socket2::Socket; use tokio::time::{sleep, Instant}; -use crate::traceroute::{parse_icmp_echo_raw, TracerouteOpt, RECV_GRACE_TIME}; +use crate::traceroute::{parse_icmp_echo_raw, Ip, TracerouteOpt, RECV_GRACE_TIME}; use crate::{Interface, LeakInfo, LeakStatus}; use super::{unix, AsyncIcmpSocket, Traceroute}; @@ -29,12 +28,16 @@ impl Traceroute for TracerouteLinux { type AsyncIcmpSocket = AsyncIcmpSocketImpl; type AsyncUdpSocket = unix::AsyncUdpSocketUnix; - fn bind_socket_to_interface(socket: &Socket, interface: &Interface) -> anyhow::Result<()> { + fn bind_socket_to_interface( + socket: &Socket, + interface: &Interface, + _: Ip, + ) -> anyhow::Result<()> { bind_socket_to_interface(socket, interface) } - fn get_interface_ip(interface: &Interface) -> anyhow::Result { - super::unix::get_interface_ip(interface) + fn get_interface_ip(interface: &Interface, ip_version: Ip) -> anyhow::Result { + super::unix::get_interface_ip(interface, ip_version) } fn configure_icmp_socket(socket: &socket2::Socket, _opt: &TracerouteOpt) -> anyhow::Result<()> { @@ -149,66 +152,100 @@ async fn recv_ttl_responses( break recv_packet; }; - // NOTE: This should be the IP destination of our ping packets. That does NOT mean the - // packets reached the destination. Instead, if we see an EHOSTUNREACH control message, - // it means the packets was instead dropped along the way. Seeing this address helps us - // identify that this is a response to the ping we sent. let RecvPacket { source_addr, packet, control_message, } = recv_packet; - debug_assert_eq!(source_addr, destination); + + macro_rules! skip_if { + ($skip_condition:expr, $message:expr) => {{ + if $skip_condition { + log::debug!("Ignoring received packet: {}", $skip_condition); + continue 'outer; + } + }}; + } + + // NOTE: This should be the IP destination of our ping packets. That does NOT mean the + // packets reached the destination. Instead, if we see an EHOSTUNREACH control message, + // it means the packets was instead dropped along the way. Seeing this address helps us + // identify that this is a response to the ping we sent. + skip_if!(source_addr != destination, "Unknown source"); let error_source = match control_message { - ControlMessageOwned::Ipv6RecvErr(socket_error, source_addr) => { + ControlMessageOwned::Ipv4RecvErr(socket_error, source_addr) => { let libc::sock_extended_err { ee_errno, // Error Number: Should be EHOSTUNREACH - ee_origin, // Error Origin: 3 = Icmp6. - ee_type, // ICMP Type: 3 = ICMP6/TimeExceeded + ee_origin, // Error Origin: 2 = Icmp + ee_type, // ICMP Type: 11 = ICMP/TimeExceeded. ee_code, // ICMP Code. 0 = TTL exceeded in transit. .. } = socket_error; let errno = Errno::from_raw(ee_errno as i32); - debug_assert_eq!(errno, Errno::EHOSTUNREACH); - debug_assert_eq!(ee_origin, nix::libc::SO_EE_ORIGIN_ICMP6); + skip_if!(errno != Errno::EHOSTUNREACH, "Unexpected errno"); + skip_if!( + ee_origin != nix::libc::SO_EE_ORIGIN_ICMP, + "Unexpected origin" + ); - let icmp_type = Icmpv6Type::new(ee_type); - debug_assert_eq!(icmp_type, Icmpv6Types::TimeExceeded); + let icmp_type = IcmpType::new(ee_type); + skip_if!(icmp_type != IcmpTypes::TimeExceeded, "Unexpected ICMP type"); - let icmp_code = Icmpv6Code::new(ee_code); - debug_assert_eq!(icmp_code, Icmpv6Code::new(0)); + let icmp_code = IcmpCode::new(ee_code); + skip_if!( + icmp_code != IcmpCodes::TimeToLiveExceededInTransit, + "Unexpected ICMP code" + ); // NOTE: This is the IP of the node that dropped the packet due to TTL exceeded. - let error_source = SockaddrIn6::from(source_addr.unwrap()); + let error_source = SockaddrIn::from(source_addr.unwrap()); log::debug!("addr: {error_source}"); + // Ensure that this is the original Echo packet that we sent. + skip_if!( + parse_icmp_echo_raw(Ip::V4(packet)).is_err(), + "Not a response to us" + ); + IpAddr::from(error_source.ip()) } - ControlMessageOwned::Ipv4RecvErr(socket_error, source_addr) => { + ControlMessageOwned::Ipv6RecvErr(socket_error, source_addr) => { let libc::sock_extended_err { ee_errno, // Error Number: Should be EHOSTUNREACH - ee_origin, // Error Origin: 2 = Icmp - ee_type, // ICMP Type: 11 = ICMP/TimeExceeded. + ee_origin, // Error Origin: 3 = Icmp6. + ee_type, // ICMP Type: 3 = ICMP6/TimeExceeded ee_code, // ICMP Code. 0 = TTL exceeded in transit. .. } = socket_error; let errno = Errno::from_raw(ee_errno as i32); - debug_assert_eq!(errno, Errno::EHOSTUNREACH); - debug_assert_eq!(ee_origin, nix::libc::SO_EE_ORIGIN_ICMP); + skip_if!(errno != Errno::EHOSTUNREACH, "Unexpected errno"); + skip_if!( + ee_origin != nix::libc::SO_EE_ORIGIN_ICMP6, + "Unexpected origin" + ); - let icmp_type = IcmpType::new(ee_type); - debug_assert_eq!(icmp_type, IcmpTypes::TimeExceeded); + let icmp_type = Icmpv6Type::new(ee_type); + skip_if!( + icmp_type != Icmpv6Types::TimeExceeded, + "Unexpected ICMP type" + ); - let icmp_code = IcmpCode::new(ee_code); - debug_assert_eq!(icmp_code, IcmpCodes::TimeToLiveExceededInTransit); + let icmp_code = Icmpv6Code::new(ee_code); + skip_if!(icmp_code != Icmpv6Code::new(0), "Unexpected ICMP code"); // NOTE: This is the IP of the node that dropped the packet due to TTL exceeded. - let error_source = SockaddrIn::from(source_addr.unwrap()); + let error_source = SockaddrIn6::from(source_addr.unwrap()); log::debug!("addr: {error_source}"); + // Ensure that this is the original Echo packet that we sent. + skip_if!( + parse_icmp_echo_raw(Ip::V6(packet)).is_err(), + "Not a response to us" + ); + IpAddr::from(error_source.ip()) } other_message => { @@ -218,10 +255,6 @@ async fn recv_ttl_responses( } }; - // Ensure that this is the original Echo packet that we sent. - // TODO: skip on error - parse_icmp_echo_raw(packet).context("")?; - log::debug!("Got a probe response, we are leaking!"); timeout_at.get_or_insert_with(|| Instant::now() + RECV_GRACE_TIME); reachable_nodes.push(error_source); diff --git a/leak-checker/src/traceroute/platform/macos.rs b/leak-checker/src/traceroute/platform/macos.rs index 1f4c4b5199a0..704a574bfabc 100644 --- a/leak-checker/src/traceroute/platform/macos.rs +++ b/leak-checker/src/traceroute/platform/macos.rs @@ -7,7 +7,7 @@ use anyhow::{anyhow, Context}; use nix::net::if_::if_nametoindex; use socket2::Socket; -use crate::traceroute::TracerouteOpt; +use crate::traceroute::{Ip, TracerouteOpt}; use crate::{Interface, LeakStatus}; use super::{common, unix, AsyncIcmpSocket, Traceroute}; @@ -20,13 +20,17 @@ impl Traceroute for TracerouteMacos { type AsyncIcmpSocket = AsyncIcmpSocketImpl; type AsyncUdpSocket = unix::AsyncUdpSocketUnix; - fn bind_socket_to_interface(socket: &Socket, interface: &Interface) -> anyhow::Result<()> { + 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) + bind_socket_to_interface(socket, interface, ip_version) } - fn get_interface_ip(interface: &Interface) -> anyhow::Result { - super::unix::get_interface_ip(interface) + fn get_interface_ip(interface: &Interface, ip_version: Ip) -> anyhow::Result { + super::unix::get_interface_ip(interface, ip_version) } fn configure_icmp_socket( @@ -67,7 +71,11 @@ impl AsyncIcmpSocket for AsyncIcmpSocketImpl { } } -pub fn bind_socket_to_interface(socket: &Socket, interface: &Interface) -> anyhow::Result<()> { +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:?}"); @@ -77,6 +85,9 @@ pub fn bind_socket_to_interface(socket: &Socket, interface: &Interface) -> anyho .and_then(|code| NonZero::new(code).ok_or(anyhow!("Non-zero error code"))) .context("Failed to get interface index")?; - socket.bind_device_by_index_v4(Some(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/mod.rs b/leak-checker/src/traceroute/platform/mod.rs index 29ff53943c26..3b351651c39e 100644 --- a/leak-checker/src/traceroute/platform/mod.rs +++ b/leak-checker/src/traceroute/platform/mod.rs @@ -5,7 +5,7 @@ use std::{ use crate::{Interface, LeakStatus}; -use super::TracerouteOpt; +use super::{Ip, TracerouteOpt}; #[cfg(any(target_os = "linux", target_os = "android"))] pub mod android; @@ -28,15 +28,16 @@ pub mod common; /// Private trait that let's us define the platform-specific implementations and types required for /// tracerouting. -pub trait Traceroute { +pub(crate) trait Traceroute { type AsyncIcmpSocket: AsyncIcmpSocket; type AsyncUdpSocket: AsyncUdpSocket; - fn get_interface_ip(interface: &Interface) -> anyhow::Result; + fn get_interface_ip(interface: &Interface, ip_version: Ip) -> anyhow::Result; fn bind_socket_to_interface( socket: &socket2::Socket, interface: &Interface, + ip_version: Ip, ) -> anyhow::Result<()>; /// Configure an ICMP socket to allow reception of ICMP/TimeExceeded errors. @@ -44,7 +45,7 @@ pub trait Traceroute { fn configure_icmp_socket(socket: &socket2::Socket, opt: &TracerouteOpt) -> anyhow::Result<()>; } -pub trait AsyncIcmpSocket { +pub(crate) trait AsyncIcmpSocket { fn from_socket2(socket: socket2::Socket) -> Self; fn set_ttl(&self, ttl: u32) -> anyhow::Result<()>; @@ -61,7 +62,7 @@ pub trait AsyncIcmpSocket { async fn recv_ttl_responses(&self, opt: &TracerouteOpt) -> anyhow::Result; } -pub trait AsyncUdpSocket { +pub(crate) trait AsyncUdpSocket { fn from_socket2(socket: socket2::Socket) -> Self; fn set_ttl(&self, ttl: u32) -> anyhow::Result<()>; diff --git a/leak-checker/src/traceroute/platform/unix.rs b/leak-checker/src/traceroute/platform/unix.rs index d6c7d06da8a9..17f292e45679 100644 --- a/leak-checker/src/traceroute/platform/unix.rs +++ b/leak-checker/src/traceroute/platform/unix.rs @@ -3,11 +3,12 @@ use std::os::fd::{FromRawFd, IntoRawFd}; use anyhow::Context; +use crate::traceroute::Ip; use crate::Interface; use super::AsyncUdpSocket; -pub fn get_interface_ip(interface: &Interface) -> anyhow::Result { +pub fn get_interface_ip(interface: &Interface, ip_version: Ip) -> anyhow::Result { let Interface::Name(interface) = interface; for interface_address in nix::ifaddrs::getifaddrs()? { @@ -17,14 +18,19 @@ pub fn get_interface_ip(interface: &Interface) -> anyhow::Result { let Some(address) = interface_address.address else { continue; }; - let Some(address) = address.as_sockaddr_in() else { - continue; - }; - - // TODO: ipv6 - //let Some(address) = address.as_sockaddr_in6() else { continue }; - return Ok(address.ip().into()); + 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"); diff --git a/leak-checker/src/traceroute/platform/windows.rs b/leak-checker/src/traceroute/platform/windows.rs index 089fb998c5cc..44d9f2261a8e 100644 --- a/leak-checker/src/traceroute/platform/windows.rs +++ b/leak-checker/src/traceroute/platform/windows.rs @@ -18,7 +18,9 @@ use windows_sys::Win32::Networking::WinSock::{ }; use crate::{ - traceroute::{TracerouteOpt, DEFAULT_TTL_RANGE, LEAK_TIMEOUT, PROBE_INTERVAL, SEND_TIMEOUT}, + traceroute::{ + Ip, TracerouteOpt, DEFAULT_TTL_RANGE, LEAK_TIMEOUT, PROBE_INTERVAL, SEND_TIMEOUT, + }, Interface, LeakInfo, LeakStatus, }; @@ -37,7 +39,12 @@ pub struct AsyncUdpSocketWindows(tokio::net::UdpSocket); /// using `ping.exe`, which does work for some reason. My best guess is that it has special kernel /// access to be able to do this. pub async fn traceroute_using_ping(opt: &TracerouteOpt) -> anyhow::Result { - let interface_ip = get_interface_ip(&opt.interface)?; + let ip_version = match opt.destination { + IpAddr::V4(..) => Ip::V4(()), + IpAddr::V6(..) => Ip::V6(()), + }; + + let interface_ip = get_interface_ip(&opt.interface, ip_version)?; let mut ping_tasks = FuturesUnordered::new(); @@ -119,12 +126,16 @@ impl Traceroute for TracerouteWindows { type AsyncIcmpSocket = AsyncIcmpSocketImpl; type AsyncUdpSocket = AsyncUdpSocketWindows; - fn bind_socket_to_interface(socket: &Socket, interface: &Interface) -> anyhow::Result<()> { - common::bind_socket_to_interface::(socket, interface) + fn bind_socket_to_interface( + socket: &Socket, + interface: &Interface, + ip_version: Ip, + ) -> anyhow::Result<()> { + common::bind_socket_to_interface::(socket, interface, ip_version) } - fn get_interface_ip(interface: &Interface) -> anyhow::Result { - get_interface_ip(interface) + fn get_interface_ip(interface: &Interface, ip_version: Ip) -> anyhow::Result { + get_interface_ip(interface, ip_version) } fn configure_icmp_socket(socket: &socket2::Socket, _opt: &TracerouteOpt) -> anyhow::Result<()> { @@ -184,17 +195,20 @@ impl AsyncUdpSocket for AsyncUdpSocketWindows { } } -pub fn get_interface_ip(interface: &Interface) -> anyhow::Result { +pub fn get_interface_ip(interface: &Interface, ip_version: Ip) -> anyhow::Result { let interface_luid = match interface { Interface::Name(name) => luid_from_alias(name)?, Interface::Luid(luid) => *luid, }; - // TODO: ipv6 - let interface_ip = get_ip_address_for_interface(AddressFamily::Ipv4, interface_luid)? - .ok_or(anyhow!("No IP for interface {interface:?}"))?; + let address_family = match ip_version { + Ip::V4(..) => AddressFamily::Ipv4, + Ip::V6(..) => AddressFamily::Ipv6, + }; - Ok(interface_ip) + get_ip_address_for_interface(address_family, interface_luid) + .with_context(|| anyhow!("Failed to get IP for interface {interface:?}"))? + .ok_or(anyhow!("No IP for interface {interface:?}")) } /// Configure the raw socket we use for listening to ICMP responses. diff --git a/mullvad-daemon/src/leak_checker/mod.rs b/mullvad-daemon/src/leak_checker/mod.rs index b1b5470a57af..154a85ce9360 100644 --- a/mullvad-daemon/src/leak_checker/mod.rs +++ b/mullvad-daemon/src/leak_checker/mod.rs @@ -216,9 +216,9 @@ async fn check_for_leaks( IpAddr::V6(..) => AddressFamily::Ipv6, }; - let Ok(Some(route)) = talpid_routing::get_best_default_route(family) else { - todo!("no best default route"); - }; + let route = talpid_routing::get_best_default_route(family) + .context("Failed to get best default route")? + .ok_or_else(|| anyhow!("No default route found"))?; leak_checker::Interface::Luid(route.iface) };