a158fe5844
IfaceStats struct gains rx_errors, tx_errors, rx_dropped, tx_dropped.
Stats output now shows error/drop line when any of these are non-zero:
eth0 rx: ...B tx: ...B
errors: rx=N tx=N drops: rx=N tx=N
Mirrors Linux ifconfig output format.
Suppresses the extra line when all counters are 0 (clean state).
295 lines
9.1 KiB
Rust
295 lines
9.1 KiB
Rust
//! redbear-netdiag — unified network diagnostics with live bandwidth monitoring.
|
|
//!
|
|
//! Mirrors the output conventions of Linux's ss, iproute2, and nstat.
|
|
|
|
use std::fs;
|
|
use std::io::{self, Write};
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
const NETCFG: &str = "/scheme/netcfg";
|
|
const NETFILTER: &str = "/scheme/netfilter";
|
|
|
|
fn read_scheme(path: &str) -> String {
|
|
fs::read_to_string(path).unwrap_or_default()
|
|
}
|
|
|
|
fn section(title: &str) {
|
|
println!();
|
|
println!("═══ {} ═══", title);
|
|
}
|
|
|
|
fn parse_counter(line: &str) -> u64 {
|
|
line.split('=')
|
|
.nth(1)
|
|
.and_then(|s| s.trim().parse().ok())
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
struct IfaceStats {
|
|
rx_bytes: u64,
|
|
rx_packets: u64,
|
|
tx_bytes: u64,
|
|
tx_packets: u64,
|
|
rx_errors: u64,
|
|
tx_errors: u64,
|
|
rx_dropped: u64,
|
|
tx_dropped: u64,
|
|
}
|
|
|
|
fn read_iface_stats(dev: &str) -> Option<IfaceStats> {
|
|
let raw = read_scheme(&format!("{NETCFG}/ifaces/{dev}/stats"));
|
|
if raw.is_empty() {
|
|
return None;
|
|
}
|
|
let mut s = IfaceStats {
|
|
rx_bytes: 0, rx_packets: 0, tx_bytes: 0, tx_packets: 0,
|
|
rx_errors: 0, tx_errors: 0, rx_dropped: 0, tx_dropped: 0,
|
|
};
|
|
for line in raw.lines() {
|
|
if let Some(v) = line.strip_prefix("rx_bytes=") { s.rx_bytes = v.trim().parse().unwrap_or(0); }
|
|
else if let Some(v) = line.strip_prefix("rx_packets=") { s.rx_packets = v.trim().parse().unwrap_or(0); }
|
|
else if let Some(v) = line.strip_prefix("tx_bytes=") { s.tx_bytes = v.trim().parse().unwrap_or(0); }
|
|
else if let Some(v) = line.strip_prefix("tx_packets=") { s.tx_packets = v.trim().parse().unwrap_or(0); }
|
|
else if let Some(v) = line.strip_prefix("rx_errors=") { s.rx_errors = v.trim().parse().unwrap_or(0); }
|
|
else if let Some(v) = line.strip_prefix("tx_errors=") { s.tx_errors = v.trim().parse().unwrap_or(0); }
|
|
else if let Some(v) = line.strip_prefix("rx_dropped=") { s.rx_dropped = v.trim().parse().unwrap_or(0); }
|
|
else if let Some(v) = line.strip_prefix("tx_dropped=") { s.tx_dropped = v.trim().parse().unwrap_or(0); }
|
|
}
|
|
Some(s)
|
|
}
|
|
|
|
fn format_bps(bytes_per_sec: u64) -> String {
|
|
if bytes_per_sec >= 1_000_000_000 {
|
|
format!("{:.2} Gbps", bytes_per_sec as f64 * 8.0 / 1_000_000_000.0)
|
|
} else if bytes_per_sec >= 1_000_000 {
|
|
format!("{:.2} Mbps", bytes_per_sec as f64 * 8.0 / 1_000_000.0)
|
|
} else if bytes_per_sec >= 1_000 {
|
|
format!("{:.2} Kbps", bytes_per_sec as f64 * 8.0 / 1_000.0)
|
|
} else {
|
|
format!("{} bps", bytes_per_sec * 8)
|
|
}
|
|
}
|
|
|
|
fn show_interfaces() {
|
|
for dev in &["eth0", "lo"] {
|
|
let addr = read_scheme(&format!("{NETCFG}/ifaces/{dev}/addr/list")).trim().to_string();
|
|
if addr.is_empty() || addr.starts_with('(') {
|
|
continue;
|
|
}
|
|
let mac = if *dev == "eth0" {
|
|
read_scheme(&format!("{NETCFG}/ifaces/eth0/mac")).trim().to_string()
|
|
} else {
|
|
String::new()
|
|
};
|
|
let link = read_scheme(&format!("{NETCFG}/ifaces/{dev}/link")).trim().to_string();
|
|
let mtu = read_scheme(&format!("{NETCFG}/ifaces/{dev}/mtu")).trim().to_string();
|
|
print!(" {:<6} ", dev);
|
|
if !mac.is_empty() { print!("mac={:<17} ", mac); }
|
|
print!("mtu={:<5} ", mtu);
|
|
print!("link={:<5} ", link);
|
|
println!("addr={}", addr);
|
|
}
|
|
}
|
|
|
|
fn show_routes() {
|
|
let routes = read_scheme(&format!("{NETCFG}/route/list"));
|
|
for line in routes.lines() {
|
|
let trimmed = line.trim();
|
|
if !trimmed.is_empty() {
|
|
println!(" {}", trimmed);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn show_arp() {
|
|
let arp = read_scheme(&format!("{NETCFG}/ifaces/eth0/arp/list"));
|
|
let mut count = 0;
|
|
for line in arp.lines() {
|
|
let trimmed = line.trim();
|
|
if !trimmed.is_empty() {
|
|
println!(" {}", trimmed);
|
|
count += 1;
|
|
}
|
|
}
|
|
if count == 0 { println!(" (empty)"); }
|
|
}
|
|
|
|
fn show_dns() {
|
|
let ns4 = read_scheme(&format!("{NETCFG}/resolv/nameserver")).trim().to_string();
|
|
print!(" nameserver4: ");
|
|
if ns4.is_empty() { println!("(not set)"); } else { println!("{}", ns4); }
|
|
let ns6 = read_scheme(&format!("{NETCFG}/resolv/nameserver6")).trim().to_string();
|
|
if !ns6.is_empty() {
|
|
println!(" nameserver6: {}", ns6);
|
|
}
|
|
}
|
|
|
|
fn show_firewall() {
|
|
let rules = read_scheme(&format!("{NETFILTER}/rule/list"));
|
|
for line in rules.lines() {
|
|
let trimmed = line.trim();
|
|
if !trimmed.is_empty() {
|
|
println!(" {}", trimmed);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn show_conntrack() {
|
|
let ct = read_scheme(&format!("{NETFILTER}/conntrack/list"));
|
|
let lines: Vec<&str> = ct.lines().filter(|l| !l.trim().is_empty()).collect();
|
|
if let Some((first, rest)) = lines.split_first() {
|
|
println!(" {}", first); // summary line
|
|
for l in rest {
|
|
println!(" {}", l);
|
|
}
|
|
} else {
|
|
println!(" (no tracked connections)");
|
|
}
|
|
}
|
|
|
|
fn show_nat() {
|
|
let nat = read_scheme(&format!("{NETFILTER}/nat/list"));
|
|
for line in nat.lines() {
|
|
let trimmed = line.trim();
|
|
if !trimmed.is_empty() {
|
|
println!(" {}", trimmed);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn show_sockets() {
|
|
let count = read_scheme(&format!("{NETCFG}/sockets/list")).trim().to_string();
|
|
if !count.is_empty() {
|
|
println!(" {}", count);
|
|
} else {
|
|
println!(" (unavailable)");
|
|
}
|
|
}
|
|
|
|
fn show_stats() {
|
|
println!(" Interface statistics:");
|
|
for dev in &["eth0", "lo"] {
|
|
if let Some(s) = read_iface_stats(dev) {
|
|
println!(
|
|
" {:<6} rx: {:>10} B ({:>6} pkts) tx: {:>10} B ({:>6} pkts)",
|
|
dev, s.rx_bytes, s.rx_packets, s.tx_bytes, s.tx_packets
|
|
);
|
|
if s.rx_errors != 0 || s.tx_errors != 0 || s.rx_dropped != 0 || s.tx_dropped != 0 {
|
|
println!(
|
|
" {:>7} errors: rx={} tx={} drops: rx={} tx={}",
|
|
"", s.rx_errors, s.tx_errors, s.rx_dropped, s.tx_dropped
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
let ct = read_scheme(&format!("{NETFILTER}/conntrack/list"));
|
|
let ct_count = ct.lines().filter(|l| l.contains("entries:")).next()
|
|
.and_then(|l| l.split("entries:").nth(1))
|
|
.and_then(|s| s.split(',').next())
|
|
.and_then(|s| s.trim().parse::<usize>().ok())
|
|
.unwrap_or(0);
|
|
let over_limit = ct.lines().filter(|l| l.contains("over_limit:")).next()
|
|
.and_then(|l| l.split("over_limit:").nth(1))
|
|
.and_then(|s| s.trim().parse::<u64>().ok())
|
|
.unwrap_or(0);
|
|
println!(" conntrack: {} active, {} over-limit (SYN flood)", ct_count, over_limit);
|
|
}
|
|
|
|
fn show_bandwidth(duration_secs: u64) {
|
|
let mut prev: Option<(IfaceStats, IfaceStats)> = None;
|
|
let interval = Duration::from_secs(1);
|
|
let iterations = duration_secs;
|
|
|
|
println!();
|
|
println!("═══ LIVE BANDWIDTH ({}s, 1s intervals) ═══", duration_secs);
|
|
println!(" {:>8} {:>16} {:>16} {:>14} {:>14}",
|
|
"Time", "eth0 RX", "eth0 TX", "lo RX", "lo TX");
|
|
|
|
for i in 1..=iterations {
|
|
thread::sleep(interval);
|
|
let eth0 = read_iface_stats("eth0");
|
|
let lo = read_iface_stats("lo");
|
|
|
|
if let (Some(e), Some(l)) = (ð0, &lo) {
|
|
if let Some((ref pe, ref pl)) = prev {
|
|
let erx = e.rx_bytes.saturating_sub(pe.rx_bytes);
|
|
let etx = e.tx_bytes.saturating_sub(pe.tx_bytes);
|
|
let lrx = l.rx_bytes.saturating_sub(pl.rx_bytes);
|
|
let ltx = l.tx_bytes.saturating_sub(pl.tx_bytes);
|
|
println!(
|
|
" {:>3}s {:>16} {:>16} {:>14} {:>14}",
|
|
i,
|
|
format_bps(erx),
|
|
format_bps(etx),
|
|
format_bps(lrx),
|
|
format_bps(ltx),
|
|
);
|
|
}
|
|
}
|
|
prev = eth0.zip(lo).into();
|
|
}
|
|
}
|
|
|
|
fn main() -> io::Result<()> {
|
|
let args: Vec<String> = std::env::args().collect();
|
|
let brief = args.iter().any(|a| a == "-b" || a == "--brief");
|
|
let monitor = args.iter().position(|a| a == "-m" || a == "--monitor");
|
|
let monitor_secs: u64 = monitor.and_then(|i| args.get(i + 1))
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(10);
|
|
|
|
if args.iter().any(|a| a == "-w" || a == "--watch") {
|
|
loop {
|
|
print!("\x1B[2J\x1B[H");
|
|
let _ = run_display(true);
|
|
thread::sleep(Duration::from_secs(monitor_secs));
|
|
}
|
|
}
|
|
|
|
if args.iter().any(|a| a == "-m" || a == "--monitor") {
|
|
run_display(brief)?;
|
|
show_bandwidth(monitor_secs);
|
|
return Ok(());
|
|
}
|
|
|
|
run_display(brief)
|
|
}
|
|
|
|
fn run_display(brief: bool) -> io::Result<()> {
|
|
if !brief {
|
|
section("INTERFACES");
|
|
show_interfaces();
|
|
|
|
section("ROUTING TABLE");
|
|
show_routes();
|
|
|
|
section("ARP / NDP CACHE");
|
|
show_arp();
|
|
|
|
section("DNS CONFIGURATION");
|
|
show_dns();
|
|
|
|
section("OPEN SOCKETS");
|
|
show_sockets();
|
|
}
|
|
|
|
section("FIREWALL RULES");
|
|
show_firewall();
|
|
|
|
section("CONNECTION TRACKING");
|
|
show_conntrack();
|
|
|
|
section("NAT RULES");
|
|
show_nat();
|
|
|
|
if !brief {
|
|
section("STATISTICS");
|
|
show_stats();
|
|
}
|
|
|
|
let _ = io::stdout().flush();
|
|
Ok(())
|
|
}
|