Commit Graph
757 Commits
Author SHA1 Message Date
ginuerzh 25cd60d42d fix(connector/ss): own the target addr instead of aliasing the pooled buffer (gost#896)
WrapConn stores the target and only writes it on first I/O, but the
connector encoded it into a bufpool buffer that was returned to the pool
when Connect returned. A later Connect could reuse the backing array and
overwrite the bytes, misrouting the first connection. Allocate the target
per connection instead of sharing a pooled buffer.

Regression test drives two Connect calls and asserts the first conn's
wire target survives a second Connect reusing the pool.
2026-08-21 21:42:43 +08:00
ginuerzh f8ca4908f3 fix(hop): skip failed nodes before priority short-circuit, last-resort fallback
Failed nodes (http.failCodes marker) were still selected when the
priority short-circuit fired or when FailFilter saw a single-node tier,
so failover never happened for a high-priority matcher node returning
429. Check IsFailed first in the candidate-pool scan, and when every
candidate is failed return the first node matching the request gates as
a best-effort last resort instead of nil.

Refactors the fail-code check into shared selector.IsFailed (cee264e)
wired through FailFilterSettingsOption (64ea518).
v0.15.4
2026-08-18 23:42:08 +08:00
ginuerzh cee264e184 refactor(selector): share IsFailed dead-node check with FailFilter
Extract the per-item failed-state decision (failure count >= maxFails,
last failure within failTimeout, metadata overrides) from FailFilter
into an exported IsFailed helper, and reuse it in the hop's
pre-selection dead-node filter instead of a local duplicate.
2026-08-18 23:21:34 +08:00
ginuerzh 64ea5184f8 fix(hop): drop marked-failed nodes before priority selection
http.failCodes (and probe) marking had no effect when the failing node
had a strictly higher matcher priority: the priority short-circuit
returned the top node directly, and FailFilter no-ops on a single-node
tier (len(vs) <= 1 guard), so the marked node was always re-selected
and failover never happened.

Pre-filter nodes marked failed (same Count >= maxFails && within
failTimeout criteria as FailFilter) out of the candidate pool before
priority selection. Single-node hops keep their lone node (safety net),
and an all-failed pool returns nil as before.

Wired maxFails/failTimeout from the hop selector config into the hop
via FailFilterSettingsOption.
2026-08-18 23:15:59 +08:00
ginuerzh fc5fc052e4 chore(deps): bump go-shadowsocks2 to v0.1.4
Pulls in the classic-AEAD UDP [addr][payload] response-framing fix that
restores shadowsocks UDP interop with third-party servers/clients
(go-shadowsocks2, shadowsocks-libev). Fixes go-gost/gost#894.
2026-08-18 22:17:20 +08:00
ginuerzh e6aeba8701 fix(listener/redirect/udp): set SO_REUSEADDR on tproxy listener socket (gost#413)
Match the dialUDP socket so per-packet tproxy sockets can bind a
destination port overlapping this listener without "address already in use".
2026-08-18 21:55:12 +08:00
ginuerzh cc02cff00c fix(logger): deduplicate keys across WithFields chains (gost#895)
slog.Logger.With does not deduplicate keys, so chained WithFields calls
(e.g. service -> listener/handler, each setting "kind") emitted duplicate
JSON keys. slogLogger now rebuilds from a pristine base logger with
accumulated attrs deduplicated by key, last write wins.
2026-08-18 21:49:11 +08:00
ginuerzh 18df26e9bc Merge branch 'master' of github.com:go-gost/x v0.15.3 2026-08-17 23:05:14 +08:00
Vincent Yang a9be7e6dce fix(masque): repair server CONNECT-UDP relay (#122)
The MASQUE handler's CONNECT-UDP path was broken in two ways, so no
standards-compliant client (e.g. Surge) could establish UDP tunnels:

1. Missing Capsule-Protocol response header. HTTPStream() flushes the
   response internally and commits the headers, but it was called before
   Header().Set("Capsule-Protocol", "?1"), so the mandatory RFC 9298
   header was silently dropped and clients rejected the tunnel.

2. WriteTo on a pre-connected socket. A direct UDP dial via the router
   returns a *net.UDPConn that satisfies net.PacketConn but is
   pre-connected; the relay called WriteTo on it, which fails with "use
   of WriteTo with pre-connected connection", so no packet reached the
   target.

Set the Capsule-Protocol header before hijacking the stream (dialing the
target first, matching handleConnectTCP), and always wrap the
router-returned conn so writes go through Conn.Write. Verified with a
standards-compliant HTTP/3 MASQUE client: CONNECT-UDP now returns
Capsule-Protocol: ?1 and datagrams relay end to end; CONNECT-TCP
unaffected.
2026-08-17 23:05:00 +08:00
ginuerzhandgithub 0e0eab0c52 fix(cmd): restore unix socket port-forward semantics
Commit b3b5986 crammed the URL path into the listen/connect address and
disabled forward mode for path-based protocols, breaking the
`unix://a.sock/b.sock` (listen on a, forward to b) port-forward form and
producing an invalid `a.sockb.sock` address.

Distinguish the two URL forms by host presence:
- three-slash (unix:///abs.sock, empty host) → path is the absolute socket
  address, no forward target;
- two-slash (unix://a.sock/b.sock) → host is the socket and the path is a
  forward target (service) or ignored (node).

Add regression tests for both forms.

Co-Authored-By: Claude (claude-opus-4-8[1m])
2026-08-17 22:51:00 +08:00
Dean Chen 98e9651361 cmd: keep absolute unix socket paths for path-based schemes (#121)
Path-based address selection used the dialer/listener type after registry
fallback, so unix remapped to tcp skipped EscapedPath and dropped
addrs like /var/run/docker.sock from three-slash URLs.

Decide path-based protocols from the original scheme components.

Signed-off-by: Dean Chen <862469039@qq.com>
2026-08-17 22:29:17 +08:00
PahaKush e4e36ca095 x: stream websocket reads instead of buffering each frame (#120)
websocketConn.Read called Conn.ReadMessage, which is NextReader plus
io.ReadAll. io.ReadAll starts at 512 bytes and grows by doubling, so every
frame allocated a fresh slice and a chain of intermediate ones: a stream of
64KB frames costs about 2.5 bytes of garbage per byte carried, even though
the caller already supplied a buffer to copy into.

On a relay+wss tunnel moving 750MB this was 2526KB of garbage per MB of
payload and 757 GC cycles in 15 seconds against a 1.5MB live heap. An
allocation profile attributed 99.92% of all process garbage to io.ReadAll
under ReadMessage; a CPU profile spent 15% in memclrNoHeapPointers and ~21%
in the collector, against 8% in AES-GCM.

Use NextReader and copy straight into the caller's buffer. The reader is
kept across calls and dropped at end of message, so a message larger than
the caller's buffer is still delivered across successive reads.

This also fixes a zero-length message surfacing as a (0, nil) read, which
io.Copy counts as no progress and rejects after 100 in a row.

Benchmark on 64KB frames:

    before  39267 ns/op  1668.97 MB/s  138444 B/op  19 allocs/op
    after   19920 ns/op  3289.98 MB/s      80 B/op   3 allocs/op
2026-08-17 22:28:48 +08:00
Niek van der Maas 746811eea5 fix: support SOCKS5 UDP associations over MASQUE (#119)
* fix: support SOCKS5 UDP over MASQUE

* fix: complete MASQUE SOCKS5 proxy support

* fix: isolate MASQUE UDP destinations
2026-08-17 22:28:20 +08:00
ginuerzh 7f72a81864 x: fix SSH authorized_keys parse failing on trailing newline (gost#699)
ParseAuthorizedKeysFile looped over ssh.ParseAuthorizedKey, but x/crypto's
parser splits on '\n' and returns rest='\n' for a trailing newline. The next
iteration called ParseAuthorizedKey('\n'), which skips whitespace and returns
'ssh: no key found', aborting the whole parse. Any normal authorized_keys file
(ends with newline, or has comments/blank lines) failed despite being valid.

Break the loop once rest is exhausted so all keys are parsed.
v0.15.2
2026-08-01 18:24:10 +08:00
ginuerzh 2112532e0a x: set TCP_NODELAY on ws/wss/tls transports to cut relay UDP latency
Disable Nagle on the underlying TCP socket for relay+ws, relay+wss, and
relay+tls listeners/dialers. Small latency-sensitive datagrams (e.g.
WireGuard UDP over a relay tunnel) were being coalesced by Nagle until a
peer ACK returned, adding up to one RTT per datagram and inflating
round-trip latency (go-gost/gost#858). wstunnel sets TCP_NODELAY on its
tunnel socket by default and keeps latency flat; gost did not.

Add ApplyNoDelay helper and WrapNoDelayListener (mirrors ApplyKeepalive),
applied on the raw *net.TCPConn before the proxyproto/tls wrapper chain.
2026-08-01 16:59:54 +08:00
ginuerzh b2d901e70f x: add Redis TCP sniffing with protocol-based routing
Add RESP protocol detection to the sniffing framework so plaintext
Redis traffic on a port-reuse listener can be identified and routed by
protocol. Sniff() now peeks 1 byte first to match RESP type bytes
(*+-:$) without blocking, then falls through to the existing 5-byte
TLS/HTTP/SSH detection.

ParseRedisMetadata consumes exactly one RESP message via io.TeeReader
and records command/key in HandlerRecorderObject.Redis. HandleRedis in
both the sniffing (injected-dial) and forwarder (hop-selected) packages
replays the captured first message and pipes the rest, so pipelined
commands beyond the first are forwarded transparently.

Wired into all 5 dispatch sites: sni, forward/local, forward/remote,
http CONNECT, and relay tunnel handlers. forwarder paths route via
resolveRedisNode using ProtocolSelectOption(ProtoRedis), enabling
`protocol: redis` hop selection.

Part of two-phase plan (2026-07-29-sniffing-redis-postgres.md);
PostgreSQL deferred to phase 2.
2026-08-01 16:03:23 +08:00
ginuerzh b106f764a6 x: bump quic-dissector to v0.1.0, remove merged plan docs
Bump quic-dissector dependency from pseudo-version to tagged v0.1.0.
Remove docs/plans that have been implemented and merged.
2026-08-01 16:03:23 +08:00
ginuerzh edb0b5d651 x: extract shared QUIC sniffing helpers, add redirect UDP QUIC sniff
Extract PopulateQUICClientHello/PopulateQUICServerHello into
internal/util/sniffing/quic.go to deduplicate the QUIC ClientHello/ServerHello
population logic that was copy-pasted across forward/local (Handle, handleRawDatagram)
and redirect/udp handlers (3 sites → 2 helpers).

- handler/redirect/udp: add QUIC ClientHello + ServerHello sniff with
  sniffing/sniffing.timeout metadata, populating ro.Proto/ro.TLS for recording.
- handler/forward/local: replace inline QUIC blocks with shared helpers;
  use h.md.bufferSize instead of hardcoded 8192 for sniff reads.
- internal/util/sniffing: add ProtoQUIC constant.

Bump quic-dissector dependency to v0.1.0.
2026-08-01 16:03:23 +08:00
dependabot[bot] e3e41abd66 build(deps): bump github.com/pion/dtls/v3 from 3.1.1 to 3.1.4 (#118)
Bumps [github.com/pion/dtls/v3](https://github.com/pion/dtls) from 3.1.1 to 3.1.4.
- [Release notes](https://github.com/pion/dtls/releases)
- [Commits](https://github.com/pion/dtls/compare/v3.1.1...v3.1.4)

---
updated-dependencies:
- dependency-name: github.com/pion/dtls/v3
  dependency-version: 3.1.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 15:59:34 +08:00
Anivice IvesandYour Name 221144fab3 fixed the issue where the pull request to exit and subsequent application writes to fail immediately in gost (#117)
Co-authored-by: Your Name <you@example.com>
2026-08-01 15:59:08 +08:00
ginuerzh 6b1c6757f6 x: record cache-hit status in HTTP recorder object v0.15.1 2026-07-29 23:02:18 +08:00
ginuerzh 3157436140 x: add Redis backend for cache
Adds a RedisCache config block and redisCache implementation for the
generic cache.Cache interface, enabling distributed HTTP response
caching across gost instances.

go-redis/redis/v8 was already a dependency (used by recorder and
internal loader) — no new imports needed.
2026-07-29 22:39:19 +08:00
ginuerzh 0ab5363f60 x: distinguish client vs server errors in service handler loop
The service Serve loop counted every handler error — including client-side
disconnects (io.EOF, net.ErrClosed, context cancellation) and policy
decisions (rate limiting, bypass) — as server handler errors, inflating
the error counter and log noise. isServerError() now gates the error-level
log, error counter, and stats increment; non-server errors are logged at
debug level only.
2026-07-29 21:44:12 +08:00
ginuerzh 88670baeae x: don't overwrite ro.Host when TLS SNI is empty
The previous empty-SNI fix clobbered ro.Host with the listener's local
address. For handlers that set ro.Host before HandleTLS (SOCKS5 via the
CONNECT address, SS via target), that destroyed the real destination used
for recording and bypass. Now ro.Host is only set when SNI is present;
the caller's pre-set value is left intact otherwise.
2026-07-29 21:42:56 +08:00
ginuerzh 226420a622 x: allow empty TLS SNI in sniffing handlers
Clients connecting to HTTPS by raw IP (e.g. https://1.1.1.1) don't
send SNI per RFC 6066. Both HandleTLS methods errored immediately
on empty SNI, closing the connection. Now they fall through to the
normal routing path — hop selection in the forwarder, or the caller's
dial closure in the sniffing sniffer (e.g. HTTP CONNECT) — which
handles the empty host gracefully or fails naturally.
2026-07-28 23:52:29 +08:00
ginuerzh 3cd4fad4bf x: close forwarder hop probe goroutines on service shutdown
Same leak class as the previous chainGroup fix: a forwarder built from
a hopGroup or inline nodes starts probe goroutines at parse time, but
nothing closed them — every SIGHUP reload leaked them permanently.

- parseForwarder returns an io.Closer for service-owned hops (hop
  group or inline nodes); registry-owned named hops return nil since
  the registry closes them on unregister
- ParseService registers the closer via ClosersOption and closes it
  when construction fails partway
- hopGroup.Close no longer closes the underlying hops: they come from
  the HopRegistry and are shared with other referents; closing them
  here would kill shared state
2026-07-28 23:45:40 +08:00
ginuerzh 291fa48fae x: add -race test coverage for chainGroup matcher/probe/close paths
Adds tests for the new chainGroup code paths the matcher+probe review flagged
as untested: matcher filtering in selectChain, probe goroutine cleanup on
Close(), and probe marking the entry marker on cmd failure. All run clean under
`CGO_ENABLED=1 go test -race`.
2026-07-28 23:36:08 +08:00
ginuerzh 6e9ff675b5 x: extract duplicated cmd-probe logic into probe.RunCmdProbe
chainGroup and hopGroup had verbatim copies of the cmd-probe block (timeout
default, CmdProber run, marker Mark/Reset, log). Move it to
internal/probe.RunCmdProbe(cfg, marker, log) so both call sites share one
implementation (cleanup5 from the chainGroup review).
2026-07-28 23:31:15 +08:00
ginuerzh 506edffc93 x: skip eligible allocation when no matchers; always run FailFilter
In selectChain: precompute whether any entry has a matcher and, when none do,
pass entries straight to the selector instead of allocating a new slice on every
Route() call. Drop the single-eligible-entry shortcut so the selector's
FailFilter always runs and can exclude probe-failed chains (design6).

Combined fix for efficiency4 (hot-path allocation) and design6 (single-match
bypass of FailFilter/probe markers) from the chainGroup review.
2026-07-28 23:26:54 +08:00
ginuerzh 6248d4c0f8 x: derive probe goroutine context from lifecycle ctx (bug2)
probeEntry used context.Background() for Route/Dial, so an in-flight probe
could not be cancelled by Close()/reload. Thread the lifecycle ctx through
probeEntry so a stuck route aborts when the group is torn down.

Also promote mapstructure to a direct dependency (now imported by config.go).
2026-07-28 23:25:28 +08:00
ginuerzh 50f4a6b096 x: close chainGroup probe goroutines on service shutdown
chainGroup.Close() existed but was never called: the group is wrapped
in a chain.Router held by the listener/handler, and service.Close()
only closed the handler, observer, and listener. Every SIGHUP reload
leaked the old config's probe goroutines permanently.

Wire the close path end to end:
- Router.Close() closes its chain when it implements io.Closer
- service ClosersOption registers io.Closers called on shutdown
- ParseService passes both routers via ClosersOption, and closes
  them on construction failure so probes started early don't leak
2026-07-28 23:14:27 +08:00
ginuerzhandClaude 30ba6d64a5 x: add matcher + probe to chainGroup with independent entry marker
chainGroup now selects chains in two stages: a matcher filter (by host/network)
builds an eligible pool, then the group selector applies FailFilter + strategy.
Each entry carries its own failure marker so chain-level probe marking is
independent of node-level failCodes markers (probe Reset() can't undo a node
failCodes Mark()).

Config gains ChainGroupEntry (chain/matcher/probe) replacing the old []string,
with a mapstructure decode hook preserving backward-compatible "chains: [a, b]"
string lists and time.Duration string parsing. Probe goroutines run per-entry
and mark the entry on failure.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 21:20:34 +08:00
ginuerzh 72db96d4cd x: add hopGroup for conditional hop-level selection with probe/failCodes marker separation
hopGroup wraps named hops with per-entry matcher + probe, implementing
hop.Hop for drop-in compatibility with the existing Forwarder.Forward().

Core design: hopEntry.marker (probe, hop-level) is independent of
node.Marker() (failCodes, node-level) so probe Reset() cannot undo a
failCodes Mark() — the two mechanisms no longer conflict on the single
chain.Node marker.

- hop/group.go: HopEntry + hopGroup + probe infrastructure
- config/config.go: ForwardHopGroupConfig, ForwardHopConfig types
- config/parsing/service/parse.go: parseHopGroup, HopGroup > Hop > Name > Nodes precedence
- config/parsing/node/parse.go: export parseProbeConfig → ParseProbeConfig
v0.15.0
2026-07-27 22:08:34 +08:00
ginuerzh 8edf82af9b hop: fix backup-priority interaction — primary beats backup regardless of priority
Stage 3 shortcut now gates on isBackupNode(maxPriNode) instead of
hasBackup — the old check disabled priority short-circuit whenever any
backup existed anywhere in the pool, even when the winning node was a
primary.

Stage 4 tier loop gains hasAnyPrimary scan + backup-tier skip — a
lower-priority primary always beats a higher-priority backup tier.
Nil-selector path now returns the first node of the first non-skipped
tier instead of unconditionally returning maxPriNode.
2026-07-27 22:06:37 +08:00
ginuerzh 839f2a280b chore: bump deps (tls-dissector v0.3.1, quic-go v0.60.0, webtransport-go v0.11.1) v0.14.2 2026-07-26 22:58:52 +08:00
ginuerzh c23a2f1e53 fix(redirect/tcp): sniffingFallback with empty SNI falls through to raw forwarding
When sniffing detects TLS but the ClientHello has no SNI, HandleTLS
errors with 'tls: sni is empty, closing connection'. Previously this
error was returned unconditionally even with sniffingFallback enabled.

Now, bytes consumed by the sniffer are captured via io.TeeReader and
replayed through io.MultiReader when sniffingFallback is true, allowing
the connection to fall through to raw forwarding to the original dstAddr.
2026-07-26 22:27:44 +08:00
ginuerzh 0ffc000d0d fix(forwarder/sniffer): update ho.recorderObject after ro copy to prevent nil HTTP panic
The HandleHTTP ro clone (486a776) only updated the local ro variable, but
dial() reads ho.recorderObject directly, getting the original with nil HTTP
field, causing nil pointer dereference at ro.HTTP.StatusCode access.
2026-07-26 22:09:55 +08:00
ginuerzh 486a77665d fix: prevent double-recording when HTTP response cache is enabled
The cache-hit path (serveCachedResponse) recorded on the same HandlerRecorderObject
as the calling handler's deferred Record(), producing two records per request.
Copy ro at HandleHTTP entry so the sniffer's internal recording stays isolated
from the caller's defer. Also zero ro.Time when the sniffer takes over, so the
handler's defer Record() skips the now-redundant connection-level record.

Fixes 10 files across sniffer and all 8 handlers that support sniffed HTTP forwarding.
v0.14.1
2026-07-26 20:11:36 +08:00
ginuerzh db3c7a375c chore: update swagger spec v0.14.0 2026-07-26 17:58:12 +08:00
ginuerzh 9aab47bcd2 x/api: add cache CRUD web API endpoints 2026-07-26 17:57:11 +08:00
ginuerzh 3474501784 x: add generic cache component and HTTP response caching
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.
2026-07-26 17:26:00 +08:00
ginuerzh b92c1b130a fix(dialer): propagate request ctx to QUIC OpenStreamSync and align keepalive/idle defaults
The issue #777 analysis was written against v3.2.4; commits 5484717 and
9fab332 already fixed the global mutex stall, the no-timeout OpenStreamSync,
and the missing IsClosed reuse check. This addresses the remaining real
problems:

- P1 (HIGH): quicDialer.Dial received the Router's 15s-timeout ctx but
  GetConn built its own context.Background()+30s, dropping request
  cancellation/deadline. Thread ctx into GetConn so OpenStreamSync honors
  the upstream deadline. Mirrored to the icmp dialer (identical structure).
- P2 (MEDIUM): listener QUIC keepalive defaulted OFF while the dialer
  defaulted ON, so a server behind NAT without an explicit keepalive=true
  could let its return-path mapping expire. Make the listener default ON
  like the dialer.
- P3 (LOW): set an explicit MaxIdleTimeout default (90s) on both dialer
  and listener so dead sessions are detected sooner; still overridable via
  maxIdleTimeout=.

Docs updated to mark the already-fixed root causes and rename the remaining
items P1-P3.

Fixes go-gost/gost#777
2026-07-26 12:51:04 +08:00
ginuerzh 431fe759dc x: matcher.bodySize defaults to DefaultMatcherBodySize (1MB) when unset
bodySize=0 now uses DefaultMatcherBodySize (1MB) instead of disabling
body reading, so BodyRegexp/BodyJSON matchers work out of the box.
MaxMatcherBodySize raised to 10MB. bodySize=-1 opt-out removed
(no real use case).
2026-07-25 21:46:48 +08:00
ginuerzh 4d15ca54af fix: rtcp listener tracks server-negotiated bind address for prefix fallback
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).
v0.13.18
2026-07-25 18:47:16 +08:00
ginuerzh 6096be44af Merge branch 'master' of github.com:go-gost/x 2026-07-25 15:58:30 +08:00
ginuerzh bf6f7713d8 chore: bump github.com/go-gost/core to v0.5.5 v0.13.17 2026-07-25 15:55:17 +08:00
ginuerzh 6a44b6f018 fix(dialer): don't force pure-Go DNS resolver on Android 2026-07-25 15:44:21 +08:00
ginuerzh 641536de3d docs: add fail-codes design plan 2026-07-25 14:36:59 +08:00
ginuerzh edd6e5f3f2 x: add failCodes support for HTTP status-based node fail marking 2026-07-25 14:36:46 +08:00
ginuerzh 88b03e0721 x: add numeric comparison operators (gt/ge/lt/le/eq/ne) to Header/Query/BodyJSON matchers
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.
2026-07-25 13:43:17 +08:00