Add redbear-traceroute and redbear-mtr tools
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
[source]
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "cargo"
|
||||
|
||||
[package.files]
|
||||
"/usr/bin/redbear-mtr" = "redbear-mtr"
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "redbear-mtr"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-mtr"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
redbear-traceroute = { path = "../../redbear-traceroute/source" }
|
||||
@@ -0,0 +1,233 @@
|
||||
use anyhow::{bail, Result};
|
||||
use redbear_traceroute::{
|
||||
destination_port, format_reply_suffix, probe, resolve_destination, ProbeReply,
|
||||
};
|
||||
use std::env;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
const DEFAULT_CYCLES: usize = 10;
|
||||
const DEFAULT_MAX_HOPS: u8 = 30;
|
||||
const DEFAULT_TIMEOUT_MS: u64 = 1_000;
|
||||
const DEFAULT_BASE_PORT: u16 = 33_434;
|
||||
|
||||
#[derive(Default)]
|
||||
struct HopStats {
|
||||
responder: Option<Ipv4Addr>,
|
||||
sent: usize,
|
||||
received: usize,
|
||||
last_ms: Option<f64>,
|
||||
total_ms: f64,
|
||||
best_ms: Option<f64>,
|
||||
worst_ms: Option<f64>,
|
||||
note: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl HopStats {
|
||||
fn record_reply(&mut self, reply: ProbeReply, rtt_ms: f64) {
|
||||
self.responder = Some(reply.hop());
|
||||
self.received += 1;
|
||||
self.last_ms = Some(rtt_ms);
|
||||
self.total_ms += rtt_ms;
|
||||
self.best_ms = Some(self.best_ms.map_or(rtt_ms, |best| best.min(rtt_ms)));
|
||||
self.worst_ms = Some(self.worst_ms.map_or(rtt_ms, |worst| worst.max(rtt_ms)));
|
||||
self.note = format_reply_suffix(reply);
|
||||
}
|
||||
|
||||
fn loss_percent(&self) -> f64 {
|
||||
if self.sent == 0 {
|
||||
0.0
|
||||
} else {
|
||||
((self.sent - self.received) as f64 / self.sent as f64) * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
fn avg_ms(&self) -> Option<f64> {
|
||||
if self.received == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.total_ms / self.received as f64)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Options {
|
||||
destination: String,
|
||||
cycles: usize,
|
||||
max_hops: u8,
|
||||
timeout: Duration,
|
||||
base_port: u16,
|
||||
}
|
||||
|
||||
enum ParseOutcome {
|
||||
Help(String),
|
||||
Options(Options),
|
||||
}
|
||||
|
||||
fn usage(program: &str) -> String {
|
||||
format!("Usage: {program} [-c cycles] [-m max_hops] [-w timeout_ms] [-p base_port] destination\n\nPath measurement tool built on redbear-traceroute. Real probing is available only when built for Redox.")
|
||||
}
|
||||
|
||||
fn parse_value<T: std::str::FromStr>(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
flag: &str,
|
||||
) -> Result<T>
|
||||
where
|
||||
T::Err: std::fmt::Display,
|
||||
{
|
||||
let value = args
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing value for {flag}"))?;
|
||||
value
|
||||
.parse::<T>()
|
||||
.map_err(|err| anyhow::anyhow!("invalid value for {flag}: {err}"))
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<ParseOutcome> {
|
||||
let mut args = env::args();
|
||||
let program = args.next().unwrap_or_else(|| "redbear-mtr".to_string());
|
||||
|
||||
let mut destination = None;
|
||||
let mut cycles = DEFAULT_CYCLES;
|
||||
let mut max_hops = DEFAULT_MAX_HOPS;
|
||||
let mut timeout_ms = DEFAULT_TIMEOUT_MS;
|
||||
let mut base_port = DEFAULT_BASE_PORT;
|
||||
|
||||
let mut rest = args;
|
||||
while let Some(arg) = rest.next() {
|
||||
match arg.as_str() {
|
||||
"-h" | "--help" => return Ok(ParseOutcome::Help(usage(&program))),
|
||||
"-c" | "--cycles" => cycles = parse_value(&mut rest, arg.as_str())?,
|
||||
"-m" | "--max-hops" => max_hops = parse_value(&mut rest, arg.as_str())?,
|
||||
"-w" | "--timeout-ms" => timeout_ms = parse_value(&mut rest, arg.as_str())?,
|
||||
"-p" | "--base-port" => base_port = parse_value(&mut rest, arg.as_str())?,
|
||||
value if value.starts_with('-') => bail!("unknown option {value}\n{}", usage(&program)),
|
||||
value => {
|
||||
if destination.replace(value.to_string()).is_some() {
|
||||
bail!("only one destination may be supplied\n{}", usage(&program));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let destination = destination.ok_or_else(|| anyhow::anyhow!(usage(&program)))?;
|
||||
if cycles == 0 {
|
||||
bail!("cycles must be at least 1");
|
||||
}
|
||||
if max_hops == 0 {
|
||||
bail!("max_hops must be at least 1");
|
||||
}
|
||||
|
||||
Ok(ParseOutcome::Options(Options {
|
||||
destination,
|
||||
cycles,
|
||||
max_hops,
|
||||
timeout: Duration::from_millis(timeout_ms),
|
||||
base_port,
|
||||
}))
|
||||
}
|
||||
|
||||
fn print_metric(value: Option<f64>) {
|
||||
match value {
|
||||
Some(value) => print!("{:>7.1}", value),
|
||||
None => print!("{:>7}", "*"),
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let options = match parse_args()? {
|
||||
ParseOutcome::Help(help) => {
|
||||
println!("{help}");
|
||||
return Ok(());
|
||||
}
|
||||
ParseOutcome::Options(options) => options,
|
||||
};
|
||||
let destination = resolve_destination(&options.destination)?;
|
||||
let mut hops = (0..usize::from(options.max_hops))
|
||||
.map(|_| HopStats::default())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for cycle in 0..options.cycles {
|
||||
for ttl in 1..=options.max_hops {
|
||||
let hop = &mut hops[usize::from(ttl - 1)];
|
||||
hop.sent += 1;
|
||||
|
||||
let sequence = cycle * usize::from(options.max_hops) + usize::from(ttl - 1);
|
||||
let dest_port = destination_port(options.base_port, sequence)?;
|
||||
let observation = probe(destination, ttl, dest_port, options.timeout)?;
|
||||
|
||||
if let Some(reply) = observation.reply {
|
||||
hop.record_reply(reply, observation.rtt.as_secs_f64() * 1_000.0);
|
||||
if reply.status != redbear_traceroute::ProbeStatus::Hop {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let last_hop = hops
|
||||
.iter()
|
||||
.rposition(|hop| hop.sent > 0)
|
||||
.map(|idx| idx + 1)
|
||||
.unwrap_or(0);
|
||||
|
||||
println!(
|
||||
"mtr report to {} ({}), {} cycles",
|
||||
options.destination, destination, options.cycles
|
||||
);
|
||||
println!(
|
||||
"{:>3} {:<15} {:>6} {:>5} {:>7} {:>7} {:>7} {:>7} Note",
|
||||
"Hop", "Host", "Loss%", "Snt", "Last", "Avg", "Best", "Wrst"
|
||||
);
|
||||
|
||||
for (index, hop) in hops.into_iter().take(last_hop).enumerate() {
|
||||
let host = hop
|
||||
.responder
|
||||
.map(|ip| ip.to_string())
|
||||
.unwrap_or_else(|| "???".to_string());
|
||||
print!(
|
||||
"{:>3}. {:<15} {:>5.1}% {:>5}",
|
||||
index + 1,
|
||||
host,
|
||||
hop.loss_percent(),
|
||||
hop.sent
|
||||
);
|
||||
print_metric(hop.last_ms);
|
||||
print_metric(hop.avg_ms());
|
||||
print_metric(hop.best_ms);
|
||||
print_metric(hop.worst_ms);
|
||||
println!(" {}", hop.note.unwrap_or(""));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
eprintln!("redbear-mtr: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::HopStats;
|
||||
|
||||
#[test]
|
||||
fn loss_percent_is_zero_without_probes() {
|
||||
let stats = HopStats::default();
|
||||
assert_eq!(stats.loss_percent(), 0.0);
|
||||
assert_eq!(stats.avg_ms(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn avg_and_loss_track_sent_and_received_counts() {
|
||||
let mut stats = HopStats::default();
|
||||
stats.sent = 4;
|
||||
stats.received = 3;
|
||||
stats.total_ms = 60.0;
|
||||
|
||||
assert!((stats.loss_percent() - 25.0).abs() < f64::EPSILON);
|
||||
assert_eq!(stats.avg_ms(), Some(20.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[source]
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "cargo"
|
||||
|
||||
[package.files]
|
||||
"/usr/bin/redbear-traceroute" = "redbear-traceroute"
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "redbear-traceroute"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "redbear_traceroute"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-traceroute"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
|
||||
[target.'cfg(target_os = "redox")'.dependencies]
|
||||
libc = "0.2"
|
||||
libredox = "0.1"
|
||||
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
|
||||
@@ -0,0 +1,332 @@
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use std::net::{IpAddr, Ipv4Addr, ToSocketAddrs};
|
||||
use std::time::Duration;
|
||||
|
||||
const ICMP_UDP_EVENT_LEN: usize = 12;
|
||||
const TRACE_KIND_TIME_EXCEEDED: u8 = 1;
|
||||
const TRACE_KIND_DST_UNREACHABLE: u8 = 2;
|
||||
#[cfg(target_os = "redox")]
|
||||
const DEFAULT_PAYLOAD_LEN: usize = 32;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TraceKind {
|
||||
TimeExceeded,
|
||||
DestinationUnreachable,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct TraceEvent {
|
||||
pub kind: TraceKind,
|
||||
pub code: u8,
|
||||
pub responder: Ipv4Addr,
|
||||
pub source_port: u16,
|
||||
pub dest_port: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ProbeStatus {
|
||||
Hop,
|
||||
Reached,
|
||||
Unreachable,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ProbeReply {
|
||||
pub event: TraceEvent,
|
||||
pub status: ProbeStatus,
|
||||
}
|
||||
|
||||
impl ProbeReply {
|
||||
pub fn hop(self) -> Ipv4Addr {
|
||||
self.event.responder
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ProbeObservation {
|
||||
pub reply: Option<ProbeReply>,
|
||||
pub rtt: Duration,
|
||||
}
|
||||
|
||||
pub fn resolve_destination(host: &str) -> Result<Ipv4Addr> {
|
||||
match (host, 0)
|
||||
.to_socket_addrs()?
|
||||
.find_map(|addr| match addr.ip() {
|
||||
IpAddr::V4(ip) => Some(ip),
|
||||
IpAddr::V6(_) => None,
|
||||
}) {
|
||||
Some(ip) => Ok(ip),
|
||||
None => bail!("{host} did not resolve to an IPv4 destination"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destination_port(base_port: u16, sequence: usize) -> Result<u16> {
|
||||
let port = u32::from(base_port)
|
||||
.checked_add(u32::try_from(sequence).context("probe sequence overflow")?)
|
||||
.ok_or_else(|| anyhow!("destination port overflow"))?;
|
||||
|
||||
u16::try_from(port).map_err(|_| anyhow!("destination port overflow"))
|
||||
}
|
||||
|
||||
pub fn decode_icmp_udp_event(buf: &[u8]) -> Result<TraceEvent> {
|
||||
if buf.len() != ICMP_UDP_EVENT_LEN {
|
||||
bail!(
|
||||
"unexpected ICMP traceroute event length: expected {}, got {}",
|
||||
ICMP_UDP_EVENT_LEN,
|
||||
buf.len()
|
||||
);
|
||||
}
|
||||
|
||||
let kind = match buf[0] {
|
||||
TRACE_KIND_TIME_EXCEEDED => TraceKind::TimeExceeded,
|
||||
TRACE_KIND_DST_UNREACHABLE => TraceKind::DestinationUnreachable,
|
||||
other => bail!("unknown ICMP traceroute event kind {other}"),
|
||||
};
|
||||
|
||||
Ok(TraceEvent {
|
||||
kind,
|
||||
code: buf[1],
|
||||
responder: Ipv4Addr::new(buf[4], buf[5], buf[6], buf[7]),
|
||||
source_port: u16::from_be_bytes([buf[8], buf[9]]),
|
||||
dest_port: u16::from_be_bytes([buf[10], buf[11]]),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn classify_reply(event: TraceEvent, destination: Ipv4Addr) -> ProbeReply {
|
||||
let status = match event.kind {
|
||||
TraceKind::TimeExceeded => ProbeStatus::Hop,
|
||||
TraceKind::DestinationUnreachable if event.code == 3 && event.responder == destination => {
|
||||
ProbeStatus::Reached
|
||||
}
|
||||
TraceKind::DestinationUnreachable => ProbeStatus::Unreachable,
|
||||
};
|
||||
|
||||
ProbeReply { event, status }
|
||||
}
|
||||
|
||||
pub fn unreachable_label(code: u8) -> &'static str {
|
||||
match code {
|
||||
0 => "!N",
|
||||
1 => "!H",
|
||||
2 => "!PR",
|
||||
3 => "!P",
|
||||
4 => "!F",
|
||||
5 => "!SR",
|
||||
9 => "!X",
|
||||
10 => "!XH",
|
||||
13 => "!A",
|
||||
_ => "!U",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_reply_suffix(reply: ProbeReply) -> Option<&'static str> {
|
||||
match reply.status {
|
||||
ProbeStatus::Hop | ProbeStatus::Reached => None,
|
||||
ProbeStatus::Unreachable => Some(unreachable_label(reply.event.code)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn probe(
|
||||
destination: Ipv4Addr,
|
||||
ttl: u8,
|
||||
dest_port: u16,
|
||||
timeout: Duration,
|
||||
) -> Result<ProbeObservation> {
|
||||
redox_probe(destination, ttl, dest_port, timeout)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn redox_probe(
|
||||
destination: Ipv4Addr,
|
||||
ttl: u8,
|
||||
dest_port: u16,
|
||||
timeout: Duration,
|
||||
) -> Result<ProbeObservation> {
|
||||
use libredox::{flag, Fd};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::mem;
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::thread;
|
||||
use std::time::Instant;
|
||||
|
||||
fn open_udp_socket(destination: Ipv4Addr, dest_port: u16) -> Result<OwnedFd> {
|
||||
let socket = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
|
||||
if socket < 0 {
|
||||
return Err(std::io::Error::last_os_error()).context("failed to create UDP socket");
|
||||
}
|
||||
|
||||
let socket = unsafe { OwnedFd::from_raw_fd(socket) };
|
||||
let mut addr: libc::sockaddr_in = unsafe { mem::zeroed() };
|
||||
addr.sin_family = libc::AF_INET as libc::sa_family_t;
|
||||
addr.sin_port = dest_port.to_be();
|
||||
addr.sin_addr = libc::in_addr {
|
||||
s_addr: u32::from_ne_bytes(destination.octets()),
|
||||
};
|
||||
|
||||
let rc = unsafe {
|
||||
libc::connect(
|
||||
socket.as_raw_fd(),
|
||||
&addr as *const libc::sockaddr_in as *const libc::sockaddr,
|
||||
mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if rc < 0 {
|
||||
return Err(std::io::Error::last_os_error()).context("failed to connect UDP socket");
|
||||
}
|
||||
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
fn udp_local_port(socket: RawFd) -> Result<u16> {
|
||||
let mut addr: libc::sockaddr_in = unsafe { mem::zeroed() };
|
||||
let mut len = mem::size_of::<libc::sockaddr_in>() as libc::socklen_t;
|
||||
let rc = unsafe {
|
||||
libc::getsockname(
|
||||
socket,
|
||||
&mut addr as *mut libc::sockaddr_in as *mut libc::sockaddr,
|
||||
&mut len,
|
||||
)
|
||||
};
|
||||
if rc < 0 {
|
||||
return Err(std::io::Error::last_os_error()).context("failed to query UDP source port");
|
||||
}
|
||||
|
||||
Ok(u16::from_be(addr.sin_port))
|
||||
}
|
||||
|
||||
fn set_hop_limit(socket: RawFd, ttl: u8) -> Result<()> {
|
||||
let raw = syscall::dup(socket as usize, b"hop_limit")
|
||||
.map_err(|err| anyhow!("failed to open hop_limit setting: {err}"))?;
|
||||
let mut hop_limit = unsafe { File::from_raw_fd(raw as RawFd) };
|
||||
hop_limit
|
||||
.write_all(&[ttl])
|
||||
.context("failed to set UDP hop_limit")
|
||||
}
|
||||
|
||||
fn open_icmp_socket(destination: Ipv4Addr, source_port: u16) -> Result<Fd> {
|
||||
let path = format!("/scheme/icmp/udp/{destination}/{source_port}");
|
||||
Fd::open(&path, flag::O_RDWR | flag::O_NONBLOCK, 0)
|
||||
.map_err(|err| anyhow!("failed to open {path}: {err}"))
|
||||
}
|
||||
|
||||
fn send_probe(socket: RawFd, payload: &[u8]) -> Result<()> {
|
||||
let written = unsafe { libc::send(socket, payload.as_ptr().cast(), payload.len(), 0) };
|
||||
if written < 0 {
|
||||
return Err(std::io::Error::last_os_error()).context("failed to send UDP probe");
|
||||
}
|
||||
if written as usize != payload.len() {
|
||||
bail!(
|
||||
"short UDP probe write: expected {}, got {}",
|
||||
payload.len(),
|
||||
written
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait_for_icmp_event(icmp_fd: &mut Fd, timeout: Duration) -> Result<Option<TraceEvent>> {
|
||||
let started = Instant::now();
|
||||
let mut raw = [0_u8; ICMP_UDP_EVENT_LEN];
|
||||
|
||||
loop {
|
||||
match icmp_fd.read(&mut raw) {
|
||||
Ok(0) => bail!("ICMP traceroute socket closed unexpectedly"),
|
||||
Ok(len) if len == raw.len() => return decode_icmp_udp_event(&raw).map(Some),
|
||||
Ok(len) => bail!(
|
||||
"unexpected ICMP traceroute event length: expected {}, got {}",
|
||||
raw.len(),
|
||||
len
|
||||
),
|
||||
Err(err) if err.is_wouldblock() => {
|
||||
if started.elapsed() >= timeout {
|
||||
return Ok(None);
|
||||
}
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
Err(err) => return Err(err).context("failed reading ICMP traceroute event"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let socket = open_udp_socket(destination, dest_port)?;
|
||||
let source_port = udp_local_port(socket.as_raw_fd())?;
|
||||
let mut icmp_fd = open_icmp_socket(destination, source_port)?;
|
||||
set_hop_limit(socket.as_raw_fd(), ttl)?;
|
||||
|
||||
let payload = vec![0x42_u8; DEFAULT_PAYLOAD_LEN];
|
||||
let started = Instant::now();
|
||||
send_probe(socket.as_raw_fd(), &payload)?;
|
||||
let reply =
|
||||
wait_for_icmp_event(&mut icmp_fd, timeout)?.map(|event| classify_reply(event, destination));
|
||||
|
||||
Ok(ProbeObservation {
|
||||
reply,
|
||||
rtt: started.elapsed(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn redox_probe(
|
||||
_destination: Ipv4Addr,
|
||||
_ttl: u8,
|
||||
_dest_port: u16,
|
||||
_timeout: Duration,
|
||||
) -> Result<ProbeObservation> {
|
||||
bail!("redbear-traceroute probing is only available when built for Redox")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
classify_reply, decode_icmp_udp_event, destination_port, format_reply_suffix, ProbeStatus,
|
||||
TraceEvent, TraceKind,
|
||||
};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[test]
|
||||
fn decodes_time_exceeded_event() {
|
||||
let raw = [1, 0, 0, 0, 192, 0, 2, 1, 0xa4, 0x10, 0x82, 0x9a];
|
||||
let event = decode_icmp_udp_event(&raw).expect("expected event decode");
|
||||
|
||||
assert_eq!(event.kind, TraceKind::TimeExceeded);
|
||||
assert_eq!(event.responder, Ipv4Addr::new(192, 0, 2, 1));
|
||||
assert_eq!(event.source_port, 42_000);
|
||||
assert_eq!(event.dest_port, 33_434);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_target_port_unreachable_as_reached() {
|
||||
let event = TraceEvent {
|
||||
kind: TraceKind::DestinationUnreachable,
|
||||
code: 3,
|
||||
responder: Ipv4Addr::new(203, 0, 113, 9),
|
||||
source_port: 42_000,
|
||||
dest_port: 33_434,
|
||||
};
|
||||
|
||||
let reply = classify_reply(event, Ipv4Addr::new(203, 0, 113, 9));
|
||||
assert_eq!(reply.status, ProbeStatus::Reached);
|
||||
assert_eq!(format_reply_suffix(reply), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_non_target_unreachable_as_terminal_error() {
|
||||
let event = TraceEvent {
|
||||
kind: TraceKind::DestinationUnreachable,
|
||||
code: 1,
|
||||
responder: Ipv4Addr::new(192, 0, 2, 7),
|
||||
source_port: 42_000,
|
||||
dest_port: 33_434,
|
||||
};
|
||||
|
||||
let reply = classify_reply(event, Ipv4Addr::new(203, 0, 113, 9));
|
||||
assert_eq!(reply.status, ProbeStatus::Unreachable);
|
||||
assert_eq!(format_reply_suffix(reply), Some("!H"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn destination_port_rejects_overflow() {
|
||||
assert!(destination_port(u16::MAX, 1).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use redbear_traceroute::{
|
||||
destination_port, format_reply_suffix, probe, resolve_destination, ProbeStatus,
|
||||
};
|
||||
use std::env;
|
||||
use std::time::Duration;
|
||||
|
||||
const DEFAULT_MAX_HOPS: u8 = 30;
|
||||
const DEFAULT_QUERIES: usize = 3;
|
||||
const DEFAULT_TIMEOUT_MS: u64 = 1_000;
|
||||
const DEFAULT_BASE_PORT: u16 = 33_434;
|
||||
|
||||
struct Options {
|
||||
destination: String,
|
||||
max_hops: u8,
|
||||
queries: usize,
|
||||
timeout: Duration,
|
||||
base_port: u16,
|
||||
}
|
||||
|
||||
enum ParseOutcome {
|
||||
Help(String),
|
||||
Options(Options),
|
||||
}
|
||||
|
||||
fn usage(program: &str) -> String {
|
||||
format!(
|
||||
"Usage: {program} [-m max_hops] [-q queries] [-w timeout_ms] [-p base_port] destination\n\nUDP-based traceroute for Red Bear OS. Real probing is available only when built for Redox."
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_value<T: std::str::FromStr>(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
flag: &str,
|
||||
) -> Result<T>
|
||||
where
|
||||
T::Err: std::fmt::Display,
|
||||
{
|
||||
let value = args
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing value for {flag}"))?;
|
||||
value
|
||||
.parse::<T>()
|
||||
.map_err(|err| anyhow::anyhow!("invalid value for {flag}: {err}"))
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<ParseOutcome> {
|
||||
let mut args = env::args();
|
||||
let program = args
|
||||
.next()
|
||||
.unwrap_or_else(|| "redbear-traceroute".to_string());
|
||||
|
||||
let mut destination = None;
|
||||
let mut max_hops = DEFAULT_MAX_HOPS;
|
||||
let mut queries = DEFAULT_QUERIES;
|
||||
let mut timeout_ms = DEFAULT_TIMEOUT_MS;
|
||||
let mut base_port = DEFAULT_BASE_PORT;
|
||||
|
||||
let mut rest = args;
|
||||
while let Some(arg) = rest.next() {
|
||||
match arg.as_str() {
|
||||
"-h" | "--help" => return Ok(ParseOutcome::Help(usage(&program))),
|
||||
"-m" | "--max-hops" => max_hops = parse_value(&mut rest, arg.as_str())?,
|
||||
"-q" | "--queries" => queries = parse_value(&mut rest, arg.as_str())?,
|
||||
"-w" | "--timeout-ms" => timeout_ms = parse_value(&mut rest, arg.as_str())?,
|
||||
"-p" | "--base-port" => base_port = parse_value(&mut rest, arg.as_str())?,
|
||||
value if value.starts_with('-') => bail!("unknown option {value}\n{}", usage(&program)),
|
||||
value => {
|
||||
if destination.replace(value.to_string()).is_some() {
|
||||
bail!("only one destination may be supplied\n{}", usage(&program));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let destination = destination.ok_or_else(|| anyhow::anyhow!(usage(&program)))?;
|
||||
if max_hops == 0 {
|
||||
bail!("max_hops must be at least 1");
|
||||
}
|
||||
if queries == 0 {
|
||||
bail!("queries must be at least 1");
|
||||
}
|
||||
|
||||
Ok(ParseOutcome::Options(Options {
|
||||
destination,
|
||||
max_hops,
|
||||
queries,
|
||||
timeout: Duration::from_millis(timeout_ms),
|
||||
base_port,
|
||||
}))
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let options = match parse_args()? {
|
||||
ParseOutcome::Help(help) => {
|
||||
println!("{help}");
|
||||
return Ok(());
|
||||
}
|
||||
ParseOutcome::Options(options) => options,
|
||||
};
|
||||
let destination = resolve_destination(&options.destination)
|
||||
.with_context(|| format!("failed to resolve {}", options.destination))?;
|
||||
|
||||
println!(
|
||||
"traceroute to {} ({}), {} hops max",
|
||||
options.destination, destination, options.max_hops
|
||||
);
|
||||
|
||||
for ttl in 1..=options.max_hops {
|
||||
print!("{:>2}", ttl);
|
||||
let mut stop = false;
|
||||
|
||||
for query in 0..options.queries {
|
||||
let sequence = (usize::from(ttl) - 1) * options.queries + query;
|
||||
let dest_port = destination_port(options.base_port, sequence)?;
|
||||
let observation = probe(destination, ttl, dest_port, options.timeout)?;
|
||||
|
||||
match observation.reply {
|
||||
Some(reply) => {
|
||||
let rtt_ms = observation.rtt.as_secs_f64() * 1_000.0;
|
||||
print!(" {} {:.1}ms", reply.hop(), rtt_ms);
|
||||
if let Some(suffix) = format_reply_suffix(reply) {
|
||||
print!(" {suffix}");
|
||||
}
|
||||
if matches!(
|
||||
reply.status,
|
||||
ProbeStatus::Reached | ProbeStatus::Unreachable
|
||||
) {
|
||||
stop = true;
|
||||
}
|
||||
}
|
||||
None => print!(" *"),
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
if stop {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
eprintln!("redbear-traceroute: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user