1use neli::attr::Attribute as _;
2use neli::consts::{nl::*, rtnl::*, socket::*};
3use neli::nl::{NlPayload, Nlmsghdr};
4use neli::rtnl::{Ifaddrmsg, Ifinfomsg, Rtmsg};
5use neli::socket::{NlSocketHandle, tokio::NlSocket};
6use neli::types::RtBuffer;
7
8use regex::Regex;
9
10use libc::c_uchar;
11
12use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
13use std::ops;
14use std::path::Path;
15
16use crate::errors::*;
17use crate::util;
18
19const RT_SCOPE_HOST: c_uchar = 254;
21
22#[derive(Debug)]
23pub struct NetDevice {
24 pub iface: Interface,
25 pub wifi_info: Option<WifiInfo>,
26 pub ip: Option<Ipv4Addr>,
27 pub ipv6: Option<Ipv6Addr>,
28 pub icon: &'static str,
29 pub tun_wg_ppp: bool,
30 pub nameservers: Vec<IpAddr>,
31}
32
33#[derive(Debug, Default)]
34pub struct WifiInfo {
35 pub ssid: Option<String>,
36 pub signal: Option<f64>,
37 pub frequency: Option<f64>,
38 pub bitrate: Option<f64>,
39}
40
41impl NetDevice {
42 pub async fn new(iface_re: Option<&Regex>) -> Result<Option<Self>> {
43 let mut sock = NlSocket::new(
44 NlSocketHandle::connect(NlFamily::Route, None, &[]).error("Socket error")?,
45 )
46 .error("Socket error")?;
47
48 let mut ifaces = get_interfaces(&mut sock, iface_re)
49 .await
50 .map_err(BoxErrorWrapper)
51 .error("Failed to fetch interfaces")?;
52 if ifaces.is_empty() {
53 return Ok(None);
54 }
55
56 let default_iface = get_default_interface(&mut sock)
57 .await
58 .map_err(BoxErrorWrapper)
59 .error("Failed to get default interface")?;
60
61 let iface_position = ifaces
62 .iter()
63 .position(|i| i.index == default_iface)
64 .or_else(|| ifaces.iter().position(|i| i.operstate == Operstate::Up))
65 .unwrap_or(0);
66
67 let iface = ifaces.swap_remove(iface_position);
68 let wifi_info = WifiInfo::new(iface.index).await?;
69 let ip = ipv4(&mut sock, iface.index).await?;
70 let ipv6 = ipv6(&mut sock, iface.index).await?;
71 let nameservers = read_nameservers()
72 .await
73 .error("Failed to read nameservers")?;
74
75 let path = Path::new("/sys/class/net").join(&iface.name);
78 let tun = iface.name.starts_with("tun")
79 || iface.name.starts_with("tap")
80 || path.join("tun_flags").exists();
81 let (wg, ppp) = util::read_file(path.join("uevent"))
82 .await
83 .map_or((false, false), |c| {
84 (c.contains("wireguard"), c.contains("ppp"))
85 });
86
87 let icon = if wifi_info.is_some() {
88 "net_wireless"
89 } else if tun || wg || ppp {
90 "net_vpn"
91 } else if iface.name == "lo" {
92 "net_loopback"
93 } else {
94 "net_wired"
95 };
96
97 Ok(Some(Self {
98 iface,
99 wifi_info,
100 ip,
101 ipv6,
102 icon,
103 tun_wg_ppp: tun | wg | ppp,
104 nameservers,
105 }))
106 }
107
108 pub fn is_up(&self) -> bool {
109 self.tun_wg_ppp
110 || self.iface.operstate == Operstate::Up
111 || (self.iface.operstate == Operstate::Unknown
112 && (self.ip.is_some() || self.ipv6.is_some()))
113 }
114
115 pub fn ssid(&self) -> Option<String> {
116 self.wifi_info.as_ref()?.ssid.clone()
117 }
118
119 pub fn frequency(&self) -> Option<f64> {
120 self.wifi_info.as_ref()?.frequency
121 }
122
123 pub fn bitrate(&self) -> Option<f64> {
124 self.wifi_info.as_ref()?.bitrate
125 }
126
127 pub fn signal(&self) -> Option<f64> {
128 self.wifi_info.as_ref()?.signal
129 }
130}
131
132impl WifiInfo {
133 async fn new(if_index: i32) -> Result<Option<Self>> {
134 fn signal_percents(raw: f64) -> f64 {
136 const MAX_LEVEL: f64 = -20.;
137 const MIN_LEVEL: f64 = -85.;
138 const DIFF: f64 = MAX_LEVEL - MIN_LEVEL;
139 (100. - (MAX_LEVEL - raw) * (15. * DIFF + 62. * (MAX_LEVEL - raw)) / (DIFF * DIFF))
140 .clamp(0., 100.)
141 }
142
143 fn ssid_from_bss_info_elements(mut bytes: &[u8]) -> Option<String> {
144 while bytes.len() > 2 && bytes[0] != 0 {
145 bytes = &bytes[(bytes[1] as usize + 2)..];
146 }
147
148 if bytes.len() < 2 || bytes.len() < bytes[1] as usize + 2 {
149 return None;
150 };
151
152 let ssid_len = bytes[1] as usize;
153 let raw_ssid = &bytes[2..][..ssid_len];
154
155 Some(String::from_utf8_lossy(raw_ssid).into_owned())
156 }
157
158 let Ok(mut socket) = neli_wifi::AsyncSocket::connect() else {
160 return Ok(None);
161 };
162
163 let interfaces = socket
164 .get_interfaces_info()
165 .await
166 .error("Failed to get nl80211 interfaces")?;
167
168 for interface in interfaces {
169 if let Some(index) = interface.index
170 && index == if_index
171 && let Ok(ap) = socket.get_station_info(index).await
172 {
173 let Some(ap) = ap.into_iter().next() else {
175 continue;
176 };
177
178 let bss = socket
179 .get_bss_info(index)
180 .await
181 .unwrap_or_default()
182 .into_iter()
183 .find(|bss| bss.status == Some(1));
184
185 let raw_signal = match ap.signal {
186 Some(signal) => Some(signal),
187 None => bss
188 .as_ref()
189 .and_then(|bss| bss.signal)
190 .map(|s| (s / 100) as i8),
191 };
192
193 let ssid = interface
194 .ssid
195 .as_deref()
196 .map(|ssid| String::from_utf8_lossy(ssid).into_owned())
197 .or_else(|| {
198 bss.as_ref()
199 .and_then(|bss| bss.information_elements.as_deref())
200 .and_then(ssid_from_bss_info_elements)
201 });
202
203 return Ok(Some(Self {
204 ssid,
205 frequency: interface.frequency.map(|f| f as f64 * 1e6),
206 signal: raw_signal.map(|s| signal_percents(s as f64)),
207 bitrate: ap.tx_bitrate.map(|b| b as f64 * 1e5), }));
209 }
210 }
211 Ok(None)
212 }
213}
214
215#[derive(Debug, Default, Clone, Copy)]
216pub struct InterfaceStats {
217 pub rx_bytes: u64,
218 pub tx_bytes: u64,
219}
220
221impl ops::Sub for InterfaceStats {
222 type Output = Self;
223
224 fn sub(mut self, rhs: Self) -> Self::Output {
225 self.rx_bytes = self.rx_bytes.saturating_sub(rhs.rx_bytes);
226 self.tx_bytes = self.tx_bytes.saturating_sub(rhs.tx_bytes);
227 self
228 }
229}
230
231impl InterfaceStats {
232 fn from_stats64(stats: &[u8]) -> Self {
233 assert!(stats.len() >= 8 * 4);
244 Self {
245 rx_bytes: u64::from_ne_bytes(stats[16..24].try_into().unwrap()),
246 tx_bytes: u64::from_ne_bytes(stats[24..32].try_into().unwrap()),
247 }
248 }
249}
250
251#[derive(Debug)]
252pub struct Interface {
253 pub index: i32,
254 pub operstate: Operstate,
255 pub name: String,
256 pub stats: Option<InterfaceStats>,
257}
258
259macro_rules! recv_until_done {
260 ($sock:ident, $payload:ident: $payload_type:ty => $($code:tt)*) => {
261 let mut buf = Vec::new();
262 'recv: loop {
263 let msgs = $sock.recv::<u16, $payload_type>(&mut buf).await?;
264 for msg in msgs {
265 if msg.nl_type == libc::NLMSG_DONE as u16 {
266 break 'recv;
267 }
268 if let NlPayload::Payload($payload) = msg.nl_payload {
269 $($code)*
270 }
271 }
272 }
273 };
274}
275
276async fn get_interfaces(
277 sock: &mut NlSocket,
278 filter: Option<&Regex>,
279) -> Result<Vec<Interface>, Box<dyn StdError + Send + Sync + 'static>> {
280 sock.send(&Nlmsghdr::new(
281 None,
282 Rtm::Getlink,
283 NlmFFlags::new(&[NlmF::Dump, NlmF::Request]),
284 None,
285 None,
286 NlPayload::Payload(Ifinfomsg::new(
287 RtAddrFamily::Unspecified,
288 Arphrd::None,
289 0,
290 IffFlags::empty(),
291 IffFlags::empty(),
292 RtBuffer::new(),
293 )),
294 ))
295 .await?;
296
297 let mut interfaces = Vec::new();
298
299 recv_until_done!(sock, msg: Ifinfomsg => {
300 let mut name = None;
301 let mut stats = None;
302 let mut operstate = Operstate::Unknown;
303 for attr in msg.rtattrs.iter() {
304 match attr.rta_type {
305 Ifla::Ifname => name = Some(attr.get_payload_as_with_len()?),
306 Ifla::Stats64 => stats = Some(InterfaceStats::from_stats64(attr.payload().as_ref())),
307 Ifla::Operstate => operstate = attr.get_payload_as::<u8>()?.into(),
308 _ => (),
309 }
310 }
311 let name: String = name.unwrap();
312 if filter.is_none_or(|f| f.is_match(&name)) {
313 interfaces.push(Interface {
314 index: msg.ifi_index,
315 operstate,
316 name,
317 stats,
318 });
319 }
320 });
321
322 Ok(interfaces)
323}
324
325async fn get_default_interface(
326 sock: &mut NlSocket,
327) -> Result<i32, Box<dyn StdError + Send + Sync + 'static>> {
328 sock.send(&Nlmsghdr::new(
329 None,
330 Rtm::Getroute,
331 NlmFFlags::new(&[NlmF::Request, NlmF::Dump]),
332 None,
333 None,
334 NlPayload::Payload(Rtmsg {
335 rtm_family: RtAddrFamily::Inet,
336 rtm_dst_len: 0,
337 rtm_src_len: 0,
338 rtm_tos: 0,
339 rtm_table: RtTable::Unspec,
340 rtm_protocol: Rtprot::Unspec,
341 rtm_scope: RtScope::Universe,
342 rtm_type: Rtn::Unspec,
343 rtm_flags: RtmFFlags::empty(),
344 rtattrs: RtBuffer::new(),
345 }),
346 ))
347 .await?;
348
349 let mut best_index = 0;
350 let mut best_metric = u32::MAX;
351
352 recv_until_done!(sock, msg: Rtmsg => {
353 if msg.rtm_type != Rtn::Unicast {
354 continue;
355 }
356 if msg.rtm_dst_len != 0 {
358 continue;
359 }
360
361 let mut index = None;
362 let mut metric = 0u32;
363 for attr in msg.rtattrs.iter() {
364 match attr.rta_type {
365 Rta::Oif => index = Some(attr.get_payload_as::<i32>()?),
366 Rta::Priority => metric = attr.get_payload_as::<u32>()?,
367 _ => (),
368 }
369 }
370 if let Some(i) = index
371 && metric < best_metric {
372 best_metric = metric;
373 best_index = i;
374 }
375 });
376
377 Ok(best_index)
378}
379
380async fn ip_payload<const BYTES: usize>(
381 sock: &mut NlSocket,
382 ifa_family: RtAddrFamily,
383 ifa_index: i32,
384) -> Result<Option<[u8; BYTES]>, Box<dyn StdError + Send + Sync + 'static>> {
385 sock.send(&Nlmsghdr::new(
386 None,
387 Rtm::Getaddr,
388 NlmFFlags::new(&[NlmF::Dump, NlmF::Request]),
389 None,
390 None,
391 NlPayload::Payload(Ifaddrmsg {
392 ifa_family,
393 ifa_prefixlen: 0,
394 ifa_flags: IfaFFlags::empty(),
395 ifa_scope: 0,
396 ifa_index: 0,
397 rtattrs: RtBuffer::new(),
398 }),
399 ))
400 .await?;
401
402 let mut payload = None;
403
404 recv_until_done!(sock, msg: Ifaddrmsg => {
405 if msg.ifa_index != ifa_index || msg.ifa_scope >= RT_SCOPE_HOST || payload.is_some() {
406 continue;
407 }
408
409 let attr_handle = msg.rtattrs.get_attr_handle();
410
411 if let Some(attr) = attr_handle.get_attribute(Ifa::Local)
412 .or_else(|| attr_handle.get_attribute(Ifa::Address))
413 && let Ok(p) = attr.rta_payload.as_ref().try_into()
414 {
415 payload = Some(p);
416 }
417 });
418
419 Ok(payload)
420}
421
422async fn ipv4(sock: &mut NlSocket, ifa_index: i32) -> Result<Option<Ipv4Addr>> {
423 Ok(ip_payload(sock, RtAddrFamily::Inet, ifa_index)
424 .await
425 .map_err(BoxErrorWrapper)
426 .error("Failed to get IP address")?
427 .map(Ipv4Addr::from))
428}
429
430async fn ipv6(sock: &mut NlSocket, ifa_index: i32) -> Result<Option<Ipv6Addr>> {
431 Ok(ip_payload(sock, RtAddrFamily::Inet6, ifa_index)
432 .await
433 .map_err(BoxErrorWrapper)
434 .error("Failed to get IPv6 address")?
435 .map(Ipv6Addr::from))
436}
437
438async fn read_nameservers() -> Result<Vec<IpAddr>> {
439 let file = util::read_file("/etc/resolv.conf")
440 .await
441 .error("Failed to read /etc/resolv.conf")?;
442 let mut nameservers = Vec::new();
443
444 for line in file.lines() {
445 let mut line_parts = line.split_whitespace();
446 if line_parts.next() == Some("nameserver")
447 && let Some(mut ip) = line_parts.next()
448 {
449 if let Some((without_zone_id, _zone_id)) = ip.split_once('%') {
451 ip = without_zone_id;
452 }
453 nameservers.push(ip.parse().error("Unable to parse ip")?);
454 }
455 }
456
457 Ok(nameservers)
458}
459
460#[derive(Debug, PartialEq, Eq)]
462pub enum Operstate {
463 Unknown,
467 Notpresent,
470 Down,
473 Lowerlayerdown,
476 Testing,
478 Dormant,
481 Up,
483}
484
485impl From<u8> for Operstate {
486 fn from(value: u8) -> Self {
487 match value {
488 1 => Self::Notpresent,
489 2 => Self::Down,
490 3 => Self::Lowerlayerdown,
491 4 => Self::Testing,
492 5 => Self::Dormant,
493 6 => Self::Up,
494 _ => Self::Unknown,
495 }
496 }
497}