anti 0.1.0

Unified CLI toolbox
Documentation
use clap::{Args, Parser, Subcommand};
use anti::ping::{
    resolve_hostname, PingConfig, PingError, PingResult, PingStatistics, PingerBuilder,
};
use std::process;
use std::time::Duration;
use anti::traceroute::{Tracer, TracerouteConfig};

const VERSION: &str = env!("CARGO_PKG_VERSION");

#[derive(Parser)]
#[command(name = "anti", version = VERSION, about = "Unified CLI toolbox")]
struct Cli {
    #[command(subcommand)]
    domain: Domain,
}

#[derive(Subcommand)]
enum Domain {
    Net {
        #[command(subcommand)]
        command: NetCmd,
    },
}

#[derive(Subcommand)]
enum NetCmd {
    Ping(PingOpts),
    Trace(TraceOpts),
}

#[derive(Args)]
struct PingOpts {
    host: String,
    #[arg(long, default_value_t = 4)]
    count: u16,
    #[arg(long, default_value_t = 5)]
    timeout: u64,
}

#[derive(Args)]
struct TraceOpts {
    host: String,
    #[arg(long)]
    tcp: bool,
}

fn main() {
    let cli = Cli::parse();
    if let Err(e) = match cli.domain {
        Domain::Net { command: cmd } => match cmd {
            NetCmd::Ping(opts) => net_ping(opts),
            NetCmd::Trace(opts) => net_trace(opts),
        },
    } {
        eprintln!("Error: {}", e);
        process::exit(1);
    }
}

fn net_ping(opts: PingOpts) -> PingResult<()> {
    let target = resolve_hostname(&opts.host)?;
    let config = PingConfig {
        target,
        count: opts.count,
        timeout: Duration::from_secs(opts.timeout),
        ..Default::default()
    };

    let pinger = PingerBuilder::new()
        .target(config.target)
        .count(config.count)
        .timeout(config.timeout)
        .interval(config.interval)
        .packet_size(config.packet_size)
        .build_icmp()?;

    println!("PING {} ({})", opts.host, target);

    let stats = pinger.ping_with_callback(|seq, result| match result {
        Ok(reply) => println!(
            "{} bytes from {}: seq={} time={:.2} ms",
            reply.bytes_received,
            reply.from,
            reply.sequence,
            reply.rtt.as_secs_f64() * 1000.0
        ),
        Err(PingError::Timeout { .. }) => println!("Request timeout for seq={}", seq),
        Err(e) => println!("Error for seq={}: {}", seq, e),
    })?;

    print_summary(&opts.host, &stats);
    Ok(())
}

fn print_summary(host: &str, stats: &PingStatistics) {
    println!("\n--- {} ping statistics ---", host);
    println!(
        "{} packets transmitted, {} received, {:.1}% packet loss",
        stats.packets_transmitted, stats.packets_received, stats.packet_loss
    );
    if stats.packets_received > 0 {
        if let (Some(min), Some(avg), Some(max)) = (stats.min_rtt, stats.avg_rtt, stats.max_rtt) {
            println!(
                "round-trip min/avg/max = {:.2}/{:.2}/{:.2} ms",
                min.as_secs_f64() * 1000.0,
                avg.as_secs_f64() * 1000.0,
                max.as_secs_f64() * 1000.0,
            );
        }
    }
}

fn net_trace(opts: TraceOpts) -> PingResult<()> {
    let target = resolve_hostname(&opts.host)?;
    if opts.tcp {
        eprintln!("TCP traceroute not implemented, using default UDP/ICMP method.");
    }
    let config = TracerouteConfig {
        target,
        ..Default::default()
    };

    println!("traceroute to {} ({})", opts.host, target);
    let tracer = Tracer::new(config)?;
    let hops = tracer.trace()?;

    for hop in hops {
        print!("{:>2}  ", hop.ttl);
        if let Some(addr) = hop.addr {
            print!("{}  ", addr);
            if let Some(rtt) = hop.rtt {
                print!("{:.3} ms", rtt.as_secs_f64() * 1000.0);
            }
        } else {
            print!("*");
        }
        println!();
    }
    Ok(())
}