diff options
| author | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2024-11-14 15:54:01 +0100 |
|---|---|---|
| committer | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-01-24 17:38:28 +0100 |
| commit | 0118410ff566cf42a20e0966a3549af93e186ce9 (patch) | |
| tree | c33cbfb9c3c3ea2e9fbf5cca035b32383a9755e5 | |
| parent | 73ecf1c4a954318359a10c1be232570798b398de (diff) | |
| download | mullvadvpn-0118410ff566cf42a20e0966a3549af93e186ce9.tar.xz mullvadvpn-0118410ff566cf42a20e0966a3549af93e186ce9.zip | |
Add experimental linux tracroute poc without root
| -rw-r--r-- | leak-checker/src/traceroute.rs | 206 |
1 files changed, 166 insertions, 40 deletions
diff --git a/leak-checker/src/traceroute.rs b/leak-checker/src/traceroute.rs index 836546c15c..59b1e0fc3f 100644 --- a/leak-checker/src/traceroute.rs +++ b/leak-checker/src/traceroute.rs @@ -1,17 +1,15 @@ use std::{ ascii::escape_default, - ffi::c_void, io, net::{IpAddr, Ipv4Addr}, ops::{Range, RangeFrom}, - os::fd::{AsFd, AsRawFd, FromRawFd, IntoRawFd}, + os::fd::{FromRawFd, IntoRawFd}, time::Duration, }; use eyre::{bail, ensure, eyre, OptionExt, WrapErr}; use futures::{future::pending, stream, StreamExt, TryFutureExt, TryStreamExt}; use match_cfg::match_cfg; -use nix::libc::setsockopt; use pnet_packet::{ icmp::{ echo_request::EchoRequestPacket, time_exceeded::TimeExceededPacket, IcmpPacket, IcmpTypes, @@ -106,48 +104,59 @@ pub async fn try_run_leak_test(opt: &TracerouteOpt) -> eyre::Result<LeakStatus> .set_nonblocking(true) .wrap_err("Failed to set icmp_socket to nonblocking")?; - let n = 1; - unsafe { - setsockopt( - icmp_socket.as_fd().as_raw_fd(), - nix::libc::SOL_IP, - nix::libc::IP_RECVERR, - &n as *const _ as *const c_void, - size_of_val(&n) as u32, - ) - }; + #[cfg(any(target_os = "linux", target_os = "android"))] + { + use std::ffi::c_void; + use std::os::fd::{AsFd, AsRawFd}; + + let n = 1; + unsafe { + setsockopt( + icmp_socket.as_fd().as_raw_fd(), + nix::libc::SOL_IP, + nix::libc::IP_RECVERR, + &n as *const _ as *const std::ffi::c_void, + size_of_val(&n) as u32, + ) + }; + } bind_socket_to_interface(&icmp_socket, &opt.interface)?; // HACK: Wrap the socket in a tokio::net::UdpSocket to be able to use it async // SAFETY: `into_raw_fd()` consumes the socket and returns an owned & open file descriptor. let icmp_socket = unsafe { std::net::UdpSocket::from_raw_fd(icmp_socket.into_raw_fd()) }; - let icmp_socket = UdpSocket::from_std(icmp_socket)?; + let mut icmp_socket = UdpSocket::from_std(icmp_socket)?; // on Windows, we need to do some additional configuration of the raw socket #[cfg(target_os = "windows")] configure_listen_socket(&icmp_socket, interface)?; - // create the socket used for sending the UDP probing packets - let udp_socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)) - .wrap_err("Failed to open UDP socket")?; - bind_socket_to_interface(&udp_socket, &opt.interface) - .wrap_err("Failed to bind UDP socket to interface")?; - udp_socket - .set_nonblocking(true) - .wrap_err("Failed to set udp_socket to nonblocking")?; + if opt.icmp { + timeout(SEND_TIMEOUT, send_icmp_probes(&mut icmp_socket, opt)) + .map_err(|_timeout| eyre!("Timed out while trying to send probe packet")) + .await??; + } else { + // create the socket used for sending the UDP probing packets + let udp_socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)) + .wrap_err("Failed to open UDP socket")?; + bind_socket_to_interface(&udp_socket, &opt.interface) + .wrap_err("Failed to bind UDP socket to interface")?; + udp_socket + .set_nonblocking(true) + .wrap_err("Failed to set udp_socket to nonblocking")?; - // HACK: Wrap the socket in a tokio::net::UdpSocket to be able to use it async - // SAFETY: `into_raw_fd()` consumes the socket and returns an owned & open file descriptor. - let udp_socket = unsafe { std::net::UdpSocket::from_raw_fd(udp_socket.into_raw_fd()) }; - let udp_socket = UdpSocket::from_std(udp_socket)?; - drop(udp_socket); + // HACK: Wrap the socket in a tokio::net::UdpSocket to be able to use it async + // SAFETY: `into_raw_fd()` consumes the socket and returns an owned & open file descriptor. + let udp_socket = unsafe { std::net::UdpSocket::from_raw_fd(udp_socket.into_raw_fd()) }; + let mut udp_socket = UdpSocket::from_std(udp_socket)?; - let mut icmp_socket = icmp_socket; - timeout(SEND_TIMEOUT, send_icmp_probes(&mut icmp_socket, opt)) - .map_err(|_timeout| eyre!("Timed out while trying to send probe packet")) - .await??; + timeout(SEND_TIMEOUT, send_udp_probes(&mut udp_socket, opt)) + .map_err(|_timeout| eyre!("Timed out while trying to send probe packet")) + .await??; + } + //let recv_task = read_probe_responses(&opt.interface, icmp_socket); let recv_task = read_probe_responses(&opt.interface, icmp_socket); // wait until either task exits, or the timeout is reached @@ -205,8 +214,6 @@ async fn send_icmp_probes(socket: &mut UdpSocket, opt: &TracerouteOpt) -> eyre:: packet.populate(&echo); packet.set_checksum(checksum(&IcmpPacket::new(packet.packet()).unwrap())); - log::error!("echo packet: {:02x?}", packet.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, port))) @@ -227,7 +234,7 @@ async fn send_icmp_probes(socket: &mut UdpSocket, opt: &TracerouteOpt) -> eyre:: Ok(()) } -async fn send_udp_probes(socket: UdpSocket, opt: &TracerouteOpt) -> eyre::Result<()> { +async fn send_udp_probes(socket: &mut UdpSocket, opt: &TracerouteOpt) -> eyre::Result<()> { // ensure we don't send anything to `opt.exclude_port` let ports = DEFAULT_PORT_RANGE // skip the excluded port @@ -265,6 +272,120 @@ async fn send_udp_probes(socket: UdpSocket, opt: &TracerouteOpt) -> eyre::Result Ok(()) } +/// Experimental PoC of a linux implementation that doesn't need root. +#[cfg(any(target_os = "linux", target_os = "android"))] +#[allow(dead_code)] +async fn read_probe_responses_no_root( + _interface: &str, + socket: UdpSocket, +) -> eyre::Result<LeakStatus> { + use nix::libc::{errno::Errno, libc::setsockopt, setsockopt, sock_extended_err}; + use std::ffi::c_void; + use std::mem::transmute; + use std::os::fd::AsRawFd; + + // the list of node IP addresses from which we received a response to our probe packets. + let mut reachable_nodes = vec![]; + + let mut read_buf = vec![0u8; usize::from(u16::MAX)].into_boxed_slice(); + loop { + log::debug!("Reading from ICMP socket"); + + // XXX: only works for ipv4 + let mut msg_name: nix::libc::sockaddr_in = unsafe { std::mem::zeroed() }; + let mut msg_iov = vec![nix::libc::iovec { + iov_base: read_buf.as_mut_ptr() as *mut _, + iov_len: read_buf.len(), + }]; + let mut msg_control = vec![0u8; 2048]; + + let mut msg_header = nix::libc::msghdr { + msg_name: &mut msg_name as *mut _ as *mut c_void, + msg_namelen: size_of_val(&msg_name) as u32, + msg_iov: msg_iov.as_mut_ptr() as *mut _, + msg_iovlen: msg_iov.len(), + msg_control: msg_control.as_mut_ptr() as *mut _, + msg_controllen: msg_control.len(), + msg_flags: 0, + }; + log::debug!("header: {msg_header:?}"); + + // Calling recvmsg with MSG_ERRQUEUE will prompt linux to tell us if we get any ICMP errorr + // replies to our Echos. + let flags = nix::libc::MSG_ERRQUEUE; + let n = loop { + match unsafe { nix::libc::recvmsg(socket.as_raw_fd(), &mut msg_header, flags) } { + ..0 => match nix::errno::Errno::last() { + nix::errno::Errno::EWOULDBLOCK => { + sleep(Duration::from_millis(10)).await; + continue; + } + e => bail!("Faileed to read from socket {e}"), + }, + n => break n as usize, + } + }; + + log::debug!("header after: {msg_header:?}"); + msg_iov.truncate(msg_header.msg_iovlen); + msg_control.truncate(msg_header.msg_controllen); + let _ = msg_header; + + log::debug!("msg_name: {msg_name:?}"); + log::debug!("msg_iov: {msg_iov:?}"); + log::debug!("msg_iov[0]: {:?}", &read_buf[..n]); + log::debug!("msg_control: {msg_control:?}"); + + let source = Ipv4Addr::from_bits(msg_name.sin_addr.s_addr); + //let source = source.ip(); + let (control_header, rest) = msg_control + .split_first_chunk::<{ size_of::<nix::libc::cmsghdr>() }>() + .ok_or_eyre("Foo")?; + let control_header: nix::libc::cmsghdr = unsafe { transmute(*control_header) }; + let _control_message_len = control_header + .cmsg_len + .saturating_sub(size_of::<nix::libc::cmsghdr>()); + + debug_assert_eq!(control_header.cmsg_level, nix::libc::IPPROTO_IP); + debug_assert_eq!(control_header.cmsg_type, nix::libc::IP_RECVERR); + + let (control_message, rest) = rest + .split_first_chunk::<{ size_of::<sock_extended_err>() }>() + .ok_or_eyre("ASADAD")?; + //debug_assert_eq!(control_message_len, control_message.len()); + + let control_message: sock_extended_err = unsafe { transmute(*control_message) }; + + let result = parse_icmp_time_exceeded_raw(&rest) + .map_err(|e| eyre!("Ignoring packet (len={n}, ip.src={source}): {e}",)); + + log::debug!("{control_header:?}"); + log::debug!("{control_message:?}"); + log::debug!("rest: {rest:?}"); + log::debug!("{:?}", Errno::from_raw(control_message.ee_errno as i32)); + + let _original_icmp_echo = &read_buf[..n]; + + // contains the source address of the ICMP Time Exceeded packet + let _icmp_source/*: nix::libc::sockaddr */ = rest; + + match result { + Ok(..) => { + log::debug!("Got a probe response, we are leaking!"); + //timeout_at.get_or_insert_with(|| Instant::now() + RECV_TIMEOUT); + //let ip = IpAddr::from(ip); + let ip = IpAddr::from(Ipv4Addr::new(1, 3, 3, 7)); + 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}"), + } + } +} + async fn read_probe_responses(interface: &str, socket: UdpSocket) -> eyre::Result<LeakStatus> { // the list of node IP addresses from which we received a response to our probe packets. let mut reachable_nodes = vec![]; @@ -385,12 +506,16 @@ fn parse_ipv4(packet: &[u8]) -> eyre::Result<Ipv4Packet<'_>> { /// If the packet fails to parse, or is not a reply to a packet sent by [send_probes], this /// function returns an error. fn parse_icmp_time_exceeded(ip_packet: &Ipv4Packet<'_>) -> eyre::Result<Ipv4Addr> { - let too_small = || eyre!("Too small"); - let ip_protocol = ip_packet.get_next_level_protocol(); ensure!(ip_protocol == IpProtocol::Icmp, "Not ICMP"); + parse_icmp_time_exceeded_raw(ip_packet.payload())?; + Ok(ip_packet.get_source()) +} + +fn parse_icmp_time_exceeded_raw(bytes: &[u8]) -> eyre::Result<()> { + let icmp_packet = IcmpPacket::new(bytes).ok_or(eyre!("Too small"))?; + let too_small = || eyre!("Too small"); - let icmp_packet = IcmpPacket::new(ip_packet.payload()).ok_or_else(too_small)?; let correct_type = icmp_packet.get_icmp_type() == IcmpTypes::TimeExceeded; ensure!(correct_type, "Not ICMP/TimeExceeded"); @@ -424,7 +549,7 @@ fn parse_icmp_time_exceeded(ip_packet: &Ipv4Packet<'_>) -> eyre::Result<Ipv4Addr } } - Ok(ip_packet.get_source()) + Ok(()) } IpProtocol::Icmp => { @@ -449,7 +574,7 @@ fn parse_icmp_time_exceeded(ip_packet: &Ipv4Packet<'_>) -> eyre::Result<Ipv4Addr bail!("Wrong ICMP/Echo payload: {echo_payload:?}"); } - Ok(ip_packet.get_source()) + Ok(()) } _ => bail!("Not UDP/ICMP"), @@ -460,12 +585,13 @@ match_cfg! { #[cfg(any(target_os = "windows", target_os = "android"))] => { fn bind_socket_to_interface(socket: &Socket, interface: &str) -> eyre::Result<()> { use crate::util::get_interface_ip; + use std::net::SocketAddr; let interface_ip = get_interface_ip(interface)?; log::info!("Binding socket to {interface_ip} ({interface:?})"); - socket.bind(&SocketAddrV4::new(interface_ip, 0).into()) + socket.bind(&SocketAddr::new(interface_ip, 0).into()) .wrap_err("Failed to bind socket to interface address")?; return Ok(()); |
