1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
10#![cfg_attr(docsrs, feature(doc_cfg))]
11
12#[cfg(not(windows))]
13mod posix;
14#[cfg(all(
15 not(windows),
16 not(all(
17 target_vendor = "apple",
18 any(
19 target_os = "macos",
20 target_os = "ios",
21 target_os = "tvos",
22 target_os = "watchos",
23 target_os = "visionos"
24 )
25 )),
26 not(target_os = "freebsd"),
27 not(target_os = "netbsd"),
28 not(target_os = "openbsd"),
29 not(target_os = "illumos")
30))]
31mod posix_not_apple;
32mod sockaddr;
33#[cfg(windows)]
34mod windows;
35
36use std::io;
37use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
38
39#[derive(Debug, PartialEq, Eq, Hash, Clone)]
41pub enum IfOperStatus {
42 Up = 1,
44
45 Down = 2,
47
48 Testing = 3,
50
51 Unknown = 4,
53
54 Dormant = 5,
56
57 NotPresent = 6,
61
62 LowerLayerDown = 7,
67}
68
69impl From<i32> for IfOperStatus {
70 fn from(value: i32) -> Self {
71 match value {
72 1 => Self::Up,
73 2 => Self::Down,
74 3 => Self::Testing,
75 4 => Self::Unknown,
76 5 => Self::Dormant,
77 6 => Self::NotPresent,
78 7 => Self::LowerLayerDown,
79 _ => Self::Unknown,
80 }
81 }
82}
83
84#[derive(Debug, PartialEq, Eq, Hash, Clone)]
86pub struct Interface {
87 pub name: String,
89 pub addr: IfAddr,
91 pub index: Option<u32>,
93
94 pub oper_status: IfOperStatus,
96
97 #[cfg(windows)]
102 pub adapter_name: String,
103}
104
105impl Interface {
106 #[must_use]
108 pub const fn is_loopback(&self) -> bool {
109 self.addr.is_loopback()
110 }
111
112 #[must_use]
114 pub const fn is_link_local(&self) -> bool {
115 self.addr.is_link_local()
116 }
117
118 #[must_use]
120 pub const fn ip(&self) -> IpAddr {
121 self.addr.ip()
122 }
123
124 #[must_use]
126 pub fn is_oper_up(&self) -> bool {
127 self.oper_status == IfOperStatus::Up
128 }
129}
130
131#[derive(Debug, PartialEq, Eq, Hash, Clone)]
133pub enum IfAddr {
134 V4(Ifv4Addr),
136 V6(Ifv6Addr),
138}
139
140impl IfAddr {
141 #[must_use]
143 pub const fn is_loopback(&self) -> bool {
144 match *self {
145 IfAddr::V4(ref ifv4_addr) => ifv4_addr.is_loopback(),
146 IfAddr::V6(ref ifv6_addr) => ifv6_addr.is_loopback(),
147 }
148 }
149
150 #[must_use]
152 pub const fn is_link_local(&self) -> bool {
153 match *self {
154 IfAddr::V4(ref ifv4_addr) => ifv4_addr.is_link_local(),
155 IfAddr::V6(ref ifv6_addr) => ifv6_addr.is_link_local(),
156 }
157 }
158
159 #[must_use]
161 pub const fn ip(&self) -> IpAddr {
162 match *self {
163 IfAddr::V4(ref ifv4_addr) => IpAddr::V4(ifv4_addr.ip),
164 IfAddr::V6(ref ifv6_addr) => IpAddr::V6(ifv6_addr.ip),
165 }
166 }
167}
168
169#[derive(Debug, PartialEq, Eq, Hash, Clone)]
171pub struct Ifv4Addr {
172 pub ip: Ipv4Addr,
174 pub netmask: Ipv4Addr,
176 pub prefixlen: u8,
178 pub broadcast: Option<Ipv4Addr>,
180}
181
182impl Ifv4Addr {
183 #[must_use]
185 pub const fn is_loopback(&self) -> bool {
186 self.ip.is_loopback()
187 }
188
189 #[must_use]
191 pub const fn is_link_local(&self) -> bool {
192 self.ip.is_link_local()
193 }
194}
195
196#[derive(Debug, PartialEq, Eq, Hash, Clone)]
198pub struct Ifv6Addr {
199 pub ip: Ipv6Addr,
201 pub netmask: Ipv6Addr,
203 pub prefixlen: u8,
205 pub broadcast: Option<Ipv6Addr>,
207}
208
209impl Ifv6Addr {
210 #[must_use]
212 pub const fn is_loopback(&self) -> bool {
213 self.ip.is_loopback()
214 }
215
216 #[must_use]
218 pub const fn is_link_local(&self) -> bool {
219 let bytes = self.ip.octets();
220
221 bytes[0] == 0xfe && bytes[1] == 0x80
222 }
223}
224
225#[cfg(not(windows))]
226mod getifaddrs_posix {
227 use libc::if_nametoindex;
228
229 use super::{IfAddr, Ifv4Addr, Ifv6Addr, Interface};
230 use crate::posix::{self as ifaddrs, IfAddrs};
231 use crate::sockaddr;
232 use crate::IfOperStatus;
233 use std::ffi::CStr;
234 use std::io;
235 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
236
237 #[cfg(not(target_os = "illumos"))]
240 const POSIX_IFF_RUNNING: u32 = 0x40; #[cfg(target_os = "illumos")]
242 const POSIX_IFF_RUNNING: u64 = 0x40; #[allow(unsafe_code)]
246 pub fn get_if_addrs() -> io::Result<Vec<Interface>> {
247 let mut ret = Vec::<Interface>::new();
248 let ifaddrs = IfAddrs::new()?;
249
250 for ifaddr in ifaddrs.iter() {
251 let addr = match sockaddr::to_ipaddr(ifaddr.ifa_addr) {
252 None => continue,
253 Some(IpAddr::V4(ipv4_addr)) => {
254 let netmask = match sockaddr::to_ipaddr(ifaddr.ifa_netmask) {
255 Some(IpAddr::V4(netmask)) => netmask,
256 _ => Ipv4Addr::new(0, 0, 0, 0),
257 };
258 let broadcast = if (ifaddr.ifa_flags & 2) != 0 {
259 match ifaddrs::do_broadcast(&ifaddr) {
260 Some(IpAddr::V4(broadcast)) => Some(broadcast),
261 _ => None,
262 }
263 } else {
264 None
265 };
266 let prefixlen = if cfg!(target_endian = "little") {
267 u32::from_le_bytes(netmask.octets()).count_ones() as u8
268 } else {
269 u32::from_be_bytes(netmask.octets()).count_ones() as u8
270 };
271 IfAddr::V4(Ifv4Addr {
272 ip: ipv4_addr,
273 netmask,
274 prefixlen,
275 broadcast,
276 })
277 }
278 Some(IpAddr::V6(ipv6_addr)) => {
279 let netmask = match sockaddr::to_ipaddr(ifaddr.ifa_netmask) {
280 Some(IpAddr::V6(netmask)) => netmask,
281 _ => Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0),
282 };
283 let broadcast = if (ifaddr.ifa_flags & 2) != 0 {
284 match ifaddrs::do_broadcast(&ifaddr) {
285 Some(IpAddr::V6(broadcast)) => Some(broadcast),
286 _ => None,
287 }
288 } else {
289 None
290 };
291 let prefixlen = if cfg!(target_endian = "little") {
292 u128::from_le_bytes(netmask.octets()).count_ones() as u8
293 } else {
294 u128::from_be_bytes(netmask.octets()).count_ones() as u8
295 };
296 IfAddr::V6(Ifv6Addr {
297 ip: ipv6_addr,
298 netmask,
299 prefixlen,
300 broadcast,
301 })
302 }
303 };
304
305 let name = unsafe { CStr::from_ptr(ifaddr.ifa_name) }
306 .to_string_lossy()
307 .into_owned();
308 let index = {
309 let index = unsafe { if_nametoindex(ifaddr.ifa_name) };
310
311 if index == 0 {
315 None
316 } else {
317 Some(index)
318 }
319 };
320
321 let oper_status = if ifaddr.ifa_flags & POSIX_IFF_RUNNING != 0 {
322 IfOperStatus::Up
323 } else {
324 IfOperStatus::Unknown
325 };
326
327 ret.push(Interface {
328 name,
329 addr,
330 index,
331 oper_status,
332 });
333 }
334
335 Ok(ret)
336 }
337}
338
339#[cfg(not(windows))]
341pub fn get_if_addrs() -> io::Result<Vec<Interface>> {
342 getifaddrs_posix::get_if_addrs()
343}
344
345#[cfg(windows)]
346mod getifaddrs_windows {
347 use super::{IfAddr, Ifv4Addr, Ifv6Addr, Interface};
348 use crate::sockaddr;
349 use crate::windows::IfAddrs;
350 use std::io;
351 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
352 use windows_sys::Win32::Networking::WinSock::IpDadStatePreferred;
353
354 pub fn get_if_addrs() -> io::Result<Vec<Interface>> {
356 let mut ret = Vec::<Interface>::new();
357 let ifaddrs = IfAddrs::new()?;
358
359 for ifaddr in ifaddrs.iter() {
360 for addr in ifaddr.unicast_addresses() {
361 if addr.DadState != IpDadStatePreferred {
362 continue;
363 }
364 let addr = match sockaddr::to_ipaddr(addr.Address.lpSockaddr) {
365 None => continue,
366 Some(IpAddr::V4(ipv4_addr)) => {
367 let mut item_netmask = Ipv4Addr::new(0, 0, 0, 0);
368 let mut item_broadcast = None;
369 let item_prefix = addr.OnLinkPrefixLength;
370
371 'prefixloopv4: for prefix in ifaddr.prefixes() {
373 let ipprefix = sockaddr::to_ipaddr(prefix.Address.lpSockaddr);
374 match ipprefix {
375 Some(IpAddr::V4(ref a)) => {
376 let mut netmask: [u8; 4] = [0; 4];
377 for (n, netmask_elt) in netmask
378 .iter_mut()
379 .enumerate()
380 .take((prefix.PrefixLength as usize + 7) / 8)
381 {
382 let x_byte = ipv4_addr.octets()[n];
383 let y_byte = a.octets()[n];
384 for m in 0..8 {
385 if (n * 8) + m >= prefix.PrefixLength as usize {
386 break;
387 }
388 let bit = 1 << (7 - m);
389 if (x_byte & bit) == (y_byte & bit) {
390 *netmask_elt |= bit;
391 } else {
392 continue 'prefixloopv4;
393 }
394 }
395 }
396 item_netmask = Ipv4Addr::new(
397 netmask[0], netmask[1], netmask[2], netmask[3],
398 );
399 let mut broadcast: [u8; 4] = ipv4_addr.octets();
400 for n in 0..4 {
401 broadcast[n] |= !netmask[n];
402 }
403 item_broadcast = Some(Ipv4Addr::new(
404 broadcast[0],
405 broadcast[1],
406 broadcast[2],
407 broadcast[3],
408 ));
409 break 'prefixloopv4;
410 }
411 _ => continue,
412 };
413 }
414 IfAddr::V4(Ifv4Addr {
415 ip: ipv4_addr,
416 netmask: item_netmask,
417 prefixlen: item_prefix,
418 broadcast: item_broadcast,
419 })
420 }
421 Some(IpAddr::V6(ipv6_addr)) => {
422 let mut item_netmask = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0);
423 let item_prefix = addr.OnLinkPrefixLength;
424 'prefixloopv6: for prefix in ifaddr.prefixes() {
426 let ipprefix = sockaddr::to_ipaddr(prefix.Address.lpSockaddr);
427 match ipprefix {
428 Some(IpAddr::V6(ref a)) => {
429 let mut netmask: [u16; 8] = [0; 8];
432 for (n, netmask_elt) in netmask
433 .iter_mut()
434 .enumerate()
435 .take((prefix.PrefixLength as usize + 15) / 16)
436 {
437 let x_word = ipv6_addr.segments()[n];
438 let y_word = a.segments()[n];
439 for m in 0..16 {
440 if (n * 16) + m >= prefix.PrefixLength as usize {
441 break;
442 }
443 let bit = 1 << (15 - m);
444 if (x_word & bit) == (y_word & bit) {
445 *netmask_elt |= bit;
446 } else {
447 continue 'prefixloopv6;
448 }
449 }
450 }
451 item_netmask = Ipv6Addr::new(
452 netmask[0], netmask[1], netmask[2], netmask[3], netmask[4],
453 netmask[5], netmask[6], netmask[7],
454 );
455 break 'prefixloopv6;
456 }
457 _ => continue,
458 };
459 }
460 IfAddr::V6(Ifv6Addr {
461 ip: ipv6_addr,
462 netmask: item_netmask,
463 prefixlen: item_prefix,
464 broadcast: None,
465 })
466 }
467 };
468
469 let index = match addr {
470 IfAddr::V4(_) => ifaddr.ipv4_index(),
471 IfAddr::V6(_) => ifaddr.ipv6_index(),
472 };
473 let oper_status = ifaddr.oper_status();
474
475 ret.push(Interface {
476 name: ifaddr.name(),
477 addr,
478 index,
479 oper_status,
480 adapter_name: ifaddr.adapter_name(),
481 });
482 }
483 }
484
485 Ok(ret)
486 }
487}
488
489#[cfg(windows)]
491pub fn get_if_addrs() -> io::Result<Vec<Interface>> {
492 getifaddrs_windows::get_if_addrs()
493}
494
495#[cfg(not(any(
496 all(
497 target_vendor = "apple",
498 any(
499 target_os = "macos",
500 target_os = "ios",
501 target_os = "tvos",
502 target_os = "watchos",
503 target_os = "visionos"
504 )
505 ),
506 target_os = "freebsd",
507 target_os = "netbsd",
508 target_os = "openbsd",
509 target_os = "illumos"
510)))]
511#[cfg_attr(
512 docsrs,
513 doc(cfg(any(
514 not(target_vendor = "apple"),
515 not(target_os = "freebsd"),
516 not(target_os = "netbsd"),
517 not(target_os = "openbsd"),
518 not(target_os = "illumos")
519 )))
520)]
521mod if_change_notifier {
522 use super::Interface;
523 use std::collections::HashSet;
524 use std::io;
525 use std::time::{Duration, Instant};
526
527 #[derive(Debug, PartialEq, Eq, Hash, Clone)]
528 pub enum IfChangeType {
529 Added(Interface),
530 Removed(Interface),
531 }
532
533 #[cfg(windows)]
534 type InternalIfChangeNotifier = crate::windows::WindowsIfChangeNotifier;
535 #[cfg(not(windows))]
536 type InternalIfChangeNotifier = crate::posix_not_apple::PosixIfChangeNotifier;
537
538 pub struct IfChangeNotifier {
542 inner: InternalIfChangeNotifier,
543 last_ifs: HashSet<Interface>,
544 }
545
546 impl IfChangeNotifier {
547 pub fn new() -> io::Result<Self> {
550 Ok(Self {
551 inner: InternalIfChangeNotifier::new()?,
552 last_ifs: HashSet::from_iter(super::get_if_addrs()?),
553 })
554 }
555
556 pub fn wait(&mut self, timeout: Option<Duration>) -> io::Result<Vec<IfChangeType>> {
569 let start = Instant::now();
570 loop {
571 self.inner
572 .wait(timeout.map(|t| t.saturating_sub(start.elapsed())))?;
573
574 let new_ifs = HashSet::from_iter(super::get_if_addrs()?);
576 let mut changes: Vec<IfChangeType> = new_ifs
577 .difference(&self.last_ifs)
578 .cloned()
579 .map(IfChangeType::Added)
580 .collect();
581 changes.extend(
582 self.last_ifs
583 .difference(&new_ifs)
584 .cloned()
585 .map(IfChangeType::Removed),
586 );
587 self.last_ifs = new_ifs;
588
589 if !changes.is_empty() {
590 return Ok(changes);
591 }
592 }
593 }
594 }
595}
596
597#[cfg(not(any(
598 all(
599 target_vendor = "apple",
600 any(
601 target_os = "macos",
602 target_os = "ios",
603 target_os = "tvos",
604 target_os = "watchos",
605 target_os = "visionos"
606 )
607 ),
608 target_os = "freebsd",
609 target_os = "netbsd",
610 target_os = "openbsd",
611 target_os = "illumos"
612)))]
613#[cfg_attr(
614 docsrs,
615 doc(cfg(any(
616 not(target_vendor = "apple"),
617 not(target_os = "freebsd"),
618 not(target_os = "netbsd"),
619 not(target_os = "openbsd"),
620 not(target_os = "illumos")
621 )))
622)]
623pub use if_change_notifier::{IfChangeNotifier, IfChangeType};
624
625#[cfg(test)]
626mod tests {
627 use super::{get_if_addrs, Interface};
628 use std::io::Read;
629 use std::net::{IpAddr, Ipv4Addr};
630 use std::process::{Command, Stdio};
631 use std::str::FromStr;
632 use std::thread;
633 use std::time::Duration;
634
635 fn list_system_interfaces(cmd: &str, args: &[&str]) -> String {
636 let start_cmd = if args.is_empty() {
637 Command::new(cmd).stdout(Stdio::piped()).spawn()
638 } else if args.len() == 1 {
639 let arg1 = args[0];
640 if arg1.is_empty() {
641 Command::new(cmd).stdout(Stdio::piped()).spawn()
642 } else {
643 Command::new(cmd).arg(arg1).stdout(Stdio::piped()).spawn()
644 }
645 } else {
646 Command::new(cmd).args(args).stdout(Stdio::piped()).spawn()
647 };
648 let mut process = match start_cmd {
649 Err(why) => {
650 println!("couldn't start cmd {} : {}", cmd, why);
651 return String::new();
652 }
653 Ok(process) => process,
654 };
655 thread::sleep(Duration::from_millis(1000));
656 let _ = process.kill();
657 let result: Vec<u8> = process
658 .stdout
659 .unwrap()
660 .bytes()
661 .map(|x| x.unwrap())
662 .collect();
663 String::from_utf8(result).unwrap()
664 }
665
666 #[cfg(windows)]
667 fn list_system_addrs() -> (Vec<IpAddr>, Vec<(String, bool)>) {
669 use std::net::Ipv6Addr;
670 let intf_list = list_system_interfaces("ipconfig", &[""]);
671 let ipaddr_list = intf_list
672 .lines()
673 .filter_map(|line| {
674 println!("{}", line);
675 if line.contains("Address") && !line.contains("Link-local") {
676 let addr_s: Vec<&str> = line.split(" : ").collect();
677 if line.contains("IPv6") {
678 return Some(IpAddr::V6(Ipv6Addr::from_str(addr_s[1]).unwrap()));
679 } else if line.contains("IPv4") {
680 return Some(IpAddr::V4(Ipv4Addr::from_str(addr_s[1]).unwrap()));
681 }
682 }
683 None
684 })
685 .collect();
686
687 let netsh_list =
699 list_system_interfaces("netsh", &["interface", "ipv4", "show", "interfaces"]);
700
701 let ipv4_status_vec: Vec<_> = netsh_list
702 .lines()
703 .filter_map(|line| {
704 if !line.contains("Idx") && !line.contains("---") && !line.is_empty() {
705 let columns: Vec<&str> = line.split_whitespace().collect();
706 let status = columns[3].trim().to_string();
707 let is_up = status == "connected";
708
709 let intf_name = columns[4..].join(" ");
711 return Some((intf_name, is_up));
712 }
713 None
714 })
715 .collect();
716
717 (ipaddr_list, ipv4_status_vec)
718 }
719
720 #[cfg(any(target_os = "linux", target_os = "android"))]
721 fn list_system_addrs() -> (Vec<IpAddr>, Vec<(String, bool)>) {
723 let intf_list = list_system_interfaces("ip", &["addr"]);
724 let ipaddr_list = intf_list
725 .lines()
726 .filter_map(|line| {
727 println!("{}", line);
728 if line.contains("inet ") {
729 let addr_s: Vec<&str> = line.split_whitespace().collect();
730 let addr: Vec<&str> = addr_s[1].split('/').collect();
731 return Some(IpAddr::V4(Ipv4Addr::from_str(addr[0]).unwrap()));
732 }
733 None
734 })
735 .collect();
736 let mut intf_status_vec = Vec::new();
737 for line in intf_list.lines() {
738 if !line.starts_with(' ') && !line.is_empty() {
739 let name_s: Vec<&str> = line.split(':').collect();
740 let is_up = !line.contains("state DOWN");
741 intf_status_vec.push((name_s[1].trim().to_string(), is_up));
742 }
743 }
744
745 (ipaddr_list, intf_status_vec)
746 }
747
748 #[cfg(any(
749 target_os = "freebsd",
750 target_os = "netbsd",
751 target_os = "openbsd",
752 target_os = "illumos",
753 all(
754 target_vendor = "apple",
755 any(
756 target_os = "macos",
757 target_os = "ios",
758 target_os = "tvos",
759 target_os = "watchos",
760 target_os = "visionos"
761 )
762 )
763 ))]
764 fn list_system_addrs() -> (Vec<IpAddr>, Vec<(String, bool)>) {
766 let intf_list = list_system_interfaces("ifconfig", &[""]);
767 let ipaddr_list = intf_list
768 .lines()
769 .filter_map(|line| {
770 println!("{}", line);
771 if line.contains("inet ") {
772 let addr_s: Vec<&str> = line.split_whitespace().collect();
773 return Some(IpAddr::V4(Ipv4Addr::from_str(addr_s[1]).unwrap()));
774 }
775 None
776 })
777 .collect();
778
779 let mut intf_status_vec = Vec::new();
780 for line in intf_list.lines() {
781 if !line.starts_with('\t') && !line.is_empty() {
793 let name_s: Vec<&str> = line.split(':').collect();
794 let is_admin_up = line.contains("<UP");
795 intf_status_vec.push((name_s[0].to_string(), is_admin_up));
796 } else if line.contains("status: inactive") {
797 if let Some(current_intf) = intf_status_vec.last_mut() {
798 current_intf.1 = false; }
800 }
801 }
802
803 (ipaddr_list, intf_status_vec)
804 }
805
806 #[test]
807 fn test_get_if_addrs() {
808 let ifaces = get_if_addrs().unwrap();
809 println!("Local interfaces:");
810 println!("{:#?}", ifaces);
811 assert!(
813 1 <= ifaces
814 .iter()
815 .filter(|interface| interface.is_loopback())
816 .count()
817 );
818 for interface in &ifaces {
820 if let Some(idx) = interface.index {
821 assert!(idx > 0);
822 }
823 }
824
825 let is_loopback =
827 |interface: &&Interface| interface.addr.ip() == IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
828 assert_eq!(1, ifaces.iter().filter(is_loopback).count());
829
830 let (system_addrs, intf_status_list) = list_system_addrs();
832 assert!(!system_addrs.is_empty());
833 for addr in system_addrs {
834 let mut listed = false;
835 println!("\n checking whether {:?} has been properly listed \n", addr);
836 for interface in &ifaces {
837 if interface.addr.ip() == addr {
838 listed = true;
839 }
840
841 assert!(interface.index.is_some());
842 }
843 assert!(listed);
844 }
845
846 println!("Interface status list: {:#?}", intf_status_list);
847 for (intf_name, is_up) in intf_status_list {
848 for interface in &ifaces {
849 if interface.name == intf_name {
850 if interface.is_oper_up() != is_up {
851 println!(
852 "Interface {} status mismatch: listed {}, detected {:?}",
853 intf_name, is_up, interface.oper_status
854 );
855 }
856 assert_eq!(interface.is_oper_up(), is_up);
857 }
858 }
859 }
860 }
861
862 #[cfg(not(any(
863 all(
864 target_vendor = "apple",
865 any(
866 target_os = "macos",
867 target_os = "ios",
868 target_os = "tvos",
869 target_os = "watchos",
870 target_os = "visionos"
871 )
872 ),
873 target_os = "freebsd",
874 target_os = "netbsd",
875 target_os = "openbsd",
876 target_os = "illumos"
877 )))]
878 #[test]
879 fn test_if_notifier() {
880 let notifier = crate::IfChangeNotifier::new();
890 assert!(notifier.is_ok());
891 let mut notifier = notifier.unwrap();
892
893 assert!(notifier.wait(Some(Duration::ZERO)).is_err());
894 }
895}