diff --git a/cmd/greywall/main.go b/cmd/greywall/main.go index 90eb31d..6c827a3 100644 --- a/cmd/greywall/main.go +++ b/cmd/greywall/main.go @@ -31,6 +31,7 @@ var ( monitor bool settingsPath string proxyURL string + httpProxyURL string dnsAddr string cmdString string exposePorts []string @@ -99,6 +100,7 @@ Configuration file format: 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().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().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)") @@ -228,6 +230,9 @@ func runCommand(cmd *cobra.Command, args []string) error { if proxyURL != "" { cfg.Network.ProxyURL = proxyURL } + if httpProxyURL != "" { + cfg.Network.HTTPProxyURL = httpProxyURL + } if 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") } } + 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 == "" { cfg.Network.DnsAddr = "localhost:42053" if debug { diff --git a/internal/config/config.go b/internal/config/config.go index 352482c..111593b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,8 +26,9 @@ type Config struct { // NetworkConfig defines network restrictions. type NetworkConfig struct { - 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) + ProxyURL string `json:"proxyUrl,omitempty"` // External SOCKS5 proxy (e.g. socks5://host:1080) + 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"` AllowAllUnixSockets bool `json:"allowAllUnixSockets,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) } } + 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 err := validateHostPort(c.Network.DnsAddr); err != nil { return fmt.Errorf("invalid network.dnsAddr %q: %w", c.Network.DnsAddr, err) @@ -273,6 +279,24 @@ func validateProxyURL(proxyURL string) error { 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. func validateHostPort(addr string) error { // Must contain a colon separating host and port @@ -407,9 +431,10 @@ func Merge(base, override *Config) *Config { AllowPty: base.AllowPty || override.AllowPty, Network: NetworkConfig{ - // ProxyURL/DnsAddr: override wins if non-empty - ProxyURL: mergeString(base.Network.ProxyURL, override.Network.ProxyURL), - DnsAddr: mergeString(base.Network.DnsAddr, override.Network.DnsAddr), + // ProxyURL/HTTPProxyURL/DnsAddr: override wins if non-empty + ProxyURL: mergeString(base.Network.ProxyURL, override.Network.ProxyURL), + HTTPProxyURL: mergeString(base.Network.HTTPProxyURL, override.Network.HTTPProxyURL), + DnsAddr: mergeString(base.Network.DnsAddr, override.Network.DnsAddr), // Append slices (base first, then override additions) AllowUnixSockets: mergeStrings(base.Network.AllowUnixSockets, override.Network.AllowUnixSockets), diff --git a/internal/daemon/tun.go b/internal/daemon/tun.go index bfc4257..7ee1bfa 100644 --- a/internal/daemon/tun.go +++ b/internal/daemon/tun.go @@ -17,7 +17,7 @@ import ( const ( tunIP = "198.18.0.1" dnsRelayIP = "127.0.0.2" - dnsRelayPort = "15353" // high port; pf rdr rewrites port 53 → this port + dnsRelayPort = "15353" // high port; pf rdr rewrites port 53 → this port defaultDNSTarget = "127.0.0.1:42053" // proxy's DNS resolver (UDP), used when dnsAddr is not configured pfAnchorName = "co.greyhaven.greywall" diff --git a/internal/sandbox/macos.go b/internal/sandbox/macos.go index ba4a6f9..b16b19f 100644 --- a/internal/sandbox/macos.go +++ b/internal/sandbox/macos.go @@ -763,27 +763,29 @@ func WrapCommandMacOS(cfg *config.Config, command string, exposedPorts []int, da if socks5hURL != "" { // ALL_PROXY uses socks5h:// (DNS resolved at proxy side) for // SOCKS5-aware apps (curl, git). - // HTTP_PROXY/HTTPS_PROXY use http:// pointing to the GreyHaven - // HTTP CONNECT proxy (port 42051) for apps that only understand - // HTTP proxies (opencode, Node.js tools, etc.). The CONNECT - // proxy resolves DNS server-side. - httpProxyURL := "http://localhost:42051" - if u, err := url.Parse(socks5hURL); err == nil { - userinfo := "" - if u.User != nil { - userinfo = u.User.String() + "@" + // 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() + } } - host := u.Hostname() - if host == "" { - host = "localhost" - } - httpProxyURL = "http://" + userinfo + host + ":42051" } sandboxEnvs = append(sandboxEnvs, "ALL_PROXY="+socks5hURL, "all_proxy="+socks5hURL, - "HTTP_PROXY="+httpProxyURL, "http_proxy="+httpProxyURL, - "HTTPS_PROXY="+httpProxyURL, "https_proxy="+httpProxyURL, ) + if httpProxyURL != "" { + sandboxEnvs = append(sandboxEnvs, + "HTTP_PROXY="+httpProxyURL, "http_proxy="+httpProxyURL, + "HTTPS_PROXY="+httpProxyURL, "https_proxy="+httpProxyURL, + ) + } } termEnvs := getTerminalEnvVars() parts = append(parts, "sudo", "-u", uid, "-g", daemonSession.SandboxGroup, "env")