Skip to main content

eggress_system_proxy/backends/
macos.rs

1use crate::apply::Command;
2use crate::command_runner::CommandRunner;
3use crate::inspection::SystemProxySettings;
4
5/// List macOS network services via `networksetup -listallnetworkservices`.
6pub fn list_network_services(runner: &dyn CommandRunner) -> Result<Vec<String>, String> {
7    let output = runner
8        .run("networksetup", &["-listallnetworkservices"])
9        .map_err(|e| format!("failed to run networksetup: {e}"))?;
10
11    if !output.status.success() {
12        return Err(String::from_utf8_lossy(&output.stderr).to_string());
13    }
14
15    let stdout = String::from_utf8_lossy(&output.stdout);
16    Ok(stdout
17        .lines()
18        .skip(1) // skip header
19        .map(|s| s.trim().to_string())
20        .filter(|s| !s.is_empty())
21        .collect())
22}
23
24/// Inspect proxy settings for a macOS network service.
25pub fn inspect_macos_proxy(
26    runner: &dyn CommandRunner,
27    service: &str,
28) -> Result<SystemProxySettings, String> {
29    let mut raw = std::collections::HashMap::new();
30
31    let http_proxy = get_macos_proxy_field(runner, service, "-getwebproxy", "Server")?;
32    let https_proxy = get_macos_proxy_field(runner, service, "-getsecurewebproxy", "Server")?;
33    let socks_proxy = get_macos_proxy_field(runner, service, "-getsocksfirewallproxy", "Server")?;
34    let no_proxy = get_macos_proxy_field(runner, service, "-getwebproxy", "BypassDomains")?;
35
36    if let Some(ref v) = http_proxy {
37        raw.insert("http_proxy".to_string(), v.clone());
38    }
39    if let Some(ref v) = https_proxy {
40        raw.insert("https_proxy".to_string(), v.clone());
41    }
42    if let Some(ref v) = socks_proxy {
43        raw.insert("socks_proxy".to_string(), v.clone());
44    }
45    if let Some(ref v) = no_proxy {
46        raw.insert("no_proxy".to_string(), v.clone());
47    }
48
49    Ok(SystemProxySettings {
50        source: format!("macos:networksetup:{service}"),
51        http_proxy,
52        https_proxy,
53        socks_proxy,
54        no_proxy,
55        raw,
56    })
57}
58
59fn get_macos_proxy_field(
60    runner: &dyn CommandRunner,
61    service: &str,
62    flag: &str,
63    field: &str,
64) -> Result<Option<String>, String> {
65    let output = runner
66        .run("networksetup", &[flag, service])
67        .map_err(|e| format!("failed to run networksetup {flag}: {e}"))?;
68
69    if !output.status.success() {
70        return Ok(None);
71    }
72
73    let stdout = String::from_utf8_lossy(&output.stdout);
74    for line in stdout.lines() {
75        if let Some(pos) = line.find(':') {
76            let key = line[..pos].trim();
77            let value = line[pos + 1..].trim();
78            if key == field && !value.is_empty() && value != "Off" {
79                return Ok(Some(value.to_string()));
80            }
81        }
82    }
83    Ok(None)
84}
85
86/// Generate `networksetup` commands to apply proxy settings (dry-run).
87pub fn generate_macos_apply_commands(
88    service: &str,
89    http_proxy: Option<&str>,
90    https_proxy: Option<&str>,
91    socks_proxy: Option<&str>,
92    no_proxy: Option<&str>,
93) -> Vec<Command> {
94    let mut commands = Vec::new();
95    if let Some(http) = http_proxy {
96        commands.push(Command::new(
97            "networksetup",
98            vec!["-setwebproxy".into(), service.into(), "on".into()],
99        ));
100        commands.push(Command::new(
101            "networksetup",
102            vec!["-setwebproxyservers".into(), service.into(), http.into()],
103        ));
104    }
105    if let Some(https) = https_proxy {
106        commands.push(Command::new(
107            "networksetup",
108            vec!["-setsecurewebproxy".into(), service.into(), "on".into()],
109        ));
110        commands.push(Command::new(
111            "networksetup",
112            vec![
113                "-setsecurewebproxyservers".into(),
114                service.into(),
115                https.into(),
116            ],
117        ));
118    }
119    if let Some(socks) = socks_proxy {
120        commands.push(Command::new(
121            "networksetup",
122            vec!["-setsocksfirewallproxy".into(), service.into(), "on".into()],
123        ));
124        commands.push(Command::new(
125            "networksetup",
126            vec![
127                "-setsocksfirewallproxyserver".into(),
128                service.into(),
129                socks.into(),
130            ],
131        ));
132    }
133    if let Some(no_proxy) = no_proxy {
134        commands.push(Command::new(
135            "networksetup",
136            vec![
137                "-setwebproxybypassdomains".into(),
138                service.into(),
139                no_proxy.into(),
140            ],
141        ));
142    }
143    commands
144}
145
146/// Generate `networksetup` commands to disable proxy settings (dry-run).
147pub fn generate_macos_disable_commands(service: &str) -> Vec<Command> {
148    vec![
149        Command::new(
150            "networksetup",
151            vec!["-setwebproxy".into(), service.into(), "off".into()],
152        ),
153        Command::new(
154            "networksetup",
155            vec!["-setsecurewebproxy".into(), service.into(), "off".into()],
156        ),
157        Command::new(
158            "networksetup",
159            vec![
160                "-setsocksfirewallproxy".into(),
161                service.into(),
162                "off".into(),
163            ],
164        ),
165    ]
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::command_runner::MockCommandRunner;
172
173    fn success_output(stdout: &str) -> std::process::Output {
174        #[cfg(unix)]
175        {
176            use std::os::unix::process::ExitStatusExt;
177            std::process::Output {
178                status: std::process::ExitStatus::from_raw(0),
179                stdout: stdout.as_bytes().to_vec(),
180                stderr: Vec::new(),
181            }
182        }
183        #[cfg(not(unix))]
184        {
185            std::process::Output {
186                status: std::process::ExitStatus::default(),
187                stdout: stdout.as_bytes().to_vec(),
188                stderr: Vec::new(),
189            }
190        }
191    }
192
193    #[test]
194    fn list_services_parses_output() {
195        let runner = MockCommandRunner::new().add_response(
196            "networksetup",
197            vec!["-listallnetworkservices".to_string()],
198            Ok(success_output("An asterisk (*) denotes that a network service is disabled.\n*Ethernet\n*Wi-Fi\nUSB 10/100/1000 LAN\n")),
199        );
200
201        let services = list_network_services(&runner).unwrap();
202        assert_eq!(services.len(), 3);
203        assert!(services.contains(&"*Wi-Fi".to_string()));
204    }
205
206    #[test]
207    fn inspect_proxy_parses_web_proxy() {
208        let runner = MockCommandRunner::new()
209            .add_response(
210                "networksetup",
211                vec!["-getwebproxy".to_string(), "*Wi-Fi".to_string()],
212                Ok(success_output(
213                    "Enabled: Yes\nServer: proxy.example.com:8080\nBypassDomains: localhost\n",
214                )),
215            )
216            .add_response(
217                "networksetup",
218                vec!["-getsecurewebproxy".to_string(), "*Wi-Fi".to_string()],
219                Ok(success_output(
220                    "Enabled: Yes\nServer: proxy.example.com:8443\nBypassDomains: localhost\n",
221                )),
222            )
223            .add_response(
224                "networksetup",
225                vec!["-getsocksfirewallproxy".to_string(), "*Wi-Fi".to_string()],
226                Ok(success_output("Enabled: No\nServer: \nPort: 0\n")),
227            );
228
229        let settings = inspect_macos_proxy(&runner, "*Wi-Fi").unwrap();
230        assert_eq!(
231            settings.http_proxy.as_deref(),
232            Some("proxy.example.com:8080")
233        );
234        assert_eq!(
235            settings.https_proxy.as_deref(),
236            Some("proxy.example.com:8443")
237        );
238        assert_eq!(settings.socks_proxy, None);
239        assert!(settings.source.contains("*Wi-Fi"));
240    }
241
242    #[test]
243    fn generate_apply_commands() {
244        let commands = generate_macos_apply_commands(
245            "*Wi-Fi",
246            Some("proxy:8080"),
247            Some("proxy:8443"),
248            Some("socks:1080"),
249            Some("localhost,127.0.0.1"),
250        );
251        assert!(commands
252            .iter()
253            .any(|c| c.to_string().contains("-setwebproxy")));
254        assert!(commands
255            .iter()
256            .any(|c| c.to_string().contains("-setsecurewebproxy")));
257        assert!(commands
258            .iter()
259            .any(|c| c.to_string().contains("-setsocksfirewallproxy")));
260        assert!(commands
261            .iter()
262            .any(|c| c.to_string().contains("-setwebproxybypassdomains")));
263    }
264
265    #[test]
266    fn generate_disable_commands() {
267        let commands = generate_macos_disable_commands("*Wi-Fi");
268        assert_eq!(commands.len(), 3);
269        assert!(commands.iter().all(|c| c.to_string().contains("off")));
270    }
271}