Only the first entry of config.Net was installed on the wintun adapter,
so dual-stack setups (e.g. net=192.168.123.1/24,fd::2/64) silently
dropped the IPv6 address and IPv6 traffic never worked. Iterate every
configured address and select the netsh context by family (ip for IPv4,
ipv6 for IPv6). A failing address is logged and skipped so one bad
entry no longer aborts the rest or triggers the listenLoop's infinite
recreate-retry. If no address can be configured, return an error
instead of a nil peer IP.
Refs: go-gost/gost#624
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.
The stdio listener serves exactly one connection: the process's own
stdin/stdout, piped from a parent such as SSH ProxyCommand. When that
connection ended, gost lingered forever. The service's accept loop runs
in a fire-and-forget goroutine and the process blocks on go-svc's
signal wait, so it never exited unless the parent sent a terminating
signal.
On macOS (darwin/arm64) the ProxyCommand child does not reliably
receive that signal, so gost hung after ssh quit.
Close the listener and call os.Exit when the stdio connection closes,
making exit deterministic regardless of whether the parent signals it.
Ref go-gost/gost#433.
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.
Fixesgo-gost/gost#550
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
The auto handler only recognized SOCKS4 (0x04), SOCKS5 (0x05), and HTTP
(first byte) as protocol indicators. TLS ClientHello bytes (0x16) fell
through to the HTTP handler, which called http.ReadRequest() on binary
TLS data and returned a malformed-HTTP error.
Add protocol sniffing via sniffing.Sniff() to detect TLS. When detected,
delegate to sniffing.Sniffer.HandleTLS() which parses the ClientHello
for SNI, dials upstream through the chain router (respecting -F config),
and proxies the TLS connection transparently.
Fixesgo-gost/gost#492
When xmetrics.IsEnabled() is false (no -api / no metrics.addr configured),
WrapListener now returns the listener unchanged instead of wrapping every
accepted connection in a metrics.serverConn. This allows bare *net.TCPConn
to reach the forwarding layer, enabling future splice(2) optimization.
On Windows, wintun.dll may pass an invalid pointer to the logMessage
callback, causing a nil pointer dereference in UTF16PtrToString that
panics the entire process. The upstream wintun-go library has not fixed
this since 2021.
Add defer/recover in tun/tungo/tap listenLoop inner closures to convert
panics from createTun/createTap into logged errors, allowing the retry
loop to recover gracefully instead of crashing the process.
Fixesgo-gost/gost#482
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.
Fixesgo-gost/gost#479
The stdio listener wraps os.Stdin/os.Stdout as a net.Listener that accepts
exactly one connection. It enables GOST to be used as an SSH ProxyCommand,
where the parent process (SSH) pipes the transport byte stream through the
child process's standard I/O.
Usage: gost -L stdio://example.com:22 -F http://proxy:8080
The URL host:port is treated as the forwarding target and converted to a
forward node in buildServiceConfig. The standard forward handler then
routes traffic through the configured chain to the destination.
Fixesgo-gost/gost#433
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
Set http.Server.ErrorLog to a logWriter that routes HTTP server errors
(TLS handshake failures, malformed requests) to the GOST logger at Debug
level instead of escaping to stderr via log.Printf.
Without this, network scanners and protocol filters probing the WSS port
produce uncontrolled output:
http: TLS handshake error from ...
Also downgrade WebSocket upgrade failures from Error to Warn — plain HTTP
requests hitting the WebSocket endpoint are operational noise.
Fixesgo-gost/gost#419
User-configured metadata on recorder objects (recorders[].metadata in YAML)
was silently discarded — MetadataRecordOption existed but was never called
anywhere in the codebase, causing plugin (HTTP/gRPC) recorders to always
receive null metadata.
Fix: wrap recorders that carry config metadata in MetadataRecorder, which
auto-appends the metadata to every Record() call. This covers all paths
(handler, router, sniffer, service) with zero call-site changes.
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.
The ICMP dialer required explicit keepalive:true to send QUIC PING frames,
while the regular QUIC dialer enabled keepalive by default (10s period).
Without PING frames, quic-go's 30s idle timeout silently killed connections,
causing HTTP+ICMP tunnels to hang after periods of inactivity.
Fixesgo-gost/gost#352
Also in this commit:
- Release session mutex before calling GetConn() in both ICMP and QUIC
dialers, so a dead session doesn't block all Dial() calls while
OpenStreamSync blocks
- Add 30s timeout context to OpenStreamSync to prevent indefinite blocking
if the QUIC session dies between liveness check and stream open
- Guard session cache deletion with pointer identity check to avoid
evicting a newly created session during a race
transportClient and transportServer each launch two goroutines for
bidirectional copy between the TUN device and the remote connection.
Previously, when one goroutine errored, the function returned
immediately while the sibling goroutine continued running — reading
from and writing to connections that would soon be closed by the
caller's deferred cleanup. In the retry loop of handleClient and
handleServer, stale goroutines accumulated across iterations, racing
with new goroutines for the TUN device and writing to closed pipes.
This caused the "io: read/write on closed pipe" error reported in
go-gost/gost#345, and more importantly, prevented recovery because
subsequent retry iterations operated on a corrupted state.
Fix:
- Derive a per-transport context in transportClient/transportServer
and check it at the top of each goroutine's loop to allow the
first-failing goroutine to signal its sibling to exit
- Replace the single-error select with a collectFirstError helper
that drains both goroutines and includes a timeout guard (5s) to
prevent deadlock when a goroutine is stuck in a non-cancelable read
- Use a per-iteration context in handleClient for the keepalive
goroutine so it is properly cancelled when an iteration ends
- Fix handleServer to use errors.Is(err, ErrTun) for consistency
- Fix transportServer's errc buffer from 1 to 2 to prevent a
goroutine leak when both goroutines error simultaneously
When a router is configured with interface: "auto", the outbound socket
is bound to the same IP that received the incoming connection (read from
DstAddr in context, which is conn.LocalAddr() from the service accept
loop). This enables the 源进源出 (source-in source-out) pattern without
requiring a hardcoded interface name.
Supports comma-separated interface lists (e.g. "auto,eth1") where only
the "auto" token is replaced with the detected ingress IP. Falls back
to the original string when the ingress address is INADDR_ANY, loopback,
or unavailable, so the remaining entries still take effect.
Closesgo-gost/gost#341
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.
Fixesgo-gost/gost#287
Adds a catch-all fallback to hostMapper.Lookup() that matches any
hostname when a '.' mapping exists. After the existing three-phase
matching (exact -> dot-prefix -> suffix walk) fails, the lookup now
checks for a standalone '.' entry as a final fallback.
This enables users to map all hostnames to a single IP (e.g., for SNI
proxy usage) with a simple configuration like 'hosts=.:<ip>'.
Fixesgo-gost/gost#228
When a QUIC session's MaxIdleTimeout expires, the underlying *quic.Conn dies
but the dead quicSession stays in the sessions map. The next Dial() finds
it and calls session.GetConn() which calls OpenStreamSync(context.Background())
against the dead connection — blocking indefinitely.
Add IsClosed() to both quicSession types (using the session context's Done()
channel) and add the liveness guard in both Dial() methods, matching the
existing pattern in KCP and MASQUE dialers.
Fixesgo-gost/gost#225
Add remote config fetching capability to the -C CLI flag, enabling
centralized fleet management of gost nodes.
- readConfig() now detects http:// and https:// URLs and fetches
them via HTTP GET with a 30s timeout and shared client pool
- Format auto-detection from Content-Type header (using mime
parsing) with fallback to URL path extension (.yaml/.json)
- 10 MiB response size limit to prevent memory exhaustion
- URL userinfo stripped from error messages for safety
- Fix ReadFile to force viper config type from file extension,
preventing format detection ambiguity
- Fix viper config search path fallback: skip cfg.Load() when
-C flags are explicitly provided, avoiding unwanted merges
- Tests: URL fetch (YAML/JSON), non-200, oversize, detectFormat,
isHTTPURL, sanitizeURL, and combined file+URL Parse scenario
Allow passing -C multiple times to load and merge several configuration
files in order. Slice fields are appended; scalar fields (TLS, Log, API,
Metrics, Profiling) are overridden by later files — the same semantics as
the existing mergeConfig() used for CLI+config merging.
- Change Args.CfgFile string to Args.CfgFiles []string
- Extract readConfig() helper from Parse() for single-source loading
- Loop over CfgFiles in Parse(), merging each into the accumulator
- Add Quotas to mergeConfig (was missing from the slice list)
- Add TestParse_MultiFile integration test for two-file merge
Closesgo-gost/gost#150
Add missing IPV6_TRANSPARENT socket option to the TCP redirect listener
Control callback, matching what the UDP redirect listener already sets.
Without this, the kernel silently drops IPv6 TPROXY-redirected connections.
Also clean up handler_linux.go getOriginalDstAddr:
- Remove encoding/binary import and heap allocation for port conversion
- Replace with zero-allocation ntohs (native-to-network byte order swap)
- Document the struct layout hack where GetsockoptIPv6Mreq and
GetsockoptIPv6MTUInfo are used to read sockaddr_in/sockaddr_in6
from SO_ORIGINAL_DST (kernel returns smaller structs than the
getter functions expect, but only the first N bytes are populated)
Fixesgo-gost/gost#126
When no forwarder or dns metadata is configured, the DNS handler now
reads system nameservers from /etc/resolv.conf (Unix) before falling
back to the hardcoded 127.0.0.1:53 default. This makes the DNS proxy
work out-of-the-box without requiring explicit DNS server configuration.
Fixesgo-gost/gost#89
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
When a TUN listener with `route=::/0` (or any route) creates its
fallback @internal router during parseMetadata/Init, the router
immediately calls setSysRoutes before the TUN device exists. This
causes an ENETUNREACH "no route to host" error logged by the kernel
because the gateway is unreachable at that point.
The system routes are correctly added later by the listener's own
addRoutes() after device creation. The @internal router only needs
in-memory routes for GetRoute lookups.
Add NoSysRouteOption which disables netlink route management while
keeping route data for in-memory queries. Use it in the TUN
listener's @internal router.
Fixesgo-gost/x#48
- listener/http3: default enableDatagrams to true when not explicitly set,
so CONNECT-UDP (RFC 9298) works out of the box without requiring
?enableDatagrams=true in the config URL
- handler/masque: dial target before sending 200 OK in handleConnectUDP,
matching the handleConnectTCP order; if target dial fails, the handler
can still return an error status instead of having already committed 200
- handler/masque: add missing WriteHeader(BadRequest) when ResolveUDPAddr
fails, so the client gets a proper HTTP error instead of an opaque
H3_REQUEST_CANCELLED stream reset
Fixes#103
The QUIC listener and the QUIC/ICMP dialers unconditionally set
tlsCfg.NextProtos = ["h3", "quic/v1"], discarding any user-specified
ALPN values from TLS options (via tls.options.alpn in config).
Clone the TLS config and only apply the default NextProtos when none are
already configured, matching the existing pattern in the MASQUE HTTP/3
dialer. This enables non-HTTP/3 QUIC use cases like SMB-over-QUIC which
requires ALPN "smb".
Fixesgo-gost/gost#872
stats.WrapUDPConn returned nil when pStats was nil (introduced in 7c95d40),
but callers in the kcp and rudp listeners passed the result directly to
admission.WrapUDPConn and limiter.WrapUDPConn, which wrapped a nil PacketConn.
When service.Serve() called conn.LocalAddr(), the auto-generated method
delegated to the nil PacketConn, causing a panic.
Root cause: WrapUDPConn behavior was inconsistent with WrapConn and
WrapPacketConn in the same package, which both return the original connection
instead of nil when the optional config is absent.
Fixes:
1. listener/kcp, listener/rudp — guard stats.WrapUDPConn with Stats != nil
(consistent with quic and wt listeners which already had this guard).
2. observer/stats/wrapper — when pStats is nil, pass through the original
connection instead of returning nil (matches WrapConn/WrapPacketConn).
3. admission/wrapper, limiter/traffic/wrapper — add defensive nil guards:
nil pc → return nil; nil config → pass through original connection.
All four WrapUDPConn implementations now share the same nil semantics.
4. listener/udp_wrapper_safety_test.go — regression test that verifies the
full metrics→stats→admission→limiter chain with all optional configs nil
does not produce a wrapper with a nil internal PacketConn.
Fixes: go-gost/gost#873
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.
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.
Fixesgo-gost/x#105
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
When Accept() returns a transient error, the service enters StateFailed,
sleeps for the retry delay, then loops back to Accept(). Previously the
state stayed StateFailed until the next successful Accept() — which
could take arbitrarily long for idle services, making status observers
see a stale 'error' even though the service has already reconnected
and is ready.
Now set StateReady (and clear lastError) immediately after the retry
sleep. The redundant state transition on successful Accept is removed
since recovery is handled once after the sleep.
When Accept() returns a transient error, the service enters StateFailed,
sleeps for the retry delay, then loops back to Accept(). Previously the
state stayed StateFailed until the next successful Accept() — which
could take arbitrarily long for idle services, making status observers
see a stale 'error' even though the service has already reconnected
and is ready.
Now set StateReady (and clear lastError) immediately after the retry
sleep. Accept will block until the next connection, but the state
correctly reflects that the service is functional.
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.
mdutil.GetStrings only matches []string and []any — a plain string from a URL query parameter (routes=0.0.0.0/0) returned nil, silently dropping the route. Added GetString fallback with comma-split, and bare-CIDR support that defaults to config.Gateway. Same fix applied to both tun and tungo listeners.
Services are the only component group that binds a port during parsing
(ParseService calls listener.Init). The generic registerGroup "parse-all-
then-swap" sequence bound every new listener while the old services were
still listening, causing EADDRINUSE on SIGHUP reload — a regression from
82e7e50.
Close and unregister all old services first (unregisterAll relies on
registry.Unregister closing io.Closer values, which frees each service's
port), then parse, bind, and register the new ones. This restores the
pre-82e7e50 close-old-before-bind ordering for the services group.
Mirror WrapConn behavior: WrapUDPConn now increments total and current
connection counts on creation (one accepted UDP client session == one
connection) and decrements the current count on Close. Close is guarded
by a mutex and a closed channel so it is safe to call multiple times —
the current-conns decrement fires exactly once and the monotonic total
is untouched.
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).
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.
The sshd handler performed a concrete type assertion on the accepted
connection, but the SSH listener's Accept() wraps each connection with
a traffic limiter (limitConn), obscuring the underlying *DirectForwardConn
type and causing 'sshd: wrong connection type' whenever a limiter was
configured.
Add an UnwrapConn() method to the traffic limiter wrapper and an
unwrapConn() helper in the handler that peels through wrapper layers
before the type switch. The handler uses the unwrapped conn only for
the type-specific DstAddr() call, while the original limiter-wrapped
conn drives data transfer so rate limiting remains effective.
Also track the last accept/bind error on service Status so callers can
retrieve the cause when a service enters the failed state.
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.
The songgao/water library's InterfaceName parameter is a selection filter
that searches the Windows registry for an existing adapter with a matching
friendly name — it cannot name a new adapter during creation. Passing
name=tap0 caused the library to fail before even creating the adapter.
Fix by omitting InterfaceName from water.New(), letting Windows auto-name
the adapter, then renaming via netsh if a custom name was specified.
Also add directory validation to file handler to catch misconfig early.
Add detailed documentation to the masque handler package covering:
- Package-level doc explaining MASQUE protocol (RFC 9298 CONNECT-UDP,
RFC 9114 CONNECT-TCP) and request flow
- Doc comments on all exported and unexported types, methods, and structs
- Inline comments explaining HTTP/3 context extraction and data path
- Field-level comments on metadata struct explaining each config param
- Step-by-step flow and data path diagrams for handleConnectUDP and
handleConnectTCP
The ssuHandler's connMap cached UDP forwarding connections but spawned
goroutines had no context cancellation mechanism. When relayPacketUDP
exited (e.g. on SS UDP session timeout), orphaned goroutines blocked
indefinitely on ReadFrom, preventing connMap cleanup and leaking both
goroutines and connections.
Fix: derive a cancellable context in relayPacketUDP so all per-session
goroutines are cancelled on exit. Add SetReadDeadline with 30s periodic
timeout so goroutines can check context cancellation instead of blocking
forever. Store cancel functions in a cancels sync.Map for cleanup.
Fixes#821
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"
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.
- serial/handler.go: capture xnet.Pipe return value in Handle() and forwardSerial()
- serial/handler.go: log Warnf when chain dial returns (nil,nil) before falling back
- sni/handler.go: use defer for read deadline reset to prevent stale deadline
- sni/handler.go: unrecognized protocol returns nil (clean close) instead of error
- sni/metadata.go: update readTimeout comment to reflect dual usage (deadline + SnifferBuilder)
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.
- Set read deadline before/after sniffing.Sniff to prevent goroutine
leak from slow clients (matches redirect/tcp and ss patterns)
- Log sniff errors at Debug level instead of silently discarding them
- Add nil Router check in dial closure to prevent NPE panic
Add package-level architecture documentation describing two modes
(hop-based forwarding and direct proxy), two-tier dialing strategy
in forwardSerial, recorderConn per-packet logging with direction
convention, and metadata parsing details. No code changes.
- conn.go: Write() now writes first then records b[:n] (not len(b)),
matching the Read() pattern and preventing phantom data on failures.
- handler.go: add nil Router check before h.options.Router.Dial()
in Handle's non-hop path to return a clear error instead of panicking.
- handler.go: guard h.options.Router against nil in forwardSerial;
restructure if/else so ReadTimeout is applied in both the direct
OpenPort path and when the Router has no chain configured.
Bug fixes:
- packetConn.Read: fix slice bounds panic when dlen > len(b);
n was set to dlen (the wire size) but only len(b) bytes were
copied to b; callers doing b[:n] would panic. Clamp n via copy().
- lockWriter.Close: add mutex lock to prevent data race with Write;
Router.Close calls connector.Close (lockWriter.Close) while
handlePacket concurrently calls Writer.Write — both touch w.w.
- Router.DelConnector: delete empty host slices from the map after
removing the last connector, preventing unbounded map growth.
Documentation:
- Package-level doc with architecture diagram, data flow, component
hierarchy, thread-safety invariants, and connector weighting rules.
- Documented every exported type (Connector, Router, ConnectorPool,
ConnectorOptions, routerHandler, metadata, lockWriter, packetConn)
and all non-trivial methods with purpose, parameters, lifecycle,
and algorithmic details (e.g., GetConnector weighted selection).
- Annotated critical code paths with step-by-step comments
(handleAssociate stages, handlePacket routing algorithm,
handleEntrypoint packet format and forwarding logic).
- Explained observeStats retry pattern and metadata key conventions.
Verification: build, vet, 98 tests race-clean.
Translate all Chinese comments in the relay handler package to English:
handler.go, connect.go, bind.go, forward.go, conn.go, entrypoint.go,
observe.go, metadata.go. All inline and doc comments are now in English.
Also set a persistent preference: all future code comments must be
written in English only.
- 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
Allow browser-based dashboards (e.g. Flutter web apps) to access the
metrics endpoint by setting Access-Control-Allow-Origin: * and handling
OPTIONS preflight requests.
The response address sent back to the tunnel client was unconditionally
set to the md5 hash of the tunnel ID. This meant the user's custom host
(e.g. "dash" from "dash:8081") was ignored — the client listened on
the hash instead.
Now the user-supplied host is used directly when:
1. A host is present in the bind address AND ingress is configured.
2. No other tunnel has already claimed that host in ingress
(conflict check via GetRule).
Fall back to the md5 hash when no host is supplied, ingress is nil, or
the host conflicts with an existing ingress rule.
Also fixes indentation in the WriteTo error handling block (extra tab
removed).
Tests:
- TestHandleBind_CustomHost: response AddrFeature uses "dash", not hash
- TestHandleBind_CustomHostConflict: conflicting host falls back to hash
- fakeIngress enhanced with ruleByHost map for conflict simulation
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.
- Fix dead-code branch in bind.go host assignment (always use endpoint hash)
- Return descriptive error on bypass match in connect.go (was masking as success)
- Update bypass test in connect_test.go for new error behavior
- Extract entrypoint subpackage from monolithic entrypoint.go (6 files)
- Fix observeStats event-loss bug (break -> fallthrough on retry success)
- Add 47 unit tests across handler, connect, bind, metadata packages
- Add architecture doc comments to all key files
- Build and vet clean, 173 tests pass with -race
Extract checkRateLimit and observeStats into their own file (observe.go).
Fix the one-tick event-loss bug in observeStats where the retry path used
'unconditional break' instead of fallthrough, causing events collected
after a successful retry to be discarded.
Add 87 unit tests across 7 files (helpers, metadata, conn, handler,
connect, forward, bind) covering all handler modes, error paths,
observer/stats integration, and conn wrapper edge cases.
handler.go: -43 lines (unused observer import, extracted functions)
observe.go: +52 lines (checkRateLimit, observeStats with fallthrough fix)
test files: +1959 lines
- 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
Add PUT method support to the file:// handler, allowing file uploads via
curl -T. Upload is opt-in, disabled by default — enable with ?put=true
metadata flag. Includes path traversal protection.
Adds RejectUnknownSNI and ServerNames fields to TLSConfig. When enabled,
TLS handshakes with missing or non-allowlisted SNI are rejected at the
handshake level via GetConfigForClient before any certificate is sent.
Works across all TLS-based listener types (tls, mtls, ws, mws, http2,
grpc, http3) since they share the same *tls.Config from LoadServerConfig.
Priority short-circuit now checks for backup nodes before bypassing the
selector. When any node has backup:true the selector (FailFilter,
BackupFilter, strategy) runs instead, ensuring BackupFilter can separate
primary from failover nodes. Previously backup was silently ignored when
all nodes shared the same non-zero priority from matcher rule length.
Wire pStats into sub-handlers to capture real byte counts from QUIC
streams instead of the dummy listener conn. Resolve target address and
validate capsule-protocol after authentication instead of before 200
response to prevent dead tunnels and unauthenticated probing. Merge
pending observer events rather than replacing on consecutive failures.
Add idle read timeout to TCP tunnel relay. Add doc comments on exported
symbols and NewHandler.
- 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
Fix two data races in Sniffer.httpRoundTrip:
- resp.Header was assigned directly to ro.HTTP.Response.Header without
cloning, creating shared mutable state between the response and recorder.
- httpSettings.RequestHeader was set on req.Header but not propagated to
ro.HTTP.Request.Header, causing the recorder to miss configured headers.
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
When rtcp listener calls Router.Bind with ":8000" (port-only address),
the relay AddrFeature.ParseFrom fails because net.SplitHostPort returns
an empty host. This leaves AType at default 0, causing AddrFeature.Encode
to return ErrBadAddrType.
Fix by normalizing port-only addresses to "0.0.0.0:<port>" in both tunnel
and relay connectors before ParseFrom. Also fix error check on
resp.WriteTo in the tunnel handler's handleBind to prevent silent
protocol desync.
- Wrap non-CONNECT upstream with traffic_wrapper and stats_wrapper (was only on CONNECT path)
- Write 503 error response on forwardRequest failure instead of silent connection close
- Decouple resp.Header from w.Header() to prevent metadata header leakage/doubling in 407 responses
- Return pipeTo signal from authenticate instead of dialing inline (matches http handler pattern)
- Add idleTimeout metadata field and pass to xnet.Pipe for CONNECT tunnels
- Add 4 tests for probe resistance host forwarding and knock bypass
Extract authenticate/probe-resistance to auth.go and roundTrip/forwarding
to proxy.go, following the handler/http/ pattern. Export sentinel errors
(ErrAuthFailed, ErrWrongConnType) for precise test assertions. Fix
flushWriter panic recovery to handle non-string/non-error panics. Default
HTTPS port to 443 when no port is specified in the Host header.
Extract Handle into handleRawForwarding, handleSniffedProtocol, SnifferBuilder,
newRecorderObject, and checkRateLimit across forward.go, sniffing.go, and
util.go. Replace redundant x509.ParseCertificate with tlsCert.Leaf (already
populated by tls.LoadX509KeyPair since Go 1.23).
Service registration (register) calls ParseService, which captures
logger.Default() into each service/handler/listener. Since
logger.SetDefault was called after register, all components captured
the initial Info-level logger, making -D/-DD ineffective for service
logging.
Extract three pure-logic components from httpHandler to eliminate I/O
coupling from auth and sniffing construction, enabling synchronous unit
tests and reducing per-request allocation in sniffAndHandle:
- Authenticator struct with AuthResult return type — auth decisions no
longer write to the connection; callers handle response I/O. Auth
tests drop net.Pipe/goroutines for direct return-value assertions.
- SnifferBuilder pre-built in Init and reused per-connection via Build(),
replacing inline sniffing.Sniffer{} construction in sniffAndHandle.
- normalizeRequest extracted to util.go with NormalizedRequest type,
collapsing 20 lines of inline URL/normalisation into a single call.
- knockMatch extracted as standalone pure function.
- clampBodySize exported as ClampBodySize for cross-package use.
- util_test.go with 22 tests covering utility functions.
- helpers_test.go consolidates shared test fakes (logger, observer, conn).
Knock previously accepted a single hostname. Now it accepts multiple
comma-separated hostnames; probe resistance is bypassed if the request
hostname matches any entry in the list. Matching is case-insensitive
and whitespace around entries is trimmed.