Repository navigation
fix(dialer): synchronize lazy TFO connection lifetime - #3436
Open
polaris-arch wants to merge 4 commits into
Open
polaris-arch wants to merge 4 commits into
polaris-arch wants to merge 4 commits into
Conversation
This was referenced Nov 9, 2026
polaris-arch
marked this pull request as draft
November 9, 2026 11:43
polaris-arch
marked this pull request as ready for review
November 9, 2026 12:25
polaris-arch
marked this pull request as draft
November 9, 2026 14:10
polaris-arch
marked this pull request as ready for review
November 10, 2026 14:26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Synchronize lazy TFO socket publication and closure, serialize the first dial, and retire a successful socket if closure or cancellation won. Network I/O remains outside the publication mutex; the existing independent dial context is retained.
Concrete bug and normal-path reachability
The old
tfoConn.Dialpublishesc.ConnafterdialFnreturns, whileClosereads that field independently. A close between successful dial completion and publication can observe no socket and return; the writer can then publish the socket and report success.closedand connection access are also unsynchronized.This requires the lazy TFO path (
tfo: true, with TFO not globally disabled) and concurrent access; it is not a defect on every connection. An existing production route to concurrent close is SOCKS5DialContext→clientHandshakeContext→SetupContextForConn: caller cancellation closes the connection while the handshake can be doing its first write. The publication/close interleaving is reachable by source inspection. No naturally occurring production instance or incidence rate is established by these tests.Source: old TFO implementation, SOCKS5 caller, context interrupter.
Executable reproduction and observed result
Run in a disposable checkout with Go 1.26.10 (the recorded validation toolchain):
To test the same relevant fixture against unchanged Alpha:
The baseline command is expected to fail an assertion, not fail to compile.
CloseRetiresLateDialSuccessWritereturns success after wrapper closureio.ErrClosedPipe, no published upstream, late socket peer observes EOFReal-path cancellation control
TestLifecycleTFORealSOCKS5Cancellationkeeps the productiontfo-godial function unchanged, sends an actual SOCKS5 greeting to a loopback peer, withholds method selection, then cancels the caller throughSetupContextForConn. It asserts the canceled handshake and socket EOF within one second. Ten repetitions pass in final validation; this proves a real consumer/cancellation path, not a natural occurrence rate of the publication race.The required Linux control can be reproduced directly:
Only an explicit library unsupported-platform/kernel error permits skipping this optional wire control. Linux validation sets
MIHOMO_TEST_TFO_REQUIRED=1, making that condition a failure rather than a skip. Fake-dial ownership tests remain platform-independent.Fault injection and defensive coverage
dialFnwith a gated fake that deliberately returns success after cancellation. It demonstrates the ownership defect deterministically; it does not demonstrate that the realtfo-godialer ignores cancellation. The context-interrupter integration fixture uses the same late-success technique and is also fault injection.net.Connconcurrency contract; simultaneous initial writers have not been demonstrated in the ordinary tunnel's single initial-write path. Treat that part as defensive contract coverage, not a separately observed production incident.Focused/race tests, full default/with_gvisor suites, vet and Windows/macOS changed-package compilation passed in standalone validation. Consumer validation covers the context-interrupter boundary. #3439 should follow this fix for safe concurrent TFO closure.
Head
b32780aapasses standalone dialer focused tests (ten repetitions) and package race tests (five repetitions) in final frozen-batch validation. Compared with06f16aaf, the runtime delta reuses the existing published state when a waiting reader observes preparation cancellation; no new field, helper or watcher is added.Synchronization scope
The two locks have separate duties:
dialMuserializes initial writers, whilemuorders socket publication againstCloseand never covers network I/O. The ready/closed atomic snapshot publishes one immutable connection for subsequent access; completion uses the existing channel. The atomic lookup is a deliberate access-cost choice, not a claim that a state mutex cannot be correct. A standardsync.Onceplus state mutex alternative was checked against the same lifetime tests; the current implementation is retained without a production-level performance claim. No new dependency, configuration option or resident watcher is introduced.Accepted connection versus waiting-reader cancellation
A reader that started before the first write can still be waiting when
Dialpublishes the accepted socket, before it closesdialed. Selecting an already-canceled preparation context must not discard that accepted connection.Readnow rechecks existing state: explicit close wins, an accepted socket remains readable, anddialErris read only after the completion channel synchronizes it. Pending cancellation keeps its previous error.The
net.Connconcurrent-read/write contract makes this ordering legal. A pre-first-write reader has not been established in the ordinary tunnel path; this is not a separately observed production incident. The regression fixture directly arranges publication-before-notification with four waiting readers andsynctest.Wait; this is deterministic scheduling injection, not a claim that the real dialer ignores cancellation.06f16aafplus the new fixtureunexpected EOF; no compile failureb32780aaThe previous-runtime control is distinct from the unchanged-Alpha close/publication reproducer above.
Final frozen-batch validation also passes full root
./...default/with_gvisor, vet and Windows/macOS changed-package compilation in the declared combination: tunnel98d8db81, TFOb32780aa, groupe8ffb994, context completion23285c8f, and native API5b6706ccthrough 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 nestedtest/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.