Skip to content
 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ConnectProxy (fork of sters/connectproxy)

A Go library implementing a proxy.ContextDialer over HTTP(S) CONNECT proxies. It is heavily based on jim3ma's gist and designed to complement the golang.org/x/net/proxy package.

GoDoc

Purpose

This library allows you to proxy net/http connections (and by extension, any TCP connection that goes through a dialer) through an HTTP or HTTPS CONNECT proxy server. It is useful for:

  • Routing HTTP client requests through a corporate proxy
  • Tunneling arbitrary TCP traffic through a CONNECT proxy
  • Domain fronting with different SNI names and Host headers
  • Integrating with Go's proxy.FromURL via RegisterDialerType

Supported URL Schemes

Scheme Connection Type TLS to Proxy
http:// Plaintext TCP No
https:// Encrypted Yes (configurable)

Any other scheme (e.g., socks5://) returns ErrUnsupportedProxyScheme. Custom schemes registered via proxy.RegisterDialerType (e.g. myproxy://) are treated as HTTP proxies — the Scheme is normalized to "http" and only the Host field determines the proxy address.

Quick Start

Direct Usage

proxyURL, _ := url.Parse("http://user:pass@proxy.example.com:8080")
dialer, err := connectproxy.New(proxyURL, proxy.Direct)
if err != nil {
    log.Fatal(err)
}

conn, err := dialer.DialContext(ctx, "tcp", "target.example.com:443")
if err != nil {
    log.Fatal(err)
}
defer conn.Close()
// Use conn as a net.Conn

With proxy.RegisterDialerType (Recommended for CLI/env-based proxies)

Register connectproxy as the handler for any scheme — not just "http"/"https". When used with proxy.FromURL, custom schemes are normalized to HTTP internally:

connectproxy.GeneratorWithConfig(&connectproxy.Config{
    DialTimeout: 30 * time.Second,
})
proxy.RegisterDialerType("http", connectproxy.GeneratorWithConfig(nil))
proxy.RegisterDialerType("https", connectproxy.GeneratorWithConfig(&connectproxy.Config{
    DialTimeout: 30 * time.Second,
}))

// Then use proxy.FromURL with any registered scheme:
proxyURL, _ := url.Parse("http://user:pass@proxy.example.com:8080")
dialer, err := proxy.FromURL(proxyURL, proxy.Direct)

Custom schemes work too — myproxy is normalized to "http" inside the generator:

proxy.RegisterDialerType("myproxy", connectproxy.GeneratorWithConfig(&connectproxy.Config{
    DialTimeout: 30 * time.Second,
}))

// proxy.FromURL strips the scheme and passes a URL with empty Scheme but Host intact.
// NewWithConfig normalizes this back to "http" before creating the dialer.
myproxyURL, _ := url.Parse("myproxy://user:pass@proxy.example.com:8080")
dialer, err := proxy.FromURL(myproxyURL, proxy.Direct)

ConfigDialer Implementation and Goroutine Leak Prevention

What Changed from the Parent Fork

This fork addresses a critical goroutine leak issue documented in golang.org/x/net/proxy:

"Custom dialers (registered via RegisterDialerType) that do not implement ContextDialer can leak a goroutine for as long as it takes the underlying Dialer implementation to timeout."

The parent fork's connectDialer implements proxy.ContextDialer, ensuring context cancellation propagates through the entire CONNECT handshake. This means:

  • No leaked timer goroutines: The old pattern of using a separate timer for read-response timeouts has been replaced with direct <-ctx.Done() select statements.
  • Deadline injection respects existing context deadlines: DialTimeout is applied only if it expires sooner than any deadline already set on the context (matching proxy.DialContext behavior).
  • GeneratorWithConfig bridges legacy and modern APIs: The returned generator matches proxy.RegisterDialerType's expected signature (func(*url.URL, proxy.Dialer) (proxy.Dialer, error)), while internally converting to/from ContextDialer. A lightweight ctxDialer wrapper ensures that legacy dialers passed via RegisterDialerType are treated as context-aware.

Header Merge Strategy

The library uses a header merge strategy (not replacement) for CONNECT request headers:

  • Entries from config.Header are appended to the request headers individually
  • Nil-valued header entries are skipped, preventing them from overwriting other headers
  • Proxy-Authorization is set after config headers via req.Header.Set("Proxy-Authorization", "Basic ..."), so it is never clobbered by a nil entry in config.Header

This fixes the issue where setting Header: http.Header{"X-Custom": {"value"}} would previously erase any Proxy-Authorization header derived from URL credentials.

Config Fields

Field Type Default Description
ServerName string Derived from proxy URL host TLS server name for SNI and certificate validation when connecting to an HTTPS proxy
InsecureSkipVerify bool false Accept any certificate from the HTTPS proxy without verification
Header http.Header nil Additional headers to append to the CONNECT request. Nil-valued entries are skipped (unlike direct assignment which would overwrite). Do not use for Proxy-Authorization; set credentials in the URL's User field instead.
DialTimeout time.Duration 0 (no timeout) Maximum duration for the CONNECT handshake only (not subsequent reads/writes). Applied as a deadline on the context — only if it expires sooner than any existing context deadline.
TLSConfig *tls.Config nil Custom TLS configuration for HTTPS proxy connections. When set, completely replaces the minimal tls.Config built from ServerName and InsecureSkipVerify.

Thread Safety

The Config struct may be mutated between calls to DialContext. Changes take effect immediately on subsequent invocations. This allows runtime reconfiguration without creating new dialers.

Examples

See examples_test.go for comprehensive examples covering all exported functions and configuration options. Run them with:

go test -v -run Example

Key Patterns

Basic proxy usage:

proxyURL, _ := url.Parse("http://user:pass@proxy.example.com:8080")
dialer, err := connectproxy.New(proxyURL, proxy.Direct)
// dialer implements proxy.ContextDialer — use DialContext with context.Background() or a cancelled context

Domain fronting (different SNI from Host header):

dialer, err := connectproxy.NewWithConfig(
    url.Parse("https://sneakyvhost.com:443"),
    proxy.Direct,
    &connectproxy.Config{ServerName: "normalhost.example.com"},
)
// TLS handshake uses SNI "normalhost.example.com" but Host header says "sneakyvhost.com"

Registering with golang.org/x/net/proxy:

proxy.RegisterDialerType("http", connectproxy.GeneratorWithConfig(&connectproxy.Config{
    DialTimeout: 30 * time.Second,
}))

// Now proxy.FromURL recognizes "http://" schemes:
proxyURL, _ := url.Parse("http://user:pass@proxy.example.com:8080")
dialer, err := proxy.FromURL(proxyURL, proxy.Direct)

HTTP client integration:

transport := &http.Transport{
    DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
        return dialer.DialContext(ctx, network, addr)
    },
}
client := &http.Client{Transport: transport}
resp, err := client.Get("https://target.example.com")

Error Types

Error When Returned
ErrUnsupportedProxyScheme URL scheme is neither "http" nor "https"
ErrNonOKResponse Proxy server responds with a status other than 200 (e.g., 403 Forbidden)

Both are exported as package-level variables for use with errors.Is():

if errors.Is(err, connectproxy.ErrUnsupportedProxyScheme) { ... }

Comparison with Parent Fork

Feature This Fork Parent Fork (magisterquis)
Context support Full DialContext with deadline injection Legacy Dial only (goroutine leak risk)
Header merge strategy Append non-nil entries, skip nils Replace entire header map
Proxy-Authorization placement Set after config headers Risk of clobbering by nil entries
GeneratorWithConfig Returns func(*url.URL, proxy.Dialer) matching RegisterDialerType signature N/A
ctxDialer wrapper Bridges legacy proxy.Dialer to internal ContextDialer requirement N/A
Example tests Comprehensive with GoDoc examples on pkg.go.dev Minimal or none

License

MIT — see LICENSE for details.

About

Package connectproxy implements a proxy.Dialer which uses HTTP(s) CONNECT requests.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages