//go:build darwin package sysproxy import ( "bufio" "bytes" "fmt" "net" "os/exec" "strings" "sync" ) type darwinController struct { mu sync.Mutex enabled bool host string httpPort string socksPort string services []string } func newPlatform() Controller { return &darwinController{} } func (c *darwinController) Enable(httpHostPort string) error { c.mu.Lock() defer c.mu.Unlock() host, port, err := net.SplitHostPort(httpHostPort) if err != nil { return fmt.Errorf("sysproxy: bad address %q: %w", httpHostPort, err) } if host == "" || host == "0.0.0.0" { host = "127.0.0.1" } socks := "1080" if port == "1081" { socks = "1080" } services, err := listNetworkServices() if err != nil { return err } if len(services) == 0 { return fmt.Errorf("sysproxy: no network services") } // One bash process for all networksetup calls (was 6 execs × N services). // setwebproxy / setsecurewebproxy / setsocksfirewallproxy also turn the proxy on. var b strings.Builder b.WriteString("set -e\n") for _, svc := range services { q := shellQuote(svc) fmt.Fprintf(&b, "networksetup -setwebproxy %s %s %s\n", q, shellQuote(host), shellQuote(port)) fmt.Fprintf(&b, "networksetup -setsecurewebproxy %s %s %s\n", q, shellQuote(host), shellQuote(port)) fmt.Fprintf(&b, "networksetup -setsocksfirewallproxy %s %s %s\n", q, shellQuote(host), shellQuote(socks)) } if err := runBash(b.String()); err != nil { return err } c.enabled = true c.host = host c.httpPort = port c.socksPort = socks c.services = append([]string(nil), services...) return nil } func (c *darwinController) Disable() error { c.mu.Lock() defer c.mu.Unlock() if !c.enabled { return nil } return c.disableServices(c.services) } func (c *darwinController) ForceDisable() error { c.mu.Lock() defer c.mu.Unlock() svcs := c.services return c.disableServices(svcs) } func (c *darwinController) disableServices(services []string) error { if len(services) == 0 { var err error services, err = listNetworkServices() if err != nil { return err } } if len(services) == 0 { c.enabled = false c.services = nil return nil } var b strings.Builder b.WriteString("set +e\n") // best-effort: turn off what we can for _, svc := range services { q := shellQuote(svc) fmt.Fprintf(&b, "networksetup -setwebproxystate %s off\n", q) fmt.Fprintf(&b, "networksetup -setsecurewebproxystate %s off\n", q) fmt.Fprintf(&b, "networksetup -setsocksfirewallproxystate %s off\n", q) } err := runBash(b.String()) c.enabled = false c.services = nil return err } func (c *darwinController) Enabled() bool { c.mu.Lock() defer c.mu.Unlock() return c.enabled } func listNetworkServices() ([]string, error) { out, err := exec.Command("networksetup", "-listallnetworkservices").Output() if err != nil { return nil, fmt.Errorf("networksetup: %w", err) } var services []string sc := bufio.NewScanner(bytes.NewReader(out)) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "An asterisk") { continue } if strings.HasPrefix(line, "*") { continue // disabled service } services = append(services, line) } return services, sc.Err() } func runBash(script string) error { cmd := exec.Command("/bin/bash", "-c", script) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("networksetup batch: %w (%s)", err, strings.TrimSpace(string(out))) } return nil } func shellQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" }