|
|
||
|---|---|---|
| ipinfo | ||
| iplimit | ||
| realip | ||
| recoverer | ||
| requestlog | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| README.md | ||
| Taskfile.yml | ||
lightmux-contrib
Opinionated middleware collection for lightmux. Each middleware lives in its own sub-package so consumers only pull in the dependencies they actually use.
Installation
go get git.juancwu.dev/juancwu/lightmux-contrib
Packages
realip
Replaces r.RemoteAddr with the originating client IP from CF-Connecting-IP, True-Client-IP, X-Real-IP, or X-Forwarded-For (in that order).
import "git.juancwu.dev/juancwu/lightmux-contrib/realip"
mux.Use(realip.New()) // always trust headers
mux.Use(realip.New(netip.MustParsePrefix("10.0.0.0/8"))) // gated by peer CIDR
With no arguments, realip.New() always honors the proxy headers — only register it when the service sits behind a trusted proxy. With one or more netip.Prefix arguments, the headers are honored only when the immediate peer's IP falls within one of them.
requestlog
Emits a structured http.request record (method, path, status, duration, client) per request via splinter.
import "git.juancwu.dev/juancwu/lightmux-contrib/requestlog"
mux.Use(requestlog.New(nil)) // splinter.Default() resolved at request time
mux.Use(requestlog.New(custom)) // custom *splinter.Logger
When pairing with realip, register realip first so the client field is the resolved client IP rather than the proxy peer:
mux.Use(realip.New(), requestlog.New(nil))
recoverer
Catches panics inside handlers, wraps the value with errx under op recoverer, logs it with stack via the standard log package, and writes a 500 response.
import "git.juancwu.dev/juancwu/lightmux-contrib/recoverer"
mux.Use(recoverer.New())
ipinfo
Looks up the client IP against the ipinfo.io API via the official Go SDK and attaches the *ipinfo.Core result to the request context. Pair with realip upstream so the lookup uses the originating client IP rather than the proxy peer.
import (
sdk "github.com/ipinfo/go/v2/ipinfo"
"git.juancwu.dev/juancwu/lightmux-contrib/ipinfo"
"git.juancwu.dev/juancwu/lightmux-contrib/realip"
)
client := sdk.NewClient(nil, nil, "YOUR_TOKEN")
mux.Use(realip.New(), ipinfo.New(client, nil))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if info, ok := ipinfo.From(r.Context()); ok {
fmt.Fprintf(w, "hello from %s, %s\n", info.City, info.Country)
}
})
Loopback, private, link-local, and unspecified addresses are skipped to preserve API quota. Lookup failures are logged at warn level via splinter (pass nil for splinter.Default() resolved at request time, or supply a custom *splinter.Logger) and let the request through with no context value — handlers should treat the From lookup as optional.
iplimit
Per-IP token-bucket rate limiter backed by golang.org/x/time/rate. Each unique IP gets its own bucket; rejected requests get a 429 with a Retry-After header (whole seconds, ceiling).
import (
"time"
"git.juancwu.dev/juancwu/lightmux-contrib/iplimit"
"git.juancwu.dev/juancwu/lightmux-contrib/realip"
"golang.org/x/time/rate"
)
// 5 req/s steady state, bursts of 10
mux.Use(realip.New(), iplimit.New(rate.Every(time.Second/5), 10))
Pair with realip upstream so the limiter keys on the originating client IP rather than the proxy peer. Loopback, private, link-local, and unspecified addresses pass through unlimited — they are typically internal callers (health checks, dev) that should not be rate limited. Idle buckets are evicted after 10 minutes of inactivity by an inline sweep, so no background goroutines are spawned.