Implements the cache.Cache interface with in-memory backend (TTL,
maxSize/maxBytes bounds, oldest/LRU eviction, background cleanup).
Adds httpcache layer for HTTP response policy (per-status TTL,
serve-stale, method/body filtering). Wires into config parser,
loader, registry, and all four sniffing handlers (forward/local,
forward/remote, sni, http) so HTTP responses are cached before
dialing upstream.
Bumps core dependency to v0.6.0 for the new cache.Cache interface.
- routing: add BodyJSON(path, regexp) matcher using gjson for JSON path
field lookup instead of regex against raw body bytes
- config: detect 'json:<path>[=regex]' prefix in Match field for
rewriteRequestBody/rewriteResponseBody, using gjson query + sjson set
- forwarder: add shouldApply() helper for rewrite type matching; JSON
rewrites match only application/json content type
Convert the concrete *mux.Session into a mux.Session interface backed by
smux and yamux, selected via the `mux.type` metadata key. Empty/unspecified
defaults to "smux", so existing deployments are unchanged. Yamux's stream
window is exposed via `mux.maxStreamWindow`.
Also fix TestHandler_observeStats: the retry_after_observer_error subtest
hung because stats.Events() drains (IsUpdated is Swap(false)) and no fresh
event was generated for the recovery tick's fall-through flush. Inject a real
client2 event so the recovery tick has new data to flush.
Plan: docs/plans/2026-07-13-mux-pluggable-yamux.md
perf(pht): eliminate /authorize HTTP round-trip by self-generating CID
Client now uses xid to generate connection IDs, removing one RTT per
tunnel setup. Server lazily creates pipe pairs in handlePush/handlePull
via getOrCreateConn helper, keeping backward compat for old clients
that still call /authorize.
fix: send close notification before canceling context
Close() was canceling c.ctx before calling write(nil). With a canceled
context, http.Client.Do returns immediately without network I/O, so
the close notification POST never reached the server. Swap the order.
fix: add Flush() after heartbeat write in handlePull
w.Write() on http.ResponseWriter goes to a 2k bufio.Writer. Without a
Flush(), the keepalive newline never reaches the wire, causing the
client long-poll GET to time out.
fix: use LoadOrStore in createPipe to prevent concurrent push/pull race
When push and pull HTTP requests arrive concurrently for a new CID,
both saw !ok from Load() and both called createPipe(), overwriting
the first conns entry. The losing pipe pair leaked and the listener
accepted a phantom connection. LoadOrStore makes the first Store win;
the second caller gets the existing pipe.
fix: close conn and delete entry on empty-data POST in handlePush
A POST body of "\n" produced empty data after TrimSuffix, leaking the
pipe pair and conns entry. Now always cleans up before returning.
fix: use caller ctx instead of context.Background() in Dial
Dial(ctx, addr) was discarding the caller context, so deadlines and
cancellation did not propagate to the HTTP long-poll requests.
fix: detect ListenAndServe failure in Init() and clean up goroutine
Non-blocking select on errChan after starting the server goroutine
surfaces immediate failures (port conflict, TLS error) instead of
leaking a goroutine blocked on server.Accept().
chore: remove dead AuthorizePath from Client struct and dialer wiring
Client no longer calls /authorize; the field and its metadata parsing
in dialer/pht, dialer/http3 are dead code.
test: add unit tests for createPipe race, close notification,
empty-data cleanup, and queue-full fallback
fix(hop): document Select() stages with inline comments
build(deps): bump plugin v0.4.0 → v0.5.0
- rewriteURL: split replacement at '?' so query portion goes to RawQuery
instead of being percent-encoded as %3F in Path
- requestHeader: empty value now deletes the header instead of setting empty
- hop: add stage-by-stage doc comments to Select() method
Lets a node match routing rules against a size-capped prefix of the
HTTP request body. The forwarder HTTP sniffer reads the prefix before
node selection when any node in the hop opts in via matcher.bodySize
(capped at MaxMatcherBodySize = 1MB), restores the stream, and passes
the prefix through hop.Select to the matcher.
- routing: BodyRegexp predicate; Body field on routing.Request
- chain/hop: plumb Body through SelectOptions
- config: matcher.bodySize on NodeFilterConfig
- sniffer_http: aggregate max body size across hop nodes, read+restore
- registry: NodeList wrapper forwards Nodes() so body-size is visible
for named hops
- bump go-gost/core to v0.5.3
- HTTPBodyRewriteConfig gains maxChunkSize field (YAML: maxChunkSize)
- newRewriteBody replaces the unconditional skip of chunked non-streaming
bodies with bounded buffering: reads up to maxChunkSize+1 bytes,
rewrites if fits, passthrough if overflows
- Default maxChunkSize is 1MB when no rule sets one
- Compressed chunked bodies: decompress → rewrite → recompress when fits
- rewriteRespBody/rewriteReqBody clear Transfer-Encoding only when
Content-Length is set (fixes latent stale chunked header bug)
Previously, HTTP body rewrite was silently skipped when the response had
a Content-Encoding (gzip, deflate, br, zstd). Now the body is decompressed,
rewritten, and recompressed transparently, making rewrites work with
production servers that serve compressed content.
Supports gzip and deflate via stdlib, br via andybalholm/brotli, and
zstd via klauspost/compress/zstd. SSE + compressed still skipped as
compression is never used with streaming event-stream.
Each rewriteBody/rewriteResponseBody/rewriteRequestBody rule can now
optionally reference a named rewriter plugin (HTTP/gRPC backend) via
the rewriter field. When set, body rewriting delegates to the plugin
rather than applying pattern/replacement, while content-type filtering
still applies.
Coincident fixes:
- rewriter/plugin/grpc: return nil when conn is nil (avoid nil-ptr panic)
- rewriter/plugin/http: drain response body in defer to prevent leaks
- config/parsing/rewriter: pass TimeoutOption to gRPC plugin
- config/parsing/service: warn when referenced rewriter is not registered
Add the first implementation of core/rewriter.Rewriter as a plugin-only
component following the bypass/admission pattern (single-value, no
RewriterObject). Includes:
- plugin/rewriter/proto/: protobuf definition with Rewrite RPC returning
transformed data in the reply
- x/rewriter/plugin/: gRPC and HTTP plugin clients
- x/registry/rewriter.go: hot-reload-safe RewriterRegistry with wrapper
- x/config/config.go: RewriterConfig, Config.Rewriters, ServiceConfig.Rewriter
- x/config/parsing/rewriter/parse.go: config parser (plugin backends only)
- core/handler/option.go: Rewriter field + RewriterOption on handler.Options
- x/config/loader/loader.go: rewriter registration during startup
- x/config/parsing/service/parse.go: inject rewriter into handler options
Add Network() matcher to the routing DSL so nodes can be selected based on
connection network type (tcp/udp). Uses prefix matching so Network("tcp")
matches tcp, tcp4, tcp6 and Network("udp") matches udp, udp4, udp6.
- core/routing: add Network field to Request struct (core v0.4.3)
- x/routing/matcher: add network() matcher registered as "Network"
- x/hop: pass options.Network to routing.Request in Select()
Fixesgo-gost/gost#703
The standard Prometheus ProcessCollector uses procfs (Linux-only /proc),
so process_resident_memory_bytes and process_virtual_memory_bytes were
silently absent on FreeBSD, Darwin, and other non-Linux platforms.
- Switch to a custom prometheus.Registry to isolate GOST metrics from
the default registry auto-registrations
- Add gopsutil/v3-based process collector (process_other.go) for
platforms without procfs (!linux && !windows)
- Keep standard ProcessCollector (process_standard.go) on Linux/Windows
- Update metrics service and handler/metrics to serve from
promhttp.HandlerFor(customRegistry) instead of promhttp.Handler()
- Add Registry() accessor for the custom registry
Fixesgo-gost/gost#509
Add a new "utls" dialer type that uses the refraction-networking/utls
library to mimic browser TLS fingerprints (Chrome, Firefox, Safari, etc.).
- x/dialer/utls/dialer.go: Registered as "utls", mirrors the TLS dialer
with Handshake() using utls.UClient() for fingerprint emulation.
- x/dialer/utls/fingerprint.go: Maps string names (chrome, firefox, ios,
safari, edge, randomized, golang, custom) to utls.ClientHelloID presets.
- x/dialer/utls/metadata.go: Parses fingerprint from dialer metadata.
- go.mod: Add github.com/refraction-networking/utls v1.8.2.
The fingerprint is dialer-only metadata — the standard TLS dialer is
untouched. Falls back to crypto/tls if no fingerprint is configured.
Closesgo-gost/gost#31
Drop the external github.com/sirupsen/logrus dependency in favor of Go's
standard log/slog package (available since Go 1.21). The core/logger.Logger
interface is preserved unchanged — this is a pure implementation swap.
Key changes:
- slogLogger replaces logrusLogger, backed by slog.Logger + slog.LevelVar
- Custom levelTrace (-8) and levelFatal (+12) constants extend slog's
built-in 4 levels to cover the 6-level logrus-compatible range
- replaceAttr normalises output: lowercase level names and RFC 3339
timestamps with milliseconds (matches previous logrus JSON format)
- Shared *slog.LevelVar ensures WithFields clones inherit parent level
- Level guard (early return) in log/logf avoids wasted fmt.Sprint/Sprintf
when the message level is below the configured threshold
- Documented the stack depth assumption for the caller skip=3 frame
Test coverage (14 tests, all passing with -race):
- convertLevel/revertLevel round-trip for all 6 levels
- levelString for all levels including custom trace/fatal
- replaceAttr timestamp format, level name, groups passthrough
- caller format, level guard, WithFields, IsLevelEnabled, GetLevel
- Add RewriteRequestBody field to HTTPNodeConfig (yaml: rewriteRequestBody)
- Add RewriteRequestBody field to HTTPNodeSettings in core chain
- Add rewriteReqBody function symmetric to rewriteRespBody in sniffer_rewrite.go
- Wire up rewriteReqBody call in httpRoundTrip before body wrapping for recording
- Add new RewriteResponseBody config field, deprecate old RewriteBody
- Parse both RewriteRequestBody and RewriteResponseBody in node config parsing
- Add 10 test cases covering nil, skip, content-type filter, and chained rewrites
- Bump core dependency to v0.4.1
- Fix: clone response header map in internal/util/sniffing/sniffer_http.go
Normalize Only/Prefer values (ip4→ipv4, ip6→ipv6) so downstream
comparisons match correctly. Fix asymmetric fallback where caller
network=ip6 fell through to IPv4 but ip4 didn't fall through to IPv6
— both branches now check hard constraints (server.Only || callerPref)
only, allowing soft Prefer to fall back. Fix data race on cache item.ts
by reading it inside RLock. Fix Store mutating caller's dns.Msg by
copying before TTL override. Add singleflight dedup, semaphore-bounded
async refresh, DNS rcode error checking, and bump EDNS0 UDPSize to
4096. Add 25 tests.
Upgrade github.com/pion/dtls from v2.2.6 to v3.1.1 to fix nonce reuse
vulnerability in AES-GCM ciphers (GHSA-9f3f-wv7r-qc8r). Migrate from
deprecated Config-struct API to ListenWithOptions/ClientWithOptions
options-based API.
v2 Config fields mapped to v3 functional options:
- Certificates, InsecureSkipVerify, ExtendedMasterSecret, ServerName,
RootCAs, ClientCAs, ClientAuth, FlightInterval, MTU
ConnectContextMaker removed — no longer exists in v3; explicit
HandshakeContext(ctx) on *Conn is the replacement for timeout control.
- Bump go-shadowsocks2 to v0.1.3 (new API symbols)
- Return error instead of nil when Target() is empty in TCP handler
- Reset wbuf unconditionally on write error to prevent unbounded growth
- Add nil guard on targetAddr before WriteTo to prevent panic
- Remove duplicate NewClientConfig call and dead ClientConfig literal
- Remove unused noDelay metadata field
- Fix buffer-size guard to catch negative values (len(b)==0 → bufSize<=0)
- Add address context to parse/session error messages
Upgrade github.com/go-gost/core from v0.3.3 to v0.4.0, adapting to
interface changes:
- Admit(ctx, network, addr, opts) now accepts a network parameter
- Router.Dial now accepts variadic chain.DialOption, forwarded to
the route's Dial call along with router-level options
- gRPC/HTTP admission plugins include the new Network field
Also fix two typos: ResoloverNodeOption -> ResolverNodeOption,
WithHostOpton -> WithHostOption.