469 lines
11 KiB
Go
469 lines
11 KiB
Go
//go:build darwin
|
|
// +build darwin
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type darwinProxyEndpoint struct {
|
|
enabled bool
|
|
server string
|
|
port string
|
|
}
|
|
|
|
type darwinProxySettings struct {
|
|
web darwinProxyEndpoint
|
|
secure darwinProxyEndpoint
|
|
}
|
|
|
|
var darwinProxyState = struct {
|
|
sync.Mutex
|
|
captured bool
|
|
previous map[string]darwinProxySettings
|
|
applied map[string]struct{}
|
|
}{}
|
|
|
|
func enabledNetworkServices() ([]string, error) {
|
|
out, err := exec.Command("networksetup", "-listallnetworkservices").CombinedOutput()
|
|
if err != nil {
|
|
return nil, formatCommandError("list network services", err, out)
|
|
}
|
|
|
|
var services []string
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "An asterisk") || strings.HasPrefix(line, "*") {
|
|
continue
|
|
}
|
|
services = append(services, line)
|
|
}
|
|
|
|
if len(services) == 0 {
|
|
return nil, fmt.Errorf("no enabled network services found")
|
|
}
|
|
|
|
return services, nil
|
|
}
|
|
|
|
func proxyNetworkServices() ([]string, error) {
|
|
service, defaultErr := defaultNetworkService()
|
|
if defaultErr == nil && service != "" {
|
|
return []string{service}, nil
|
|
}
|
|
|
|
services, activeErr := activeNetworkServices()
|
|
if activeErr == nil && len(services) > 0 {
|
|
return services, nil
|
|
}
|
|
|
|
if defaultErr != nil || activeErr != nil {
|
|
return nil, errors.Join(defaultErr, activeErr)
|
|
}
|
|
return nil, fmt.Errorf("no active network services found")
|
|
}
|
|
|
|
func defaultNetworkService() (string, error) {
|
|
device, err := defaultNetworkDevice()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
servicesByDevice, err := networkServicesByDevice()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
service := servicesByDevice[device]
|
|
if service == "" {
|
|
return "", fmt.Errorf("network service for default device %s not found", device)
|
|
}
|
|
return service, nil
|
|
}
|
|
|
|
func defaultNetworkDevice() (string, error) {
|
|
out, err := exec.Command("route", "-n", "get", "default").CombinedOutput()
|
|
if err != nil {
|
|
return "", formatCommandError("get default network route", err, out)
|
|
}
|
|
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
key, value, ok := strings.Cut(line, ":")
|
|
if ok && strings.TrimSpace(key) == "interface" {
|
|
device := strings.TrimSpace(value)
|
|
if device != "" {
|
|
return device, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("default network interface not found")
|
|
}
|
|
|
|
func networkServicesByDevice() (map[string]string, error) {
|
|
out, err := exec.Command("networksetup", "-listnetworkserviceorder").CombinedOutput()
|
|
if err != nil {
|
|
return nil, formatCommandError("list network service order", err, out)
|
|
}
|
|
|
|
services := make(map[string]string)
|
|
currentService := ""
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if service, ok := parseNetworkServiceOrderName(line); ok {
|
|
currentService = service
|
|
continue
|
|
}
|
|
if currentService == "" {
|
|
continue
|
|
}
|
|
if device, ok := parseNetworkServiceOrderDevice(line); ok {
|
|
services[device] = currentService
|
|
currentService = ""
|
|
}
|
|
}
|
|
|
|
if len(services) == 0 {
|
|
return nil, fmt.Errorf("no network service devices found")
|
|
}
|
|
return services, nil
|
|
}
|
|
|
|
func parseNetworkServiceOrderName(line string) (string, bool) {
|
|
if !strings.HasPrefix(line, "(") {
|
|
return "", false
|
|
}
|
|
|
|
end := strings.Index(line, ")")
|
|
if end <= 1 {
|
|
return "", false
|
|
}
|
|
|
|
if _, err := strconv.Atoi(line[1:end]); err != nil {
|
|
return "", false
|
|
}
|
|
|
|
service := strings.TrimSpace(line[end+1:])
|
|
return service, service != ""
|
|
}
|
|
|
|
func parseNetworkServiceOrderDevice(line string) (string, bool) {
|
|
_, value, ok := strings.Cut(line, "Device:")
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
|
|
value = strings.TrimSpace(value)
|
|
if idx := strings.IndexAny(value, ",)"); idx != -1 {
|
|
value = value[:idx]
|
|
}
|
|
|
|
device := strings.TrimSpace(value)
|
|
return device, device != ""
|
|
}
|
|
|
|
func activeNetworkServices() ([]string, error) {
|
|
services, err := enabledNetworkServices()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
active := make([]string, 0, len(services))
|
|
for _, service := range services {
|
|
ok, err := isNetworkServiceActive(service)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if ok {
|
|
active = append(active, service)
|
|
}
|
|
}
|
|
|
|
if len(active) == 0 {
|
|
return nil, fmt.Errorf("no active network services found")
|
|
}
|
|
return active, nil
|
|
}
|
|
|
|
func isNetworkServiceActive(service string) (bool, error) {
|
|
out, err := exec.Command("networksetup", "-getinfo", service).CombinedOutput()
|
|
if err != nil {
|
|
return false, formatCommandError("get network service info "+service, err, out)
|
|
}
|
|
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
key, value, ok := strings.Cut(line, ":")
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
key = strings.TrimSpace(key)
|
|
if key != "IP address" && key != "IPv6 IP address" {
|
|
continue
|
|
}
|
|
|
|
value = strings.TrimSpace(value)
|
|
if value != "" && !strings.EqualFold(value, "none") {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func getProxySettings(service string) (darwinProxySettings, error) {
|
|
web, webErr := getProxyEndpoint("-getwebproxy", service)
|
|
secure, secureErr := getProxyEndpoint("-getsecurewebproxy", service)
|
|
return darwinProxySettings{
|
|
web: web,
|
|
secure: secure,
|
|
}, errors.Join(webErr, secureErr)
|
|
}
|
|
|
|
func getProxyEndpoint(flag string, service string) (darwinProxyEndpoint, error) {
|
|
out, err := exec.Command("networksetup", flag, service).CombinedOutput()
|
|
if err != nil {
|
|
return darwinProxyEndpoint{}, formatCommandError(flag+" "+service, err, out)
|
|
}
|
|
|
|
values := make(map[string]string)
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
key, value, ok := strings.Cut(line, ":")
|
|
if ok {
|
|
values[strings.TrimSpace(key)] = strings.TrimSpace(value)
|
|
}
|
|
}
|
|
|
|
return darwinProxyEndpoint{
|
|
enabled: values["Enabled"] == "Yes",
|
|
server: values["Server"],
|
|
port: values["Port"],
|
|
}, nil
|
|
}
|
|
|
|
func setProxyForServices(services []string, host string, port string) error {
|
|
var errs []error
|
|
for _, service := range services {
|
|
errs = append(errs,
|
|
runNetworkSetup("-setwebproxy", service, host, port),
|
|
runNetworkSetup("-setsecurewebproxy", service, host, port),
|
|
runNetworkSetup("-setwebproxystate", service, "on"),
|
|
runNetworkSetup("-setsecurewebproxystate", service, "on"),
|
|
)
|
|
}
|
|
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
func restoreProxySettings(settings map[string]darwinProxySettings) error {
|
|
var errs []error
|
|
for service, setting := range settings {
|
|
if setting.web.server != "" && setting.web.port != "" {
|
|
errs = append(errs, runNetworkSetup("-setwebproxy", service, setting.web.server, setting.web.port))
|
|
}
|
|
errs = append(errs, runNetworkSetup("-setwebproxystate", service, proxyState(setting.web.enabled)))
|
|
|
|
if setting.secure.server != "" && setting.secure.port != "" {
|
|
errs = append(errs, runNetworkSetup("-setsecurewebproxy", service, setting.secure.server, setting.secure.port))
|
|
}
|
|
errs = append(errs, runNetworkSetup("-setsecurewebproxystate", service, proxyState(setting.secure.enabled)))
|
|
}
|
|
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
func disableProxyForServices(services []string) error {
|
|
var errs []error
|
|
for _, service := range services {
|
|
errs = append(errs,
|
|
runNetworkSetup("-setwebproxystate", service, "off"),
|
|
runNetworkSetup("-setsecurewebproxystate", service, "off"),
|
|
)
|
|
}
|
|
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
func proxyState(enabled bool) string {
|
|
if enabled {
|
|
return "on"
|
|
}
|
|
return "off"
|
|
}
|
|
|
|
func runNetworkSetup(args ...string) error {
|
|
out, err := exec.Command("networksetup", args...).CombinedOutput()
|
|
if err != nil {
|
|
return formatCommandError("networksetup "+strings.Join(args, " "), err, out)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func formatCommandError(action string, err error, out []byte) error {
|
|
msg := strings.TrimSpace(string(out))
|
|
if msg == "" {
|
|
return fmt.Errorf("%s: %w", action, err)
|
|
}
|
|
return fmt.Errorf("%s: %w: %s", action, err, msg)
|
|
}
|
|
|
|
func captureProxySettings(services []string) (map[string]darwinProxySettings, error) {
|
|
settings := make(map[string]darwinProxySettings, len(services))
|
|
var errs []error
|
|
|
|
for _, service := range services {
|
|
proxySettings, err := getProxySettings(service)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
settings[service] = proxySettings
|
|
}
|
|
|
|
if err := errors.Join(errs...); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return settings, nil
|
|
}
|
|
|
|
func captureMissingProxySettings(services []string) error {
|
|
missing := make([]string, 0, len(services))
|
|
for _, service := range services {
|
|
if _, ok := darwinProxyState.previous[service]; !ok {
|
|
missing = append(missing, service)
|
|
}
|
|
}
|
|
|
|
if len(missing) == 0 {
|
|
return nil
|
|
}
|
|
|
|
settings, err := captureProxySettings(missing)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for service, setting := range settings {
|
|
darwinProxyState.previous[service] = setting
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func serviceSet(services []string) map[string]struct{} {
|
|
set := make(map[string]struct{}, len(services))
|
|
for _, service := range services {
|
|
set[service] = struct{}{}
|
|
}
|
|
return set
|
|
}
|
|
|
|
func appliedServicesNotIn(selected map[string]struct{}) []string {
|
|
services := make([]string, 0, len(darwinProxyState.applied))
|
|
for service := range darwinProxyState.applied {
|
|
if _, ok := selected[service]; !ok {
|
|
services = append(services, service)
|
|
}
|
|
}
|
|
return services
|
|
}
|
|
|
|
func appliedServices() []string {
|
|
services := make([]string, 0, len(darwinProxyState.applied))
|
|
for service := range darwinProxyState.applied {
|
|
services = append(services, service)
|
|
}
|
|
return services
|
|
}
|
|
|
|
func proxySettingsForServices(settings map[string]darwinProxySettings, services []string) map[string]darwinProxySettings {
|
|
filtered := make(map[string]darwinProxySettings, len(services))
|
|
for _, service := range services {
|
|
if setting, ok := settings[service]; ok {
|
|
filtered[service] = setting
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func resetDarwinProxyState() {
|
|
darwinProxyState.previous = nil
|
|
darwinProxyState.applied = nil
|
|
darwinProxyState.captured = false
|
|
}
|
|
|
|
func restoreAppliedProxySettings() error {
|
|
if len(darwinProxyState.applied) == 0 {
|
|
return nil
|
|
}
|
|
|
|
err := restoreProxySettings(proxySettingsForServices(
|
|
darwinProxyState.previous,
|
|
appliedServices(),
|
|
))
|
|
if err == nil {
|
|
darwinProxyState.applied = nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func setProxy(enable bool, host string, port string) error {
|
|
darwinProxyState.Lock()
|
|
defer darwinProxyState.Unlock()
|
|
|
|
if enable {
|
|
if host == "" || port == "" {
|
|
return fmt.Errorf("host and port are required to enable proxy")
|
|
}
|
|
|
|
services, err := proxyNetworkServices()
|
|
if err != nil {
|
|
return errors.Join(err, restoreAppliedProxySettings())
|
|
}
|
|
|
|
if darwinProxyState.previous == nil {
|
|
darwinProxyState.previous = make(map[string]darwinProxySettings)
|
|
}
|
|
if err := captureMissingProxySettings(services); err != nil {
|
|
return err
|
|
}
|
|
darwinProxyState.captured = true
|
|
|
|
if err := setProxyForServices(services, host, port); err != nil {
|
|
restoreErr := restoreProxySettings(darwinProxyState.previous)
|
|
resetDarwinProxyState()
|
|
return errors.Join(err, restoreErr)
|
|
}
|
|
|
|
selected := serviceSet(services)
|
|
removed := appliedServicesNotIn(selected)
|
|
restoreErr := restoreProxySettings(proxySettingsForServices(darwinProxyState.previous, removed))
|
|
if restoreErr != nil {
|
|
for _, service := range removed {
|
|
selected[service] = struct{}{}
|
|
}
|
|
}
|
|
darwinProxyState.applied = selected
|
|
return restoreErr
|
|
}
|
|
|
|
if darwinProxyState.captured {
|
|
err := restoreProxySettings(darwinProxyState.previous)
|
|
if err == nil {
|
|
resetDarwinProxyState()
|
|
}
|
|
return err
|
|
}
|
|
|
|
services, err := enabledNetworkServices()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return disableProxyForServices(services)
|
|
}
|