[client] Hand off dialed connections to the sweeper atomically

WrapDialContext and WrapConn registered the dial and the connection
independently, so a sweep landing between the dial finishing and WrapConn
cancelled only the dial registration: the connection dialed on the old
network entered the fresh registry and survived the network change.

Replace the pair with a Dial handle. Sweep marks pending dials under the
sweeper mutex, and WrapConn decides under the same mutex: a swept dial's
connection is closed and ErrSwept returned, so the caller redials on the
new network; otherwise the connection transfers to the registry with no
window in between.
This commit is contained in:
Zoltán Papp
2026-08-11 18:09:37 +02:00
parent 77e7d82d5a
commit 71bfc73cd1
4 changed files with 144 additions and 52 deletions

View File

@@ -400,8 +400,9 @@ func (c *Client) Close() error {
func (c *Client) connect(ctx context.Context) (*RelayAddr, error) {
// A sweep cancels this context, so a dial started on the old network
// aborts instead of waiting out its handshake timeout.
ctx, releaseDial := c.sweeper.WrapDialContext(ctx)
defer releaseDial()
dial := c.sweeper.StartDial(ctx)
defer dial.Release()
ctx = dial.Ctx()
mode := transportModeFromEnv()
dialers := c.getDialers(mode)
@@ -433,7 +434,10 @@ func (c *Client) connect(ctx context.Context) (*RelayAddr, error) {
c.transport = tc.Protocol()
}
conn = c.sweeper.WrapConn(conn)
conn, err := dial.WrapConn(conn)
if err != nil {
return nil, fmt.Errorf("register connection: %w", err)
}
c.relayConn = conn
c.datagramFallbackTriggered.Store(false)