The rtcp listener now stores the address returned by Router.Bind() separately
from the requested bind address. This allows callers to distinguish between
the requested prefix (may be rejected by the relay) and the actual server-
assigned address (used for the entrypoint URL).
Adds a 3-parameter form to Header(), Query(), and BodyJSON() matchers
for numeric comparisons: Header("Content-Length", "gt", "1024"). Extracts
the duplicated comparison setup and dispatch into shared compareOp and
parseCompareParams helpers. Includes tests for all six operators across
header, query, and body JSON matchers.
Populate the existing Node field on HandlerRecorderObject after every
hop.Select() call in forwarder code paths. Previously the field was
never set, though it was already consumed by tunnel entrypoint HTTP
handler (ephttp.go:145) and tested for round-trip in recorder tests.
- 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
Supports three modes via Gost-Record request header:
- (absent)/full: full recording with body (existing behavior)
- headers: metadata only, skip body/payload capture
- off: no recording at all
Header is stripped before forwarding. Per-request granularity
via HandlerRecorderObject.RecordMode field propagating through
the ro clone chain to all sub-paths (WS frames, H2, sniffing).
Collapse the separate anyBackupNode scan and partitionByPriority allocation
into the single candidate loop. Replace the old short-circuit + tiered logic
with four explicit stages:
1. Single-pass candidate scan — priority stats + backup detection
2. Priority short-circuit — top node wins when strictly higher, no backups
3. Uniform fast path — skip sort when all priorities equal (common case)
4. Tiered fallthrough — sort once, feed each priority tier to selector
Removes two helper functions and an extra O(n) allocation. Fixes hasBackup
to use mdutil.GetBool for consistency with BackupFilter.
When nodes match with different priorities (more-specific vs broader
matchers), select only within the highest-priority tier first.
Round-robin / FailFilter still apply within a tier for load balancing.
If the top tier has no surviving node (all dead), fall through to the
next tier, preserving fallback semantics.
ForwardNodeConfig has a Probe field but parseForwarder was missing it
when converting to NodeConfig, silently dropping probe configuration
for forwarder nodes. cmd probe works regardless (no transport needed),
tcp/http probes are limited to the default tcp dialer (plain TCP).
Adds SOCKS5 handler and connector support for Tor-specific RESOLVE (0xF0)
and RESOLVE_PTR (0xF1) commands, enabling tools like torsocks and
tor-resolve to work through GOST when forwarding to a Tor SOCKS5 proxy.
- Define CmdResolve (0xF0) and CmdResolvePTR (0xF1) in x/internal/util/socks
- Add ContextWithSocks5Cmd/Socks5CmdFromContext for per-request command override
- Connector reads context command, sends it upstream, returns resolveConn
- resolveConn wraps net.Conn and exposes resolved addr via Metadata()
- Handler dispatches 0xF0/0xF1 to handleResolve/handleResolvePTR
- Gate behind enableTor metadata (tor=true), reject with CmdUnsupported by default
- 9 unit tests across ctx/, connector/, handler/ packages
Expose a gost_service_status{host,state} gauge with the number of
services per state (ready/running/failed/closed), collected at scrape
time via a registered callback that aggregates over the service
registry. Lets alerting catch dead/failed services without scraping the
config API.
The callback is registered via RegisterServiceStatusFunc and wired by
the app against registry.ServiceRegistry(). The collector registers on
the custom defaultRegistry (which the metrics endpoint serves from),
not the global prometheus registry.
The whitelist comments described the opposite of actual behavior: in
whitelist mode matching addresses go through the proxy chain and others
bypass, not the other way around.
Excludes Go test binaries (*.test), coverage output, generated certs,
stray Node tooling, and index/IDE/agent dirs (.codegraph, .claude,
.remember) so they don't get committed.
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
When sniffing is enabled, the redirect handler dials the sniffed hostname (HTTP Host / TLS SNI). Add an opt-in metadata flag sniffing.dialOriginalDst that dials the original transparent-redirect destination address instead, while sniffing still runs for hostname-bypass matching and recording.
IP/CIDR bypass rules were silently ignored when sniffing was enabled because the sniffer's internal bypass only matches sniffed hostnames. The post-sniffing dstAddr check was unreachable since HandleHTTP/HandleTLS return early.
serverConn.Close() called limiter.Allow(-1) on every invocation. Close
is routinely called more than once (deferred + explicit teardown paths),
so each extra call leaked a limiter slot, progressively under-counting
the current connection count until the climiter cap admitted more
connections than configured.
Guard the release behind an atomic.Bool CAS so the slot is returned
exactly once, regardless of how many times Close is called.
Go's crypto/tls (1.24+) appends post-quantum hybrid curves
(X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024) to the
default CurvePreferences. toUTLSCurveIDs blindly cast each
crypto/tls.CurveID to utls.CurveID, but the two enums diverge:
utls v1.8.x does not define the two SecP* hybrids, so the cast
produces unsupported curve IDs and the handshake aborts with
"tls: CurvePreferences includes unsupported curve".
Translate only the four curves utls understands and drop the rest;
return nil when none survive so utls falls back to the fingerprint's
defaults. This is the curve half of #887 (the unsafe cast that
dropped ServerName/InsecureSkipVerify was the first half).
The utls dialer built its config with
(*utls.Config)(unsafe.Pointer(d.options.TLSConfig)), but crypto/tls.Config
and utls.Config are distinct types whose field offsets diverge from
ServerName onward (ServerName: 128 vs 136, InsecureSkipVerify: 160 vs 168).
The cast read garbage for ServerName/InsecureSkipVerify, so
InsecureSkipVerify set by secure:false came back false and the handshake
failed with 'at least one of ServerName, InsecureSkipVerify or
InsecureServerNameToVerify must be specified'.
Replace the cast with toUTLSConfig, which copies fields explicitly and
converts the types that differ between the two packages (Certificate,
ClientAuth, CurveID, RenegotiationSupport, and the VerifyConnection
callback's ConnectionState).
Refs go-gost/gost#887.
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
Response header overrides were applied before rewriteRespBody, so overriding
Content-Type caused the body rewriter to see the override instead of the
original upstream value, breaking the streaming-vs-non-streaming decision.
Move responseHeader and ro.HTTP.Response capture after the body rewrite.
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
Extracts the verified mTLS peer certificate (CN, SANs, SHA-256
fingerprint) from the TLS connection in the HTTP handler after the
handshake completes, stores it in the request context, and forwards
it to both gRPC and HTTP auth plugins via new AuthenticateRequest
fields (client_cn, client_san, client_cert_fingerprint).
Capture client-sent Host/URI/headers/body before node HTTP rewrites are applied, so the recorder sees both the original and forwarded values. Adds a borrowBodyPrefix helper that inspects a body prefix without consuming the stream. Also serializes file recorder writes unconditionally (the underlying io.WriteCloser is not safe for concurrent Write) and drops a debug log from the bodyRegexp matcher.
The forwarded body keeps its original encoding; only the prefix shown to
body matchers is decoded so BodyRegexp sees plaintext regardless of
Content-Encoding. Best-effort: a truncated compressed stream yields
whatever decoded so far, where leading JSON fields live.
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
Add udp.bindRange.min/max (udp.minPort/maxPort) metadata to constrain the
client-facing UDP relay socket to a fixed port set, so NAT routers can
forward a small range to the SOCKS server. listenPacketInRange scans
ports from a random offset within [lo, hi] and retries on EADDRINUSE;
with no range configured, the OS chooses (port 0) as before. RFC 1928
leaves relay-port selection to the server, so BND.PORT reports the
actually-bound port.
Expose HTTP request direction (request/response) and request URI to
Rewriter plugins via metadata, alongside the existing sid. Consolidate
sid/direction/uri assembly into baseMetadata helpers and remove the now
unused sidMetadata function.
eventsource_stream requires \n\n to release each SSE event. The end phase
(EOF and scanner-error paths) wrote rewritten data without the terminator,
so response.completed was buffered and never dispatched. Adds writeSSEStreamDelim
helper which only appends \n\n when the data doesn't already end with it.
Adds scanner error end phase path that was missing entirely.
When handler/relay bindUDP and handler/socks5 udpTun create a raw *net.UDPConn,
domainAddr targets from udpTunConn.ReadFrom cannot be consumed by WriteTo (which
expects *net.UDPAddr). Wrap with resolvePacketConn BEFORE metrics/stats wrappers
so domain targets go through hostMapper → configured resolver → system DNS
instead of failing or resolving via system resolver.
Also wrap handler/http handleUDP for the same reason.
Fixes gost#882 part 2 (relay exit server side DNS leak).
- 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)
- Replace recursive b.Read(p) with for loop to prevent O(N) stack growth
on consecutively filtered SSE events
- Skip Pattern gating for end-of-stream signal so Rewriter plugins with
match patterns still receive the lifecycle signal
- Cache sid once in rewriteBody struct instead of extracting from context
on every Read call
- Add event_index to end-phase metadata for consistency with start/event
phases
The first SSE event now carries sse_phase:"start" (detected via eventIndex==0)
instead of a separate apply(nil, start) call before any real events. This
avoids both synthetic JSON bodies and metadata-only empty-body calls for
the start phase, while preserving the end-of-stream apply(nil, end) signal
needed for trailing events like message_stop.
Also adds comprehensive doc comments explaining the three processing modes
(non-streaming plain, non-streaming compressed, SSE streaming with lifecycle).
SSE start/end phase signals now use nil body + metadata-only RewriteOption
instead of synthetic JSON events injected into the data stream. The Rewriter
decides what to produce from the phase signal — nothing enters the stream
unless the plugin explicitly returns data.
The apply() guard is relaxed from `len(body) == 0` to
`len(body) == 0 && len(opts) == 0` so that metadata-only phase signals
reach the Rewriter. Event-level metadata (sse_phase, event_index, sid)
is retained since it annotates real events without injecting fake data.
Pass session ID metadata to plugin Rewriters in non-streaming rewrite
paths (plain and compressed) for consistency with the SSE streaming path.
Also restore SSE lifecycle metadata (start/end phase emission, event index
tracking) and fix a missing Rewriters config merge in parser.
The non-streaming paths now carry sid metadata via apply(), while the SSE
path provides sid, sse_phase (start/event/end), and event_index for each
stream event.