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

View File

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

View File

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

View File

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