1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
//! Network throughput sensor.
use super::data::HISTORY_SIZE;
use super::Sensor;
use std::collections::VecDeque;
use std::ffi::CStr;
use std::fs;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::time::Instant;
use tracing::info;
/// Network throughput sensor.
pub struct NetworkSensor {
name: String,
interface: String,
last_rx: u64,
last_tx: u64,
last_time: Option<Instant>,
last_rx_rate: f64,
last_tx_rate: f64,
cached_ipv4: Option<String>,
/// IPv6 Global Unicast Address (2000::/3)
cached_ipv6_gua: Option<String>,
/// IPv6 Link-Local Address (fe80::/10)
cached_ipv6_lla: Option<String>,
/// IPv6 Unique Local Address (fc00::/7)
cached_ipv6_ula: Option<String>,
last_ip_check: Option<Instant>,
/// History of combined I/O rates (bytes/sec)
history: VecDeque<f64>,
/// History of receive rates (bytes/sec)
rx_history: VecDeque<f64>,
/// History of transmit rates (bytes/sec)
tx_history: VecDeque<f64>,
}
impl NetworkSensor {
/// Creates a new network sensor for a specific interface.
pub fn new(interface: &str) -> Self {
Self {
name: format!("network_{}", interface),
interface: interface.to_string(),
last_rx: 0,
last_tx: 0,
last_time: None,
last_rx_rate: 0.0,
last_tx_rate: 0.0,
cached_ipv4: None,
cached_ipv6_gua: None,
cached_ipv6_lla: None,
cached_ipv6_ula: None,
last_ip_check: None,
history: VecDeque::with_capacity(HISTORY_SIZE),
rx_history: VecDeque::with_capacity(HISTORY_SIZE),
tx_history: VecDeque::with_capacity(HISTORY_SIZE),
}
}
/// Creates a new network sensor with auto-detected interface.
/// Tries to find the default gateway interface, falls back to first active interface.
pub fn auto() -> Self {
let interface = Self::detect_interface().unwrap_or_else(|| "eth0".to_string());
info!("Network sensor using interface: {}", interface);
Self::new(&interface)
}
/// Changes the monitored network interface. Resets rate counters.
pub fn set_interface(&mut self, interface: &str) {
self.name = format!("network_{}", interface);
self.interface = interface.to_string();
self.last_rx = 0;
self.last_tx = 0;
self.last_time = None;
self.last_rx_rate = 0.0;
self.last_tx_rate = 0.0;
self.cached_ipv4 = None;
self.cached_ipv6_gua = None;
self.cached_ipv6_lla = None;
self.cached_ipv6_ula = None;
self.last_ip_check = None;
self.history.clear();
self.rx_history.clear();
self.tx_history.clear();
info!("Network sensor switched to interface: {}", interface);
}
/// Sets interface to auto-detected default.
pub fn set_auto(&mut self) {
let interface = Self::detect_interface().unwrap_or_else(|| "eth0".to_string());
self.set_interface(&interface);
}
/// Lists all available network interfaces (excludes loopback and virtual interfaces).
pub fn list_interfaces() -> Vec<String> {
let mut interfaces = Vec::new();
if let Ok(entries) = fs::read_dir("/sys/class/net") {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
// Skip loopback and virtual interfaces
if name == "lo" || name.starts_with("veth") || name.starts_with("docker") {
continue;
}
// Check if interface has stats (indicates a real interface)
let stats_path = format!("/sys/class/net/{}/statistics/rx_bytes", name);
if fs::metadata(&stats_path).is_ok() {
interfaces.push(name);
}
}
}
interfaces.sort();
interfaces
}
/// Detects the primary network interface.
/// Checks /proc/net/route for the default gateway interface.
pub fn detect_interface() -> Option<String> {
// Try to find the default route interface from /proc/net/route
if let Ok(content) = fs::read_to_string("/proc/net/route") {
for line in content.lines().skip(1) {
// Skip header
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() >= 2 {
let iface = fields[0];
let destination = fields[1];
// Default route has destination 00000000
if destination == "00000000" {
return Some(iface.to_string());
}
}
}
}
// Fallback: find first non-loopback interface with statistics
if let Ok(entries) = fs::read_dir("/sys/class/net") {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
// Skip loopback and virtual interfaces
if name == "lo" || name.starts_with("veth") || name.starts_with("docker") {
continue;
}
// Check if interface has stats
let stats_path = format!("/sys/class/net/{}/statistics/rx_bytes", name);
if fs::metadata(&stats_path).is_ok() {
return Some(name);
}
}
}
None
}
fn read_stats(&self) -> Option<(u64, u64)> {
let rx_path = format!("/sys/class/net/{}/statistics/rx_bytes", self.interface);
let tx_path = format!("/sys/class/net/{}/statistics/tx_bytes", self.interface);
let rx = fs::read_to_string(&rx_path).ok()?.trim().parse().ok()?;
let tx = fs::read_to_string(&tx_path).ok()?.trim().parse().ok()?;
Some((rx, tx))
}
/// Returns the current RX rate in bytes/second.
pub fn rx_rate(&self) -> f64 {
self.last_rx_rate
}
/// Returns the current TX rate in bytes/second.
pub fn tx_rate(&self) -> f64 {
self.last_tx_rate
}
/// Returns the network interface name.
pub fn interface_name(&self) -> &str {
&self.interface
}
/// Returns the I/O history (combined rx+tx rates).
pub fn history(&self) -> &VecDeque<f64> {
&self.history
}
/// Returns the receive rate history (bytes/sec).
pub fn rx_history(&self) -> &VecDeque<f64> {
&self.rx_history
}
/// Returns the transmit rate history (bytes/sec).
pub fn tx_history(&self) -> &VecDeque<f64> {
&self.tx_history
}
/// Returns the IPv4 address for this interface (cached, refreshed every 30s).
pub fn ipv4_address(&mut self) -> Option<String> {
self.refresh_ip_cache();
self.cached_ipv4.clone()
}
/// Returns the IPv6 GUA (Global Unicast Address) for this interface.
pub fn ipv6_gua(&mut self) -> Option<String> {
self.refresh_ip_cache();
self.cached_ipv6_gua.clone()
}
/// Returns the IPv6 LLA (Link-Local Address) for this interface.
pub fn ipv6_lla(&mut self) -> Option<String> {
self.refresh_ip_cache();
self.cached_ipv6_lla.clone()
}
/// Returns the IPv6 ULA (Unique Local Address) for this interface.
pub fn ipv6_ula(&mut self) -> Option<String> {
self.refresh_ip_cache();
self.cached_ipv6_ula.clone()
}
/// Refreshes the IP address cache if stale (older than 30 seconds).
fn refresh_ip_cache(&mut self) {
let should_refresh = self
.last_ip_check
.map(|t| t.elapsed().as_secs() > 30)
.unwrap_or(true);
if should_refresh {
let addrs = Self::get_ip_addresses(&self.interface);
self.cached_ipv4 = addrs.ipv4;
self.cached_ipv6_gua = addrs.ipv6_gua;
self.cached_ipv6_lla = addrs.ipv6_lla;
self.cached_ipv6_ula = addrs.ipv6_ula;
self.last_ip_check = Some(Instant::now());
}
}
/// Gets all IP addresses for an interface using getifaddrs.
fn get_ip_addresses(interface: &str) -> IpAddresses {
let mut addrs = IpAddresses::default();
// SAFETY: getifaddrs is a standard POSIX function. We properly free the
// list with freeifaddrs when done.
unsafe {
let mut ifaddrs: *mut libc::ifaddrs = std::ptr::null_mut();
if libc::getifaddrs(&mut ifaddrs) != 0 {
return addrs;
}
let mut current = ifaddrs;
while !current.is_null() {
let ifa = &*current;
// Check if this is the interface we're looking for
if !ifa.ifa_name.is_null() {
let name = CStr::from_ptr(ifa.ifa_name).to_string_lossy();
if name == interface && !ifa.ifa_addr.is_null() {
let family = (*ifa.ifa_addr).sa_family as i32;
if family == libc::AF_INET && addrs.ipv4.is_none() {
// IPv4 address
let sockaddr_in = ifa.ifa_addr as *const libc::sockaddr_in;
let addr_bytes = (*sockaddr_in).sin_addr.s_addr.to_ne_bytes();
let addr = Ipv4Addr::from(addr_bytes);
addrs.ipv4 = Some(addr.to_string());
} else if family == libc::AF_INET6 {
// IPv6 address - classify by type
let sockaddr_in6 = ifa.ifa_addr as *const libc::sockaddr_in6;
let addr_bytes = (*sockaddr_in6).sin6_addr.s6_addr;
let addr = Ipv6Addr::from(addr_bytes);
let addr_str = addr.to_string();
// Classify IPv6 address type
let first_byte = addr_bytes[0];
if first_byte == 0xfe && (addr_bytes[1] & 0xc0) == 0x80 {
// Link-Local (fe80::/10)
if addrs.ipv6_lla.is_none() {
addrs.ipv6_lla = Some(addr_str);
}
} else if first_byte == 0xfc || first_byte == 0xfd {
// Unique Local (fc00::/7, typically fd00::/8)
if addrs.ipv6_ula.is_none() {
addrs.ipv6_ula = Some(addr_str);
}
} else if (first_byte & 0xe0) == 0x20 {
// Global Unicast (2000::/3)
if addrs.ipv6_gua.is_none() {
addrs.ipv6_gua = Some(addr_str);
}
}
}
}
}
current = ifa.ifa_next;
}
libc::freeifaddrs(ifaddrs);
}
addrs
}
}
/// Container for all IP address types.
#[derive(Default)]
struct IpAddresses {
ipv4: Option<String>,
ipv6_gua: Option<String>,
ipv6_lla: Option<String>,
ipv6_ula: Option<String>,
}
impl Sensor for NetworkSensor {
fn name(&self) -> &str {
&self.name
}
fn sample(&mut self) -> f64 {
if let Some((rx, tx)) = self.read_stats() {
if let Some(last_time) = self.last_time {
let elapsed = last_time.elapsed().as_secs_f64();
if elapsed > 0.0 {
let rx_delta = rx.saturating_sub(self.last_rx);
let tx_delta = tx.saturating_sub(self.last_tx);
self.last_rx_rate = rx_delta as f64 / elapsed;
self.last_tx_rate = tx_delta as f64 / elapsed;
// Record combined rate in history
let combined = self.last_rx_rate + self.last_tx_rate;
if self.history.len() >= HISTORY_SIZE {
self.history.pop_front();
}
self.history.push_back(combined);
// Record separate rx/tx histories
if self.rx_history.len() >= HISTORY_SIZE {
self.rx_history.pop_front();
}
self.rx_history.push_back(self.last_rx_rate);
if self.tx_history.len() >= HISTORY_SIZE {
self.tx_history.pop_front();
}
self.tx_history.push_back(self.last_tx_rate);
}
}
self.last_rx = rx;
self.last_tx = tx;
self.last_time = Some(Instant::now());
}
// Return combined rate in KB/s
(self.last_rx_rate + self.last_tx_rate) / 1024.0
}
fn min(&self) -> f64 {
0.0
}
fn max(&self) -> f64 {
1000000.0 // 1 GB/s max
}
fn unit(&self) -> &str {
"KB/s"
}
}