Forwarder Proxy (fork of Sauce Labs Forwarder)
Fork of the original Sauce Labs Forwarder — a production-ready Go library providing an HTTP forward proxy with MITM (SSL bumping), PAC (Proxy Auto-Configuration), Kerberos authentication, header manipulation, domain filtering, rate limiting, and dual-stack IPv4/IPv6 support.
What this fork solves: The original Forwarder's PAC evaluator relies on the Goja JavaScript engine for all
isInNet()subnet matching — each call costs ~130μs/op with 212 allocations due to JS interop overhead. This fork adds pure-Go implementations of the PAC helper functions (isInNet,dnsResolve, etc.) that bypass Goja entirely, delivering ~600ns/op (~217x faster) — critical for high-throughput scenarios with thousands of concurrent connections.
Mozilla Public License Version 2.0 — same as the upstream Sauce Labs Forwarder. Third-party licenses in LICENSE.3RD_PARTY.
go get github.com/cymertek/go-forwarder@latestMinimum Go version: 1.23
package main
import (
"context"
"log"
"github.com/cymertek/go-forwarder"
"github.com/cymertek/go-forwarder/log/slog"
)
func main() {
cfg := forwarder.DefaultHTTPProxyConfig()
cfg.Address = ":3128" // listen address
logger := slog.Default()
// Create a PACResolver if needed (see pac package section below)
var pr forwarder.PACResolver = nil
proxy, err := forwarder.NewHTTPProxy(cfg, pr, nil /* no credentials */, nil /* use default transport */, logger, nil)
if err != nil {
log.Fatal(err)
}
defer proxy.Close()
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-ctx.Done()
cancel()
}()
log.Printf("proxy listening on %s", proxy.Addr())
if err := proxy.Run(ctx); err != nil {
log.Fatal(err)
}
}cfg := forwarder.DefaultHTTPProxyConfig()
cfg.Address = ":8443"
cfg.Protocol = forwarder.HTTPSScheme
cfg.CertFile = "cert.pem"
cfg.KeyFile = "key.pem"
cfg.BasicAuth = url.UserPassword("admin", "secret")
proxy, err := forwarder.NewHTTPProxy(cfg, nil, nil, nil, slog.Default(), nil)| Feature | Description | Key Types / Functions |
|---|---|---|
| Forward proxy | HTTP/HTTPS/HTTP2/WS/SSE/TCP traffic | NewHTTPProxy, NewHTTPProxyHandler |
| MITM / SSL bumping | On-the-fly certificate generation for HTTPS inspection | MITMConfig, proxy.MITMCACert() |
| PAC support | Mozilla FindProxyForURL + Microsoft IPv6 extensions (see below) | PACResolver, pac.NewProxyResolver |
| Upstream chaining | HTTP proxy, HTTPS tunnel, SOCKS5 via nested dialers | dialvia.HTTPProxy(), dialvia.SOCKS5Proxy() |
| Kerberos auth | SPNEGO/Kerberos for upstream proxy authentication | KerberosConfig, NewKerberosAdapter |
| Header manipulation | Add/remove/empty/rename headers on request/response | header.Header{}, header.Headers |
| Domain filtering | Block or bypass domains via regex matchers | HTTPProxyConfig.DenyDomains, .DirectDomains |
| Time-frame access control | Allow traffic only during specified weekdays/hours | ruleset.TimeFrameEntry, .AllowTimeFrame |
| Credentials matching | Per-host/port/basic auth for proxy and upstream | CredentialsMatcher, HostPortUser |
| DNS connect-to / rebinding | Redirect dial targets to different hosts/ports | DialRedirectFromHostPortPairs, HostPortPair |
| Rate limiting | Bandwidth limits on listener connections | ratelimit.NewListener, SizeSuffix |
| Multiple listeners | Bind proxy to multiple addresses simultaneously | MultiListener, HTTPProxyConfig.ExtraListeners |
| Prometheus metrics | Request latency, errors, dial counts, TLS cert expiry | APIHandler (/metrics) |
type PACResolver interface {
FindProxyForURL(url *url.URL, hostname string) (string, error)
}import "github.com/cymertek/go-forwarder/pac"
cfg := &pac.ProxyResolverConfig{
Script: pacScriptString, // your FindProxyForURL JS function
AlertSink: os.Stderr,
DNSTTL: 5 * time.Minute, // DNS cache TTL (default 3 minutes if zero)
}
resolver, err := pac.NewProxyResolver(cfg, nil /* use default net.Resolver */)
if err != nil { log.Fatal(err) }
// Evaluate proxy choice for a URL — uses pure-Go IsInNet (~217x faster than JS interop)
result, err := resolver.FindProxyForURL(&url.URL{Scheme: "https", Host: "example.com"}, "")
// result = "PROXY upstream1.example.com:8080; DIRECT"
// Direct pure-Go subnet matching (bypasses Goja entirely):
if resolver.IsInNet("10.5.6.7", "10.0.0.0", "255.0.0.0") { ... }For concurrent use, wrap in a pool:
// For concurrent use, wrap in a pool (each call gets its own VM)
pool, err := pac.NewProxyResolverPool(cfg, nil)
// Optionally wrap with logging for diagnostics
resolver := &forwarder.LoggingPACResolver{
Resolver: pool,
Logger: slog.Default().Named("pac"),
}| Function | Upstream (Goja interop) | This fork (pure-Go) | Speedup |
|---|---|---|---|
isInNet() |
JS via Goja runtime | Go net.ParseIP + bitwise math |
~217x |
dnsResolve() |
JS dnsResolve() → Goja interop |
Direct net.Resolver call with cache |
~10-50x |
myIpAddress() |
JS myIpAddress() → Goja interop |
Direct net.InterfaceAddrs() + cache |
~20x |
The pure-Go implementations are registered on the Goja VM as native bindings (isInNetGo, convertAddrGo, isValidIpv6), so existing PAC scripts work without modification. The fork also adds:
- DNS caching: Thread-safe per-resolver cache keyed by hostname with configurable TTL (default 3 minutes).
- Microsoft IPv6 extensions:
isResolvableEx,isInNetEx,dnsResolveEx,myIpAddressEx,sortIpAddressList,getClientVersion— all implemented in Go where possible. - Unified subnet matching:
isInNetGo()handles dotted-decimal masks, CIDR/Nnotation, and IPv6 dotted-suffix addresses in a single pure-Go path.
Use pac.SupportedFunctions() to get the complete sorted list of all available functions (Mozilla standard + Microsoft extensions + pure-Go implementations). See goja/README.md for detailed benchmarks and architecture notes.
Client → HTTP(S) Proxy (forwarder.HTTPProxy)
│
┌───────┴────────┐
▼ ▼
MITM CA PAC Resolver (fork: pure-Go paths)
generates cert FindProxyForURL + IsInNet() ~600ns/op
│ │
▼ ▼
Upstream dialvia chain
Target (HTTP/SOCKS5)
HTTPProxyis not safe for concurrent modification after construction. Configure all settings before callingNewHTTPProxy.ProxyResolverPoolusessync.Poolto recycle Goja VM instances — eachFindProxyForURLcall gets a fresh resolver from the pool, making it safe for concurrent use.CredentialsMatcheris immutable once constructed and safe for concurrent reads.