Stopping a service closes its listener, and a closed listener answers Accept
with core's listener.ErrClosed — which the guard never matched (it knew only
net.ErrClosed), so every stop reached the log at error level and read like a
crash. The tun handler logged the same shape a second time: its client wrapped a
closed device with %s, throwing the cause away, so the caller could not tell a
device closed under the reader from a real read failure. %w keeps the chain and
the handler logs the closed case at debug.
Three fixes found while wiring the p2p hub topology:
- The client keepalive was gated on network == "udp", but on a p2p link
raddr is the peer's base64 key (no port), so SplitHostPort fails and
network is "ip": the keepalive never ran, a tun server never learned the
client's route, and the read deadline was never armed. Gate on the udp
network OR a configured keepalive period; the udp branch keeps the one-shot
registration handshake it always sent.
- Liveness was refreshed only by the keepalive echo; a transient channel
hiccup can drop that single packet and redial a healthy tunnel after
3xperiod. Refresh the read deadline after any successful read instead.
- Routes were never removed: a departed client's IP -> addr stayed forever
and the device kept blackholing to a dead address. Expire a route once its
last keepalive is older than 3x the configured period, checked lazily on
the write path (no sweeper). The TTL is opt-in: without a server-side
keepalive/ttl, routes keep their current never-expiring behavior.
The read deadline set for ClientHello sniffing was reset via defer, so it
remained on the client connection until Handle returned, i.e. for the whole
session. xnet.Pipe is called without a read timeout for TLS/HTTP, so nothing
refreshed it and any connection was torn down after readTimeout (15s default)
regardless of traffic. Clear the deadline immediately after sniffing, matching
the relay/router/tunnel/socks4 handlers.
Regression test asserts the deadline is zero when the upstream dial fires.
Fixes#904
udp.Relay.Run ignored ctx cancellation, so an idle MASQUE association (or a
disconnecting client) leaked its target UDP socket and both copy goroutines,
and service shutdown blocked on the stuck handlers. quic-go's
ReceiveDatagram only unblocks on ctx cancellation, never on stream close, so
closing the datagram conn could not release the read either.
Make Relay.Run cancellation-aware and close both conns on exit, add
WithReadTimeout for an idle read deadline, give DatagramConn a cancelable
context that Close cancels, and apply the parsed idleTimeout to the UDP
relay.
Ref: https://github.com/go-gost/x/issues/128
OpenTunnel carries a network: "udp" asks the plugin for a datagram endpoint
(one datagram per write) instead of a byte stream, which is what a tun
handler needs — a stream endpoint coalesces and splits packets. tcp stays
the default, so existing deployments are untouched.
The tun handler could not reach client mode without a service forwarder, so
a chain with no forwarder now means client mode with an empty dial address:
the link is transparent and the peer comes from the chain node (a p2p node's
addr is its peer key). That combination used to be dead config, so it is
logged. A forwarder still wins when present, and server mode is unchanged.
Alongside: the udp dialer no longer hard-asserts *net.UDPConn (a wrapped
base dialer yields a plain conn), udp joins the p2p dialer whitelist, and a
udp tunnel announces its client with an empty datagram right after dialing —
the endpoint learns where to send peer packets only from a packet it
receives, so without it a link whose peer speaks first would never come up.
Requires plugin v0.7.0 for the new field.
Drop the classic github.com/shadowsocks/go-shadowsocks2 dependency. tap and
internal/util/ss were its only remaining users after the ss/ssu handlers
moved to the go-gost fork. The fork's shadowaead is byte-identical on wire
(classic AEAD: same md5-KDF, HKDF-SHA1 subkey, salt sizes), so tap now uses
a small stateless ss.AEADPacketConn built on fork Pack/Unpack, and
ss.ShadowCipher becomes a fork builder (dummy/none -> existing none.Cipher).
Removes the undocumented tap handler key metadata and the dead ShadowConn
helper in the process.
Replace the sole govalidator.IsDNSName call in normalizeRequest with a local
isDNSName helper that mirrors the library's exact behavior (empty/size guard,
net.ParseIP exclusion, and the same label regex), removing the
asaskevich/govalidator dependency from x.
Domain-only whitelist bypass never matched the bare destination IP, so the
pre-sniffing bypass check (fe394a8) rejected connections before SNI sniffing
could match the sniffed host (gost#899).
- skip the pre-sniffing check when bypass IsWhitelist(); the sniffer applies
the whitelist against the sniffed host
- restore the post-sniffing bypass check on dstAddr (removed by fe394a8) so
whitelist mode still rejects non-matching hosts on the sniffingFallback
path and when sniffing is disabled
- bypassGroup.IsWhitelist() now reports true only for all-whitelist groups
instead of always false (sole external consumer is the redirect handler)
- rewrite handler tests to drive a real TLS ClientHello through Init()'d
metadata: whitelisted SNI is forwarded, non-whitelisted SNI gets ErrBypass
A valid Gost-Target / X-Gost-Target header replaced only req.Host while
leaving req.URL.Host untouched. Destination-policy checks (bypass) evaluated
the header-derived authority, but http.Transport dialed the unchanged
absolute-URL authority. A client could label a forbidden URL with an allowed
authority and reach the blocked destination (403 -> 200 with backend body).
setRequestHost now applies the decoded target to both req.Host and req.URL.Host
so policy evaluation and transport dialing always resolve to the same authority.
Origin-form requests ("GET / HTTP/1.1" with only a Host header, as
sent by nginx proxy_pass to an upstream proxy) failed RoundTrip with
"http: no Host in request URL": normalizeRequest infers URL.Scheme
but leaves URL.Host empty, and http.Transport requires a non-empty
URL.Host. Also affected every keep-alive request after the first,
which bypass normalizeRequest entirely.
Fill scheme+Host from req.Host in the shared proxyRoundTrip path,
covering both initial and subsequent requests.
e2e-verified: raw origin-form request proxied by Host header
(gost/tests/e2e TestHTTPProxyOriginForm).
The tunnel bind conns (bindConn/bindUDPConn) now expose their metadata (the
tunneled hostname) via the xctx.Context interface, so the service accept loop
forwards it into the handler context. handleRawForwarding selects a hop node by
that hostname when a hop is configured, so a node with a matching filter.host is
picked; otherwise it falls back to dialing the hostname directly. Fixes
reverse-proxy-tunnel TCP routing (gost#898) where the docs example failed with
"node not available" because the mux path never set the host metadata.
Replace the 5 duplicated switch statements that dispatch sniffed
connections (forward/local, forward/remote, http CONNECT, sni, relay)
with per-package registries keyed by proto string. Protocol handlers
self-register via init(), following the house registration pattern, so
adding a protocol no longer requires touching every dispatch site.
Unify the two HandleOptions types (forwarder lacked dialTLS; sniffing
lacked hop/node/httpKeepalive) into one exported struct in sniffing;
forwarder aliases it. Unknown protocols fall through to raw forwarding
exactly as before; SSH remains detection-only. redirect/tcp's special
fallback-replay dispatch is intentionally left untouched.
Adds Dispatch helpers and 8 registry tests (register/get/duplicate/
unknown/known) in both packages. Net -151 lines.
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.
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.
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.
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.
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.
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.
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.
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).
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
When sniffing is enabled, the redirect handler dials the sniffed hostname (HTTP Host / TLS SNI). Add an opt-in metadata flag sniffing.dialOriginalDst that dials the original transparent-redirect destination address instead, while sniffing still runs for hostname-bypass matching and recording.
IP/CIDR bypass rules were silently ignored when sniffing was enabled because the sniffer's internal bypass only matches sniffed hostnames. The post-sniffing dstAddr check was unreachable since HandleHTTP/HandleTLS return early.
Extracts the verified mTLS peer certificate (CN, SANs, SHA-256
fingerprint) from the TLS connection in the HTTP handler after the
handshake completes, stores it in the request context, and forwards
it to both gRPC and HTTP auth plugins via new AuthenticateRequest
fields (client_cn, client_san, client_cert_fingerprint).
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.
Sniff() calls Peek(5) which blocks on SOCKS5 greeting (3 bytes:
VER, NMETHODS, METHODS). The SOCKS5 connector sends those 3 bytes
and then waits for the server's method-selection reply — deadlock.
Restore the original 1-byte peek for SOCKS4 (0x04) / SOCKS5 (0x05)
detection first, routing them to their handlers immediately. Only
call Sniff() for non-SOCKS traffic (TLS, HTTP, SSH) where 5 bytes
are guaranteed.
Fixesgo-gost/gost#879
- 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)
- Extract newRecorderObject, checkRateLimit, sniffingDial, and
handleSniffedProtocol from the Handle method into dedicated files
(sniffing.go, util.go) to reduce Handle from ~100 to ~65 lines.
- Introduce SnifferBuilder: populated once during Init, reused via
Build() per connection. Avoids reconstructing a 9-field sniffing.Sniffer
literal on every inbound connection.
- Hoist router nil check from the lazy dial closure into Handle so
misconfigured handlers fail immediately instead of silently dropping
unrecognised traffic with no error.
- Add nil-addr guard in checkRateLimit (missing from the old inline
version — would panic if the remote address were somehow nil).
- Add comprehensive unit tests (44 tests across 5 test files) covering
handler creation, Init, metadata parsing (defaults, custom, MITM,
error paths), protocol dispatch (HTTP, TLS, unknown, empty), rate
limiting, recorder objects, dial plumbing, and SnifferBuilder.
- Add package-level godoc explaining the handler scope, limitations
vs tcp/forward, and the connection processing flow.
- Fix metadata comment: s/SnifferBuilder/sniffing.Sniffer/.
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
Probe resistance (probeResist=code:403) was always returning 400 Bad
Request instead of the configured status code because the
non-proxy-form request validation check (req.URL.Scheme != "http")
ran before Authenticate(). Browser/scanner probes send GET / HTTP/1.1
which has an empty URL scheme, so they were rejected at the validation
gate before probe resistance ever had a chance to respond.
Swap the order: run Authenticate() first so probe resistance can
intercept non-proxy-form probes with the configured decoy response.
Legitimate clients with correct credentials proceed to request
validation and routing as before.
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 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
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
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
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
- 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 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
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 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.
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.