summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJoakim Hulthe <joakim.hulthe@mullvad.net>2024-11-14 15:54:01 +0100
committerJoakim Hulthe <joakim.hulthe@mullvad.net>2025-01-24 17:38:28 +0100
commit0118410ff566cf42a20e0966a3549af93e186ce9 (patch)
treec33cbfb9c3c3ea2e9fbf5cca035b32383a9755e5
parent73ecf1c4a954318359a10c1be232570798b398de (diff)
downloadmullvadvpn-0118410ff566cf42a20e0966a3549af93e186ce9.tar.xz
mullvadvpn-0118410ff566cf42a20e0966a3549af93e186ce9.zip
Add experimental linux tracroute poc without root
-rw-r--r--leak-checker/src/traceroute.rs206
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(());