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

@@ -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),

View File

@@ -17,7 +17,7 @@ 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; 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 defaultDNSTarget = "127.0.0.1:42053" // proxy's DNS resolver (UDP), used when dnsAddr is not configured
pfAnchorName = "co.greyhaven.greywall" pfAnchorName = "co.greyhaven.greywall"

View File

@@ -763,27 +763,29 @@ 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,
"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() termEnvs := getTerminalEnvVars()
parts = append(parts, "sudo", "-u", uid, "-g", daemonSession.SandboxGroup, "env") parts = append(parts, "sudo", "-u", uid, "-g", daemonSession.SandboxGroup, "env")