Skip to content

Commit

Permalink
Add experimental linux tracroute poc without root
Browse files Browse the repository at this point in the history
  • Loading branch information
hulthe committed Dec 12, 2024
1 parent 4e388c2 commit 04d9e43
Showing 1 changed file with 168 additions and 42 deletions.
210 changes: 168 additions & 42 deletions leak-checker/src/traceroute.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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")?;

// 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);

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??;
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 mut udp_socket = UdpSocket::from_std(udp_socket)?;

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
Expand Down Expand Up @@ -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)))
Expand All @@ -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
Expand Down Expand Up @@ -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![];
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -424,7 +549,7 @@ fn parse_icmp_time_exceeded(ip_packet: &Ipv4Packet<'_>) -> eyre::Result<Ipv4Addr
}
}

Ok(ip_packet.get_source())
Ok(())
}

IpProtocol::Icmp => {
Expand All @@ -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"),
Expand All @@ -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(());
Expand Down

0 comments on commit 04d9e43

Please sign in to comment.