185 lines
4.9 KiB
Go
185 lines
4.9 KiB
Go
//go:build darwin
|
|
|
|
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// dnsManager manages macOS DNS settings via networksetup(8). Direct port of
|
|
// MacOSDNSManager in macos_service_wrapper.py.
|
|
type dnsManager struct{}
|
|
|
|
func (dnsManager) getNetworkServices() []string {
|
|
out, err := exec.Command("networksetup", "-listallnetworkservices").CombinedOutput()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to list network services: %v\n", err)
|
|
return nil
|
|
}
|
|
lines := strings.Split(string(out), "\n")
|
|
var services []string
|
|
for _, line := range lines[1:] { // skip header
|
|
line = strings.TrimSpace(line)
|
|
if line != "" && !strings.HasPrefix(line, "*") {
|
|
services = append(services, line)
|
|
}
|
|
}
|
|
return services
|
|
}
|
|
|
|
func (dnsManager) getCurrentDNS(service string) []string {
|
|
out, err := exec.Command("networksetup", "-getdnsservers", service).CombinedOutput()
|
|
if err != nil {
|
|
return []string{"Empty"}
|
|
}
|
|
var servers []string
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" && !strings.Contains(line, " ") {
|
|
servers = append(servers, line)
|
|
}
|
|
}
|
|
return servers
|
|
}
|
|
|
|
func (dnsManager) setDNS(service string, servers []string) bool {
|
|
var err error
|
|
if len(servers) == 0 {
|
|
_, err = exec.Command("networksetup", "-setdnsservers", service, "empty").CombinedOutput()
|
|
} else {
|
|
args := append([]string{"-setdnsservers", service}, servers...)
|
|
_, err = exec.Command("networksetup", args...).CombinedOutput()
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to set DNS for %s: %v\n", service, err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// mihomoWrapper wraps the mihomo process on macOS, managing DNS settings
|
|
// around its lifecycle. Direct port of MihomoServiceWrapper in
|
|
// macos_service_wrapper.py.
|
|
type mihomoWrapper struct {
|
|
executable string
|
|
args []string
|
|
configDir string
|
|
process *exec.Cmd
|
|
dns dnsManager
|
|
originalDNS map[string][]string
|
|
shuttingDown bool
|
|
}
|
|
|
|
func (w *mihomoWrapper) start() bool {
|
|
cmd := exec.Command(w.executable, w.args...)
|
|
cmd.Dir = w.configDir
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to start service: %v\n", err)
|
|
return false
|
|
}
|
|
w.process = cmd
|
|
fmt.Fprintf(os.Stderr, "Started mihomo service (PID: %d)\n", cmd.Process.Pid)
|
|
|
|
w.originalDNS = make(map[string][]string)
|
|
dnsServer := "223.6.6.6"
|
|
for _, svc := range w.dns.getNetworkServices() {
|
|
original := w.dns.getCurrentDNS(svc)
|
|
w.originalDNS[svc] = original
|
|
|
|
if w.dns.setDNS(svc, []string{dnsServer}) {
|
|
fmt.Fprintf(os.Stderr, "Set DNS for %s: %s\n", svc, dnsServer)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "Warning: Failed to set DNS for %s\n", svc)
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (w *mihomoWrapper) stop() bool {
|
|
for svc, servers := range w.originalDNS {
|
|
if w.dns.setDNS(svc, servers) {
|
|
if len(servers) > 0 && servers[0] != "Empty" {
|
|
fmt.Fprintf(os.Stderr, "Restored DNS for %s: %s\n", svc, strings.Join(servers, ", "))
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "Reset DNS for %s to automatic\n", svc)
|
|
}
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "Warning: Failed to restore DNS for %s\n", svc)
|
|
}
|
|
}
|
|
|
|
if w.process != nil && w.process.Process != nil {
|
|
w.process.Process.Signal(syscall.SIGTERM)
|
|
done := make(chan error, 1)
|
|
go func() { done <- w.process.Wait() }()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
w.process.Process.Kill()
|
|
<-done
|
|
}
|
|
fmt.Fprintf(os.Stderr, "Stopped mihomo service (PID: %d)\n", w.process.Process.Pid)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (w *mihomoWrapper) run() {
|
|
w.shuttingDown = false
|
|
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
|
|
|
|
if !w.start() {
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Fprintln(os.Stderr, "Service started, press Ctrl+C to stop")
|
|
|
|
waitCh := make(chan error, 1)
|
|
go func() { waitCh <- w.process.Wait() }()
|
|
|
|
select {
|
|
case <-waitCh:
|
|
fmt.Fprintf(os.Stderr, "Process exited with code: %d\n", w.process.ProcessState.ExitCode())
|
|
w.stop()
|
|
case sig := <-sigCh:
|
|
fmt.Fprintf(os.Stderr, "Received signal %v, shutting down...\n", sig)
|
|
w.shuttingDown = true
|
|
w.stop()
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
// RunMacOSWrapper runs the mihomo process with DNS management, blocking
|
|
// until it exits or a termination signal is received. Invoked by the
|
|
// hidden `ssm service run-macos <executable> <configDir> [args...]` command
|
|
// that the generated launchd plist points at (see service_darwin.go),
|
|
// replacing the standalone macos_service_wrapper.py script.
|
|
func RunMacOSWrapper(executable, configDir string, args []string) error {
|
|
if _, err := os.Stat(executable); err != nil {
|
|
return fmt.Errorf("executable not found: %s", executable)
|
|
}
|
|
if _, err := os.Stat(configDir); err != nil {
|
|
return fmt.Errorf("config directory not found: %s", configDir)
|
|
}
|
|
|
|
w := &mihomoWrapper{
|
|
executable: executable,
|
|
args: args,
|
|
configDir: configDir,
|
|
}
|
|
w.run()
|
|
return nil
|
|
}
|