Skip to content

fix(tunnel): cancel preparation on native frontend closure - #3438

Draft
polaris-arch wants to merge 13 commits into
MetaCubeX:Alphafrom
polaris-arch:fix/tunnel-dial-lifetime
Draft

polaris-arch wants to merge 13 commits into
MetaCubeX:Alphafrom
polaris-arch:fix/tunnel-dial-lifetime

Conversation

@polaris-arch

@polaris-arch polaris-arch commented Nov 9, 2026 •

Copy link
Copy Markdown

Cancel outbound preparation when a capable native TCP frontend fully terminates, and detach the notification before successful relay handoff. Peer FIN alone retains the response direction.

Concrete problem and normal-path reachability

A native frontend can terminate while its outbound is still preparing. With only #3439, preparation can continue until its remaining dial budget expires. This draft adds earlier cancellation; it is not another claim of removing an unbounded wait after that generic fix.

The intended path is Mips forwarder → accepted mipstack TCP connection → sing listener → buffered tunnel connection → CancelOnClose. A valid peer RST or full application Close can terminate preparation. The new tests use real native forwarded endpoints and the tunnel's actual buffered connection. They replace route/proxy selection, do not instantiate the configured sing-tun listener, and do not claim a physical-TUN or external-node measurement. Source inspection confirms the ordinary accepted native connection reaches this wrapper path.

Prerequisites and draft status

This stacked draft includes #3439. git diff 4f6bd36b3231ab996fb232f3d46fdc06194ae613 74eb0b0aa696c06659af6868b44cbdac773b4ad2 -- tunnel/ isolates the native hook, ownership handoff and native-specific tests between the current trees; the shared deferred-handshake test file is identical.

Executable native protocol reproduction

In a disposable Mihomo checkout, with Go 1.26.10:

git fetch https://github.com/polaris-arch/mihomo.git 74eb0b0aa696c06659af6868b44cbdac773b4ad2
git checkout --detach 74eb0b0aa696c06659af6868b44cbdac773b4ad2
git clone https://github.com/polaris-arch/mipstack.git ../mipstack-native-review
git -C ../mipstack-native-review checkout --detach 5b6706cc9d94126c606c13ec0c9fe8f4f360b628
cp go.mod native-review.mod
cp go.sum native-review.sum
go mod edit -modfile=native-review.mod -replace github.com/metacubex/mipstack=../mipstack-native-review
MIHOMO_TEST_NATIVE_CLOSE_REQUIRED=1 GOFLAGS='-mod=mod -modfile=native-review.mod' go test -count=5 -timeout 1m -v -run '^TestLifecycleNative' ./tunnel
MIHOMO_TEST_NATIVE_CLOSE_REQUIRED=1 GOFLAGS='-mod=mod -modfile=native-review.mod' CGO_ENABLED=1 go test -race -count=1 -timeout 2m -run '^TestLifecycleNative' ./tunnel

The required flag makes an unavailable API a failure, not a skip. The override changes a test-only modfile, not the production declaration.

Test the same native reset fixture with only the generic budget, then restore the candidate:

git show 28b58eb038b70d322df1b369af5d65c5afd3af58:tunnel/tunnel.go > tunnel/tunnel.go
MIHOMO_TEST_NATIVE_CLOSE_REQUIRED=1 GOFLAGS='-mod=mod -modfile=native-review.mod' go test -count=1 -timeout 30s -v -run '^TestLifecycleNativeResetCancelsWebSocket$' ./tunnel
git restore --source=74eb0b0aa696c06659af6868b44cbdac773b4ad2 -- tunnel/tunnel.go

The negative command must fail its one-second assertion after the Upgrade request was read and reset was sent, not fail to build. Cleanup then closes the peer; total failing-test duration is not a measurement of five-second retention.

