arp: add statistics counters and netcfg exposure

EthernetLink gains per-interface ARP counters:
- arp_requests: ARP requests sent (incremented in send_arp)
- arp_replies: ARP replies received (incremented on cache insert)
- arp_cache_hits: successful neighbor cache lookups
- arp_cache_misses: cache misses (vacant or expired entries)

LinkDevice trait gains arp_stats() → String method.
EthernetLink implementation: 'requests=N replies=N hits=N misses=N entries=N'

netcfg exposes at /scheme/netcfg/ifaces/eth0/arp/stats:
  echo /scheme/netcfg/ifaces/eth0/arp/stats
  → requests=5 replies=3 hits=142 misses=8 entries=4

Existing arp/list (MAC→IP) and arp/flush (clear) are unchanged.
Useful for monitoring ARP churn, detecting ARP storms, and debugging
neighbor discovery issues.
This commit is contained in:
Red Bear OS
2026-07-08 15:38:49 +03:00
parent faf777b990
commit 27f8e351e6
3 changed files with 39 additions and 1 deletions
+27 -1
View File
@@ -87,6 +87,10 @@ pub struct EthernetLink {
ip_address: Option<IpCidr>,
qdisc_config: QdiscConfig,
stats: Stats,
arp_requests: u64,
arp_replies: u64,
arp_cache_hits: u64,
arp_cache_misses: u64,
}
impl EthernetLink {
@@ -119,6 +123,10 @@ impl EthernetLink {
qdisc_config: QdiscConfig::default(),
stats: Stats::default(),
neighbor_cache: Default::default(),
arp_requests: 0,
arp_replies: 0,
arp_cache_hits: 0,
arp_cache_misses: 0,
}
}
@@ -205,6 +213,7 @@ impl EthernetLink {
expires_at: now + Self::NEIGHBOR_LIVE_TIME,
},
);
self.arp_replies += 1;
if let ArpOperation::Request = operation {
let response = ArpRepr::EthernetIpv4 {
@@ -398,6 +407,7 @@ impl EthernetLink {
}
fn send_arp(&mut self, now: Instant) {
self.arp_requests += 1;
let Some(hardware_address) = self.hardware_address else {
return;
};
@@ -721,12 +731,17 @@ impl LinkDevice for EthernetLink {
}
match self.neighbor_cache.entry(next_hop) {
Entry::Vacant(_) => self.handle_missing_neighbor(next_hop, &packet, now),
Entry::Vacant(_) => {
self.arp_cache_misses += 1;
self.handle_missing_neighbor(next_hop, &packet, now)
}
Entry::Occupied(e) => {
if e.get().expires_at < now {
e.remove();
self.arp_cache_misses += 1;
self.handle_missing_neighbor(next_hop, &packet, now)
} else {
self.arp_cache_hits += 1;
let mac = e.get().hardware_address;
self.send_to(
mac,
@@ -849,6 +864,17 @@ impl LinkDevice for EthernetLink {
self.neighbor_cache.clear();
}
fn arp_stats(&self) -> String {
format!(
"requests={} replies={} hits={} misses={} entries={}\n",
self.arp_requests,
self.arp_replies,
self.arp_cache_hits,
self.arp_cache_misses,
self.neighbor_cache.len(),
)
}
fn statistics(&self) -> Stats {
self.stats
}
+4
View File
@@ -45,6 +45,10 @@ pub trait LinkDevice {
fn flush_arp(&mut self) {}
fn arp_stats(&self) -> String {
String::new()
}
fn mtu(&self) -> usize {
1500
}
+8
View File
@@ -360,6 +360,14 @@ fn mk_root_node(
}
}
},
"stats" => {
ro [devices] || {
match devices.borrow().get("eth0") {
Some(dev) => dev.arp_stats(),
None => "Device not found\n".into(),
}
}
},
"flush" => {
wo [devices] (Option<()>, None)
|_cur_value, _line| {