Skip to main content

nd_300/diagnostics/
firewall.rs

1use 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    // Get policy info
69    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    // Try ufw first
153    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    // Try iptables
177    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}