Controlled protocol check Generic budget only Native integration
Actual native peer RST after a real WebSocket Upgrade request; peer withholds HTTP 101 Misses the one-second completion bound Completes within one second; socket/peer wait ends
Actual native peer FIN while preparation is held; send response after releasing preparation No separate native baseline comparison claimed Request bytes, EOF and reverse response survive

Both native cases pass five repetitions in validation. The earlier four-way run also records the same-source negative and protocol controls. These establish cancellation/half-close correctness under controlled events, not production frequency or whole-process resource savings.

Fault injection and defensive coverage

Existing fake-source tests explicitly force claim-before-callback scheduling and late successful dial returns. They remain labelled adversarial ownership/fault-injection coverage. The portable synthetic-source WebSocket test exercises cancellation during the actual deferred first Write and now requires completion within one second; its cleanup allowance stays seven seconds. It is retained because real native tests can skip when the proposed API is unavailable. Successful relay lifetime and registration detachment remain positive controls.

Head 98d8db81 changes tests only relative to aeb69e57. The final frozen-batch validation executes the required real native reset/FIN controls five times, focused/race suites, the already-terminal registration test, and the explicit with_gvisor capability-negative test. The production dependency is unchanged. Keep draft until API acceptance, an actual dependency update and safe integration prerequisites are resolved.

Additional boundary controls

  • TestLifecycleAlreadyTerminatedBeforeRegistration enters the actual handler with a mock that cancels immediately during registration; no outbound dial is attempted. This is an injected registration-state boundary, not a real already-terminal socket test.
  • TestLifecycleGVisorHasNoNativeCloseCapability checks the real pinned gonet.TCPConn type through the ordinary connection wrapper. Its zero value is used for capability traversal only; no live gVisor forwarding is claimed.
  • The healthy real-TCP WebSocket control inherited from fix(tunnel): bound deferred handshakes by the TCP dial timeout #3439 verifies the production first-write path and continued I/O after preparation expiry. It uses a capability-free TCP frontend and does not by itself prove native-hook detachment with WS. The existing detachment/relay and native FIN tests remain separate controls.
  • DNS, sniffing and other work before hook registration remain outside early native cancellation.

Final frozen-batch validation also passes full root ./... default/with_gvisor, vet and Windows/macOS changed-package compilation in the declared combination: tunnel 98d8db81, TFO b32780aa, group e8ffb994, context completion 23285c8f, and native API 5b6706cc through a test-only modfile. These full-suite results bind that combination, not a new standalone full-suite run for every PR. SKIP_INTEROP_TEST=1; the nested test/ module is outside root ./.... Older validation links retain their original source identities. No physical-TUN, kernel Fast Open SYN/cookie, production incidence or whole-process resource claim is made.

Current test-only revision

Head 74eb0b0a changes only one existing test assertion relative to 98d8db81: primary completion wait 7s → 1s. Runtime source, test count and cleanup are unchanged. The inherited #3439 test file remains identical; it is dependency coverage, not a second independently added suite.

Targeted validation of this exact head used Go 1.26.10 in private CI: the default tunnel package, race/with_gvisor, required native RST/FIN controls (three repetitions each), and vet passed. Public GitHub Actions is disabled for the fork, so no new public CI pass is claimed. The earlier public full-suite result above remains bound to 98d8db81 and its stated combination.

The additional generic-budget-only control uses 4f6bd36b:tunnel/tunnel.go. That implementation never registers the mock hook, so its control-only fixture guards a nil source.claim() callback to reach the one-second assertion. The control fails that assertion; no panic/build failure is counted as reproduction. This fixture guard is not part of the PR.

The portable positive check is reproducible without the proposed native API:

go test -count=1 -timeout 30s -v -run '^TestLifecycleWebSocketEarlyHandshakeCancellation$' ./tunnel

@polaris-arch polaris-arch changed the title fix(tunnel): bind deferred TCP preparation to its dial lifetime fix(tunnel): cancel preparation on native frontend closure Nov 9, 2026
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.

1 participant