Skip to content

Commit b772c92

Browse files
committed
Add experimental linux tracroute poc without root
1 parent 76891a7 commit b772c92

File tree

1 file changed

+168
-42
lines changed

1 file changed

+168
-42
lines changed

leak-checker/src/traceroute.rs

+168-42
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
use std::{
22
ascii::escape_default,
3-
ffi::c_void,
43
io,
54
net::{IpAddr, Ipv4Addr},
65
ops::{Range, RangeFrom},
7-
os::fd::{AsFd, AsRawFd, FromRawFd, IntoRawFd},
6+
os::fd::{FromRawFd, IntoRawFd},
87
time::Duration,
98
};
109

1110
use eyre::{bail, ensure, eyre, OptionExt, WrapErr};
1211
use futures::{future::pending, stream, StreamExt, TryFutureExt, TryStreamExt};
1312
use match_cfg::match_cfg;
14-
use nix::libc::setsockopt;
1513
use pnet_packet::{
1614
icmp::{
1715
echo_request::EchoRequestPacket, time_exceeded::TimeExceededPacket, IcmpPacket, IcmpTypes,
@@ -106,48 +104,59 @@ pub async fn try_run_leak_test(opt: &TracerouteOpt) -> eyre::Result<LeakStatus>
106104
.set_nonblocking(true)
107105
.wrap_err("Failed to set icmp_socket to nonblocking")?;
108106

109-
let n = 1;
110-
unsafe {
111-
setsockopt(
112-
icmp_socket.as_fd().as_raw_fd(),
113-
nix::libc::SOL_IP,
114-
nix::libc::IP_RECVERR,
115-
&n as *const _ as *const c_void,
116-
size_of_val(&n) as u32,
117-
)
118-
};
107+
#[cfg(any(target_os = "linux", target_os = "android"))]
108+
{
109+
use std::ffi::c_void;
110+
use std::os::fd::{AsFd, AsRawFd};
111+
112+
let n = 1;
113+
unsafe {
114+
setsockopt(
115+
icmp_socket.as_fd().as_raw_fd(),
116+
nix::libc::SOL_IP,
117+
nix::libc::IP_RECVERR,
118+
&n as *const _ as *const std::ffi::c_void,
119+
size_of_val(&n) as u32,
120+
)
121+
};
122+
}
119123

120124
bind_socket_to_interface(&icmp_socket, &opt.interface)?;
121125

122126
// HACK: Wrap the socket in a tokio::net::UdpSocket to be able to use it async
123127
// SAFETY: `into_raw_fd()` consumes the socket and returns an owned & open file descriptor.
124128
let icmp_socket = unsafe { std::net::UdpSocket::from_raw_fd(icmp_socket.into_raw_fd()) };
125-
let icmp_socket = UdpSocket::from_std(icmp_socket)?;
129+
let mut icmp_socket = UdpSocket::from_std(icmp_socket)?;
126130

127131
// on Windows, we need to do some additional configuration of the raw socket
128132
#[cfg(target_os = "windows")]
129133
configure_listen_socket(&icmp_socket, interface)?;
130134

131-
// create the socket used for sending the UDP probing packets
132-
let udp_socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))
133-
.wrap_err("Failed to open UDP socket")?;
134-
bind_socket_to_interface(&udp_socket, &opt.interface)
135-
.wrap_err("Failed to bind UDP socket to interface")?;
136-
udp_socket
137-
.set_nonblocking(true)
138-
.wrap_err("Failed to set udp_socket to nonblocking")?;
139-
140-
// HACK: Wrap the socket in a tokio::net::UdpSocket to be able to use it async
141-
// SAFETY: `into_raw_fd()` consumes the socket and returns an owned & open file descriptor.
142-
let udp_socket = unsafe { std::net::UdpSocket::from_raw_fd(udp_socket.into_raw_fd()) };
143-
let udp_socket = UdpSocket::from_std(udp_socket)?;
144-
drop(udp_socket);
145-
146-
let mut icmp_socket = icmp_socket;
147-
timeout(SEND_TIMEOUT, send_icmp_probes(&mut icmp_socket, opt))
148-
.map_err(|_timeout| eyre!("Timed out while trying to send probe packet"))
149-
.await??;
135+
if opt.icmp {
136+
timeout(SEND_TIMEOUT, send_icmp_probes(&mut icmp_socket, opt))
137+
.map_err(|_timeout| eyre!("Timed out while trying to send probe packet"))
138+
.await??;
139+
} else {
140+
// create the socket used for sending the UDP probing packets
141+
let udp_socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))
142+
.wrap_err("Failed to open UDP socket")?;
143+
bind_socket_to_interface(&udp_socket, &opt.interface)
144+
.wrap_err("Failed to bind UDP socket to interface")?;
145+
udp_socket
146+
.set_nonblocking(true)
147+
.wrap_err("Failed to set udp_socket to nonblocking")?;
148+
149+
// HACK: Wrap the socket in a tokio::net::UdpSocket to be able to use it async
150+
// SAFETY: `into_raw_fd()` consumes the socket and returns an owned & open file descriptor.
151+
let udp_socket = unsafe { std::net::UdpSocket::from_raw_fd(udp_socket.into_raw_fd()) };
152+
let mut udp_socket = UdpSocket::from_std(udp_socket)?;
153+
154+
timeout(SEND_TIMEOUT, send_udp_probes(&mut udp_socket, opt))
155+
.map_err(|_timeout| eyre!("Timed out while trying to send probe packet"))
156+
.await??;
157+
}
150158

