nd_300/diagnostics/
firewall.rs1use serde::Serialize;
2
3#[derive(Debug, Clone, Serialize)]
4pub struct FirewallInfo {
5 pub enabled: bool,
6 pub profiles: Vec<FirewallProfile>,
7 pub summary: String,
8}
9
10#[derive(Debug, Clone, Serialize)]
11pub struct FirewallProfile {
12 pub name: String,
13 pub enabled: bool,
14 pub default_inbound: Option<String>,
15 pub default_outbound: Option<String>,
16}
17
18pub async fn collect() -> Option<FirewallInfo> {
19 #[cfg(windows)]
20 {
21 collect_windows().await
22 }
23
24 #[cfg(target_os = "macos")]
25 {
26 collect_macos().await
27 }
28
29 #[cfg(target_os = "linux")]
30 {
31 collect_linux().await
32 }
33}
34
35#[cfg(windows)]
36async fn collect_windows() -> Option<FirewallInfo> {
37 let mut cmd = tokio::process::Command::new("netsh");
38 cmd.args(["advfirewall", "show", "allprofiles", "state"]);
39 let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
40
41 let text = String::from_utf8_lossy(&output.stdout);
42 let mut profiles = Vec::new();
43 let mut current_name = String::new();
44
45 for line in text.lines() {
46 let line = line.trim();
47 if (line.contains("Profile Settings") || line.contains("Profile")) && line.ends_with(':') {
48 current_name = line
49 .replace("Profile Settings:", "")
50 .replace("Profile:", "")
51 .trim()
52 .to_string();
53 if current_name.is_empty() {
54 current_name = "Unknown".to_string();
55 }
56 }
57 if line.contains("State") {
58 let enabled = line.contains("ON");
59 profiles.push(FirewallProfile {
60 name: current_name.clone(),
61 enabled,
62 default_inbound: None,
63 default_outbound: None,
64 });
65 }
66 }
67
68 let mut cmd = tokio::process::Command::new("netsh");
70 cmd.args(["advfirewall", "show", "allprofiles"]);
71 if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
72 let text = String::from_utf8_lossy(&output.stdout);
73 let mut idx = 0;
74
75 for line in text.lines() {
76 let line = line.trim();
77 if line.contains("Firewall Policy") {
78 if let Some(val) = line.split_whitespace().last() {
79 if idx < profiles.len() {
80 let parts: Vec<&str> = val.split(',').collect();
81 profiles[idx].default_inbound = parts.first().map(|s| s.to_string());
82 profiles[idx].default_outbound = parts.get(1).map(|s| s.to_string());
83 }
84 idx += 1;
85 }
86 }
87 }
88 }
89
90 let any_enabled = profiles.iter().any(|p| p.enabled);
91 let summary = if profiles.is_empty() {
92 "Could not determine firewall status".to_string()
93 } else if any_enabled {
94 let enabled: Vec<&str> = profiles
95 .iter()
96 .filter(|p| p.enabled)
97 .map(|p| p.name.as_str())
98 .collect();
99 format!("Active: {}", enabled.join(", "))
100 } else {
101 "All profiles disabled".to_string()
102 };
103
104 Some(FirewallInfo {
105 enabled: any_enabled,
106 profiles,
107 summary,
108 })
109}
110
111#[cfg(target_os = "macos")]
112async fn collect_macos() -> Option<FirewallInfo> {
113 let mut cmd = tokio::process::Command::new("/usr/libexec/ApplicationFirewall/socketfilterfw");
114 cmd.args(["--getglobalstate"]);
115 let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
116
117 let text = String::from_utf8_lossy(&output.stdout);
118 let enabled = text.contains("enabled");
119
120 let mut stealth = false;
121 let mut cmd = tokio::process::Command::new("/usr/libexec/ApplicationFirewall/socketfilterfw");
122 cmd.args(["--getstealthmode"]);
123 if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
124 let text = String::from_utf8_lossy(&output.stdout);
125 stealth = text.contains("enabled");
126 }
127
128 let summary = if enabled {
129 if stealth {
130 "Application Firewall enabled (stealth mode on)".to_string()
131 } else {
132 "Application Firewall enabled".to_string()
133 }
134 } else {
135 "Application Firewall disabled".to_string()
136 };
137
138 Some(FirewallInfo {
139 enabled,
140 profiles: vec![FirewallProfile {
141 name: "Application Firewall".to_string(),
142 enabled,
143 default_inbound: None,
144 default_outbound: None,
145 }],
146 summary,
147 })
148}
149
150#[cfg(target_os = "linux")]
151async fn collect_linux() -> Option<FirewallInfo> {
152 let mut cmd = tokio::process::Command::new("ufw");
154 cmd.args(["status"]);
155 if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
156 let text = String::from_utf8_lossy(&output.stdout);
157 if text.contains("Status:") {
158 let enabled = text.contains("active");
159 return Some(FirewallInfo {
160 enabled,
161 profiles: vec![FirewallProfile {
162 name: "UFW".to_string(),
163 enabled,
164 default_inbound: None,
165 default_outbound: None,
166 }],
167 summary: if enabled {
168 "UFW active".to_string()
169 } else {
170 "UFW inactive".to_string()
171 },
172 });
173 }
174 }
175
176 let mut cmd = tokio::process::Command::new("iptables");
178 cmd.args(["-L", "-n", "--line-numbers"]);
179 if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
180 let text = String::from_utf8_lossy(&output.stdout);
181 let rule_count = text
182 .lines()
183 .filter(|l| l.starts_with(char::is_numeric))
184 .count();
185 return Some(FirewallInfo {
186 enabled: rule_count > 0,
187 profiles: vec![FirewallProfile {
188 name: "iptables".to_string(),
189 enabled: rule_count > 0,
190 default_inbound: None,
191 default_outbound: None,
192 }],
193 summary: format!("iptables: {} rules", rule_count),
194 });
195 }
196
197 Some(FirewallInfo {
198 enabled: false,
199 profiles: vec![],
200 summary: "No firewall detected".to_string(),
201 })
202}