Files
RedBear-OS/netdiag/src/main.rs
T
Red Bear OS a158fe5844 netdiag: parse and display MIB-II error/drop counters
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).
2026-07-08 19:20:33 +03:00

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)) = (&eth0, &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(())
}