1use crate::core::models::localhost::{FirewallStatus, IpServiceGroup, Service};
8use anyhow;
9use pnet::datalink::NetworkInterface;
10use std::collections::{HashMap, HashSet};
11use std::net::IpAddr;
12#[cfg(target_os = "linux")]
13use std::process::Command;
14
15#[derive(Debug)]
17struct SocketInfo {
18 ip: IpAddr,
19 port: u16,
20 protocol: String, process_name: String,
22}
23
24pub fn get_local_services() -> anyhow::Result<Vec<IpServiceGroup>> {
25 let entries = retrieve_sockets()?;
26 Ok(aggregate_services(entries))
27}
28
29#[cfg(not(target_os = "windows"))]
30fn retrieve_sockets() -> anyhow::Result<Vec<SocketInfo>> {
31 let raw_data = retrieve_raw_socket_data()?;
32 Ok(parse_socket_data(&raw_data))
33}
34
35#[cfg(target_os = "windows")]
36fn retrieve_sockets() -> anyhow::Result<Vec<SocketInfo>> {
37 windows_impl::retrieve_native_sockets()
38}
39
40#[cfg(not(target_os = "windows"))]
41fn retrieve_raw_socket_data() -> anyhow::Result<String> {
42 use std::process::Command;
43 let output = Command::new("ss").arg("-lntuH").arg("-p").output()?;
44
45 if output.status.success() {
46 Ok(String::from_utf8_lossy(&output.stdout).to_string())
47 } else {
48 Ok(String::new())
49 }
50}
51
52#[cfg(not(target_os = "windows"))]
53fn parse_socket_data(stdout: &str) -> Vec<SocketInfo> {
54 stdout.lines().filter_map(parse_socket_line).collect()
55}
56
57#[cfg(not(target_os = "windows"))]
58fn parse_socket_line(line: &str) -> Option<SocketInfo> {
59 let parts: Vec<&str> = line.split_whitespace().collect();
60 if parts.len() < 5 {
61 return None;
62 }
63
64 let netid = parts[0];
65 let state = parts[1];
66
67 if netid == "tcp" && state != "LISTEN" {
69 return None;
70 }
71 if netid != "tcp" && netid != "udp" {
72 return None;
73 }
74
75 let local_addr_port = parts[4];
76 let (ip, port) = parse_address_port(local_addr_port)?;
77
78 let process_name = if let Some(users_field) = parts.last() {
80 parse_process_name(users_field).unwrap_or_else(|| "Unknown".to_string())
81 } else {
82 "Unknown".to_string()
83 };
84
85 Some(SocketInfo {
86 ip,
87 port,
88 protocol: netid.to_string(),
89 process_name,
90 })
91}
92
93#[cfg(not(target_os = "windows"))]
94fn parse_address_port(addr_port: &str) -> Option<(IpAddr, u16)> {
95 use std::net::Ipv4Addr;
96 use std::str::FromStr;
97 let idx = addr_port.rfind(':')?;
98 let ip_str = &addr_port[..idx];
99 let port_str = &addr_port[idx + 1..];
100
101 let clean_ip_str = ip_str
102 .trim_start_matches('[')
103 .trim_end_matches(']')
104 .trim_start_matches('*'); let ip = if clean_ip_str.is_empty() || clean_ip_str == "*" {
107 IpAddr::V4(Ipv4Addr::UNSPECIFIED)
108 } else if let Ok(addr) = IpAddr::from_str(clean_ip_str) {
109 addr
110 } else {
111 let pct_idx = clean_ip_str.find('%')?;
113 IpAddr::from_str(&clean_ip_str[..pct_idx]).ok()?
114 };
115
116 let port = port_str.parse::<u16>().ok()?;
117 if port == 0 {
118 return None;
119 }
120
121 Some((ip, port))
122}
123
124#[cfg(not(target_os = "windows"))]
125fn parse_process_name(users_field: &str) -> Option<String> {
126 if !users_field.starts_with("users:((") {
127 return None;
128 }
129
130 let start_quote = users_field.find('"')?;
132 let remainder = &users_field[start_quote + 1..];
133 let end_quote = remainder.find('"')?;
134
135 Some(remainder[..end_quote].to_string())
136}
137
138fn aggregate_services(entries: Vec<SocketInfo>) -> Vec<IpServiceGroup> {
139 struct ServiceBuilder {
140 tcp_ports: HashMap<String, HashSet<u16>>,
141 udp_ports: HashMap<String, HashSet<u16>>,
142 }
143
144 let mut ip_groups: HashMap<IpAddr, ServiceBuilder> = HashMap::new();
145
146 for entry in entries {
147 let builder = ip_groups.entry(entry.ip).or_insert(ServiceBuilder {
148 tcp_ports: HashMap::new(),
149 udp_ports: HashMap::new(),
150 });
151
152 let target_map = if entry.protocol == "tcp" {
153 &mut builder.tcp_ports
154 } else {
155 &mut builder.udp_ports
156 };
157
158 target_map
159 .entry(entry.process_name)
160 .or_default()
161 .insert(entry.port);
162 }
163
164 let mut result = Vec::new();
165 for (ip, builder) in ip_groups {
166 let mut tcp_services = convert_to_services(ip, builder.tcp_ports);
167 let mut udp_services = convert_to_services(ip, builder.udp_ports);
168
169 tcp_services.sort_by(|a, b| a.name.cmp(&b.name));
170 udp_services.sort_by(|a, b| a.name.cmp(&b.name));
171
172 result.push(IpServiceGroup::new(ip, tcp_services, udp_services));
173 }
174
175 result.sort_by_key(|g| g.ip_addr);
176 result
177}
178
179fn convert_to_services(ip: IpAddr, port_map: HashMap<String, HashSet<u16>>) -> Vec<Service> {
180 port_map
181 .into_iter()
182 .map(|(name, ports)| Service::new(name, ip, ports))
183 .collect()
184}
185
186pub fn get_firewall_status() -> anyhow::Result<FirewallStatus> {
187 #[cfg(target_os = "linux")]
188 {
189 let ufw_active = Command::new("ufw").arg("status").output().is_ok();
190 let firewalld_active = Command::new("firewall-cmd").arg("--state").output().is_ok();
191
192 if ufw_active || firewalld_active {
193 Ok(FirewallStatus::Active)
194 } else {
195 Ok(FirewallStatus::NotDetected)
196 }
197 }
198 #[cfg(target_os = "windows")]
199 {
200 let output = Command::new("netsh")
201 .args(["advfirewall", "show", "allprofiles", "state"])
202 .output()?;
203
204 if output.status.success() {
205 let stdout = String::from_utf8_lossy(&output.stdout);
206 if stdout.to_lowercase().contains("on") {
207 return Ok(FirewallStatus::Active);
208 }
209 }
210 Ok(FirewallStatus::NotDetected)
211 }
212 #[cfg(not(any(target_os = "linux", target_os = "windows")))]
213 {
214 Ok(FirewallStatus::NotDetected)
215 }
216}
217
218#[cfg(target_os = "windows")]
219mod windows_impl {
220 use super::*;
221 use std::net::Ipv4Addr;
222 use sysinfo::{Pid, ProcessesToUpdate, System};
223 use windows_sys::Win32::Foundation::NO_ERROR;
224 use windows_sys::Win32::NetworkManagement::IpHelper::{
225 GetExtendedTcpTable, GetExtendedUdpTable, MIB_TCP_STATE_LISTEN,
226 TCP_TABLE_OWNER_PID_LISTENER, UDP_TABLE_OWNER_PID,
227 };
228 use windows_sys::Win32::Networking::WinSock::AF_INET;
229
230 #[repr(C)]
231 #[allow(non_snake_case)]
232 struct MIB_TCPROW_OWNER_PID {
233 dwState: u32,
234 dwLocalAddr: u32,
235 dwLocalPort: u32,
236 dwRemoteAddr: u32,
237 dwRemotePort: u32,
238 dwOwningPid: u32,
239 }
240
241 #[repr(C)]
242 #[allow(non_snake_case)]
243 struct MIB_TCPTABLE_OWNER_PID_INTERNAL {
244 dwNumEntries: u32,
245 table: [MIB_TCPROW_OWNER_PID; 1],
246 }
247
248 #[repr(C)]
249 #[allow(non_snake_case)]
250 struct MIB_UDPROW_OWNER_PID {
251 dwLocalAddr: u32,
252 dwLocalPort: u32,
253 dwOwningPid: u32,
254 }
255
256 #[repr(C)]
257 #[allow(non_snake_case)]
258 struct MIB_UDPTABLE_OWNER_PID_INTERNAL {
259 dwNumEntries: u32,
260 table: [MIB_UDPROW_OWNER_PID; 1],
261 }
262
263 pub fn retrieve_native_sockets() -> anyhow::Result<Vec<SocketInfo>> {
264 let mut entries = Vec::new();
265 let mut sys = System::new();
266 sys.refresh_processes(ProcessesToUpdate::All, true);
267
268 let tcp_table = get_tcp_ipv4_table()?;
270 for i in 0..tcp_table.dwNumEntries as usize {
271 let row = unsafe { &*tcp_table.table.as_ptr().add(i) };
272 if row.dwState == MIB_TCP_STATE_LISTEN as u32 {
273 let pid = row.dwOwningPid;
274 let process_name = sys
275 .process(Pid::from(pid as usize))
276 .map(|p| p.name().to_string_lossy().into_owned())
277 .unwrap_or_else(|| "Unknown".to_string());
278
279 entries.push(SocketInfo {
280 ip: IpAddr::V4(Ipv4Addr::from(u32::from_be(row.dwLocalAddr))),
281 port: u16::from_be(row.dwLocalPort as u16),
282 protocol: "tcp".to_string(),
283 process_name,
284 });
285 }
286 }
287
288 let udp_table = get_udp_ipv4_table()?;
290 for i in 0..udp_table.dwNumEntries as usize {
291 let row = unsafe { &*udp_table.table.as_ptr().add(i) };
292 let pid = row.dwOwningPid;
293 let process_name = sys
294 .process(Pid::from(pid as usize))
295 .map(|p| p.name().to_string_lossy().into_owned())
296 .unwrap_or_else(|| "Unknown".to_string());
297
298 entries.push(SocketInfo {
299 ip: IpAddr::V4(Ipv4Addr::from(u32::from_be(row.dwLocalAddr))),
300 port: u16::from_be(row.dwLocalPort as u16),
301 protocol: "udp".to_string(),
302 process_name,
303 });
304 }
305
306 Ok(entries)
307 }
308
309 fn get_tcp_ipv4_table() -> anyhow::Result<Box<MIB_TCPTABLE_OWNER_PID_INTERNAL>> {
310 let mut size = 0;
311 unsafe {
312 GetExtendedTcpTable(
313 std::ptr::null_mut(),
314 &mut size,
315 1,
316 AF_INET as u32,
317 TCP_TABLE_OWNER_PID_LISTENER,
318 0,
319 );
320 }
321
322 let mut buffer = vec![0u8; size as usize];
323 let ret = unsafe {
324 GetExtendedTcpTable(
325 buffer.as_mut_ptr() as *mut _,
326 &mut size,
327 1,
328 AF_INET as u32,
329 TCP_TABLE_OWNER_PID_LISTENER,
330 0,
331 )
332 };
333
334 if ret == NO_ERROR {
335 let ptr =
336 Box::into_raw(buffer.into_boxed_slice()) as *mut MIB_TCPTABLE_OWNER_PID_INTERNAL;
337 Ok(unsafe { Box::from_raw(ptr) })
338 } else {
339 anyhow::bail!("Failed to get TCP table: {}", ret)
340 }
341 }
342
343 fn get_udp_ipv4_table() -> anyhow::Result<Box<MIB_UDPTABLE_OWNER_PID_INTERNAL>> {
344 let mut size = 0;
345 unsafe {
346 GetExtendedUdpTable(
347 std::ptr::null_mut(),
348 &mut size,
349 1,
350 AF_INET as u32,
351 UDP_TABLE_OWNER_PID,
352 0,
353 );
354 }
355
356 let mut buffer = vec![0u8; size as usize];
357 let ret = unsafe {
358 GetExtendedUdpTable(
359 buffer.as_mut_ptr() as *mut _,
360 &mut size,
361 1,
362 AF_INET as u32,
363 UDP_TABLE_OWNER_PID,
364 0,
365 )
366 };
367
368 if ret == NO_ERROR {
369 let ptr =
370 Box::into_raw(buffer.into_boxed_slice()) as *mut MIB_UDPTABLE_OWNER_PID_INTERNAL;
371 Ok(unsafe { Box::from_raw(ptr) })
372 } else {
373 anyhow::bail!("Failed to get UDP table: {}", ret)
374 }
375 }
376}
377
378pub fn get_network_interfaces() -> anyhow::Result<Vec<NetworkInterface>> {
379 crate::system::interface::get_prioritized_interfaces(10)
380}