- 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
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.
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.
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).
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.
- Split host:port in Host header before DNS/IP validation, accept bare IPs
- Guard sniffer connection ownership with snifferHandled flag to prevent
double-close when sniffers take over the tunnel
- Close req.Body in handleProxy keep-alive loop on both error and success
paths to prevent goroutine leaks from idle HTTP transport connections
- Log all resp.Write errors (5 call sites) instead of silently discarding
- Respect resp.Close for all responses, not just ContentLength >= 0,
fixing unnecessarily closed chunked keep-alive connections
- Harden probe resistance: validate strconv.Atoi result, add 15s timeout
to web redirect HTTP client, document pr.Knock empty/set/mismatch logic
Extract newRecorderObject, selectTarget, sniffingDial, buildSniffer,
handleSniffedProtocol, and handleRawForwarding from Handle (200→87 lines).
Move nil Router guard before sniffing for early failure. Log previously
silent sniff and pipe errors at Debug level. Apply readTimeout to Pipe.
Append :0 when target addr lacks a port. Reorder metadata fields and
add doc comments on all exported symbols.
- Return clear error when Router is nil instead of panicking
- Capture and log sniffing.Sniff and xnet.Pipe errors at Debug level
- Move dial closure and Sniffer construction into HTTP/TLS switch branch
to avoid unnecessary allocation on non-HTTP/TLS traffic
- Merge ProtoHTTP and ProtoTLS cases, branch internally on proto
- Add doc comments on NewHandler and Handle
- Add comment explaining nil hop is valid (no forwarder config)
- Pass readTimeout to xnet.Pipe via WithReadTimeout
Fix bugs in the file handler: unbuffered send channel deadlock on Close,
conn leak when send drops on done, and Addr() returning nil. Add done-
priority select in send(), explicit ln.Close() in Close(), Unwrap() on
responseWriter for http.ResponseController, and nil guards.
Also add package doc, NewHandler doc, and 39 unit tests covering the
listener, responseWriter, handler lifecycle, auth, and file serving.
Fix bufpool leak when PackBuffer fails by extracting packResponse helper.
Add write deadline before conn.Write to prevent goroutine hangs on slow
clients. Log async exchange errors instead of silently discarding them.
Raise defaultBufferSize from 1024 to 4096 to match EDNS0 standard.
Deduplicate lookupHosts TypeA/TypeAAAA branches.
- entrypoint.go: guard ictx.RecorderObjectFromContext with nil checks on
ClientID and Redirect assignments
- io.go: fix CloseRead checking Writer instead of Reader, add Reader
fallback to CloseWrite, unwrap *readWriter/*readWriteCloser in
SetReadDeadline to reach underlying net.Conn
- docs: add package doc and exported symbol comments to internal/ctx
and internal/io
- Bump go-shadowsocks2 to v0.1.3 (new API symbols)
- Return error instead of nil when Target() is empty in TCP handler
- Reset wbuf unconditionally on write error to prevent unbounded growth
- Add nil guard on targetAddr before WriteTo to prevent panic
- Remove duplicate NewClientConfig call and dead ClientConfig literal
- Remove unused noDelay metadata field
- Fix buffer-size guard to catch negative values (len(b)==0 → bufSize<=0)
- Add address context to parse/session error messages
- Use write lock (Lock) instead of read lock (RLock) in CloseOnIdle since
it modifies the close channel, preventing a race with Close() that could
panic on double-close of channel.
- Buffer sniffingWebsocketFrame errc to capacity 2 and close both
connections on first error to ensure the remaining copy goroutine
unblocks and exits cleanly.