224 Commits
Author SHA1 Message Date
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.
2026-07-26 20:11:36 +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 6096be44af Merge branch 'master' of github.com:go-gost/x 2026-07-25 15:58:30 +08:00
ginuerzh 6a44b6f018 fix(dialer): don't force pure-Go DNS resolver on Android 2026-07-25 15:44:21 +08:00
ginuerzh edd6e5f3f2 x: add failCodes support for HTTP status-based node fail marking 2026-07-25 14:36:46 +08:00
ginuerzh bc06c7f5da x: wire MetadataFeature (relay v0.7.0) for tunnel recording mode negotiation
Tunnel connectors send record.mode via MetadataFeature in relay requests.
The handler parses MetadataFeature, stores on tunnel during bind, and
propagates to entrypoint services where recorder context applies it
(per-request Gost-Record header still takes precedence).

- connector/tunnel: send record.mode MetadataFeature in Connect/Bind
- connector/tunnel: parse record.mode from metadata config
- handler/tunnel: parse MetadataFeature from relay request features
- handler/tunnel: pass connMetadata through handleBind → pool.SetMetadata
- handler/tunnel: SetMetadata/GetMetadata on ConnectorPool + Tunnel
- handler/tunnel/entrypointsvc: apply pool metadata to recorder context
- deps: bump relay v0.6.2 → v0.7.0
- sniffer_sse.go: whitespace gofmt fixes
2026-07-24 22:19:41 +08:00
ginuerzh d0287998cf feat: record selected node name in HandlerRecorderObject.Node
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.
2026-07-24 14:20:10 +08:00
ginuerzh 8602043e8d x: add BodyJSON matcher and json: rewrite prefix
- 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
2026-07-24 12:28:08 +08:00
ginuerzh 7952b4f55d x: add Gost-Record header for client-controlled recording level
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).
2026-07-17 15:39:07 +08:00
ginuerzh 7b088f8f42 feat(socks5): add Tor RESOLVE/RESOLVE_PTR extension command support (gost#524)
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
2026-07-16 21:10:13 +08:00
ginuerzh f59eac64d9 probe: add cmd probe type with cross-platform shell support
- Add CmdProber to x/internal/probe/cmd.go with shellCmd() for Windows/Unix
- Add cmd probe dispatch in x/chain/node.go (early-return, no transport needed)
- Add Command field to config.ProbeConfig and parseProbeConfig mapping
- Add 5 CmdProber unit tests (success, failure, timeout, not-found, pipeline)
- Add 2 chain integration tests (cmd probe + cmd disabled)
2026-07-16 20:27:21 +08:00
ginuerzh 46fc054ad5 x: implement node-level liveness probe
- x/chain/node.go: StartNodeProbe goroutine — periodic TCP/HTTP probing
- x/internal/probe/probe.go: HTTPProber with status code validation
- x/selector/latency.go: LatencyFilter, LowestLatencyStrategy (generic)
- x/config: ProbeConfig, MaxLatency, parseProbeConfig, lowestlatency strategy
- x/hop/hop.go: Close() cascade to node.Close()
- x/chain/chain.go: Chain.Close() cascade to hop.Close()

Tests: 4 latency selector tests, 4 node probe tests, 4 HTTP prober tests,
1 hop failover integration test.
2026-07-16 20:27:21 +08:00
ginuerzh c5c8f0903c feat(mux): make mux layer pluggable and add Yamux backend
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
2026-07-13 23:12:27 +08:00
ginuerzh a2e2810669 fix(pht): eliminate authorize round-trip, fix close notification and race
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
2026-07-06 22:03:34 +08:00
ginuerzh 2baf376c87 fix(forwarder): apply responseHeader after body rewrite to fix Content-Type precedence
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.
2026-07-05 19:57:54 +08:00
ginuerzh 19be0882db fix(forwarder): handle ? in rewriteURL replacement and empty requestHeader for deletion
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
2026-07-05 14:34:21 +08:00
ginuerzh 4bc3886825 feat(forwarder): record pre-rewrite HTTP originals in recorder object
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.
2026-07-03 20:13:11 +08:00
ginuerzh 52b80b257f feat(forwarder): decode compressed body prefix for BodyRegexp matching
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.
2026-07-03 17:56:18 +08:00
ginuerzh 7b5a92817f feat(routing): add BodyRegexp matcher for HTTP request body matching
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
2026-07-03 17:22:10 +08:00
ginuerzh d024c7786e feat(forwarder): pass direction and uri metadata to Rewriter plugins
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.
2026-07-02 20:12:04 +08:00
ginuerzh d8317d876f fix(forwarder): append SSE \n\n terminator after end phase data, guard against double delim
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.
2026-06-30 20:46:12 +08:00
ginuerzh 7f36d10605 add MaxChunkSize for bounded buffering of chunked non-streaming bodies
- 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)
2026-06-30 11:02:48 +08:00
ginuerzh 5b1ff8330b fix(forwarder): default rewriter to match all content types when type is empty 2026-06-28 22:39:39 +08:00
ginuerzh 3193eebd6c fix(forwarder): resolve SSE rewrite bugs from code review
- 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
2026-06-28 19:59:12 +08:00
ginuerzh c71b16490b refactor(forwarder): attach SSE lifecycle phases to real events, remove synthetic start signal
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).
2026-06-28 19:25:30 +08:00
ginuerzh ae73d1ba61 refactor(forwarder): replace synthetic SSE phase JSON with pure metadata signals
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.
2026-06-28 19:04:48 +08:00
ginuerzh 4c086685b1 feat(forwarder): add sid metadata to non-streaming body rewrite paths
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.
2026-06-28 18:42:58 +08:00
ginuerzh 56c72823cb feat(forwarder): decompress, rewrite, and recompress body when Content-Encoding is set
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.
2026-06-28 14:44:52 +08:00
ginuerzh 2a9e059bc2 feat(forwarder): support SSE streaming body rewrite with unified wrapper
Introduce a unified rewriteBody io.ReadCloser wrapper that handles both
streaming (text/event-stream) and non-streaming body rewrites through
the same code path. SSE events are rewritten individually per-event,
while finite bodies are buffered and rewritten at once. Both
rewriteRespBody and rewriteReqBody are simplified to delegate to this
wrapper.
2026-06-28 13:26:34 +08:00
ginuerzh f63f36805b fix(forwarder): enforce match pattern as content filter when rewriter is set
When a rewrite rule has both match and rewriter set, the body is only
sent to the rewriter plugin if it matches the pattern. A nil pattern
(non-empty match) means unconditional rewrite via the plugin.

Coincidence: fix indentation drift in the previous commit's rewrite
condition blocks (tab alignment was off by one level).
2026-06-27 23:19:54 +08:00
ginuerzh 1b07475bf3 feat(forwarder): support plugin-based rewriter in HTTP body rewrite rules
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
2026-06-27 22:52:17 +08:00
ginuerzh fb0cf72446 fix(forwarder): record post-rewrite URI in HTTP recorder
After RewriteURL mutates req.URL.Path, ro.HTTP.URI still held the
pre-rewrite value because the recorder object was initialized before
the rewrite loop. Update ro.HTTP.URI to match the rewritten path so
the recorded HTTP request reflects the URI actually sent upstream.
2026-06-27 18:50:46 +08:00
ginuerzh ee7b1e9462 fix(handler/socks5): resolve UDP domains via configured resolver to stop DNS leak
SOCKS5 UDP datagrams carrying ATYP=DOMAINNAME were force-resolved via
net.ResolveUDPAddr in udpConn.ReadFrom, which always used the system
resolver and ignored any configured handler resolver (resolver=1.1.1.1),
leaking the query locally. The premature domain→IP conversion also
stripped the hostname before it reached the relay chain, so the exit
node could not resolve DNS itself.

udpConn.ReadFrom now returns a domainAddr for domain targets, letting
the name flow untouched through udp.Relay into the upstream WriteTo.
Chain-backed PacketConns (udpTunConn, udpRelayConn) encode it as
ATYP=Domain and forward to the exit. Direct (no-chain) associations
yield a raw *net.UDPConn that cannot consume a domainAddr, so they are
wrapped with resolvePacketConn, which resolves via hostMapper →
configured resolver → system DNS.
2026-06-27 16:12:36 +08:00
ginuerzh 8fd5200bf1 fix(net): defer DNS resolution to final dialer to preserve domain in proxy chain
Commit 0522ae8 added inline DNS resolution (PreferGo: true) in Resolve(),
resolving domain names to IPs before routing. This caused the SOCKS5
connector to send CONNECT with ATYP=IPv4 instead of ATYP=domain, breaking
chains where the backend (e.g., ssh -D) cannot reach the gost-resolved IP.

Revert the DNS fallback in Resolve() — return the address unchanged so
the original hostname flows through the proxy chain. Instead, add
net.Resolver{PreferGo: true} to the TCP dialer in internal/net/dialer,
which resolves DNS at the point of actual TCP connection — still pure
Go (no cgo thread exhaustion) but after the proxy chain routing is done.

Also rename resovle.go → resolve.go (fix typo).

Fixes go-gost/gost#877
2026-06-25 11:02:36 +08:00
ginuerzh c06ba17916 fix(dialer): skip SO_BINDTODEVICE when interface is specified as IP address
When the user specifies an IP address as the interface (e.g.
interface=10.1.1.1), GOST resolved it to its hosting interface and
called SO_BINDTODEVICE on it — breaking outbound connectivity for
IP aliases on loopback or dummy interfaces (go-gost/gost#785).

The initial fix (caa82f2) only skipped SO_BINDTODEVICE for loopback
interfaces, missing non-loopback dummy interfaces like ip-aliases.

This change adds an isIP return value to ParseInterfaceAddr so callers
can distinguish 'user specified an IP' (intent: source IP binding for
policy routing — skip SO_BINDTODEVICE) from 'user specified an
interface name' (intent: force traffic through that NIC — keep
SO_BINDTODEVICE). The dialOnce method now only calls bindDevice when
bindToDevice is true AND the input was an interface name (!isIP).

The loopback-specific guard in bindDevice is reverted — superseded by
the broader isIP-based fix that covers all interface types.
2026-06-23 21:43:54 +08:00
ginuerzh a32ffd5608 fix(sniffing): pass bypass option to sniffer HandleHTTP/HandleTLS calls
When sniffing is enabled, the sniffer extracts the real domain from HTTP
Host header or TLS SNI and checks bypass rules against it. However, most
handlers were not passing WithBypass(...) to the sniffer, so the sniffer
internal bypass check was always skipped.

Additionally, the TLS-to-HTTP fallback paths in internal util sniffer
and forwarder also omitted bypass when constructing options for the
recursive HandleHTTP call after decrypting TLS.

Add WithBypass to all sniffer call sites that were missing it across
handlers (http, relay, socks4, socks5, ss, sshd, unix) and internal
TLS fallback paths.

Also fix import ordering in handler/http/connect.go.

Fixes go-gost/gost#874
2026-06-23 20:54:26 +08:00
ginuerzh 0522ae8155 fix(net): use pure-Go DNS fallback to prevent thread exhaustion
When no custom resolver or host mapper is configured, xnet.Resolve now
resolves hostnames via Go's native pure-Go resolver (PreferGo: true)
instead of returning the hostname unchanged.

Previously, the unresolved hostname would reach net.Dialer.DialContext,
which on CGO_ENABLED=1 builds used cgo-based DNS — blocking a dedicated
OS thread per concurrent lookup. At scale (1000+ listeners), this
exhausted Go's 10000-thread limit.

Fixes go-gost/gost#550
2026-06-21 22:34:43 +08:00
ginuerzh e814852ca1 fix(sniffing): re-dial upstream on HTTP Host change + surface no-SNI errors
sniffer_http:
When DNS override directs multiple domains to the same proxy IP, the
browser may reuse a keep-alive connection for a different host. The
HTTP keep-alive loop now detects Host header changes and re-dials a
new upstream connection with correct node selection, preventing CDN
errors (Fastly unknown domain, CloudFront 403).

sniffer_tls:
Return a descriptive error when TLS ClientHello has no SNI, instead
of silently returning nil. This makes the connection drop visible in
logs and recorder output.

Fixes go-gost/gost#479
2026-06-21 16:37:47 +08:00
ginuerzh c45d27113e fix(dialer): evict dead SSHD sessions before reuse to prevent channel-open failure (#382)
When an SSH session behind an SSHD dialer dies silently (NAT timeout,
server disconnect, idle TCP drop), IsClosed() may still return false
because the health-check goroutine hasn't detected the failure yet.
Returning this dead cached session to the connector causes:

  ssh: unexpected packet in response to channel open:

Add a Ping(req)-based liveness check before returning a cached session.
If the ping fails, evict the session so the next Dial rebuilds a fresh
connection. This matches the dead-session eviction pattern already used
by the SSH, KCP, QUIC, and ICMP dialers.

Also export the ping-as-health-check as Session.Ping(timeout) so it can
be used by external health probes. Retain the existing ping() helper
unmodified.
2026-06-21 13:02:10 +08:00
ginuerzh ae92f5aee6 fix(dialer): skip SO_BINDTODEVICE for empty-addr UDP relay sockets
Removes bindDevice from dialOnce's UDP empty-addr path. For relay/listener
UDP sockets, the ListenUDP laddr binding already pins the source IP correctly.
The additional SO_BINDTODEVICE forced all outbound datagrams through the named
interface, which conflicts with the kernel routing table for unconnected UDP
sockets and causes silent packet drops — breaking SOCKS5 UDP associate when
interface is configured.

Fixes go-gost/gost#287
2026-06-20 21:13:49 +08:00
ginuerzh dc31d5c15f fix(dialer): add switchNetns stubs for all non-Linux platforms
The switchNetns function was only defined for linux (dialer_netns.go,
build tag linux && !android) and android (dialer_android.go, stub).
This left darwin, freebsd, openbsd, windows, and other platforms with
an undefined symbol at dialer.go:55.

Add no-op stubs to all remaining platform files so the dialer package
compiles on every GOOS.
2026-06-18 14:33:02 +08:00
ginuerzh a86b5ab66b feat(ss): support none/dummy cipher via no-op AEAD implementation
The go-gost fork of go-shadowsocks2 dropped built-in support for the
none/dummy cipher that was present in the original go-shadowsocks2.
Combined with the SS components' hard requirement for auth (and
therefore a valid cipher), this made gost fail to start with
method=none or method=dummy.

Add a no-op cipher in x/internal/util/ss/none/ that implements the
core.ShadowCipher interface using the standard go-shadowsocks2 AEAD
framing (salt + length-prefixed chunks, target address embedding)
without actual encryption.  SaltSize=16 ensures random salts on every
connection to avoid bloom-ring replay detection.

Update SS TCP connector/handler and SS UDP connector/handler to detect
method=none/dummy (case-insensitive) and use the no-op cipher instead
of utils.NewClientConfig/NewServerConfig.

Fixes go-gost/x#105
2026-06-17 22:18:23 +08:00
ginuerzh 81dc9331b8 fix(dialer): split netns into platform file to support android cross-compile 2026-06-15 16:50:44 +08:00
ginuerzh 3b3eaa609a fix(listener/pht): set X-Accel-Buffering to fix hang behind Nginx (issue #721)
The /pull handler streams data as base64 chunks with a flush per chunk.
With Nginx proxy_buffering on (the default), these tiny chunks and the
heartbeat newlines are buffered and never flushed to the client, so
active data transfer hangs until the buffer fills or the upstream closes.

Set X-Accel-Buffering: no on the pull response so Nginx forwards each
flushed chunk immediately without buffering. Nginx consumes the header
(it is not forwarded to the client) and it is inert when no proxy sits
in front of the listener, so it is safe to set unconditionally.

Covers pht, phts and h3, which share the same Server handler.
2026-06-14 14:15:22 +08:00
ginuerzh 49ceda6cc2 feat(wrapper): add UnwrapConn to remaining TCP conn wrappers
Add UnwrapConn() to the admission, proxyproto, conn limiter, metrics,
and observer/stats TCP connection wrappers, complementing the traffic
limiter's limitConn added in the previous commit.

This makes the unwrapConn() helper in handler/sshd able to peel through
any combination of these wrappers uniformly, keeping the pattern
complete and future-proof for handlers that assert on concrete
connection types.
2026-06-13 14:43:49 +08:00
ginuerzh caa82f2861 fix(dialer): skip SO_BINDTODEVICE for loopback interface (issue #785)
When using IP aliases on loopback (e.g. ip addr add 10.1.1.1/32 dev lo)
with policy routing, findInterfaceByIP() resolves the alias IP to the
loopback interface. Binding the socket to lo via SO_BINDTODEVICE prevents
outbound traffic from reaching non-local destinations.

Skip SO_BINDTODEVICE when the resolved interface is loopback — source IP
binding via LocalAddr is sufficient for policy routing to steer traffic
through the correct physical interface.
2026-06-08 21:36:33 +08:00
ginuerzh e45d9a8cc8 feat: add stateless UDP forwarding mode (#853)
Add stateless=true metadata option to UDP listener/handler for raw
datagram forwarding without per-client session tracking, similar to
NGINX Stream Module behavior.

- udp.NewListener: add Stateless field to ListenConfig; branch in
  NewListener/Accept to skip connPool/listenLoop when stateless
- datagramConn: lightweight net.Conn+PacketConn wrapping a single
  UDP datagram — no channels, mutexes, or pool logic
- forward handler: add handleRawDatagram for single request-response
  cycle; extract dialTarget to share hop selection/dial/proxyproto
  preamble with handleRawForwarding

Usage: gost -L "udp://:10000/127.0.0.1:2000?stateless=true"
2026-06-05 23:26:03 +08:00
ginuerzh f09a7a69b3 fix(sniffing): return early on empty SNI to avoid 'missing port in address' errors (#645)
When TLS ClientHello has no SNI extension (ServerName is empty),
normalizeHost returns empty string. Both sniffer_tls.go paths (sniffing
and forwarder packages) previously still called dial with an empty host,
cascading 'missing port in address' errors through connector, router,
and service layers.

Now returns early with a debug-level log, silently closing the
connection instead of attempting to dial upstream.
2026-06-05 20:46:00 +08:00
ginuerzh 382d47ea12 fix: nil HTTP recorder ref in WebSocket goroutines, add tunnel loop detection
- handler/http/websocket, forwarder/sniffer_ws, sniffing/sniffer_ws: nil
  ro2.HTTP in WebSocket copy goroutines to avoid data races on the shared
  HTTP recorder object
- handler/tunnel/bind: remove ingress rule check that incorrectly
  overrode the endpoint host for non-matching ingress rules
- handler/tunnel/entrypoint: add forwarding loop detection by checking
  Gost-Forwarded-Node header for the entrypoint's own node ID
2026-06-01 18:28:01 +08:00