1use 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
18pub struct NetworkInterface {
20 pub name: String,
22 pub description: String,
24 pub cidr: String,
26 pub ipv4: Ipv4Addr,
28 pub ips: Vec<IpNetwork>,
30 pub mac: MacAddr,
32 pub flags: u32,
34 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
82pub 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
93pub 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
106pub 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#[cfg(target_os = "macos")]
142pub fn get_default_gateway() -> Option<Ipv4Addr> {
143 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#[cfg(target_os = "linux")]
165pub fn get_default_gateway() -> Option<Ipv4Addr> {
166 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#[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;