Edge is an intercepting forward proxy library for Go, designed for building debugging proxies, fault injection tools, traffic recorders, egress gateways, or other applications that need to inspect and control the traffic a client sends over the network. It terminates TLS with certificates issued on demand by a certificate authority the client trusts, and supports HTTP/1.1, HTTP/2, HTTP/3, gRPC, WebSocket, TCP, QUIC and UDP. Edge aims to give every kind of traffic, from an HTTP request to a UDP datagram, its own handler, as simple to write as an HTTP handler.
Edge supports mutation on its routing tree while proxying traffic concurrently. Internally, it uses a Radix Tree adapted from Fox that supports lock-free reads while allowing a concurrent writer. Entries support flexible patterns on the hostname, the port and the path, with named parameters, catch-all wildcards, regexp constraints, address ranges and request matchers. Edge enforces clear priority rules and performs strict validation to prevent misconfigurations.
Warning
Edge is experimental. The API is not stabilized, and breaking changes may occur at any time.
| Traffic | Explicit1 | Transparent2 routed on |
||||
|---|---|---|---|---|---|---|
| HTTP/1.1 | HTTP/2 | HTTP/3 | SOCKS5 | |||
| Over TCP | HTTP HTTP/1.1, HTTP/2 |
request CONNECT |
request CONNECT |
request CONNECT |
CONNECT | SNI, Host or destination |
| gRPC | as HTTP, HTTP/3 included | |||||
| WebSocket | as HTTP/1.1 | |||||
| TCP | CONNECT | SNI, Host or destination |
||||
| Over UDP | HTTP HTTP/3 |
CONNECT-UDP3 DATAGRAM capsules |
request CONNECT-UDP QUIC DATAGRAM frames |
UDP ASSOCIATE | SNI or destination |
|
| QUIC | CONNECT-UDP QUIC DATAGRAM frames |
|||||
| UDP | destination | |||||
- Proxy authentication with HTTP Basic or a custom
ProxyAuthenticator, and with a username and a password on SOCKS5. - Requires Linux with REDIRECT or TPROXY, TPROXY for UDP, or macOS with pf and read access to
/dev/pf. - CONNECT-UDP over HTTP/2 requires
GODEBUG=http2xconnect=1, which enables extended CONNECT in the Go HTTP/2 server.
| Traffic | Direct | Chained | ||||
|---|---|---|---|---|---|---|
http, httpsHTTP/1.1 |
httpsHTTP/2 |
httpsHTTP/3 |
socks5, socks5h |
|||
| Over TCP | HTTP HTTP/1.1, HTTP/2 |
HTTP/1.1, HTTP/2, h2c1 | CONNECT | CONNECT | ||
| gRPC | ||||||
| WebSocket | HTTP/1.1 | |||||
| TCP | TCP or TLS | |||||
| Over UDP | HTTP HTTP/3 |
HTTP/32 | CONNECT-UDP DATAGRAM capsules |
CONNECT-UDP QUIC DATAGRAM frames |
UDP ASSOCIATE | |
| QUIC | QUIC | |||||
| UDP | UDP | |||||
- h2c for gRPC, and for HTTP only when the client used h2c through a tunnel or a transparent listener.
- Opt-in with
Transport.AllowHTTP3.
With a correctly configured Go toolchain:
go get -u github.com/stdapi/edgeThis library requires Go 1.27 or above.
The proxy below intercepts the outbound traffic of a checkout service under test. It refuses a Stripe charge sent without an idempotency key, fails one method of a gRPC inventory service, and throttles the responses of a Redis cache to 64 KiB/s.
package main
import (
"log"
"net/http"
"github.com/stdapi/edge"
)
func main() {
ca, err := edge.LoadCAFile("ca.pem", "ca-key.pem")
if err != nil {
log.Fatal(err)
}
p := edge.Must(edge.WithCA(ca))
// Refuse a charge without an idempotency key, forward the others to Stripe.
p.MustAdd("api.stripe.com/v1/charges", func(c *edge.HTTPCtx) (*http.Response, error) {
if c.Header("Idempotency-Key") == "" {
return edge.Text(http.StatusBadRequest, "missing idempotency key"), nil
}
return c.Forward()
}, edge.WithMethods[edge.HTTP](http.MethodPost))
// Every other Stripe endpoint is forwarded unchanged.
p.MustAdd("api.stripe.com/*{any}", func(c *edge.HTTPCtx) (*http.Response, error) {
return c.Forward()
})
// One gRPC method fails with UNAVAILABLE, the other methods are forwarded.
p.MustAdd("inventory.internal/inventory.v1.Inventory/Reserve", func(c *edge.GRPCCtx) error {
return c.Fail(edge.CodeUnavailable, "inventory unavailable")
})
p.MustAdd("inventory.internal/*{method}", func(c *edge.GRPCCtx) error {
return c.Forward()
})
// Throttle the responses of the cache to 64 KiB/s.
p.MustAdd("cache.internal:6379", func(c *edge.TCPCtx) error {
up, err := c.Dial()
if err != nil {
return err
}
_, _, err = edge.Relay(edge.Throttled(c.Client(), 64<<10), up)
return err
})
go func() { log.Fatal(p.ListenAndServeSOCKS5(":1080")) }()
log.Fatal(p.ListenAndServe(":8080"))
}Apache-2.0. The routing core is adapted from Fox, under the same license.