feat: add --http-proxy flag for configurable HTTP CONNECT proxy

Add network.httpProxyUrl config field and --http-proxy CLI flag
(default: http://localhost:42051) for apps that only understand
HTTP proxies (opencode, Node.js tools, etc.).

macOS daemon mode now sets:
- ALL_PROXY=socks5h:// for SOCKS5-aware apps (curl, git)
- HTTP_PROXY/HTTPS_PROXY=http:// for HTTP-proxy-aware apps

Credentials from the SOCKS5 proxy URL are automatically injected
into the HTTP proxy URL when not explicitly configured.
This commit is contained in:
2026-03-04 12:47:57 -06:00
parent f05b4a6b4c
commit 58626c64e5
4 changed files with 60 additions and 22 deletions

View File

@@ -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 {

View File

@@ -27,6 +27,7 @@ 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)
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) 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"`
@@ -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,8 +431,9 @@ 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),
HTTPProxyURL: mergeString(base.Network.HTTPProxyURL, override.Network.HTTPProxyURL),
DnsAddr: mergeString(base.Network.DnsAddr, override.Network.DnsAddr), DnsAddr: mergeString(base.Network.DnsAddr, override.Network.DnsAddr),
// Append slices (base first, then override additions) // Append slices (base first, then override additions)

View File

@@ -763,28 +763,30 @@ func WrapCommandMacOS(cfg *config.Config, command string, exposedPorts []int, da
if socks5hURL != "" { if socks5hURL != "" {
// ALL_PROXY uses socks5h:// (DNS resolved at proxy side) for // ALL_PROXY uses socks5h:// (DNS resolved at proxy side) for
// SOCKS5-aware apps (curl, git). // SOCKS5-aware apps (curl, git).
// HTTP_PROXY/HTTPS_PROXY use http:// pointing to the GreyHaven // HTTP_PROXY/HTTPS_PROXY use the configured HTTP CONNECT proxy
// HTTP CONNECT proxy (port 42051) for apps that only understand // for apps that only understand HTTP proxies (opencode, Node.js
// HTTP proxies (opencode, Node.js tools, etc.). The CONNECT // tools, etc.). The CONNECT proxy resolves DNS server-side.
// proxy resolves DNS server-side. httpProxyURL := cfg.Network.HTTPProxyURL
httpProxyURL := "http://localhost:42051" // Inject credentials from the SOCKS5 proxy URL into the HTTP proxy
if u, err := url.Parse(socks5hURL); err == nil { // URL if the HTTP proxy URL doesn't already have credentials.
userinfo := "" if httpProxyURL != "" {
if u.User != nil { if hu, err := url.Parse(httpProxyURL); err == nil && hu.User == nil {
userinfo = u.User.String() + "@" 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, sandboxEnvs = append(sandboxEnvs,
"ALL_PROXY="+socks5hURL, "all_proxy="+socks5hURL, "ALL_PROXY="+socks5hURL, "all_proxy="+socks5hURL,
)
if httpProxyURL != "" {
sandboxEnvs = append(sandboxEnvs,
"HTTP_PROXY="+httpProxyURL, "http_proxy="+httpProxyURL, "HTTP_PROXY="+httpProxyURL, "http_proxy="+httpProxyURL,
"HTTPS_PROXY="+httpProxyURL, "https_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, sandboxEnvs...) parts = append(parts, sandboxEnvs...)