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:
2026-04-14 22:53:04 +01:00
parent 2dcc1c1234
commit d273bf718b
7 changed files with 763 additions and 0 deletions
@@ -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);
}
}