159+
//let recv_task = read_probe_responses(&opt.interface, icmp_socket);
151160
let recv_task = read_probe_responses(&opt.interface, icmp_socket);
152161

153162
// 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::
205214
packet.populate(&echo);
206215
packet.set_checksum(checksum(&IcmpPacket::new(packet.packet()).unwrap()));
207216

208-
log::error!("echo packet: {:02x?}", packet.packet());
209-
210217
let result: io::Result<()> = stream::iter(0..number_of_sends)
211218
// call `send_to` `number_of_sends` times
212219
.then(|_| socket.send_to(&packet.packet(), (opt.destination, port)))
@@ -227,7 +234,7 @@ async fn send_icmp_probes(socket: &mut UdpSocket, opt: &TracerouteOpt) -> eyre::
227234
Ok(())
228235
}
229236

230-
async fn send_udp_probes(socket: UdpSocket, opt: &TracerouteOpt) -> eyre::Result<()> {
237+
async fn send_udp_probes(socket: &mut UdpSocket, opt: &TracerouteOpt) -> eyre::Result<()> {
231238
// ensure we don't send anything to `opt.exclude_port`
232239
let ports = DEFAULT_PORT_RANGE
233240
// skip the excluded port
@@ -265,6 +272,120 @@ async fn send_udp_probes(socket: UdpSocket, opt: &TracerouteOpt) -> eyre::Result
265272
Ok(())
266273
}
267274

275+
/// Experimental PoC of a linux implementation that doesn't need root.
276+
#[cfg(any(target_os = "linux", target_os = "android"))]
277+
#[allow(dead_code)]
278+
async fn read_probe_responses_no_root(
279+
_interface: &str,
280+
socket: UdpSocket,
281+
) -> eyre::Result<LeakStatus> {
282+
use nix::libc::{errno::Errno, libc::setsockopt, setsockopt, sock_extended_err};
283+
use std::ffi::c_void;
284+
use std::mem::transmute;
285+
use std::os::fd::AsRawFd;
286+
287+
// the list of node IP addresses from which we received a response to our probe packets.
288+
let mut reachable_nodes = vec![];
289+
290+
let mut read_buf = vec![0u8; usize::from(u16::MAX)].into_boxed_slice();
291+
loop {
292+
log::debug!("Reading from ICMP socket");
293+
294+
// XXX: only works for ipv4
295+
let mut msg_name: nix::libc::sockaddr_in = unsafe { std::mem::zeroed() };
296+
let mut msg_iov = vec![nix::libc::iovec {
297+
iov_base: read_buf.as_mut_ptr() as *mut _,
298+
iov_len: read_buf.len(),
299+
}];
300+
let mut msg_control = vec![0u8; 2048];
301+
302+
let mut msg_header = nix::libc::msghdr {
303+
msg_name: &mut msg_name as *mut _ as *mut c_void,
304+
msg_namelen: size_of_val(&msg_name) as u32,
305+
msg_iov: msg_iov.as_mut_ptr() as *mut _,
306+
msg_iovlen: msg_iov.len(),
307+
msg_control: msg_control.as_mut_ptr() as *mut _,
308+
msg_controllen: msg_control.len(),
309+
msg_flags: 0,
310+
};
311+
log::debug!("header: {msg_header:?}");
312+
313+
// Calling recvmsg with MSG_ERRQUEUE will prompt linux to tell us if we get any ICMP errorr
314+
// replies to our Echos.
315+
let flags = nix::libc::MSG_ERRQUEUE;
316+
let n = loop {
317+
match unsafe { nix::libc::recvmsg(socket.as_raw_fd(), &mut msg_header, flags) } {
318+
..0 => match nix::errno::Errno::last() {
319+
nix::errno::Errno::EWOULDBLOCK => {
320+
sleep(Duration::from_millis(10)).await;
321+
continue;
322+
}
323+
e => bail!("Faileed to read from socket {e}"),
324+
},
325+
n => break n as usize,
326+
}
327+
};
328+
329+
log::debug!("header after: {msg_header:?}");
330+
msg_iov.truncate(msg_header.msg_iovlen);
331+
msg_control.truncate(msg_header.msg_controllen);
332+
let _ = msg_header;
333+
334+
log::debug!("msg_name: {msg_name:?}");
335+
log::debug!("msg_iov: {msg_iov:?}");
336+
log::debug!("msg_iov[0]: {:?}", &read_buf[..n]);
337+
log::debug!("msg_control: {msg_control:?}");
338+
339+
let source = Ipv4Addr::from_bits(msg_name.sin_addr.s_addr);
340+
//let source = source.ip();
341+
let (control_header, rest) = msg_control
342+
.split_first_chunk::<{ size_of::<nix::libc::cmsghdr>() }>()
343+
.ok_or_eyre("Foo")?;
344+
let control_header: nix::libc::cmsghdr = unsafe { transmute(*control_header) };
345+
let _control_message_len = control_header
346+
.cmsg_len
347+
.saturating_sub(size_of::<nix::libc::cmsghdr>());
348+
349+
debug_assert_eq!(control_header.cmsg_level, nix::libc::IPPROTO_IP);
350+
debug_assert_eq!(control_header.cmsg_type, nix::libc::IP_RECVERR);
351+
352+
let (control_message, rest) = rest
353+
.split_first_chunk::<{ size_of::<sock_extended_err>() }>()
354+
.ok_or_eyre("ASADAD")?;
355+
//debug_assert_eq!(control_message_len, control_message.len());
356+
357+
let control_message: sock_extended_err = unsafe { transmute(*control_message) };
358+
359+
let result = parse_icmp_time_exceeded_raw(&rest)
360+
.map_err(|e| eyre!("Ignoring packet (len={n}, ip.src={source}): {e}",));
361+
362+
log::debug!("{control_header:?}");
363+
log::debug!("{control_message:?}");
364+
log::debug!("rest: {rest:?}");
365+
log::debug!("{:?}", Errno::from_raw(control_message.ee_errno as i32));
366+
367+
let _original_icmp_echo = &read_buf[..n];
368+
369+
// contains the source address of the ICMP Time Exceeded packet
370+
let _icmp_source/*: nix::libc::sockaddr */ = rest;
371+
372+
match result {
373+
Ok(..) => {
374+
log::debug!("Got a probe response, we are leaking!");
375+
//timeout_at.get_or_insert_with(|| Instant::now() + RECV_TIMEOUT);
376+
//let ip = IpAddr::from(ip);
377+
let ip = IpAddr::from(Ipv4Addr::new(1, 3, 3, 7));
378+
if !reachable_nodes.contains(&ip) {
379+
reachable_nodes.push(ip);
380+
}
381+
}
382+
383+
// an error means the packet wasn't the ICMP/TimeExceeded we're listening for.
384+
Err(e) => log::debug!("{e}"),
385+
}
386+
}
387+
}
388+
268389
async fn read_probe_responses(interface: &str, socket: UdpSocket) -> eyre::Result<LeakStatus> {
269390
// the list of node IP addresses from which we received a response to our probe packets.
270391
let mut reachable_nodes = vec![];
@@ -385,12 +506,16 @@ fn parse_ipv4(packet: &[u8]) -> eyre::Result<Ipv4Packet<'_>> {
385506
/// If the packet fails to parse, or is not a reply to a packet sent by [send_probes], this
386507
/// function returns an error.
387508
fn parse_icmp_time_exceeded(ip_packet: &Ipv4Packet<'_>) -> eyre::Result<Ipv4Addr> {
388-
let too_small = || eyre!("Too small");
389-
390509
let ip_protocol = ip_packet.get_next_level_protocol();
391510
ensure!(ip_protocol == IpProtocol::Icmp, "Not ICMP");
511+
parse_icmp_time_exceeded_raw(ip_packet.payload())?;
512+
Ok(ip_packet.get_source())
513+
}
514+
515+
fn parse_icmp_time_exceeded_raw(bytes: &[u8]) -> eyre::Result<()> {
516+
let icmp_packet = IcmpPacket::new(bytes).ok_or(eyre!("Too small"))?;
517+
let too_small = || eyre!("Too small");
392518

393-
let icmp_packet = IcmpPacket::new(ip_packet.payload()).ok_or_else(too_small)?;
394519
let correct_type = icmp_packet.get_icmp_type() == IcmpTypes::TimeExceeded;
395520
ensure!(correct_type, "Not ICMP/TimeExceeded");
396521

@@ -424,7 +549,7 @@ fn parse_icmp_time_exceeded(ip_packet: &Ipv4Packet<'_>) -> eyre::Result<Ipv4Addr
424549
}
425550
}
426551

427-
Ok(ip_packet.get_source())
552+
Ok(())
428553
}
429554

430555
IpProtocol::Icmp => {
@@ -449,7 +574,7 @@ fn parse_icmp_time_exceeded(ip_packet: &Ipv4Packet<'_>) -> eyre::Result<Ipv4Addr
449574
bail!("Wrong ICMP/Echo payload: {echo_payload:?}");
450575
}
451576

452-
Ok(ip_packet.get_source())
577+
Ok(())
453578
}
454579

455580
_ => bail!("Not UDP/ICMP"),
@@ -460,12 +585,13 @@ match_cfg! {
460585
#[cfg(any(target_os = "windows", target_os = "android"))] => {
461586
fn bind_socket_to_interface(socket: &Socket, interface: &str) -> eyre::Result<()> {
462587
use crate::util::get_interface_ip;
588+
use std::net::SocketAddr;
463589

464590
let interface_ip = get_interface_ip(interface)?;
465591

466592
log::info!("Binding socket to {interface_ip} ({interface:?})");
467593

468-
socket.bind(&SocketAddrV4::new(interface_ip, 0).into())
594+
socket.bind(&SocketAddr::new(interface_ip, 0).into())
469595
.wrap_err("Failed to bind socket to interface address")?;
470596

471597
return Ok(());

0 commit comments

Comments
 (0)