nd_300/diagnostics/
connections.rs1use serde::Serialize;
2
3use super::shared_cache::SharedCache;
4
5#[derive(Debug, Clone, Serialize)]
6pub struct ConnectionEntry {
7 pub protocol: String,
8 pub local_addr: String,
9 pub remote_addr: String,
10 pub state: String,
11 pub pid: Option<u32>,
12 pub process_name: Option<String>,
13}
14
15pub async fn collect_with_cache(cache: &SharedCache) -> Option<Vec<ConnectionEntry>> {
16 #[cfg(windows)]
17 {
18 if let Some(ref nc) = cache.netstat {
19 return Some(parse_windows_connections(&nc.lines, &nc.process_map));
20 }
21 }
22
23 #[cfg(target_os = "macos")]
24 {
25 if let Some(ref nc) = cache.netstat {
26 return Some(parse_macos_connections(&nc.lines));
27 }
28 }
29
30 let _ = cache;
32 collect().await
33}
34
35pub async fn collect() -> Option<Vec<ConnectionEntry>> {
36 #[cfg(windows)]
37 {
38 collect_windows().await
39 }
40
41 #[cfg(target_os = "macos")]
42 {
43 collect_macos().await
44 }
45
46 #[cfg(target_os = "linux")]
47 {
48 collect_linux().await
49 }
50}
51
52#[cfg(windows)]
53fn parse_windows_connections(
54 lines: &[String],
55 process_map: &std::collections::HashMap<u32, String>,
56) -> Vec<ConnectionEntry> {
57 let mut entries = Vec::new();
58
59 for line in lines {
60 let line = line.trim();
61 let parts: Vec<&str> = line.split_whitespace().collect();
62
63 if parts.len() >= 4 && (parts[0] == "TCP" || parts[0] == "UDP") {
64 let pid = parts.last().and_then(|s| s.parse::<u32>().ok());
65 let state = if parts[0] == "TCP" && parts.len() >= 5 {
66 parts[3].to_string()
67 } else {
68 String::new()
69 };
70
71 let process_name = pid.and_then(|p| process_map.get(&p).cloned());
72
73 entries.push(ConnectionEntry {
74 protocol: parts[0].to_string(),
75 local_addr: parts[1].to_string(),
76 remote_addr: parts[2].to_string(),
77 state,
78 pid,
79 process_name,
80 });
81 }
82 }
83
84 entries
85}
86
87#[cfg(windows)]
88async fn collect_windows() -> Option<Vec<ConnectionEntry>> {
89 use sysinfo::System;
90
91 let mut cmd = tokio::process::Command::new("netstat");
92 cmd.args(["-ano"]);
93 let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
94
95 let text = String::from_utf8_lossy(&output.stdout);
96 let lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
97
98 let mut sys = System::new();
99 sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
100 let mut process_map = std::collections::HashMap::new();
101 for (pid, process) in sys.processes() {
102 process_map.insert(pid.as_u32(), process.name().to_string_lossy().to_string());
103 }
104
105 Some(parse_windows_connections(&lines, &process_map))
106}
107
108#[cfg(target_os = "macos")]
109fn parse_macos_connections(lines: &[String]) -> Vec<ConnectionEntry> {
110 let mut entries = Vec::new();
111
112 for line in lines {
113 let parts: Vec<&str> = line.split_whitespace().collect();
114 if parts.len() >= 6 && parts[0].starts_with("tcp") {
115 entries.push(ConnectionEntry {
116 protocol: "TCP".to_string(),
117 local_addr: parts[3].to_string(),
118 remote_addr: parts[4].to_string(),
119 state: parts[5].to_string(),
120 pid: None,
121 process_name: None,
122 });
123 }
124 }
125
126 entries
127}
128
129#[cfg(target_os = "macos")]
130async fn collect_macos() -> Option<Vec<ConnectionEntry>> {
131 let mut cmd = tokio::process::Command::new("netstat");
132 cmd.args(["-anp", "tcp"]);
133 let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
134
135 let text = String::from_utf8_lossy(&output.stdout);
136 let lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
137 Some(parse_macos_connections(&lines))
138}
139
140#[cfg(target_os = "linux")]
141async fn collect_linux() -> Option<Vec<ConnectionEntry>> {
142 let mut cmd = tokio::process::Command::new("ss");
143 cmd.args(["-tupn"]);
144 let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
145
146 let text = String::from_utf8_lossy(&output.stdout);
147 let mut entries = Vec::new();
148
149 for line in text.lines().skip(1) {
150 let parts: Vec<&str> = line.split_whitespace().collect();
151 if parts.len() >= 5 {
152 let (pid, pname) = parse_ss_process(parts.get(6).unwrap_or(&""));
153
154 entries.push(ConnectionEntry {
155 protocol: parts[0].to_uppercase(),
156 local_addr: parts[4].to_string(),
157 remote_addr: parts.get(5).unwrap_or(&"*:*").to_string(),
158 state: parts[1].to_string(),
159 pid,
160 process_name: pname,
161 });
162 }
163 }
164
165 Some(entries)
166}
167
168#[cfg(target_os = "linux")]
169fn parse_ss_process(s: &str) -> (Option<u32>, Option<String>) {
170 if let Some(start) = s.find("pid=") {
172 let after = &s[start + 4..];
173 let pid_str: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
174 let pid = pid_str.parse().ok();
175
176 let pname = if let Some(name_start) = s.find("((\"") {
177 let after = &s[name_start + 3..];
178 let name: String = after.chars().take_while(|c| *c != '"').collect();
179 Some(name)
180 } else {
181 None
182 };
183
184 (pid, pname)
185 } else {
186 (None, None)
187 }
188}