Skip to main content

nd_300/diagnostics/
proxy.rs

1use serde::Serialize;
2
3#[derive(Debug, Clone, Serialize)]
4pub struct ProxyConfig {
5    pub http_proxy: Option<String>,
6    pub https_proxy: Option<String>,
7    pub socks_proxy: Option<String>,
8    pub no_proxy: Option<String>,
9    pub pac_url: Option<String>,
10    pub proxy_enabled: bool,
11    /// The configured PAC script could actually be fetched (additive, v3.4.0+).
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub pac_reachable: Option<bool>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub pac_size_bytes: Option<u64>,
16    /// A bare `wpad` hostname resolves on this network — proxy auto-discovery
17    /// is live, which is also a well-known LAN attack vector worth surfacing.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub wpad_dns_detected: Option<bool>,
20}
21
22pub async fn collect() -> Option<ProxyConfig> {
23    // Check environment variables first
24    let http_proxy = std::env::var("HTTP_PROXY")
25        .or_else(|_| std::env::var("http_proxy"))
26        .ok();
27    let https_proxy = std::env::var("HTTPS_PROXY")
28        .or_else(|_| std::env::var("https_proxy"))
29        .ok();
30    let socks_proxy = std::env::var("ALL_PROXY")
31        .or_else(|_| std::env::var("all_proxy"))
32        .ok();
33    let no_proxy = std::env::var("NO_PROXY")
34        .or_else(|_| std::env::var("no_proxy"))
35        .ok();
36
37    let mut config = ProxyConfig {
38        http_proxy,
39        https_proxy,
40        socks_proxy,
41        no_proxy,
42        pac_url: None,
43        proxy_enabled: false,
44        pac_reachable: None,
45        pac_size_bytes: None,
46        wpad_dns_detected: None,
47    };
48
49    // Check OS-specific proxy settings
50    #[cfg(windows)]
51    {
52        check_windows_proxy(&mut config).await;
53    }
54
55    #[cfg(target_os = "macos")]
56    {
57        check_macos_proxy(&mut config).await;
58    }
59
60    // Linux desktop fallback: GNOME proxy settings (env vars are empty for
61    // GUI-configured proxies).
62    #[cfg(target_os = "linux")]
63    {
64        if config.http_proxy.is_none() && config.https_proxy.is_none() {
65            check_gnome_proxy(&mut config).await;
66        }
67    }
68
69    config.proxy_enabled =
70        config.http_proxy.is_some() || config.https_proxy.is_some() || config.socks_proxy.is_some();
71
72    // PAC validation (v3.4.0+): a configured-but-dead PAC script silently
73    // breaks browsing in ways env-var checks can't see.
74    if let Some(ref pac_url) = config.pac_url {
75        if let Ok(client) = reqwest::Client::builder()
76            .timeout(std::time::Duration::from_secs(5))
77            .no_proxy()
78            .build()
79        {
80            match client.get(pac_url).send().await {
81                Ok(resp) if resp.status().is_success() => {
82                    config.pac_reachable = Some(true);
83                    config.pac_size_bytes = resp.bytes().await.ok().map(|b| b.len() as u64);
84                }
85                _ => config.pac_reachable = Some(false),
86            }
87        }
88    }
89
90    // WPAD auto-discovery probe: a resolvable bare `wpad` name means clients
91    // here may auto-configure a proxy from the LAN.
92    config.wpad_dns_detected = Some(
93        super::util::lookup_host_timeout("wpad:80".to_string(), super::util::RESOLVE)
94            .await
95            .is_some_and(|a| !a.is_empty()),
96    );
97
98    Some(config)
99}
100
101/// GNOME proxy settings via gsettings (best-effort; absent on servers).
102#[cfg(target_os = "linux")]
103async fn check_gnome_proxy(config: &mut ProxyConfig) {
104    let mut mode_cmd = tokio::process::Command::new("gsettings");
105    mode_cmd.args(["get", "org.gnome.system.proxy", "mode"]);
106    let Some(output) = super::util::run_with_timeout(mode_cmd, super::util::QUICK).await else {
107        return;
108    };
109    let mode = String::from_utf8_lossy(&output.stdout)
110        .trim()
111        .replace('\'', "");
112    match mode.as_str() {
113        "manual" => {
114            let mut host_cmd = tokio::process::Command::new("gsettings");
115            host_cmd.args(["get", "org.gnome.system.proxy.http", "host"]);
116            if let Some(host_out) =
117                super::util::run_with_timeout(host_cmd, super::util::QUICK).await
118            {
119                let host = String::from_utf8_lossy(&host_out.stdout)
120                    .trim()
121                    .replace('\'', "");
122                if !host.is_empty() {
123                    config.http_proxy = Some(host);
124                }
125            }
126        }
127        "auto" => {
128            let mut url_cmd = tokio::process::Command::new("gsettings");
129            url_cmd.args(["get", "org.gnome.system.proxy", "autoconfig-url"]);
130            if let Some(url_out) = super::util::run_with_timeout(url_cmd, super::util::QUICK).await
131            {
132                let url = String::from_utf8_lossy(&url_out.stdout)
133                    .trim()
134                    .replace('\'', "");
135                if !url.is_empty() {
136                    config.pac_url = Some(url);
137                }
138            }
139        }
140        _ => {}
141    }
142}
143
144#[cfg(windows)]
145async fn check_windows_proxy(config: &mut ProxyConfig) {
146    // Check Windows registry via reg query
147    let mut cmd = tokio::process::Command::new("reg");
148    cmd.args([
149        "query",
150        r"HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings",
151        "/v",
152        "ProxyEnable",
153    ]);
154    if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
155        let text = String::from_utf8_lossy(&output.stdout);
156        if text.contains("0x1") {
157            config.proxy_enabled = true;
158
159            // Get proxy server
160            let mut cmd = tokio::process::Command::new("reg");
161            cmd.args([
162                "query",
163                r"HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings",
164                "/v",
165                "ProxyServer",
166            ]);
167            if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
168                let text = String::from_utf8_lossy(&output.stdout);
169                for line in text.lines() {
170                    if line.contains("ProxyServer") {
171                        if let Some(val) = line.split_whitespace().last() {
172                            if config.http_proxy.is_none() {
173                                config.http_proxy = Some(val.to_string());
174                            }
175                        }
176                    }
177                }
178            }
179
180            // Get PAC URL
181            let mut cmd = tokio::process::Command::new("reg");
182            cmd.args([
183                "query",
184                r"HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings",
185                "/v",
186                "AutoConfigURL",
187            ]);
188            if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
189                let text = String::from_utf8_lossy(&output.stdout);
190                for line in text.lines() {
191                    if line.contains("AutoConfigURL") {
192                        config.pac_url = line.split_whitespace().last().map(|s| s.to_string());
193                    }
194                }
195            }
196        }
197    }
198}
199
200#[cfg(target_os = "macos")]
201async fn check_macos_proxy(config: &mut ProxyConfig) {
202    let mut cmd = tokio::process::Command::new("scutil");
203    cmd.args(["--proxy"]);
204    if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
205        let text = String::from_utf8_lossy(&output.stdout);
206
207        for line in text.lines() {
208            let line = line.trim();
209            if line.contains("HTTPEnable") && line.contains("1") {
210                config.proxy_enabled = true;
211            }
212            if line.contains("HTTPProxy") {
213                config.http_proxy = line.split_once(':').map(|x| x.1.trim().to_string());
214            }
215            if line.contains("HTTPSProxy") {
216                config.https_proxy = line.split_once(':').map(|x| x.1.trim().to_string());
217            }
218            if line.contains("SOCKSProxy") {
219                config.socks_proxy = line.split_once(':').map(|x| x.1.trim().to_string());
220            }
221            if line.contains("ProxyAutoConfigURLString") {
222                config.pac_url = line.split_once(':').map(|x| x.1.trim().to_string());
223            }
224        }
225    }
226}