r-lanlib 0.11.0

A library crate for performing network scanning operations on any local area network (LAN)
Documentation
//! Provides helpers for selecting a network interface on the current host
//! through which to preform network scanning, and for detecting the default
//! gateway from the OS routing table.

use itertools::Itertools;
use pnet::{
    datalink::NetworkInterface as PNetNetworkInterface, ipnetwork::IpNetwork,
    util::MacAddr,
};
use std::{
    net::{Ipv4Addr, TcpListener},
    process::Command,
    str::FromStr,
};

use crate::error::{RLanLibError, Result};

/// Represents a network interface on current host
pub struct NetworkInterface {
    /// The name of the network interface i.e. "en0"
    pub name: String,
    /// A description of the network interface
    pub description: String,
    /// The cidr block associated with interface
    pub cidr: String,
    /// The assigned IPV4 address on the interface
    pub ipv4: Ipv4Addr,
    /// The IpNetwork of the interface
    pub ips: Vec<IpNetwork>,
    /// The MAC address of the interface
    pub mac: MacAddr,
    /// Any defined flags on the interface
    pub flags: u32,
    /// The index of the interface
    pub index: u32,
}

impl TryFrom<PNetNetworkInterface> for NetworkInterface {
    type Error = RLanLibError;

    fn try_from(value: PNetNetworkInterface) -> Result<Self> {
        let mac = value.mac.ok_or(RLanLibError::NetworkInterface(
            "failed to get mac address for interface".into(),
        ))?;
        let (ip, cidr) = get_interface_ipv4_and_cidr(&value).ok_or(
            RLanLibError::NetworkInterface(
                "failed to get ip and cidr for interface".into(),
            ),
        )?;
        let ipv4 = Ipv4Addr::from_str(&ip).map_err(|e| {
            RLanLibError::NetworkInterface(format!(
                "failed to parse interface ip address '{ip}': {e}"
            ))
        })?;

        Ok(Self {
            name: value.name,
            description: value.description,
            flags: value.flags,
            index: value.index,
            mac,
            ips: value.ips,
            cidr,
            ipv4,
        })
    }
}

impl From<&NetworkInterface> for PNetNetworkInterface {
    fn from(value: &NetworkInterface) -> Self {
        Self {
            name: value.name.clone(),
            flags: value.flags,
            description: value.description.clone(),
            index: value.index,
            ips: value.ips.clone(),
            mac: Some(value.mac),
        }
    }
}

/// Finds and returns a NetworkInterface by name for current host
pub fn get_interface(name: &str) -> Result<NetworkInterface> {
    let iface = pnet::datalink::interfaces()
        .into_iter()
        .find(|i| i.name == name)
        .ok_or(RLanLibError::NetworkInterface(format!(
            "failed to find network interface with name: {name}"
        )))?;
    NetworkInterface::try_from(iface)
}

/// Finds and returns the default NetworkInterface for current host
pub fn get_default_interface() -> Result<NetworkInterface> {
    let iface = pnet::datalink::interfaces()
        .into_iter()
        .find(|e| {
            e.is_up() && !e.is_loopback() && e.ips.iter().any(|i| i.is_ipv4())
        })
        .ok_or(RLanLibError::NetworkInterface(
            "failed to get default network interface".into(),
        ))?;
    NetworkInterface::try_from(iface)
}

/// Finds an available port on the current host. This is useful when setting the
/// listening port on a scanner where packets will be received.
pub fn get_available_port() -> Result<u16> {
    let listener = TcpListener::bind(("127.0.0.1", 0)).map_err(|e| {
        RLanLibError::NetworkInterface(format!(
            "failed to bind loopback to find open port: {e}"
        ))
    })?;
    let addr = listener.local_addr().map_err(|e| {
        RLanLibError::NetworkInterface(format!(
            "failed to get local address for open port: {e}"
        ))
    })?;
    Ok(addr.port())
}

fn get_interface_ipv4_and_cidr(
    interface: &PNetNetworkInterface,
) -> Option<(String, String)> {
    let ipnet = interface.ips.iter().find(|i| i.is_ipv4())?;
    let host_ip = ipnet.ip().to_string();
    let first_ip = ipnet
        .iter()
        .find_or_first(|p| p.is_ipv4() && !p.to_string().ends_with(".0"));
    let base = first_ip
        .map(|i| i.to_string())
        .unwrap_or_else(|| ipnet.network().to_string());
    let prefix = ipnet.prefix().to_string();
    let cidr = format!("{base}/{prefix}");
    Some((host_ip, cidr))
}

/// Returns the default gateway IPv4 address by parsing the system routing
/// table. Works on macOS (`netstat -rn`).
/// Returns `None` if the gateway cannot be determined.
#[cfg(target_os = "macos")]
pub fn get_default_gateway() -> Option<Ipv4Addr> {
    // `netstat -rn` output contains a line like:
    //   default    192.168.1.1    UGScg  en0
    let output = Command::new("netstat").args(["-rn"]).output().ok()?;
    let stdout = String::from_utf8_lossy(&output.stdout);

    for line in stdout.lines() {
        let mut parts = line.split_whitespace();
        if parts.next() == Some("default")
            && let Some(gw) = parts.next()
            && let Ok(ip) = Ipv4Addr::from_str(gw)
        {
            return Some(ip);
        }
    }

    None
}

/// Returns the default gateway IPv4 address by parsing the system routing
/// table. Works on Linux (`ip route show`).
/// Returns `None` if the gateway cannot be determined.
#[cfg(target_os = "linux")]
pub fn get_default_gateway() -> Option<Ipv4Addr> {
    // `ip route show` output contains a line like:
    //   default via 192.168.1.1 dev eth0
    let output = Command::new("ip").args(["route", "show"]).output().ok()?;
    let stdout = String::from_utf8_lossy(&output.stdout);

    for line in stdout.lines() {
        let mut parts = line.split_whitespace();
        if parts.next() == Some("default")
            && parts.next() == Some("via")
            && let Some(gw) = parts.next()
            && let Ok(ip) = Ipv4Addr::from_str(gw)
        {
            return Some(ip);
        }
    }

    None
}

/// Returns `None` on platforms where gateway detection is not implemented.
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
pub fn get_default_gateway() -> Option<Ipv4Addr> {
    None
}

#[cfg(test)]
#[path = "./network_tests.rs"]
mod tests;