groxy

package module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2026 License: MIT Imports: 20 Imported by: 0

README

Groxy

Go Reference CI

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.0 release. 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

Try it in 60 seconds

Run the demo proxy:

git clone https://github.com/SalzDevs/groxy.git
cd groxy
go run ./cmd/groxy

In another terminal, send HTTP and HTTPS requests through it:

curl -x http://127.0.0.1:8080 http://example.com
curl -x http://127.0.0.1:8080 https://example.com

You should see requests pass through the local proxy. By default, HTTPS uses normal CONNECT tunneling, so encrypted HTTPS bodies are not inspected unless you explicitly enable HTTPS inspection.

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.pem into the System or login keychain, open the certificate, and set Trust → Secure Sockets Layer (SSL) to Always Trust.
  • Windows: Run certmgr.msc or Manage User Certificates, then import groxy-ca.pem into Trusted Root Certification Authorities → Certificates.
  • Linux system trust: Copy groxy-ca.pem to the distribution's local CA directory and refresh trust, for example /usr/local/share/ca-certificates/groxy-ca.crt with update-ca-certificates on Debian/Ubuntu, or /etc/pki/ca-trust/source/anchors/groxy-ca.pem with update-ca-trust on 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 and guides

Examples:

Guides:

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.

Documentation

Overview

Package groxy provides a small forward HTTP proxy with support for plain HTTP forwarding and HTTPS tunneling through CONNECT requests.

Index

Examples

Constants

View Source
const DefaultMaxBodySize int64 = 10 << 20 // 10 MiB

DefaultMaxBodySize is the default maximum body size read by body helpers.

Variables

This section is empty.

Functions

func Block

func Block(statusCode int, message string) error

Block creates an error that tells Groxy to stop processing and return a response with statusCode and message to the client.

Example
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)
	}

	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)
	}
}

Types

type BlockError

type BlockError struct {
	StatusCode int
	Message    string
}

BlockError represents an intentional block response returned by a hook.

func (*BlockError) Error

func (e *BlockError) Error() string

type BodyTooLargeError added in v0.2.0

type BodyTooLargeError struct {
	Limit int64
}

BodyTooLargeError is returned when a body helper reads more than the configured maximum body size.

func (*BodyTooLargeError) Error added in v0.2.0

func (e *BodyTooLargeError) Error() string

type BodyTransform

type BodyTransform func([]byte) ([]byte, error)

BodyTransform transforms a request or response body.

type CA added in v0.3.0

type CA struct {
	// contains filtered or unexported fields
}

CA is a certificate authority used to sign per-host certificates for HTTPS inspection.

func LoadCAFiles added in v0.3.0

func LoadCAFiles(certFile, keyFile string) (*CA, error)

LoadCAFiles loads a CA certificate and RSA private key from PEM files.

func NewCA added in v0.3.0

func NewCA(config CAConfig) (*CA, error)

NewCA creates a new local certificate authority for HTTPS inspection.

func (*CA) WriteFiles added in v0.3.0

func (ca *CA) WriteFiles(certFile, keyFile string) error

WriteFiles writes the CA certificate and private key to PEM files.

type CAConfig added in v0.3.0

type CAConfig struct {
	// CommonName is the certificate common name. If empty, Groxy uses a default.
	CommonName string

	// ValidFor is how long the generated CA certificate is valid. If zero, Groxy
	// uses a default validity period.
	ValidFor time.Duration
}

CAConfig configures local CA generation.

type Config

type Config struct {
	// Addr is the TCP address the proxy listens on, such as "127.0.0.1:8080".
	Addr string

	// Timeouts controls network timeout behavior for the proxy.
	//
	// If nil, Groxy uses DefaultTimeouts. If provided, zero-valued fields are
	// filled with their default values.
	Timeouts *Timeouts

	// MaxBodySize is the maximum number of bytes BodyBytes and body transform
	// middleware will read into memory.
	//
	// If zero, Groxy uses DefaultMaxBodySize.
	MaxBodySize int64

	// Logger receives log messages from the proxy.
	//
	// If nil, Groxy discards log messages.
	Logger Logger

	// HTTPSInspection enables opt-in HTTPS inspection using local TLS
	// interception.
	//
	// If nil, HTTPS requests use normal CONNECT tunneling and encrypted bodies
	// cannot be inspected or modified.
	HTTPSInspection *HTTPSInspectionConfig
}

Config contains settings used to create a Proxy.

type ConnectContext

type ConnectContext struct {
	Host string
}

ConnectContext contains data available to CONNECT hooks.

type ConnectHook

type ConnectHook func(*ConnectContext) error

ConnectHook is called before a CONNECT tunnel is opened.

