getifs 0.6.0

Cross-platform enumeration of network interfaces and their MTU, gateway, multicast, and local/private/public IP addresses.
Documentation
use smol_str::SmolStr;
use std::io;

/// Returns the name of the interface by the given index.
///
/// ## Example
///
/// ```rust
/// use getifs::{ifindex_to_name, interfaces};
///
/// let interface = interfaces().unwrap().into_iter().next().unwrap();
/// let name = ifindex_to_name(interface.index()).unwrap();
///
/// assert_eq!(interface.name(), &name);
/// ```
pub fn ifindex_to_name(idx: u32) -> io::Result<SmolStr> {
  ifindex_to_name_in(idx)
}

#[cfg(bsd_like)]
fn ifindex_to_name_in(idx: u32) -> io::Result<SmolStr> {
  use std::ffi::CStr;

  let mut ifname = [0u8; libc::IF_NAMESIZE + 1];
  let res = unsafe { libc::if_indextoname(idx as _, ifname.as_mut_ptr() as *mut libc::c_char) };

  if res.is_null() {
    return Err(io::Error::last_os_error());
  }

  // Use CStr to handle null-terminated string
  let name = unsafe { CStr::from_ptr(ifname.as_ptr() as *const libc::c_char) };

  // Convert to string and then to SmolStr
  name
    .to_str()
    .map(SmolStr::new)
    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}

#[cfg(linux_like)]
fn ifindex_to_name_in(idx: u32) -> io::Result<SmolStr> {
  use rustix::net::{netdevice::index_to_name_inlined, socket, AddressFamily, SocketType};

  let socket_fd = socket(AddressFamily::INET, SocketType::DGRAM, None)?;

  // `index_to_name_inlined` (rustix 1.1) returns a stack-allocated
  // `InlinedName` — no intermediate `String` on the heap. Interface
  // names are bounded by `IF_NAMESIZE` (16), which fits within
  // `SmolStr`'s inline capacity (23), so this stays allocation-free.
  index_to_name_inlined(socket_fd, idx)
    .map(|name| SmolStr::new(name.as_str()))
    .map_err(Into::into)
}

/// Returns the name of the interface by the given index.
#[cfg(windows)]
fn ifindex_to_name_in(idx: u32) -> io::Result<SmolStr> {
  use std::ffi::CStr;
  use windows_sys::Win32::NetworkManagement::{
    IpHelper::{ConvertInterfaceIndexToLuid, ConvertInterfaceLuidToAlias},
    Ndis::NET_LUID_LH,
  };

  let mut luid = NET_LUID_LH { Value: 0 };

  // Convert index to LUID
  let result = unsafe { ConvertInterfaceIndexToLuid(idx, &mut luid) };
  if result != 0 {
    return Err(io::Error::last_os_error());
  }

  // Get alias (friendly name)
  let mut name_buf = [0u16; 256]; // IF_MAX_STRING_SIZE + 1
  let result = unsafe { ConvertInterfaceLuidToAlias(&luid, name_buf.as_mut_ptr(), name_buf.len()) };
  if result != 0 {
    return Err(io::Error::last_os_error());
  }

  // Convert to string
  match crate::utils::friendly_name(name_buf.as_mut_ptr()) {
    Some(name) => Ok(name),
    None => {
      // Last-ditch fallback via `if_indextoname`. Guard against a null
      // return — feeding that straight into `CStr::from_ptr` is UB.
      let mut name_buf = [0u8; 256];
      // SAFETY: `if_indextoname` writes into `name_buf` (≥ IF_NAMESIZE)
      // and returns either a pointer into that buffer or null.
      let hname = unsafe {
        windows_sys::Win32::NetworkManagement::IpHelper::if_indextoname(idx, name_buf.as_mut_ptr())
      };
      if hname.is_null() {
        return Err(io::Error::last_os_error());
      }
      // SAFETY: non-null `hname` points into `name_buf`, which is
      // alive for the duration of this call and holds a NUL-terminated
      // C string produced by `if_indextoname`.
      unsafe { Ok(CStr::from_ptr(hname as _).to_string_lossy().into()) }
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  // Covers the `Err(...)` arm — a wildly out-of-range index has no
  // matching interface on any platform, so `if_indextoname` /
  // `ConvertInterfaceIndexToLuid` returns null/non-zero and we
  // surface the OS error.
  #[test]
  fn out_of_range_index_returns_err() {
    // 0xFFFE_FFFE is far above any real interface index on any host.
    assert!(ifindex_to_name(0xFFFE_FFFE).is_err());
  }

  // Covers the success arm by round-tripping the first interface
  // returned by `interfaces()`. Skipped on DragonFly because of
  // vmactions interface churn — see the matching gate on
  // `name_to_idx::tests::round_trip_first_interface`.
  #[cfg(not(target_os = "dragonfly"))]
  #[test]
  fn round_trip_first_interface() {
    let ift = crate::interfaces().unwrap();
    let first = ift.iter().next().unwrap();
    let name = ifindex_to_name(first.index()).unwrap();
    assert_eq!(first.name(), &name);
  }
}