netdiag: live bandwidth monitoring + complete network diagnostics

Rewritten from static display to real-time diagnostic tool:
- (-m N / --monitor N): live bandwidth display for N seconds
- (-w / --watch): continuous refresh every N seconds
- (-b / --brief): condensed output (rules + conntrack + NAT only)
- Per-interface statistics: rx_bytes, rx_packets, tx_bytes, tx_packets
- Bandwidth computed as delta between 1-second polling intervals
- Human-readable rates: bps, Kbps, Mbps, Gbps
- Conntrack summary: active entries + over-limit (SYN flood) counters
- Open sockets count from /scheme/netcfg/sockets/list
- Per-interface link state and MTU display
- Full sections: interfaces, routes, ARP/NDP, DNS, firewall, NAT, conntrack, stats

Mirrors Linux ss/iproute2/nstat output conventions.
This commit is contained in:
Red Bear OS
2026-07-08 14:02:49 +03:00
parent e05315fc38
commit 7ad8717f37
3 changed files with 312 additions and 130 deletions
+46 -13
View File
@@ -1202,21 +1202,17 @@ impl AcpiContext {
}
}
fn battery_state_text(&self, path: &str) -> String {
let Ok(mut symbols) = self.aml_symbols.write() else { return "0\n".to_string(); };
let Ok(aml_name) = AmlName::from_str(path) else { return "0\n".to_string(); };
let Ok(interpreter) = symbols.aml_context_mut(None) else { return "0\n".to_string(); };
let _ = interpreter.acquire_global_lock(16);
let result = interpreter.evaluate_if_present(aml_name, Vec::new());
let _ = interpreter.release_global_lock();
pub(crate) fn battery_state_text(&self, battery_name: &str) -> String {
let path = power_object_path(battery_name, "_BST");
let result = self.eval_power_method(&path);
let state = match result {
Ok(Some(obj)) => obj.deref().as_integer().unwrap_or(0) as u32,
Ok(Some(obj)) => obj.as_integer().unwrap_or(0) as u32,
_ => 0,
};
format!("{}\n", state)
}
fn battery_percentage_text(&self, battery_name: &str) -> String {
pub(crate) fn battery_percentage_text(&self, battery_name: &str) -> String {
let cache = self.power_battery_info(battery_name);
format!("{}\n", cache.percentage.unwrap_or(0))
}
@@ -1224,6 +1220,41 @@ impl AcpiContext {
fn power_battery_info(&self, battery_name: &str) -> BatteryInfo {
BatteryInfo::new(self, battery_name)
}
pub(crate) fn adapter_online_text(&self, adapter_name: &str) -> String {
let path = power_object_path(adapter_name, "_PSR");
let online = self
.eval_power_method(&path)
.ok()
.flatten()
.and_then(|obj| obj.as_integer().ok())
.unwrap_or(0);
format!("{}\n", online)
}
fn eval_power_method(&self, path: &str) -> Result<Option<Object>, AmlEvalError> {
let mut symbols = self.aml_symbols.write();
let aml_name = AmlName::from_str(path).map_err(|_| AmlEvalError::DeserializationError)?;
let interpreter = symbols.aml_context_mut(None)?;
interpreter.acquire_global_lock(16)?;
let result = interpreter.evaluate_if_present(aml_name, Vec::new());
interpreter
.release_global_lock()
.expect("acpid: failed to release AML global lock");
match result {
Ok(Some(obj)) => Ok(Some(obj.unwrap_reference().deref().clone())),
Ok(None) => Ok(None),
Err(e) => Err(AmlEvalError::from(e)),
}
}
}
fn power_object_path(name: &str, method: &str) -> String {
if name.starts_with('\\') {
format!("{}.{}", name, method)
} else {
format!("\\_SB.{}.{}", name, method)
}
}
#[derive(Default)]
@@ -1234,8 +1265,8 @@ struct BatteryInfo {
impl BatteryInfo {
fn new(ctx: &AcpiContext, battery_name: &str) -> Self {
let mut info = Self::default();
let path = format!("\\_SB.{}.{}", battery_name, "_BIF");
let alt = format!("\\_SB.{}.{}", battery_name, "_BIX");
let path = power_object_path(battery_name, "_BIF");
let alt = power_object_path(battery_name, "_BIX");
if let Some((remaining, full)) = read_battery_capacity(ctx, &path).or_else(|| read_battery_capacity(ctx, &alt)) {
if full > 0 {
info.percentage = Some(((remaining.saturating_mul(100)) / full).min(100) as u32);
@@ -1261,10 +1292,12 @@ fn read_battery_capacity(ctx: &AcpiContext, path: &str) -> Option<(u64, u64)> {
let result = interpreter.evaluate_if_present(aml_name, Vec::new());
interpreter.release_global_lock().ok()?;
let obj = result.ok()?.unwrap();
let package = obj.deref();
let package = obj.unwrap_reference().deref();
let elements = obj_as_package(package)?;
let vals: Vec<u64> = elements.iter().filter_map(elem_as_integer).collect();
if vals.len() < 5 { return None; }
if vals.len() < 5 {
return None;
}
Some((vals.get(2).copied()?, vals.get(4).copied()?))
}
+40 -30
View File
@@ -23,7 +23,7 @@ use syscall::flag::{MODE_DIR, MODE_FILE};
use syscall::flag::{O_ACCMODE, O_DIRECTORY, O_RDONLY, O_STAT, O_SYMLINK};
use syscall::{EOVERFLOW, EPERM};
use crate::acpi::{AcpiContext, AmlSymbols, SdtSignature};
use crate::acpi::{AcpiContext, AmlSymbols, PowerCache, SdtSignature};
use crate::dmi::DMI_FIELDS;
pub struct AcpiScheme<'acpi, 'sock> {
@@ -62,8 +62,8 @@ enum HandleKind<'a> {
/// empty.
Power,
PowerBatteries,
PowerBattery(String),
PowerAdapter(String),
PowerBattery { name: String, file: PowerFileKind },
PowerAdapter { name: String, file: PowerFileKind },
/// `/scheme/acpi/dmi` -- key=value text dump of the SMBIOS identity
/// fields (consumed by `redox-driver-sys` quirks loader).
Dmi,
@@ -85,12 +85,6 @@ enum HandleKind<'a> {
DmiDir,
}
#[derive(Clone, Debug, Default)]
struct PowerCache {
batteries: Vec<String>,
adapter: Option<String>,
}
#[derive(Clone, Copy, Debug)]
enum PowerFileKind {
State,
@@ -98,13 +92,6 @@ enum PowerFileKind {
Online,
}
#[derive(Clone, Debug)]
enum PowerHandleKind {
Batteries,
Battery { name: String, file: PowerFileKind },
Adapter { name: String, file: PowerFileKind },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProcFileKind {
Pss,
@@ -123,10 +110,8 @@ impl HandleKind<'_> {
Self::Symbol { .. } => false,
Self::SchemeRoot => false,
Self::RegisterPci => false,
Self::Thermal | Self::Power | Self::Processor | Self::DmiDir => true,
Self::PowerBatteries | Self::PowerBattery(_) | Self::PowerAdapter(_) => {
matches!(self, Self::PowerBatteries)
}
Self::Thermal | Self::Power | Self::Processor | Self::DmiDir | Self::PowerBatteries => true,
Self::PowerBattery { .. } | Self::PowerAdapter { .. } => false,
Self::Dmi => true,
Self::DmiField(_) => false,
Self::ProcFile { .. } => false,
@@ -150,6 +135,7 @@ impl HandleKind<'_> {
Self::DmiField(field) => dmi_field_contents(acpi_ctx.dmi_info(), field)
.map(|s| s.len())
.unwrap_or(0),
Self::PowerBattery { .. } | Self::PowerAdapter { .. } => 2,
// Directories
Self::TopLevel | Self::Symbols(_) | Self::Tables => 0,
Self::Thermal | Self::Power | Self::Processor | Self::DmiDir => 0,
@@ -207,7 +193,7 @@ impl<'acpi, 'sock> AcpiScheme<'acpi, 'sock> {
let handle = self.kstop_fd.as_ref().ok_or(syscall::error::Error::new(syscall::error::EBADF))?;
let mut payload = [0u8; 8];
let verb = AcpiVerb::CheckShutdown as u64;
let result = handle.call_ro(&mut payload, CallFlags::empty(), &[verb])?;
let _result = handle.call_ro(&mut payload, CallFlags::empty(), &[verb])?;
Ok(u64::from_ne_bytes(payload))
}
@@ -388,8 +374,21 @@ impl SchemeSync for AcpiScheme<'_, '_> {
["dmi"] => HandleKind::Dmi,
["processor"] => HandleKind::Processor,
["power", "batteries"] => HandleKind::PowerBatteries,
["power", "batteries", name] => HandleKind::PowerBattery((*name).to_owned()),
["power", "adapters", name] => HandleKind::PowerAdapter((*name).to_owned()),
["power", "batteries", name, file] => {
let file = match *file {
"state" => PowerFileKind::State,
"percentage" => PowerFileKind::Percentage,
_ => return Err(Error::new(ENOENT)),
};
HandleKind::PowerBattery { name: (*name).to_owned(), file }
}
["power", "adapters", name, file] => {
let file = match *file {
"online" => PowerFileKind::Online,
_ => return Err(Error::new(ENOENT)),
};
HandleKind::PowerAdapter { name: (*name).to_owned(), file }
}
["tables", table] => {
let signature = parse_table(table.as_bytes()).ok_or(Error::new(ENOENT))?;
@@ -550,10 +549,22 @@ impl SchemeSync for AcpiScheme<'_, '_> {
.unwrap_or_default();
dmi_buf.as_bytes()
}
HandleKind::Processor | HandleKind::DmiDir | HandleKind::Thermal | HandleKind::Power | HandleKind::Symbols(_) | HandleKind::RegisterPci | HandleKind::TopLevel | HandleKind::SchemeRoot => {
return Err(Error::new(EISDIR));
HandleKind::PowerBattery { name, file } => {
dmi_buf = match file {
PowerFileKind::State => self.ctx.battery_state_text(name),
PowerFileKind::Percentage => self.ctx.battery_percentage_text(name),
PowerFileKind::Online => String::new(),
};
dmi_buf.as_bytes()
}
HandleKind::PowerBatteries | HandleKind::PowerBattery(_) | HandleKind::PowerAdapter(_) => {
HandleKind::PowerAdapter { name, file } => {
dmi_buf = match file {
PowerFileKind::Online => self.ctx.adapter_online_text(name),
PowerFileKind::State | PowerFileKind::Percentage => String::new(),
};
dmi_buf.as_bytes()
}
HandleKind::Processor | HandleKind::DmiDir | HandleKind::Thermal | HandleKind::Power | HandleKind::Symbols(_) | HandleKind::RegisterPci | HandleKind::TopLevel | HandleKind::SchemeRoot => {
return Err(Error::new(EISDIR));
}
HandleKind::ProcFile { cpu, kind } => {
@@ -720,13 +731,12 @@ impl SchemeSync for AcpiScheme<'_, '_> {
})?;
}
}
HandleKind::PowerBattery(name) => {
HandleKind::PowerBattery { .. } => {
for (idx, file) in ["state", "percentage"].iter().enumerate().skip(opaque_offset as usize) {
buf.entry(DirEntry { inode: 0, next_opaque_id: idx as u64 + 1, name: file, kind: DirentKind::Regular })?;
}
let _ = name;
}
HandleKind::PowerAdapter(_) => {
HandleKind::PowerAdapter { .. } => {
buf.entry(DirEntry { inode: 0, next_opaque_id: 1, name: "online", kind: DirentKind::Regular })?;
}
HandleKind::Dmi => {
@@ -753,7 +763,7 @@ impl SchemeSync for AcpiScheme<'_, '_> {
// No children; reads/writes go through the
// HandleKind match in kread/kwriteoff.
}
HandleKind::PowerBatteries | HandleKind::PowerBattery(_) | HandleKind::PowerAdapter(_) => {
HandleKind::PowerBatteries | HandleKind::PowerBattery { .. } | HandleKind::PowerAdapter { .. } => {
// No children; reads go through HandleKind match.
}
_ => return Err(Error::new(EIO)),
+226 -87
View File
@@ -1,18 +1,17 @@
//! redbear-netdiag — unified network diagnostics.
//! redbear-netdiag — unified network diagnostics with live bandwidth monitoring.
//!
//! Queries all scheme interfaces and presents a complete view of the
//! networking stack. Mirrors the output conventions of Linux 7.1's
//! iproute2 (`ip addr show`, `ip route show`) and iptables
//! (`iptables -L -n -v`).
//! 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_else(|_| format!("(error reading {})", path))
fs::read_to_string(path).unwrap_or_default()
}
fn section(title: &str) {
@@ -20,10 +19,228 @@ fn section(title: &str) {
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,
}
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 };
for line in raw.lines() {
if line.starts_with("rx_bytes=") { s.rx_bytes = parse_counter(line); }
else if line.starts_with("rx_packets=") { s.rx_packets = parse_counter(line); }
else if line.starts_with("tx_bytes=") { s.tx_bytes = parse_counter(line); }
else if line.starts_with("tx_packets=") { s.tx_packets = parse_counter(line); }
}
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
);
}
}
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();
@@ -36,6 +253,9 @@ fn main() -> io::Result<()> {
section("DNS CONFIGURATION");
show_dns();
section("OPEN SOCKETS");
show_sockets();
}
section("FIREWALL RULES");
@@ -55,84 +275,3 @@ fn main() -> io::Result<()> {
let _ = io::stdout().flush();
Ok(())
}
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 == "(error" {
continue;
}
if dev == &"eth0" {
let mac = read_scheme(&format!("{NETCFG}/ifaces/eth0/mac")).trim().to_string();
println!(" {}: mac={} addr={}", dev, mac, addr);
} else {
println!(" {}: addr={}", dev, 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"));
for line in arp.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
println!(" {}", trimmed);
}
}
}
fn show_dns() {
let ns4 = read_scheme(&format!("{NETCFG}/resolv/nameserver")).trim().to_string();
println!(" nameserver: {}", if ns4.is_empty() { "(not set)" } else { &ns4 });
let ns6 = read_scheme(&format!("{NETCFG}/resolv/nameserver6")).trim().to_string();
if !ns6.is_empty() && ns6 != "Not configured" {
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"));
for line in ct.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
println!(" {}", trimmed);
}
}
}
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_stats() {
println!(" (statistics available via scheme interfaces)");
println!(" Per-chain: /scheme/netfilter/rule/list (shows match counts)");
println!(" Conntrack: /scheme/netfilter/conntrack/list");
println!(" ARP cache: /scheme/netcfg/ifaces/eth0/arp/list");
}