type HTTPSInspectionConfig added in v0.3.0

type HTTPSInspectionConfig struct {
	// CA signs generated per-host certificates used for inspected HTTPS traffic.
	CA *CA

	// Intercept decides which CONNECT hosts should be inspected.
	//
	// This field is required when HTTPS inspection is enabled. Use MatchAllHosts
	// in a future release if you explicitly want to inspect every host.
	Intercept HostMatcher

	// CertificateTTL controls how long generated per-host certificates are valid.
	//
	// If zero, Groxy uses a safe default. Generated certificates are kept in
	// memory only and renewed before they expire.
	CertificateTTL time.Duration

	// PassthroughOnError controls whether Groxy falls back to a normal CONNECT
	// tunnel if HTTPS inspection setup fails.
	//
	// The default is false, so inspection failures fail closed instead of
	// silently bypassing inspection policy.
	PassthroughOnError bool
}

HTTPSInspectionConfig configures opt-in HTTPS inspection for CONNECT traffic.

HTTPS inspection uses local TLS interception: Groxy terminates TLS from the client with a certificate signed by CA, then opens its own TLS connection to the upstream server. This allows normal request/response middleware and body transforms to run on selected HTTPS traffic.

If HTTPSInspectionConfig is nil, Groxy keeps the current safe default and tunnels HTTPS traffic without inspecting encrypted request or response bodies.

type HostMatcher added in v0.3.0

type HostMatcher func(host string) bool

HostMatcher decides whether a host should be selected for HTTPS inspection.

The host may include a port, such as "example.com:443". Matchers should normalize hosts as needed before comparing them.

func MatchAllHosts added in v0.3.0

func MatchAllHosts() HostMatcher

MatchAllHosts returns a HostMatcher that matches every host.

func MatchHosts added in v0.3.0

func MatchHosts(patterns ...string) HostMatcher

MatchHosts returns a HostMatcher for exact and wildcard host patterns.

Patterns are case-insensitive. Hosts may include ports. A pattern beginning with "*." matches subdomains only, so "*.example.com" matches "api.example.com" but not "example.com".

type Logger

type Logger interface {
	Printf(format string, args ...any)
}

Logger is used by Groxy to write log messages.

It matches the Printf method provided by the standard library's log.Logger.

type Middleware

type Middleware struct {
	// contains filtered or unexported fields
}

Middleware configures proxy behavior.

func AddRequestHeader

func AddRequestHeader(key, value string) Middleware

AddRequestHeader returns middleware that sets a request header before the request is sent upstream.

func AddResponseHeader

func AddResponseHeader(key, value string) Middleware

AddResponseHeader returns middleware that sets a response header before the response is sent back to the client.

func BlockConnectHost

func BlockConnectHost(host string, statusCode int, message string) Middleware

BlockConnectHost returns middleware that blocks CONNECT tunnels to host.

func BlockHost

func BlockHost(host string, statusCode int, message string) Middleware

BlockHost returns middleware that blocks normal HTTP requests to host.

func OnConnect

func OnConnect(fn ConnectHook) Middleware

OnConnect creates middleware that runs fn before CONNECT tunnels are opened.

func OnRequest

func OnRequest(fn RequestHook) Middleware

OnRequest creates middleware that runs fn before HTTP requests are sent upstream.

func OnResponse

func OnResponse(fn ResponseHook) Middleware

OnResponse creates middleware that runs fn before HTTP responses are sent to the client.

func RemoveRequestHeader

func RemoveRequestHeader(key string) Middleware

RemoveRequestHeader returns middleware that deletes a request header before the request is sent upstream.

func RemoveResponseHeader

func RemoveResponseHeader(key string) Middleware

RemoveResponseHeader returns middleware that deletes a response header before the response is sent back to the client.

func TransformRequestBody

func TransformRequestBody(transform BodyTransform) Middleware

TransformRequestBody returns middleware that replaces a request body with the bytes returned by transform.

Example
package main

import (
	"bytes"
	"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)
	}

	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)
	}
}

func TransformResponseBody

func TransformResponseBody(transform BodyTransform) Middleware

TransformResponseBody returns middleware that replaces a response body with the bytes returned by transform.

func (Middleware) Name

func (m Middleware) Name() string

Name returns the middleware name used in logs and error messages.

type Proxy

type Proxy struct {
	// contains filtered or unexported fields
}

Proxy represents a forward proxy server.

A Proxy can be started as a standalone server with Start, gracefully stopped with Shutdown, or used directly as an http.Handler through ServeHTTP.

func New

func New(config Config) (*Proxy, error)

New creates a Proxy from config.

New validates the config and prepares the internal server/client state, but it does not start listening. Call Start to begin accepting proxy requests.

