Skip to content

fix: keep TCP tunnels recovering under sustained traffic - #66

Merged
AminMGMT merged 12 commits into
AminMGMT:mainfrom
dr-hoseyn:codex/tcp-stability-throughput
Oct 9, 2026
Merged

AminMGMT merged 12 commits into
AminMGMT:mainfrom
dr-hoseyn:codex/tcp-stability-throughput

Conversation

@dr-hoseyn

@dr-hoseyn dr-hoseyn commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Under sustained traffic, stale generation workers, undercounted concurrent pool dials, retained mux claims and unbounded control/destination writes could stop recovery until restart. Raw UDP also expired active one-way flows, truncated large datagrams and could panic when the opening packet raced queue shutdown.

Close and join generation work before replacement, replenish and accurately account for pools, release mux capacity on every exit, drain retired queues and bound handshake/control/destination writes. SMUX growth respects the initial pool and a 128 MiB aggregate receive-window budget. Framing handles short writes and rejects oversized frames. Pooled WebSocket pings use the existing ten-second write bound, and TCP-to-WebSocket copies preserve bytes returned with EOF.

Raw UDP now tracks activity in either direction and joins both copy workers on failure. Queue the initial payload before publishing a flow. Full-sized receive buffers preserve legal large and empty datagrams; each packet is written once, and a short write ends the flow rather than splitting it. Larger raw buffers increase memory per flow; framed UDP's adaptive buffers remain separate.

Guard disabled per-packet Debug logging. Same-runner measurements reduce client UDP logging overhead from 112 B / 10 allocations to 4 B / 1 allocation per packet, and server copy directions from 64 B / 5 allocations to zero. Exact-source races and paired measurements pass at b7c23a6. Separate actual-tunnel checks cover delayed replies after 60.5 seconds of continuous one-way traffic, 16,384/16,385/60,000/zero-byte packets and repeated shutdown admission.

Carries forward the lifecycle/pool intent of #17, #19 and #20 on current main. One earlier KCP churn goroutine-count failure did not reproduce in the final full suite or five isolated race repetitions.

Validation: Combined full tests, full race suite and unchanged coverage gates pass, including five separate KCP churn race repetitions. Final integration with UDP backend health passes affected runtime races, vet, staticcheck and Linux amd64/arm64 builds. The full-suite run precedes the small UDP backend health update; the final integration validates that update.

Only existing production and test files change. No files, test files, dependencies or configuration/wire-format changes are added. Supplemental fixtures and CI workflows remain outside this PR on fork validation branches. Measurements use local test traffic and do not establish WAN throughput.

@dr-hoseyn
dr-hoseyn force-pushed the codex/tcp-stability-throughput branch from d610e9f to 8781687 Compare October 7, 2026 19:20
@AminMGMT
AminMGMT merged commit 6e6fd6e into AminMGMT:main Oct 9, 2026
AminMGMT pushed a commit that referenced this pull request Oct 9, 2026
…d openUDPPaths wrapper

These three branches were the only ones of the 32 that could not be merged directly: each conflicted in exactly one file against a sibling that had already landed, and the branches live on a fork this session cannot push to.

Resolutions, each verified rather than guessed:

- #87 (reverse QUIC DNS bounded by the dial timeout) conflicted with #71 in internal/utils/network/quic.go, in the import block alone. Both sides' imports are kept: strings, sync and sync/atomic.

- #84 (TCP proxy setup bounded, retry backoff cancellable) conflicted with #66 in internal/utils/network/tcp_dialer.go. main's copy of that file was byte-identical to #66's, so #66 was its only other author, and #84's version is a superset of it: it keeps #66's cancellable retry timer and AfterFunc guard and adds the per-attempt deadline. #84's version is taken whole.

- #60 (direct tunnel recovery) conflicted with #85 in internal/tunnel/limits/limits.go. main's copy is byte-identical to #85's, which already carries #60's ctx/cancel/Close plus the pacingDeadline work, so it supersedes #60's change to that file and is kept. #60's other ten files apply unchanged.

Separately, #76 kept openUDPPaths as a context.Background() shim after moving its only caller to openUDPPathsContext. Nothing calls it, so staticcheck fails the audit job with U1000 — red on main since #76 landed, and clean on main before it. The shim is removed and its explanation moved onto the function that does the work.

Verified on the result: gofmt clean, go build ./... and go vet ./... pass, staticcheck clean, and go test ./... plus go test -race ./... match main's baseline (internal/socks fails only because this container has no IPv6, which fails on plain main too).

Co-authored-by: dr-hoseyn <95236112+dr-hoseyn@users.noreply.github.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wBoD8a2hetHgBWnfsRAuY
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants