Groxy is a small Go library for building forward proxy servers.
Status: Groxy is currently pre-v1. The API is usable, but breaking changes may still happen before a stable
v1.0.0release. See the roadmap for planned work.
It supports:
- HTTP request forwarding
- HTTPS tunneling with
CONNECT - opt-in HTTPS inspection with local TLS interception
- middleware hooks for requests, responses, and CONNECT tunnels
- request/response blocking
- header helpers
- request/response body transforms
- configurable timeouts
- configurable logging
Install
go get github.com/SalzDevs/groxy
Basic usage
package main import ( "log" "github.com/SalzDevs/groxy" ) func main() { proxy, err := groxy.New(groxy.Config{ Addr: "127.0.0.1:8080", }) if err != nil { log.Fatal(err) } log.Printf("proxy listening on %s", proxy.Addr()) if err := proxy.Start(); err != nil { log.Fatal(err) } }
Test it with:
curl -x http://127.0.0.1:8080 http://example.com curl -x http://127.0.0.1:8080 https://example.com
Middleware
Groxy middleware can inspect, modify, or block traffic.
if err := proxy.Use( groxy.AddRequestHeader("X-Groxy-Request", "true"), groxy.AddResponseHeader("X-Groxy-Response", "true"), ); err != nil { log.Fatal(err) }
You can also use hooks directly:
if err := proxy.OnRequest(func(ctx *groxy.RequestContext) error { ctx.Request.Header.Set("X-From-Groxy", "true") return nil }); err != nil { log.Fatal(err) }
Named functions work too:
func logRequest(ctx *groxy.RequestContext) error { log.Printf("request: %s %s", ctx.Request.Method, ctx.Request.URL.String()) return nil } if err := proxy.OnRequest(logRequest); err != nil { log.Fatal(err) }
Blocking traffic
Use groxy.Block inside hooks:
if err := proxy.OnRequest(func(ctx *groxy.RequestContext) error { if ctx.Request.URL.Hostname() == "blocked.example" { return groxy.Block(403, "blocked by policy") } return nil }); err != nil { log.Fatal(err) }
Or use built-in helpers:
if err := proxy.Use( groxy.BlockHost("blocked.example", 403, "blocked by groxy"), groxy.BlockConnectHost("blocked.example", 403, "CONNECT blocked by groxy"), ); err != nil { log.Fatal(err) }
Body transforms
Groxy can transform HTTP request and response bodies.
if err := proxy.Use(groxy.TransformRequestBody(func(body []byte) ([]byte, error) { return bytes.ReplaceAll(body, []byte("secret"), []byte("[redacted]")), nil })); err != nil { log.Fatal(err) }
if err := proxy.Use(groxy.TransformResponseBody(func(body []byte) ([]byte, error) { return bytes.ReplaceAll(body, []byte("Example Domain"), []byte("Groxy Domain")), nil })); err != nil { log.Fatal(err) }
Body helpers and body transform middleware buffer the full body in memory. Groxy
limits how much data they can read with Config.MaxBodySize.
proxy, err := groxy.New(groxy.Config{ Addr: "127.0.0.1:8080", MaxBodySize: 5 << 20, // 5 MiB })
If MaxBodySize is zero, Groxy uses DefaultMaxBodySize.
By default, HTTPS traffic uses CONNECT tunneling. Encrypted HTTPS bodies can only be inspected or transformed when HTTPS inspection is explicitly enabled.
HTTPS inspection
Groxy can inspect selected HTTPS traffic using local TLS interception/MITM. This is opt-in only. Without this config, HTTPS traffic is tunneled normally and Groxy cannot read encrypted request or response bodies.
Only inspect traffic you own or are authorized to inspect. Users must install and trust your Groxy CA certificate in their browser or operating system.
ca, err := groxy.LoadCAFiles("groxy-ca.pem", "groxy-ca-key.pem") if err != nil { ca, err = groxy.NewCA(groxy.CAConfig{ CommonName: "Groxy Local CA", ValidFor: 365 * 24 * time.Hour, }) if err != nil { log.Fatal(err) } if err := ca.WriteFiles("groxy-ca.pem", "groxy-ca-key.pem"); err != nil { log.Fatal(err) } } proxy, err := groxy.New(groxy.Config{ Addr: "127.0.0.1:8080", HTTPSInspection: &groxy.HTTPSInspectionConfig{ CA: ca, Intercept: groxy.MatchHosts("example.com", "*.example.com"), }, })
Trusting the Groxy CA
CA.WriteFiles("groxy-ca.pem", "groxy-ca-key.pem") writes the public CA
certificate and private key separately. Install only groxy-ca.pem on client
devices; keep groxy-ca-key.pem private.
Common trust-store setup:
- Firefox: Settings → Privacy & Security → Certificates → View
Certificates → Authorities → Import, select
groxy-ca.pem, then enable trust for websites. - Chrome/Chromium: Chrome uses the operating system trust store on macOS and
Windows. On Linux, import the CA into the NSS database used by Chromium-based
browsers, for example with
certutil -A -d sql:$HOME/.pki/nssdb -n "Groxy Local CA" -t "C,," -i groxy-ca.pem. - macOS: Open Keychain Access, import
groxy-ca.peminto the System or login keychain, open the certificate, and set Trust → Secure Sockets Layer (SSL) to Always Trust. - Windows: Run
certmgr.mscor Manage User Certificates, then importgroxy-ca.peminto Trusted Root Certification Authorities → Certificates. - Linux system trust: Copy
groxy-ca.pemto the distribution's local CA directory and refresh trust, for example/usr/local/share/ca-certificates/groxy-ca.crtwithupdate-ca-certificateson Debian/Ubuntu, or/etc/pki/ca-trust/source/anchors/groxy-ca.pemwithupdate-ca-truston Fedora/RHEL.
Restart the browser or application after importing the certificate. Remove the CA from the trust store when you no longer need HTTPS inspection.
After enabling inspection, normal middleware works on matched HTTPS traffic:
if err := proxy.Use(groxy.TransformResponseBody(func(body []byte) ([]byte, error) { return bytes.ReplaceAll(body, []byte("Example Domain"), []byte("Groxy Domain")), nil })); err != nil { log.Fatal(err) }
Host matching helpers:
groxy.MatchHosts("example.com", "*.example.org") groxy.MatchAllHosts() // explicitly inspect every CONNECT host
Current HTTPS inspection limitations:
- intercepted client traffic is HTTP/1.1 over TLS
- users must trust the generated CA manually
- generated per-host certificates are kept in memory and renewed before expiry
Timeouts
If no timeouts are provided, Groxy uses safe defaults.
proxy, err := groxy.New(groxy.Config{ Addr: "127.0.0.1:8080", })
You can override only the values you care about:
timeouts := groxy.DefaultTimeouts() timeouts.Dial = 2 * time.Second proxy, err := groxy.New(groxy.Config{ Addr: "127.0.0.1:8080", Timeouts: &timeouts, })
Logging
Groxy is silent by default. Pass a logger if you want logs:
logger := log.New(os.Stdout, "groxy: ", log.LstdFlags) proxy, err := groxy.New(groxy.Config{ Addr: "127.0.0.1:8080", Logger: logger, })
Examples
See:
Roadmap
See ROADMAP.md for planned work and good first issue ideas.
Contributing
Contributions are welcome. See CONTRIBUTING.md for setup,
testing, and pull request guidelines.
Security
Please do not report security vulnerabilities in public issues. See
SECURITY.md for responsible disclosure guidance.
Changelog
See CHANGELOG.md for release history.
Development
Run tests:
go test ./...Run race tests:
go test -race ./...Run benchmarks:
go test -bench=. -benchmem ./...Benchmarks cover HTTP forwarding, middleware overhead, body transforms, blocking, and CONNECT tunneling. Results depend on your machine, Go version, OS, and network environment, so treat them as local performance baselines rather than universal numbers.
Run vet:
go vet ./...
License
Groxy is released under the MIT License.
Current limitations
- HTTPS traffic is tunneled by default; inspection requires explicit HTTPS inspection config and a trusted local CA.
- Body transforms buffer the full body in memory.
- HTTPS inspection currently targets HTTP/1.1 over TLS.
- No authentication helpers yet.
- No metrics/observability helpers yet.




