Example
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)
	}

	_ = proxy
}

func (*Proxy) Addr

func (p *Proxy) Addr() string

Addr returns the configured TCP address the proxy listens on.

func (*Proxy) IsRunning

func (p *Proxy) IsRunning() bool

IsRunning reports whether the proxy server is currently running.

func (*Proxy) OnConnect

func (p *Proxy) OnConnect(fn ConnectHook) error

OnConnect adds a CONNECT hook to the proxy.

func (*Proxy) OnRequest

func (p *Proxy) OnRequest(fn RequestHook) error

OnRequest adds a request hook to the proxy.

Example
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)
	}

	if err := proxy.OnRequest(func(ctx *groxy.RequestContext) error {
		ctx.Request.Header.Set("X-From-Groxy", "true")
		return nil
	}); err != nil {
		log.Fatal(err)
	}
}

func (*Proxy) OnResponse

func (p *Proxy) OnResponse(fn ResponseHook) error

OnResponse adds a response hook to the proxy.

func (*Proxy) ServeHTTP

func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP handles incoming proxy requests.

ServeHTTP allows Proxy to satisfy http.Handler, so it can be mounted on a custom http.Server instead of being started with Start.

func (*Proxy) Shutdown

func (p *Proxy) Shutdown(ctx context.Context) error

Shutdown gracefully stops the proxy server.

Shutdown stops accepting new requests and waits for active requests to finish until ctx is canceled or expires.

func (*Proxy) Start

func (p *Proxy) Start() error

Start starts the proxy server and blocks until the server stops.

Start returns nil when the server is stopped through Shutdown. It returns an error if the server fails to start or stops unexpectedly.

func (*Proxy) Use

func (p *Proxy) Use(middleware ...Middleware) error

Use adds middleware to the proxy.

Middleware must be registered before Start is called or before the proxy is used to serve requests through ServeHTTP. Use returns an error if middleware is added after the proxy has started.

Example
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)
	}

	if err := proxy.Use(
		groxy.AddRequestHeader("X-Groxy-Request", "true"),
		groxy.AddResponseHeader("X-Groxy-Response", "true"),
	); err != nil {
		log.Fatal(err)
	}
}

type RequestContext

type RequestContext struct {
	Request *http.Request
	// contains filtered or unexported fields
}

RequestContext contains data available to request hooks.

func (*RequestContext) BodyBytes

func (ctx *RequestContext) BodyBytes() ([]byte, error)

BodyBytes reads and restores the request body.

HTTP bodies are streams: reading them consumes them. BodyBytes puts the bytes back with SetBody so later hooks and the proxy forwarding logic can read the body again.

func (*RequestContext) SetBody

func (ctx *RequestContext) SetBody(body []byte)

SetBody replaces the request body with body.

type RequestHook

type RequestHook func(*RequestContext) error

RequestHook is called before a normal HTTP request is sent upstream.

type ResponseContext

type ResponseContext struct {
	Request  *http.Request
	Response *http.Response
	// contains filtered or unexported fields
}

ResponseContext contains data available to response hooks.

func (*ResponseContext) BodyBytes

func (ctx *ResponseContext) BodyBytes() ([]byte, error)

BodyBytes reads and restores the response body.

HTTP bodies are streams: reading them consumes them. BodyBytes puts the bytes back with SetBody so later hooks and the response writing logic can read the body again.

func (*ResponseContext) SetBody

func (ctx *ResponseContext) SetBody(body []byte)

SetBody replaces the response body with body.

type ResponseHook

type ResponseHook func(*ResponseContext) error

ResponseHook is called before an upstream HTTP response is sent back to the client.

type Timeouts

type Timeouts struct {
	// Dial is the maximum time allowed to connect to an upstream server.
	Dial time.Duration

	// TLSHandshake is the maximum time allowed for TLS handshakes made by the proxy HTTP client.
	TLSHandshake time.Duration

	// ResponseHeader is the maximum time allowed to wait for upstream response headers.
	ResponseHeader time.Duration

	// IdleConn is the maximum time an unused upstream keep-alive connection stays open.
	IdleConn time.Duration

	// ReadHeader is the maximum time allowed for a client to send request headers to the proxy.
	ReadHeader time.Duration

	// Idle is the maximum time an idle client connection to the proxy stays open.
	Idle time.Duration
}

Timeouts contains timeout settings for client, upstream, and idle proxy connections.

func DefaultTimeouts

func DefaultTimeouts() Timeouts

DefaultTimeouts returns Groxy's default timeout values.

Directories

Path Synopsis
cmd
groxy command
examples
basic command
body-transform command
middleware command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL