Handlers write one recorder entry when a session ends, so long-lived
SOCKS5 tunnels, streaming HTTP responses and UDP associations stay
invisible while they run and report their traffic as one late record.
With recorder.period set, a session now emits interim records on the
interval while it is active and one final record when it ends. Each
record carries the cumulative counters alongside the increments since the
previous one, so a consumer sums records instead of replacing them, plus
sessionID, recordIndex and phase. A session that ends inside one period
still writes a single record, as it does today.
One goroutine per handler samples every live session on the tick and
writes its record straight to the recorder. There is no queue and no
retry loop, and none is needed: a session moves its cursor past a record
only once the recorder has taken it, so a refused write costs no bytes.
They stay in the delta and go out with the next record. Holding on to
what the sink cannot take right now is the sink's job.
Interim and Finish take a context and return an error, so a handler
learns what became of every record it asked for. Start writes nothing, so
it takes neither. Close stops the sampling and reports the sessions its
owner had not finished; nothing is buffered, so there is nothing to
drain.
With recorder.period unset or non-positive the previous behaviour is
kept: one record per session, written inline, in the old shape. An object
its handler never stamped, and one whose record mode is off, is dropped
in both modes.
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
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
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.
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).
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.
- selector.go: Select() now only returns methods the client actually offered,
defaulting to MethodNoAcceptable (0xFF) when no mutually-supported method
exists. Prevents protocol desync when authenticator is set but client
doesn't offer UserPass. (#1, #2)
- connect.go: CONNECT success reply now carries the outbound connection's
actual local address as BND.ADDR per RFC 1928 §6, instead of 0.0.0.0:0. (#4)
- udp.go: UDP ASSOCIATE now verifies the upstream chain is reachable before
sending Success to the client; on failure sends a proper Failure reply.
Also adds safe type assertions for net.TCPAddr and net.UDPAddr to prevent
panics on wrapped connections, and returns errors on DNS resolution failure
instead of silently dropping datagrams. (#5, #1, #2, #3)
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.
Fixesgo-gost/gost#874
Add 8-case table-driven test covering nil addr, IPv4/IPv6/domain/unknown
addr types for both tcp and udp networks.
Ref: 03f4b12 (fix(handler): use addr-family-specific network for SOCKS5 BIND)
When the SOCKS5 BIND request carries an IPv4 address (e.g. 0.0.0.0:0),
pass the network as "tcp4"/"udp4" instead of "tcp"/"udp". On Linux,
net.Listen("tcp", "0.0.0.0:0") creates a dual-stack IPv6 socket
binding on [::]:port, so the bound address returned to the client
mismatches the actual listening socket. Same fix for CmdMuxBind and
CmdUDPTun. CmdConnect is left unchanged since dual-stack dialing is
correct for outbound connections.
Add a wrapper that intercepts WriteTo on the
client-facing SOCKS5 UDP relay and resolves domain addresses to IPs
before the SOCKS5 header is encoded. Controlled via the
(or ) handler metadata flag.
When enabled, SOCKS5 UDP response datagrams always carry ATYP=IPv4 or
ATYP=IPv6 — never ATYP=Domain (0x03) — making them compatible with
clients like tun2proxy and Surge that cannot parse domain-typed
addresses in UDP.
The wrapper resolves domains through the router's infrastructure:
host mapper → resolver → system DNS fallback. Unresolvable domains
cause the datagram to be silently dropped rather than forwarded in a
format the client rejects.
Fixesgo-gost/gost#434
The client-facing UDP relay listener (cc) in handleUDP was bound to the
wildcard address, so replies sent from it used the kernel-selected source
IP (the interface's primary IP), which mismatches the BND.ADDR advertised
to the client (the TCP control connection's local IP). Per RFC 1928 6,
compliant clients drop such replies, breaking SOCKS5 UDP associate under
multiple IP aliases where metadata.interface differs from the primary IP.
Bind cc to the TCP control connection's local IP so its reply source IP
always matches BND.ADDR. The outbound relay socket (pc) already binds the
interface IP via Router.Dial -> dialer.ListenUDP(laddr).
The SOCKS5 handler recorder only tracked TCP traffic metrics. UDP
associations (CmdUdp) overwrote TCP byte counts with UDP counts, and
UDP tunnels (CmdUDPTun) recorded zero bytes.
Changes:
- udp.go: change UDP stats assignment (=) to accumulation (+=) so the
TCP control connection bytes tracked by Handle() are preserved.
- udp_tun.go: add xstats.Stats{} wrapper around the PacketConn with
deferred accumulation into HandlerRecorderObject, mirroring the
handleUDP pattern.
- api: add fillServiceStatus helper and call it from getServiceList/getService
so status field appears in service list and detail API responses
- observeStats: unify retry pattern across all 9 handlers (http, http2, masque,
relay, router, socks4, socks5, tungo, tunnel) — buffer events on failure,
continue to skip fresh collection, clear on success; fix event-loss bug
where interim events were dropped during retry cycles
- handler/router: check WriteTo/Write return errors in associate.go and
entrypoint.go; fix DelConnector and ConnectorPool.Del using RLock instead
of Lock (write under read lock); remove unused fields t and cancel
Move SetDefaultTLSConfig before register(cfg) in loader.go so
ParseService can access it when building handlers. Also add a
defensive nil check in socks5 serverSelector.Select() for
MethodTLS to prevent future ordering regressions.
Root cause: in selector.go, Select() tried to read ClientHello
via TLSConfig.GetConfigForClient but s.TLSConfig was nil because
register(cfg) -> ParseService -> handler Init ran before
SetDefaultTLSConfig was called from loader.go.
The readTimeout field (default 15s) was being applied via xnet.Pipe to both
directions of a bidirectional proxy connection. During asymmetric transfers
(e.g. HTTP file download), the direction reading from the tunnel sees no
data after the initial request is forwarded, causing SetReadDeadline to fire
after 15s and abort the entire transfer.
Fix: add a separate idleTimeout field (default 0=disabled) to the metadata
structs in both forward/local and forward/remote handlers, and switch
xnet.Pipe to use idleTimeout instead of readTimeout. The readTimeout field
now only applies to the initial protocol sniffing/handshake phase.
Also document readTimeout vs idleTimeout semantics across all 24 locations
in the x/ module where these timeouts appear:
- readTimeout: handshake sniffing deadline (handlers), upstream response
header timeout (http.Transport), or transport-level read deadline
- idleTimeout: idle read deadline per Pipe direction (0=disabled)
- ReadTimeout on Sniffer/SnifferBuilder: upstream response header/TLS
handshake read timeout during sniffing