Skip to main content

r_lanlib/
network.rs

1//! Provides helpers for selecting a network interface on the current host
2//! through which to preform network scanning, and for detecting the default
3//! gateway from the OS routing table.
4
5use itertools::Itertools;
6use pnet::{
7    datalink::NetworkInterface as PNetNetworkInterface, ipnetwork::IpNetwork,
8    util::MacAddr,
9};
10use std::{
11    net::{Ipv4Addr, TcpListener},
12    process::Command,
13    str::FromStr,
14};
15
16use crate::error::{RLanLibError, Result};
17
18/// Represents a network interface on current host
19pub struct NetworkInterface {
20    /// The name of the network interface i.e. "en0"
21    pub name: String,
22    /// A description of the network interface
23    pub description: String,
24    /// The cidr block associated with interface
25    pub cidr: String,
26    /// The assigned IPV4 address on the interface
27    pub ipv4: Ipv4Addr,
28    /// The IpNetwork of the interface
29    pub ips: Vec<IpNetwork>,
30    /// The MAC address of the interface
31    pub mac: MacAddr,
32    /// Any defined flags on the interface
33    pub flags: u32,
34    /// The index of the interface
35    pub index: u32,
36}
37
38impl TryFrom<PNetNetworkInterface> for NetworkInterface {
39    type Error = RLanLibError;
40
41    fn try_from(value: PNetNetworkInterface) -> Result<Self> {
42        let mac = value.mac.ok_or(RLanLibError::NetworkInterface(
43            "failed to get mac address for interface".into(),
44        ))?;
45        let (ip, cidr) = get_interface_ipv4_and_cidr(&value).ok_or(
46            RLanLibError::NetworkInterface(
47                "failed to get ip and cidr for interface".into(),
48            ),
49        )?;
50        let ipv4 = Ipv4Addr::from_str(&ip).map_err(|e| {
51            RLanLibError::NetworkInterface(format!(
52                "failed to parse interface ip address '{ip}': {e}"
53            ))
54        })?;
55
56        Ok(Self {
57            name: value.name,
58            description: value.description,
59            flags: value.flags,
60            index: value.index,
61            mac,
62            ips: value.ips,
63            cidr,
64            ipv4,
65        })
66    }
67}
68
69impl From<&NetworkInterface> for PNetNetworkInterface {
70    fn from(value: &NetworkInterface) -> Self {
71        Self {
72            name: value.name.clone(),
73            flags: value.flags,
74            description: value.description.clone(),
75            index: value.index,
76            ips: value.ips.clone(),
77            mac: Some(value.mac),
78        }
79    }
80}
81
82/// Finds and returns a NetworkInterface by name for current host
83pub fn get_interface(name: &str) -> Result<NetworkInterface> {
84    let iface = pnet::datalink::interfaces()
85        .into_iter()
86        .find(|i| i.name == name)
87        .ok_or(RLanLibError::NetworkInterface(format!(
88            "failed to find network interface with name: {name}"
89        )))?;
90    NetworkInterface::try_from(iface)
91}
92
93/// Finds and returns the default NetworkInterface for current host
94pub fn get_default_interface() -> Result<NetworkInterface> {
95    let iface = pnet::datalink::interfaces()
96        .into_iter()
97        .find(|e| {
98            e.is_up() && !e.is_loopback() && e.ips.iter().any(|i| i.is_ipv4())
99        })
100        .ok_or(RLanLibError::NetworkInterface(
101            "failed to get default network interface".into(),
102        ))?;
103    NetworkInterface::try_from(iface)
104}
105
106/// Finds an available port on the current host. This is useful when setting the
107/// listening port on a scanner where packets will be received.
108pub fn get_available_port() -> Result<u16> {
109    let listener = TcpListener::bind(("127.0.0.1", 0)).map_err(|e| {
110        RLanLibError::NetworkInterface(format!(
111            "failed to bind loopback to find open port: {e}"
112        ))
113    })?;
114    let addr = listener.local_addr().map_err(|e| {
115        RLanLibError::NetworkInterface(format!(
116            "failed to get local address for open port: {e}"
117        ))
118    })?;
119    Ok(addr.port())
120}
121
122fn get_interface_ipv4_and_cidr(
123    interface: &PNetNetworkInterface,
124) -> Option<(String, String)> {
125    let ipnet = interface.ips.iter().find(|i| i.is_ipv4())?;
126    let host_ip = ipnet.ip().to_string();
127    let first_ip = ipnet
128        .iter()
129        .find_or_first(|p| p.is_ipv4() && !p.to_string().ends_with(".0"));
130    let base = first_ip
131        .map(|i| i.to_string())
132        .unwrap_or_else(|| ipnet.network().to_string());
133    let prefix = ipnet.prefix().to_string();
134    let cidr = format!("{base}/{prefix}");
135    Some((host_ip, cidr))
136}
137
138/// Returns the default gateway IPv4 address by parsing the system routing
139/// table. Works on macOS (`netstat -rn`).
140/// Returns `None` if the gateway cannot be determined.
141#[cfg(target_os = "macos")]
142pub fn get_default_gateway() -> Option<Ipv4Addr> {
143    // `netstat -rn` output contains a line like:
144    //   default    192.168.1.1    UGScg  en0
145    let output = Command::new("netstat").args(["-rn"]).output().ok()?;
146    let stdout = String::from_utf8_lossy(&output.stdout);
147
148    for line in stdout.lines() {
149        let mut parts = line.split_whitespace();
150        if parts.next() == Some("default")
151            && let Some(gw) = parts.next()
152            && let Ok(ip) = Ipv4Addr::from_str(gw)
153        {
154            return Some(ip);
155        }
156    }
157
158    None
159}
160
161/// Returns the default gateway IPv4 address by parsing the system routing
162/// table. Works on Linux (`ip route show`).
163/// Returns `None` if the gateway cannot be determined.
164#[cfg(target_os = "linux")]
165pub fn get_default_gateway() -> Option<Ipv4Addr> {
166    // `ip route show` output contains a line like:
167    //   default via 192.168.1.1 dev eth0
168    let output = Command::new("ip").args(["route", "show"]).output().ok()?;
169    let stdout = String::from_utf8_lossy(&output.stdout);
170
171    for line in stdout.lines() {
172        let mut parts = line.split_whitespace();
173        if parts.next() == Some("default")
174            && parts.next() == Some("via")
175            && let Some(gw) = parts.next()
176            && let Ok(ip) = Ipv4Addr::from_str(gw)
177        {
178            return Some(ip);
179        }
180    }
181
182    None
183}
184
185/// Returns `None` on platforms where gateway detection is not implemented.
186#[cfg(not(any(target_os = "macos", target_os = "linux")))]
187pub fn get_default_gateway() -> Option<Ipv4Addr> {
188    None
189}
190
191#[cfg(test)]
192#[path = "./network_tests.rs"]
193mod tests;