Compare commits
6 Commits
562f9bb65e
...
mathieu/ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 473f1620d5 | |||
| 58626c64e5 | |||
| f05b4a6b4c | |||
| 0e3dc23639 | |||
| 20ee23c1c3 | |||
| 796c22f736 |
@@ -4,12 +4,10 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"gitea.app.monadical.io/monadical/greywall/internal/daemon"
|
"gitea.app.monadical.io/monadical/greywall/internal/daemon"
|
||||||
@@ -22,6 +20,9 @@ import (
|
|||||||
// install - Install the LaunchDaemon (requires root)
|
// install - Install the LaunchDaemon (requires root)
|
||||||
// uninstall - Uninstall the LaunchDaemon (requires root)
|
// uninstall - Uninstall the LaunchDaemon (requires root)
|
||||||
// run - Run the daemon (called by LaunchDaemon plist)
|
// run - Run the daemon (called by LaunchDaemon plist)
|
||||||
|
// start - Start the daemon service
|
||||||
|
// stop - Stop the daemon service
|
||||||
|
// restart - Restart the daemon service
|
||||||
// status - Show daemon status
|
// status - Show daemon status
|
||||||
func newDaemonCmd() *cobra.Command {
|
func newDaemonCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -35,6 +36,9 @@ and pf rules that enable transparent proxy routing for sandboxed processes.
|
|||||||
Commands:
|
Commands:
|
||||||
sudo greywall daemon install Install and start the daemon
|
sudo greywall daemon install Install and start the daemon
|
||||||
sudo greywall daemon uninstall Stop and remove the daemon
|
sudo greywall daemon uninstall Stop and remove the daemon
|
||||||
|
sudo greywall daemon start Start the daemon service
|
||||||
|
sudo greywall daemon stop Stop the daemon service
|
||||||
|
sudo greywall daemon restart Restart the daemon service
|
||||||
greywall daemon status Check daemon status
|
greywall daemon status Check daemon status
|
||||||
greywall daemon run Run the daemon (used by LaunchDaemon)`,
|
greywall daemon run Run the daemon (used by LaunchDaemon)`,
|
||||||
}
|
}
|
||||||
@@ -43,6 +47,9 @@ Commands:
|
|||||||
newDaemonInstallCmd(),
|
newDaemonInstallCmd(),
|
||||||
newDaemonUninstallCmd(),
|
newDaemonUninstallCmd(),
|
||||||
newDaemonRunCmd(),
|
newDaemonRunCmd(),
|
||||||
|
newDaemonStartCmd(),
|
||||||
|
newDaemonStopCmd(),
|
||||||
|
newDaemonRestartCmd(),
|
||||||
newDaemonStatusCmd(),
|
newDaemonStatusCmd(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -151,6 +158,57 @@ func newDaemonRunCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newDaemonStartCmd creates the "daemon start" subcommand.
|
||||||
|
func newDaemonStartCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "start",
|
||||||
|
Short: "Start the daemon service",
|
||||||
|
Long: `Start the greywall daemon service. Requires root privileges.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return daemonControl("start")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDaemonStopCmd creates the "daemon stop" subcommand.
|
||||||
|
func newDaemonStopCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "stop",
|
||||||
|
Short: "Stop the daemon service",
|
||||||
|
Long: `Stop the greywall daemon service. Requires root privileges.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return daemonControl("stop")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDaemonRestartCmd creates the "daemon restart" subcommand.
|
||||||
|
func newDaemonRestartCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "restart",
|
||||||
|
Short: "Restart the daemon service",
|
||||||
|
Long: `Restart the greywall daemon service. Requires root privileges.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return daemonControl("restart")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// daemonControl sends a control action (start/stop/restart) to the daemon
|
||||||
|
// service via kardianos/service.
|
||||||
|
func daemonControl(action string) error {
|
||||||
|
p := daemon.NewProgram(daemon.DefaultSocketPath, daemon.DefaultTun2socksPath(), debug)
|
||||||
|
s, err := service.New(p, daemon.NewServiceConfig())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create service: %w", err)
|
||||||
|
}
|
||||||
|
if err := service.Control(s, action); err != nil {
|
||||||
|
return fmt.Errorf("failed to %s daemon: %w", action, err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Daemon %sed successfully.\n", action)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// newDaemonStatusCmd creates the "daemon status" subcommand.
|
// newDaemonStatusCmd creates the "daemon status" subcommand.
|
||||||
func newDaemonStatusCmd() *cobra.Command {
|
func newDaemonStatusCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
@@ -159,11 +217,16 @@ func newDaemonStatusCmd() *cobra.Command {
|
|||||||
Long: `Check whether the greywall daemon is installed and running. Does not require root.`,
|
Long: `Check whether the greywall daemon is installed and running. Does not require root.`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
installed := daemon.IsInstalled()
|
installed := daemon.IsInstalled()
|
||||||
running := daemon.IsRunning()
|
|
||||||
|
// Try kardianos/service status first for reliable state detection.
|
||||||
|
serviceState := daemonServiceState()
|
||||||
|
|
||||||
|
running := serviceState == "running"
|
||||||
|
|
||||||
fmt.Printf("Greywall daemon status:\n")
|
fmt.Printf("Greywall daemon status:\n")
|
||||||
fmt.Printf(" Installed: %s\n", boolStatus(installed))
|
fmt.Printf(" Installed: %s\n", boolStatus(installed))
|
||||||
fmt.Printf(" Running: %s\n", boolStatus(running))
|
fmt.Printf(" Running: %s\n", boolStatus(running))
|
||||||
|
fmt.Printf(" Service: %s\n", serviceState)
|
||||||
fmt.Printf(" Plist: %s\n", daemon.LaunchDaemonPlistPath)
|
fmt.Printf(" Plist: %s\n", daemon.LaunchDaemonPlistPath)
|
||||||
fmt.Printf(" Binary: %s\n", daemon.InstallBinaryPath)
|
fmt.Printf(" Binary: %s\n", daemon.InstallBinaryPath)
|
||||||
fmt.Printf(" User: %s\n", daemon.SandboxUserName)
|
fmt.Printf(" User: %s\n", daemon.SandboxUserName)
|
||||||
@@ -178,7 +241,7 @@ func newDaemonStatusCmd() *cobra.Command {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("The daemon is installed but not running.")
|
fmt.Println("The daemon is installed but not running.")
|
||||||
fmt.Printf("Check logs: cat /var/log/greywall.log\n")
|
fmt.Printf("Check logs: cat /var/log/greywall.log\n")
|
||||||
fmt.Printf("Start it: sudo launchctl load %s\n", daemon.LaunchDaemonPlistPath)
|
fmt.Printf("Start it: sudo greywall daemon start\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -186,38 +249,47 @@ func newDaemonStatusCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runDaemon is the main entry point for the daemon process. It starts the
|
// runDaemon is the main entry point for the daemon process. It uses
|
||||||
// Unix socket server and blocks until a termination signal is received.
|
// kardianos/service to manage the lifecycle, handling signals and
|
||||||
// CLI clients connect to the server to request sessions (which create
|
// calling Start/Stop on the program.
|
||||||
// utun tunnels, DNS relays, and pf rules on demand).
|
|
||||||
func runDaemon(cmd *cobra.Command, args []string) error {
|
func runDaemon(cmd *cobra.Command, args []string) error {
|
||||||
tun2socksPath := filepath.Join(daemon.InstallLibDir, "tun2socks-darwin-"+runtime.GOARCH)
|
p := daemon.NewProgram(daemon.DefaultSocketPath, daemon.DefaultTun2socksPath(), debug)
|
||||||
if _, err := os.Stat(tun2socksPath); err != nil {
|
s, err := service.New(p, daemon.NewServiceConfig())
|
||||||
return fmt.Errorf("tun2socks binary not found at %s (run 'sudo greywall daemon install' first)", tun2socksPath)
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create service: %w", err)
|
||||||
|
}
|
||||||
|
return s.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// daemonServiceState returns the daemon's service state as a string.
|
||||||
|
// It tries kardianos/service status first, then falls back to socket check.
|
||||||
|
func daemonServiceState() string {
|
||||||
|
p := daemon.NewProgram(daemon.DefaultSocketPath, daemon.DefaultTun2socksPath(), debug)
|
||||||
|
s, err := service.New(p, daemon.NewServiceConfig())
|
||||||
|
if err != nil {
|
||||||
|
if daemon.IsRunning() {
|
||||||
|
return "running"
|
||||||
|
}
|
||||||
|
return "stopped"
|
||||||
}
|
}
|
||||||
|
|
||||||
daemon.Logf("Starting daemon (tun2socks=%s, socket=%s)", tun2socksPath, daemon.DefaultSocketPath)
|
status, err := s.Status()
|
||||||
|
if err != nil {
|
||||||
srv := daemon.NewServer(daemon.DefaultSocketPath, tun2socksPath, debug)
|
// Fall back to socket check.
|
||||||
if err := srv.Start(); err != nil {
|
if daemon.IsRunning() {
|
||||||
return fmt.Errorf("failed to start daemon server: %w", err)
|
return "running"
|
||||||
|
}
|
||||||
|
return "stopped"
|
||||||
}
|
}
|
||||||
|
|
||||||
daemon.Logf("Daemon started, listening on %s", daemon.DefaultSocketPath)
|
switch status {
|
||||||
|
case service.StatusRunning:
|
||||||
// Wait for termination signal.
|
return "running"
|
||||||
sigCh := make(chan os.Signal, 1)
|
case service.StatusStopped:
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
return "stopped"
|
||||||
sig := <-sigCh
|
default:
|
||||||
daemon.Logf("Received signal %s, shutting down", sig)
|
return "unknown"
|
||||||
|
|
||||||
if err := srv.Stop(); err != nil {
|
|
||||||
daemon.Logf("Shutdown error: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
daemon.Logf("Daemon stopped")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// boolStatus returns a human-readable string for a boolean status value.
|
// boolStatus returns a human-readable string for a boolean status value.
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ var (
|
|||||||
monitor bool
|
monitor bool
|
||||||
settingsPath string
|
settingsPath string
|
||||||
proxyURL string
|
proxyURL string
|
||||||
|
httpProxyURL string
|
||||||
dnsAddr string
|
dnsAddr string
|
||||||
cmdString string
|
cmdString string
|
||||||
exposePorts []string
|
exposePorts []string
|
||||||
@@ -99,6 +100,7 @@ Configuration file format:
|
|||||||
rootCmd.Flags().BoolVarP(&monitor, "monitor", "m", false, "Monitor and log sandbox violations")
|
rootCmd.Flags().BoolVarP(&monitor, "monitor", "m", false, "Monitor and log sandbox violations")
|
||||||
rootCmd.Flags().StringVarP(&settingsPath, "settings", "s", "", "Path to settings file (default: OS config directory)")
|
rootCmd.Flags().StringVarP(&settingsPath, "settings", "s", "", "Path to settings file (default: OS config directory)")
|
||||||
rootCmd.Flags().StringVar(&proxyURL, "proxy", "", "External SOCKS5 proxy URL (default: socks5://localhost:42052)")
|
rootCmd.Flags().StringVar(&proxyURL, "proxy", "", "External SOCKS5 proxy URL (default: socks5://localhost:42052)")
|
||||||
|
rootCmd.Flags().StringVar(&httpProxyURL, "http-proxy", "", "HTTP CONNECT proxy URL (default: http://localhost:42051)")
|
||||||
rootCmd.Flags().StringVar(&dnsAddr, "dns", "", "DNS server address on host (default: localhost:42053)")
|
rootCmd.Flags().StringVar(&dnsAddr, "dns", "", "DNS server address on host (default: localhost:42053)")
|
||||||
rootCmd.Flags().StringVarP(&cmdString, "c", "c", "", "Run command string directly (like sh -c)")
|
rootCmd.Flags().StringVarP(&cmdString, "c", "c", "", "Run command string directly (like sh -c)")
|
||||||
rootCmd.Flags().StringArrayVarP(&exposePorts, "port", "p", nil, "Expose port for inbound connections (can be used multiple times)")
|
rootCmd.Flags().StringArrayVarP(&exposePorts, "port", "p", nil, "Expose port for inbound connections (can be used multiple times)")
|
||||||
@@ -228,6 +230,9 @@ func runCommand(cmd *cobra.Command, args []string) error {
|
|||||||
if proxyURL != "" {
|
if proxyURL != "" {
|
||||||
cfg.Network.ProxyURL = proxyURL
|
cfg.Network.ProxyURL = proxyURL
|
||||||
}
|
}
|
||||||
|
if httpProxyURL != "" {
|
||||||
|
cfg.Network.HTTPProxyURL = httpProxyURL
|
||||||
|
}
|
||||||
if dnsAddr != "" {
|
if dnsAddr != "" {
|
||||||
cfg.Network.DnsAddr = dnsAddr
|
cfg.Network.DnsAddr = dnsAddr
|
||||||
}
|
}
|
||||||
@@ -240,6 +245,12 @@ func runCommand(cmd *cobra.Command, args []string) error {
|
|||||||
fmt.Fprintf(os.Stderr, "[greywall] Defaulting proxy to socks5://localhost:42052\n")
|
fmt.Fprintf(os.Stderr, "[greywall] Defaulting proxy to socks5://localhost:42052\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if cfg.Network.HTTPProxyURL == "" {
|
||||||
|
cfg.Network.HTTPProxyURL = "http://localhost:42051"
|
||||||
|
if debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "[greywall] Defaulting HTTP proxy to http://localhost:42051\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
if cfg.Network.DnsAddr == "" {
|
if cfg.Network.DnsAddr == "" {
|
||||||
cfg.Network.DnsAddr = "localhost:42053"
|
cfg.Network.DnsAddr = "localhost:42053"
|
||||||
if debug {
|
if debug {
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -4,6 +4,7 @@ go 1.25
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bmatcuk/doublestar/v4 v4.9.1
|
github.com/bmatcuk/doublestar/v4 v4.9.1
|
||||||
|
github.com/kardianos/service v1.2.4
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/tidwall/jsonc v0.3.2
|
github.com/tidwall/jsonc v0.3.2
|
||||||
golang.org/x/sys v0.39.0
|
golang.org/x/sys v0.39.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -3,6 +3,8 @@ github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS
|
|||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk=
|
||||||
|
github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ type Config struct {
|
|||||||
|
|
||||||
// NetworkConfig defines network restrictions.
|
// NetworkConfig defines network restrictions.
|
||||||
type NetworkConfig struct {
|
type NetworkConfig struct {
|
||||||
ProxyURL string `json:"proxyUrl,omitempty"` // External SOCKS5 proxy (e.g. socks5://host:1080)
|
ProxyURL string `json:"proxyUrl,omitempty"` // External SOCKS5 proxy (e.g. socks5://host:1080)
|
||||||
DnsAddr string `json:"dnsAddr,omitempty"` // DNS server address on host (e.g. localhost:3153)
|
HTTPProxyURL string `json:"httpProxyUrl,omitempty"` // HTTP CONNECT proxy (e.g. http://host:42051)
|
||||||
|
DnsAddr string `json:"dnsAddr,omitempty"` // DNS server address on host (e.g. localhost:3153)
|
||||||
AllowUnixSockets []string `json:"allowUnixSockets,omitempty"`
|
AllowUnixSockets []string `json:"allowUnixSockets,omitempty"`
|
||||||
AllowAllUnixSockets bool `json:"allowAllUnixSockets,omitempty"`
|
AllowAllUnixSockets bool `json:"allowAllUnixSockets,omitempty"`
|
||||||
AllowLocalBinding bool `json:"allowLocalBinding,omitempty"`
|
AllowLocalBinding bool `json:"allowLocalBinding,omitempty"`
|
||||||
@@ -203,6 +204,11 @@ func (c *Config) Validate() error {
|
|||||||
return fmt.Errorf("invalid network.proxyUrl %q: %w", c.Network.ProxyURL, err)
|
return fmt.Errorf("invalid network.proxyUrl %q: %w", c.Network.ProxyURL, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if c.Network.HTTPProxyURL != "" {
|
||||||
|
if err := validateHTTPProxyURL(c.Network.HTTPProxyURL); err != nil {
|
||||||
|
return fmt.Errorf("invalid network.httpProxyUrl %q: %w", c.Network.HTTPProxyURL, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
if c.Network.DnsAddr != "" {
|
if c.Network.DnsAddr != "" {
|
||||||
if err := validateHostPort(c.Network.DnsAddr); err != nil {
|
if err := validateHostPort(c.Network.DnsAddr); err != nil {
|
||||||
return fmt.Errorf("invalid network.dnsAddr %q: %w", c.Network.DnsAddr, err)
|
return fmt.Errorf("invalid network.dnsAddr %q: %w", c.Network.DnsAddr, err)
|
||||||
@@ -273,6 +279,24 @@ func validateProxyURL(proxyURL string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateHTTPProxyURL validates an HTTP CONNECT proxy URL.
|
||||||
|
func validateHTTPProxyURL(proxyURL string) error {
|
||||||
|
u, err := url.Parse(proxyURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid URL: %w", err)
|
||||||
|
}
|
||||||
|
if u.Scheme != "http" && u.Scheme != "https" {
|
||||||
|
return errors.New("HTTP proxy URL must use http:// or https:// scheme")
|
||||||
|
}
|
||||||
|
if u.Hostname() == "" {
|
||||||
|
return errors.New("HTTP proxy URL must include a hostname")
|
||||||
|
}
|
||||||
|
if u.Port() == "" {
|
||||||
|
return errors.New("HTTP proxy URL must include a port")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// validateHostPort validates a host:port address.
|
// validateHostPort validates a host:port address.
|
||||||
func validateHostPort(addr string) error {
|
func validateHostPort(addr string) error {
|
||||||
// Must contain a colon separating host and port
|
// Must contain a colon separating host and port
|
||||||
@@ -407,9 +431,10 @@ func Merge(base, override *Config) *Config {
|
|||||||
AllowPty: base.AllowPty || override.AllowPty,
|
AllowPty: base.AllowPty || override.AllowPty,
|
||||||
|
|
||||||
Network: NetworkConfig{
|
Network: NetworkConfig{
|
||||||
// ProxyURL/DnsAddr: override wins if non-empty
|
// ProxyURL/HTTPProxyURL/DnsAddr: override wins if non-empty
|
||||||
ProxyURL: mergeString(base.Network.ProxyURL, override.Network.ProxyURL),
|
ProxyURL: mergeString(base.Network.ProxyURL, override.Network.ProxyURL),
|
||||||
DnsAddr: mergeString(base.Network.DnsAddr, override.Network.DnsAddr),
|
HTTPProxyURL: mergeString(base.Network.HTTPProxyURL, override.Network.HTTPProxyURL),
|
||||||
|
DnsAddr: mergeString(base.Network.DnsAddr, override.Network.DnsAddr),
|
||||||
|
|
||||||
// Append slices (base first, then override additions)
|
// Append slices (base first, then override additions)
|
||||||
AllowUnixSockets: mergeStrings(base.Network.AllowUnixSockets, override.Network.AllowUnixSockets),
|
AllowUnixSockets: mergeStrings(base.Network.AllowUnixSockets, override.Network.AllowUnixSockets),
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package daemon
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -77,9 +76,7 @@ func (d *DNSRelay) ListenAddr() string {
|
|||||||
// listening socket and spawns a goroutine per query to forward it to the
|
// listening socket and spawns a goroutine per query to forward it to the
|
||||||
// upstream DNS server and relay the response back.
|
// upstream DNS server and relay the response back.
|
||||||
func (d *DNSRelay) Start() error {
|
func (d *DNSRelay) Start() error {
|
||||||
if d.debug {
|
Logf("DNS relay listening on %s, forwarding to %s", d.listenAddr, d.targetAddr)
|
||||||
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Listening on %s, forwarding to %s\n", d.listenAddr, d.targetAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.wg.Add(1)
|
d.wg.Add(1)
|
||||||
go d.readLoop()
|
go d.readLoop()
|
||||||
@@ -94,9 +91,7 @@ func (d *DNSRelay) Stop() {
|
|||||||
_ = d.udpConn.Close()
|
_ = d.udpConn.Close()
|
||||||
d.wg.Wait()
|
d.wg.Wait()
|
||||||
|
|
||||||
if d.debug {
|
Logf("DNS relay stopped")
|
||||||
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Stopped\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// readLoop is the main loop that reads incoming DNS queries from the listening socket.
|
// readLoop is the main loop that reads incoming DNS queries from the listening socket.
|
||||||
@@ -112,7 +107,7 @@ func (d *DNSRelay) readLoop() {
|
|||||||
// Shutting down, expected error from closed socket.
|
// Shutting down, expected error from closed socket.
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Read error: %v\n", err)
|
Logf("DNS relay: read error: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,36 +131,33 @@ func (d *DNSRelay) readLoop() {
|
|||||||
func (d *DNSRelay) handleQuery(query []byte, clientAddr *net.UDPAddr) {
|
func (d *DNSRelay) handleQuery(query []byte, clientAddr *net.UDPAddr) {
|
||||||
defer d.wg.Done()
|
defer d.wg.Done()
|
||||||
|
|
||||||
if d.debug {
|
Logf("DNS relay: query from %s (%d bytes)", clientAddr, len(query))
|
||||||
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Query from %s (%d bytes)\n", clientAddr, len(query))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a dedicated UDP connection to the upstream DNS server.
|
// Create a dedicated UDP connection to the upstream DNS server.
|
||||||
upstreamConn, err := net.Dial("udp", d.targetAddr)
|
// Use "udp4" to force IPv4, since the upstream may only listen on 127.0.0.1.
|
||||||
|
upstreamConn, err := net.Dial("udp4", d.targetAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Failed to connect to upstream %s: %v\n", d.targetAddr, err)
|
Logf("DNS relay: failed to connect to upstream %s: %v", d.targetAddr, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer upstreamConn.Close() //nolint:errcheck // best-effort cleanup of per-query UDP connection
|
defer upstreamConn.Close() //nolint:errcheck // best-effort cleanup of per-query UDP connection
|
||||||
|
|
||||||
// Send the query to the upstream server.
|
// Send the query to the upstream server.
|
||||||
if _, err := upstreamConn.Write(query); err != nil {
|
if _, err := upstreamConn.Write(query); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Failed to send query to upstream: %v\n", err)
|
Logf("DNS relay: failed to send query to upstream: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the response with a timeout.
|
// Wait for the response with a timeout.
|
||||||
if err := upstreamConn.SetReadDeadline(time.Now().Add(upstreamTimeout)); err != nil {
|
if err := upstreamConn.SetReadDeadline(time.Now().Add(upstreamTimeout)); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Failed to set read deadline: %v\n", err)
|
Logf("DNS relay: failed to set read deadline: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := make([]byte, maxDNSPacketSize)
|
resp := make([]byte, maxDNSPacketSize)
|
||||||
n, err := upstreamConn.Read(resp)
|
n, err := upstreamConn.Read(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if d.debug {
|
Logf("DNS relay: upstream response error from %s: %v", d.targetAddr, err)
|
||||||
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Upstream response error: %v\n", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,11 +168,9 @@ func (d *DNSRelay) handleQuery(query []byte, clientAddr *net.UDPAddr) {
|
|||||||
case <-d.done:
|
case <-d.done:
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Failed to send response to %s: %v\n", clientAddr, err)
|
Logf("DNS relay: failed to send response to %s: %v", clientAddr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.debug {
|
Logf("DNS relay: response to %s (%d bytes)", clientAddr, n)
|
||||||
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Response to %s (%d bytes)\n", clientAddr, n)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -546,9 +546,3 @@ func logDebug(debug bool, format string, args ...interface{}) {
|
|||||||
Logf(format, args...)
|
Logf(format, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logf writes a timestamped message to stderr with the [greywall:daemon] prefix.
|
|
||||||
func Logf(format string, args ...interface{}) {
|
|
||||||
ts := time.Now().Format("2006-01-02 15:04:05")
|
|
||||||
fmt.Fprintf(os.Stderr, ts+" [greywall:daemon] "+format+"\n", args...)
|
|
||||||
}
|
|
||||||
|
|||||||
13
internal/daemon/log.go
Normal file
13
internal/daemon/log.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logf writes a timestamped message to stderr with the [greywall:daemon] prefix.
|
||||||
|
func Logf(format string, args ...interface{}) {
|
||||||
|
ts := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
fmt.Fprintf(os.Stderr, ts+" [greywall:daemon] "+format+"\n", args...) //nolint:gosec // logging to stderr, not user-facing HTML
|
||||||
|
}
|
||||||
81
internal/daemon/program.go
Normal file
81
internal/daemon/program.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// program implements the kardianos/service.Interface for greywall daemon
|
||||||
|
// lifecycle management. It delegates actual work to the Server type.
|
||||||
|
type program struct {
|
||||||
|
server *Server
|
||||||
|
socketPath string
|
||||||
|
tun2socksPath string
|
||||||
|
debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProgram creates a new program instance for use with kardianos/service.
|
||||||
|
func NewProgram(socketPath, tun2socksPath string, debug bool) *program {
|
||||||
|
return &program{
|
||||||
|
socketPath: socketPath,
|
||||||
|
tun2socksPath: tun2socksPath,
|
||||||
|
debug: debug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start is called by kardianos/service when the service starts. It verifies
|
||||||
|
// the tun2socks binary exists, creates and starts the Server. The accept loop
|
||||||
|
// already runs in a goroutine, so this returns immediately.
|
||||||
|
func (p *program) Start(_ service.Service) error {
|
||||||
|
if _, err := os.Stat(p.tun2socksPath); err != nil {
|
||||||
|
return fmt.Errorf("tun2socks binary not found at %s (run 'sudo greywall daemon install' first)", p.tun2socksPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logf("Starting daemon (tun2socks=%s, socket=%s)", p.tun2socksPath, p.socketPath)
|
||||||
|
|
||||||
|
p.server = NewServer(p.socketPath, p.tun2socksPath, p.debug)
|
||||||
|
if err := p.server.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start daemon server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logf("Daemon started, listening on %s", p.socketPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop is called by kardianos/service when the service stops.
|
||||||
|
func (p *program) Stop(_ service.Service) error {
|
||||||
|
if p.server == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
Logf("Stopping daemon")
|
||||||
|
if err := p.server.Stop(); err != nil {
|
||||||
|
Logf("Shutdown error: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
Logf("Daemon stopped")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServiceConfig returns a kardianos/service config matching the existing
|
||||||
|
// LaunchDaemon setup. The Name matches LaunchDaemonLabel so service.Control()
|
||||||
|
// can find and manage the already-installed service.
|
||||||
|
func NewServiceConfig() *service.Config {
|
||||||
|
return &service.Config{
|
||||||
|
Name: LaunchDaemonLabel,
|
||||||
|
DisplayName: "Greywall Daemon",
|
||||||
|
Description: "Greywall transparent network sandboxing daemon",
|
||||||
|
Arguments: []string{"daemon", "run"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTun2socksPath returns the expected tun2socks binary path based on
|
||||||
|
// the install directory and current architecture.
|
||||||
|
func DefaultTun2socksPath() string {
|
||||||
|
return filepath.Join(InstallLibDir, "tun2socks-darwin-"+runtime.GOARCH)
|
||||||
|
}
|
||||||
@@ -295,19 +295,27 @@ func (s *Server) handleCreateSession(req Request) Response {
|
|||||||
return Response{OK: false, Error: fmt.Sprintf("failed to start tunnel: %v", err)}
|
return Response{OK: false, Error: fmt.Sprintf("failed to start tunnel: %v", err)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Create DNS relay if dns_addr is provided.
|
// Step 2: Create DNS relay. pf rules always redirect DNS (UDP:53) from
|
||||||
var dr *DNSRelay
|
// the sandbox group to the relay address, so we must always start the
|
||||||
if req.DNSAddr != "" {
|
// relay when a proxy session is active. If no explicit DNS address was
|
||||||
var err error
|
// provided, default to the proxy's DNS resolver.
|
||||||
dr, err = NewDNSRelay(dnsRelayIP+":"+dnsRelayPort, req.DNSAddr, s.debug)
|
dnsTarget := req.DNSAddr
|
||||||
if err != nil {
|
if dnsTarget == "" {
|
||||||
_ = tm.Stop() // best-effort cleanup
|
dnsTarget = defaultDNSTarget
|
||||||
return Response{OK: false, Error: fmt.Sprintf("failed to create DNS relay: %v", err)}
|
Logf("No dns_addr provided, defaulting DNS relay upstream to %s", dnsTarget)
|
||||||
|
}
|
||||||
|
dr, err := NewDNSRelay(dnsRelayIP+":"+dnsRelayPort, dnsTarget, s.debug)
|
||||||
|
if err != nil {
|
||||||
|
if stopErr := tm.Stop(); stopErr != nil {
|
||||||
|
Logf("Warning: failed to stop tunnel during cleanup: %v", stopErr)
|
||||||
}
|
}
|
||||||
if err := dr.Start(); err != nil {
|
return Response{OK: false, Error: fmt.Sprintf("failed to create DNS relay: %v", err)}
|
||||||
_ = tm.Stop() // best-effort cleanup
|
}
|
||||||
return Response{OK: false, Error: fmt.Sprintf("failed to start DNS relay: %v", err)}
|
if err := dr.Start(); err != nil {
|
||||||
|
if stopErr := tm.Stop(); stopErr != nil {
|
||||||
|
Logf("Warning: failed to stop tunnel during cleanup: %v", stopErr)
|
||||||
}
|
}
|
||||||
|
return Response{OK: false, Error: fmt.Sprintf("failed to start DNS relay: %v", err)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Resolve the sandbox group GID. pfctl in the LaunchDaemon
|
// Step 3: Resolve the sandbox group GID. pfctl in the LaunchDaemon
|
||||||
@@ -318,9 +326,7 @@ func (s *Server) handleCreateSession(req Request) Response {
|
|||||||
grp, err := user.LookupGroup(SandboxGroupName)
|
grp, err := user.LookupGroup(SandboxGroupName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tm.Stop()
|
_ = tm.Stop()
|
||||||
if dr != nil {
|
dr.Stop()
|
||||||
dr.Stop()
|
|
||||||
}
|
|
||||||
return Response{OK: false, Error: fmt.Sprintf("failed to resolve group %s: %v", SandboxGroupName, err)}
|
return Response{OK: false, Error: fmt.Sprintf("failed to resolve group %s: %v", SandboxGroupName, err)}
|
||||||
}
|
}
|
||||||
sandboxGID = grp.Gid
|
sandboxGID = grp.Gid
|
||||||
@@ -328,9 +334,7 @@ func (s *Server) handleCreateSession(req Request) Response {
|
|||||||
}
|
}
|
||||||
Logf("Loading pf rules for group %s (GID %s)", SandboxGroupName, sandboxGID)
|
Logf("Loading pf rules for group %s (GID %s)", SandboxGroupName, sandboxGID)
|
||||||
if err := tm.LoadPFRules(sandboxGID); err != nil {
|
if err := tm.LoadPFRules(sandboxGID); err != nil {
|
||||||
if dr != nil {
|
dr.Stop()
|
||||||
dr.Stop()
|
|
||||||
}
|
|
||||||
_ = tm.Stop() // best-effort cleanup
|
_ = tm.Stop() // best-effort cleanup
|
||||||
return Response{OK: false, Error: fmt.Sprintf("failed to load pf rules: %v", err)}
|
return Response{OK: false, Error: fmt.Sprintf("failed to load pf rules: %v", err)}
|
||||||
}
|
}
|
||||||
@@ -338,9 +342,7 @@ func (s *Server) handleCreateSession(req Request) Response {
|
|||||||
// Step 4: Generate session ID and store.
|
// Step 4: Generate session ID and store.
|
||||||
sessionID, err := generateSessionID()
|
sessionID, err := generateSessionID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if dr != nil {
|
dr.Stop()
|
||||||
dr.Stop()
|
|
||||||
}
|
|
||||||
_ = tm.UnloadPFRules() // best-effort cleanup
|
_ = tm.UnloadPFRules() // best-effort cleanup
|
||||||
_ = tm.Stop() // best-effort cleanup
|
_ = tm.Stop() // best-effort cleanup
|
||||||
return Response{OK: false, Error: fmt.Sprintf("failed to generate session ID: %v", err)}
|
return Response{OK: false, Error: fmt.Sprintf("failed to generate session ID: %v", err)}
|
||||||
@@ -349,7 +351,7 @@ func (s *Server) handleCreateSession(req Request) Response {
|
|||||||
session := &Session{
|
session := &Session{
|
||||||
ID: sessionID,
|
ID: sessionID,
|
||||||
ProxyURL: req.ProxyURL,
|
ProxyURL: req.ProxyURL,
|
||||||
DNSAddr: req.DNSAddr,
|
DNSAddr: dnsTarget,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
s.sessions[sessionID] = session
|
s.sessions[sessionID] = session
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tunIP = "198.18.0.1"
|
tunIP = "198.18.0.1"
|
||||||
dnsRelayIP = "127.0.0.2"
|
dnsRelayIP = "127.0.0.2"
|
||||||
dnsRelayPort = "15353" // high port to avoid conflicts with system DNS (mDNSResponder, Docker/Lima)
|
dnsRelayPort = "15353" // high port; pf rdr rewrites port 53 → this port
|
||||||
pfAnchorName = "co.greyhaven.greywall"
|
defaultDNSTarget = "127.0.0.1:42053" // proxy's DNS resolver (UDP), used when dnsAddr is not configured
|
||||||
|
pfAnchorName = "co.greyhaven.greywall"
|
||||||
|
|
||||||
// tun2socksStopGracePeriod is the time to wait for tun2socks to exit
|
// tun2socksStopGracePeriod is the time to wait for tun2socks to exit
|
||||||
// after SIGTERM before sending SIGKILL.
|
// after SIGTERM before sending SIGKILL.
|
||||||
@@ -158,19 +159,15 @@ func (t *TunManager) LoadPFRules(sandboxGroup string) error {
|
|||||||
return fmt.Errorf("failed to ensure pf anchor: %w", err)
|
return fmt.Errorf("failed to ensure pf anchor: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the anchor rules. pf requires strict ordering:
|
// Build pf anchor rules for the sandbox group:
|
||||||
// translation (rdr) before filtering (pass).
|
// 1. Route all non-loopback TCP through the utun → tun2socks → SOCKS proxy.
|
||||||
// Note: macOS pf does not support "group" in rdr rules, so DNS
|
// Loopback (127.0.0.0/8) is excluded so that ALL_PROXY=socks5h://
|
||||||
// redirection uses a two-step approach:
|
// connections to the local proxy don't get double-proxied.
|
||||||
// 1. rdr on lo0 — redirects DNS arriving on loopback to our relay
|
// 2. (DNS is handled via ALL_PROXY=socks5h:// env var, not via pf,
|
||||||
// 2. pass out route-to lo0 — sends sandbox group's DNS to loopback
|
// because macOS getaddrinfo uses mDNSResponder via Mach IPC and
|
||||||
// 3. pass out route-to utun — sends sandbox group's TCP through tunnel
|
// blocking those services doesn't cause a UDP DNS fallback.)
|
||||||
rules := fmt.Sprintf(
|
rules := fmt.Sprintf(
|
||||||
"rdr on lo0 proto udp from any to any port 53 -> %s port %s\n"+
|
"pass out route-to (%s %s) proto tcp from any to !127.0.0.0/8 group %s\n",
|
||||||
"pass out on !lo0 route-to (lo0 127.0.0.1) proto udp from any to any port 53 group %s\n"+
|
|
||||||
"pass out route-to (%s %s) proto tcp from any to any group %s\n",
|
|
||||||
dnsRelayIP, dnsRelayPort,
|
|
||||||
sandboxGroup,
|
|
||||||
t.tunDevice, tunIP, sandboxGroup,
|
t.tunDevice, tunIP, sandboxGroup,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -451,7 +451,13 @@ func GenerateSandboxProfile(params MacOSSandboxParams) string {
|
|||||||
(global-name "com.apple.system.logger")
|
(global-name "com.apple.system.logger")
|
||||||
(global-name "com.apple.system.notification_center")
|
(global-name "com.apple.system.notification_center")
|
||||||
(global-name "com.apple.trustd.agent")
|
(global-name "com.apple.trustd.agent")
|
||||||
(global-name "com.apple.system.opendirectoryd.libinfo")
|
`)
|
||||||
|
// macOS DNS resolution goes through mDNSResponder via Mach IPC — blocking
|
||||||
|
// opendirectoryd.libinfo or configd does NOT cause a fallback to direct UDP
|
||||||
|
// DNS. getaddrinfo() simply fails with EAI_NONAME. So we must allow these
|
||||||
|
// services in all modes. In daemon mode, DNS for proxy-aware apps (curl, git)
|
||||||
|
// is handled via ALL_PROXY=socks5h:// env var instead.
|
||||||
|
profile.WriteString(` (global-name "com.apple.system.opendirectoryd.libinfo")
|
||||||
(global-name "com.apple.system.opendirectoryd.membership")
|
(global-name "com.apple.system.opendirectoryd.membership")
|
||||||
(global-name "com.apple.bsd.dirhelper")
|
(global-name "com.apple.bsd.dirhelper")
|
||||||
(global-name "com.apple.securityd.xpc")
|
(global-name "com.apple.securityd.xpc")
|
||||||
@@ -733,17 +739,57 @@ func WrapCommandMacOS(cfg *config.Config, command string, exposedPorts []int, da
|
|||||||
|
|
||||||
if daemonMode {
|
if daemonMode {
|
||||||
// In daemon mode: run as the real user but with EGID=_greywall via sudo.
|
// In daemon mode: run as the real user but with EGID=_greywall via sudo.
|
||||||
// pf routes all traffic from group _greywall through utun → tun2socks → proxy.
|
// pf routes all TCP from group _greywall through utun → tun2socks → proxy.
|
||||||
// Using -u #<uid> preserves the user's identity (home dir, SSH keys, etc.)
|
// Using -u #<uid> preserves the user's identity (home dir, SSH keys, etc.)
|
||||||
// while -g _greywall sets the effective GID for pf matching.
|
// while -g _greywall sets the effective GID for pf matching.
|
||||||
//
|
//
|
||||||
|
// DNS on macOS goes through mDNSResponder (Mach IPC), which runs outside
|
||||||
|
// the _greywall group, so pf can't intercept DNS. Instead, we set
|
||||||
|
// ALL_PROXY=socks5h:// so proxy-aware apps (curl, git, etc.) resolve DNS
|
||||||
|
// through the SOCKS5 proxy. The "h" suffix means "resolve hostname at proxy".
|
||||||
|
//
|
||||||
|
// Set ALL_PROXY and HTTP_PROXY/HTTPS_PROXY with socks5h:// so both
|
||||||
|
// SOCKS5-aware apps (curl, git) and HTTP-proxy-aware apps (opencode,
|
||||||
|
// Node.js tools) resolve DNS through the proxy. The "h" suffix means
|
||||||
|
// "resolve hostname at proxy side". Note: apps that read HTTP_PROXY
|
||||||
|
// but don't support SOCKS5 protocol (e.g., Bun) may fail to connect.
|
||||||
|
//
|
||||||
// sudo resets the environment, so we use `env` after sudo to re-inject
|
// sudo resets the environment, so we use `env` after sudo to re-inject
|
||||||
// terminal vars (TERM, COLORTERM, etc.) needed for TUI apps and proxy vars.
|
// terminal vars (TERM, COLORTERM, etc.) needed for TUI apps.
|
||||||
uid := fmt.Sprintf("#%d", os.Getuid())
|
uid := fmt.Sprintf("#%d", os.Getuid())
|
||||||
proxyEnvs := GenerateProxyEnvVars(cfg.Network.ProxyURL)
|
sandboxEnvs := GenerateProxyEnvVars("")
|
||||||
|
// Convert socks5:// → socks5h:// for hostname resolution through proxy.
|
||||||
|
socks5hURL := strings.Replace(cfg.Network.ProxyURL, "socks5://", "socks5h://", 1)
|
||||||
|
if socks5hURL != "" {
|
||||||
|
// ALL_PROXY uses socks5h:// (DNS resolved at proxy side) for
|
||||||
|
// SOCKS5-aware apps (curl, git).
|
||||||
|
// HTTP_PROXY/HTTPS_PROXY use the configured HTTP CONNECT proxy
|
||||||
|
// for apps that only understand HTTP proxies (opencode, Node.js
|
||||||
|
// tools, etc.). The CONNECT proxy resolves DNS server-side.
|
||||||
|
httpProxyURL := cfg.Network.HTTPProxyURL
|
||||||
|
// Inject credentials from the SOCKS5 proxy URL into the HTTP proxy
|
||||||
|
// URL if the HTTP proxy URL doesn't already have credentials.
|
||||||
|
if httpProxyURL != "" {
|
||||||
|
if hu, err := url.Parse(httpProxyURL); err == nil && hu.User == nil {
|
||||||
|
if su, err := url.Parse(socks5hURL); err == nil && su.User != nil {
|
||||||
|
hu.User = su.User
|
||||||
|
httpProxyURL = hu.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sandboxEnvs = append(sandboxEnvs,
|
||||||
|
"ALL_PROXY="+socks5hURL, "all_proxy="+socks5hURL,
|
||||||
|
)
|
||||||
|
if httpProxyURL != "" {
|
||||||
|
sandboxEnvs = append(sandboxEnvs,
|
||||||
|
"HTTP_PROXY="+httpProxyURL, "http_proxy="+httpProxyURL,
|
||||||
|
"HTTPS_PROXY="+httpProxyURL, "https_proxy="+httpProxyURL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
termEnvs := getTerminalEnvVars()
|
termEnvs := getTerminalEnvVars()
|
||||||
parts = append(parts, "sudo", "-u", uid, "-g", daemonSession.SandboxGroup, "env")
|
parts = append(parts, "sudo", "-u", uid, "-g", daemonSession.SandboxGroup, "env")
|
||||||
parts = append(parts, proxyEnvs...)
|
parts = append(parts, sandboxEnvs...)
|
||||||
parts = append(parts, termEnvs...)
|
parts = append(parts, termEnvs...)
|
||||||
parts = append(parts, "sandbox-exec", "-p", profile, shellPath, "-c", command)
|
parts = append(parts, "sandbox-exec", "-p", profile, shellPath, "-c", command)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user