// Package proxyconf centralises outbound-proxy configuration for the // HTTP and (where possible) DNS clients used across God's Eye modules. // // Why this lives in its own package: every source/probe/module needs to // honour the same proxy setting, and duplicating URL parsing + dialer // wiring across `internal/http`, `internal/sources`, and individual // modules would be a fountain of bugs. This package is the single // source of truth. // // Supported schemes: // // "" → direct (no proxy) // http://host:port → HTTP CONNECT proxy (e.g. Burp, ZAP, mitmproxy) // https://host:port → HTTPS CONNECT proxy // socks5://host:port → SOCKS5 (DNS resolved locally by god-eye) // socks5h://host:port → SOCKS5 (DNS resolved by the proxy — Tor convention) // // Basic auth (http://user:pass@host) is honoured for every scheme. // // DNS-over-SOCKS caveat: Go's net package uses the OS resolver by default, // which does NOT route through SOCKS. `socks5h://` only applies to HTTP // requests — the brute-force DNS resolver (`internal/dns`) continues to // hit its configured resolvers directly. Users who need full Tor // isolation for DNS should run god-eye inside a torsocks-wrapped shell // or a netns with all traffic captured. package proxyconf import ( "context" "errors" "fmt" "net" "net/http" "net/url" "strings" "golang.org/x/net/proxy" ) // DialFunc is the signature used by http.Transport.DialContext. type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error) // ProxyFunc is the signature used by http.Transport.Proxy. type ProxyFunc func(*http.Request) (*url.URL, error) // Validate returns a descriptive error if proxyURL is non-empty and // doesn't parse to a supported scheme. Call this early (e.g. during // validator.ValidateXxx) so bad flags fail before module startup. func Validate(proxyURL string) error { proxyURL = strings.TrimSpace(proxyURL) if proxyURL == "" { return nil } u, err := url.Parse(proxyURL) if err != nil { return fmt.Errorf("proxy URL malformed: %w", err) } if u.Host == "" { return errors.New("proxy URL missing host:port") } switch strings.ToLower(u.Scheme) { case "http", "https", "socks5", "socks5h": return nil default: return fmt.Errorf("unsupported proxy scheme %q (use http/https/socks5/socks5h)", u.Scheme) } } // BuildDialer returns a DialFunc that routes TCP through the configured // proxy. For HTTP(S) CONNECT proxies (handled at the transport layer via // Proxy field), this returns a direct dialer — the transport layer does // the CONNECT dance itself. // // For empty proxyURL, returns the direct-dialer from net.Dialer. func BuildDialer(proxyURL string, base *net.Dialer) (DialFunc, error) { if base == nil { base = &net.Dialer{} } if strings.TrimSpace(proxyURL) == "" { return base.DialContext, nil } u, err := url.Parse(proxyURL) if err != nil { return nil, err } switch strings.ToLower(u.Scheme) { case "http", "https": // CONNECT proxy — direct TCP, Transport.Proxy handles the handshake. return base.DialContext, nil case "socks5", "socks5h": var auth *proxy.Auth if u.User != nil { pass, _ := u.User.Password() auth = &proxy.Auth{User: u.User.Username(), Password: pass} } // proxy.Direct is the fallthrough dialer — we pass our base so // timeouts/keepalive settings are preserved. dialer, err := proxy.SOCKS5("tcp", u.Host, auth, &directAdapter{base: base}) if err != nil { return nil, fmt.Errorf("create SOCKS5 dialer: %w", err) } if ctxDialer, ok := dialer.(proxy.ContextDialer); ok { return ctxDialer.DialContext, nil } // Older x/net versions: wrap non-context Dial with ctx-aware shim. return func(ctx context.Context, network, addr string) (net.Conn, error) { type result struct { conn net.Conn err error } ch := make(chan result, 1) go func() { c, e := dialer.Dial(network, addr) ch <- result{c, e} }() select { case r := <-ch: return r.conn, r.err case <-ctx.Done(): return nil, ctx.Err() } }, nil default: return nil, fmt.Errorf("unsupported proxy scheme: %s", u.Scheme) } } // BuildProxyFunc returns the http.Transport.Proxy callback for HTTP(S) // CONNECT proxies. Returns nil for SOCKS5 (handled by the dialer) and // for empty proxyURL. func BuildProxyFunc(proxyURL string) (ProxyFunc, error) { if strings.TrimSpace(proxyURL) == "" { return nil, nil } u, err := url.Parse(proxyURL) if err != nil { return nil, err } switch strings.ToLower(u.Scheme) { case "http", "https": return http.ProxyURL(u), nil case "socks5", "socks5h": return nil, nil } return nil, fmt.Errorf("unsupported proxy scheme: %s", u.Scheme) } // Humanize returns a redacted, user-facing description of the proxy. // Strips credentials so logs don't leak tokens. func Humanize(proxyURL string) string { proxyURL = strings.TrimSpace(proxyURL) if proxyURL == "" { return "direct (no proxy)" } u, err := url.Parse(proxyURL) if err != nil { return "invalid" } auth := "" if u.User != nil { auth = "(auth)@" } return fmt.Sprintf("%s://%s%s", u.Scheme, auth, u.Host) } // directAdapter adapts a *net.Dialer to the proxy.Dialer interface so // our configured timeouts/keepalive flow through to the socks hop. type directAdapter struct { base *net.Dialer } func (d *directAdapter) Dial(network, addr string) (net.Conn, error) { return d.base.Dial(network, addr) }