Skip to main content

zond_engine/
host_sys.rs

1// Copyright (c) 2026 Erik Lening (hollowpointer) and Contributors
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4// If a copy of the MPL was not distributed with this file, You can obtain one at
5// https://mozilla.org/MPL/2.0/.
6
7use 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/// Intermediate representation of a socket entry.
16#[derive(Debug)]
17struct SocketInfo {
18    ip: IpAddr,
19    port: u16,
20    protocol: String, // "tcp" or "udp"
21    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    // Filter relevant sockets: TCP sets to LISTEN, UDP is usually UNCONN (but listed due to -l)
68    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    // Process info is usually in the last column
79    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('*'); // ss sometimes uses * for 0.0.0.0
105
106    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        // Handle interface suffix (e.g., fe80::1%eth0)
112        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    // Format: users:(("name",...))
131    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        // 1. TCP IPv4
269        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        // 2. UDP IPv4
289        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}