Skip to content

fix(iOS): arm TLS before connectToHost so connect is never announced pre-handshake - #238

Open
MidnightCoke wants to merge 1 commit into
Rapsssito:masterfrom
MidnightCoke:fix/ios-arm-tls-before-connect
Open

MidnightCoke wants to merge 1 commit into
Rapsssito:masterfrom
MidnightCoke:fix/ios-arm-tls-before-connect

Conversation

@MidnightCoke

Copy link
Copy Markdown

Fixes #237.

The bug

connect: armed TLS after starting the connect:

result = [_tcpSocket connectToHost:host onPort:port ...];
if (result && tlsOptions) {
    [self startTLS:tlsOptions];   // sets _tls at its END, after the keychain work
}

startTLS: sets _tls = true only once the settings dictionary is built, and for client-certificate auth that build imports a PKCS#12 and touches the keychain. didConnectToHost: runs on the delegate queue (dispatch_get_main_queue()) and is gated on if (!_tls), so a connect that finished first announced a plain connect for a socket whose handshake had not begun. connectTLS wires secureConnect to that event (src/index.js:49), so callers were told the socket was secure before the handshake and writes from the callback went out ahead of the TLS packet.

The change

  • Split the settings construction out of startTLS: into tlsSettingsForOptions:, declared in the class extension so the header is untouched. The body is unchanged; only the last two lines become return settings;.
  • In connect:, build the settings and set _tls before connectToHost:, then queue [_tcpSocket startTLS:settings] right behind the connect. CocoaAsyncSocket queues it as a write packet and starts the handshake once connected, so there is no ordering left for didConnectToHost: to lose: it can never observe _tls == false for a TLS connect. _tls is cleared again when the connect never started.
  • Clear _connecting on the plain-connect path in didConnectToHost:. This line is taken from fix(iOS): defer startTLS until didConnectToHost #217 and fixes a second bug: without it a later new TLSSocket(existingSocket) upgrade announces a duplicate connect from socketDidSecure.

The keychain work stays on the module queue rather than moving to the main queue.

Testing

  • Reproduced on the iOS simulator against a local mock TLS server: the client emitted connect before any handshake byte.
  • The ordering change is verified in a shipping iOS app across three TLS LAN protocols: Android TV Remote v2 (mutual TLS, 6467/6466), Samsung Tizen (:8002) and LG webOS (:3001). Before it, the premature connect was read as a socket that never completed its handshake and torn down in a retry loop; in one release the median connect time went from 517 ms to 5589 ms. After it, connect is unconditionally a secure connect.
  • The _connecting line is reviewed but not exercised by our app, which only uses connectTLS. It carries fix(iOS): defer startTLS until didConnectToHost #217's own testing, not ours.
  • No JS changed; Android untouched.

Relationship to #217

#217 found the same race first and fixes it by deferring startTLS: into didConnectToHost:. Credit to @manoaxcend. Two reasons this PR takes the other direction:

  1. Deferring moves the PKCS#12 import and keychain identity build onto the main queue for every TLS connect, since that is this class's delegate queue.
  2. _tlsOptionsPending is assigned after connectToHost: returns, on a different queue from the one that reads it, so the window is narrowed rather than closed. Setting _tls before the connect is dispatched removes it entirely.

Happy to close this in favour of #217 plus a one-line suggestion if you prefer that shape.

🤖 Generated with Claude Code

…pre-handshake

`connect:` called `startTLS:` after `connectToHost:`, and `startTLS:` set `_tls`
only at its end, after building the TLS settings. With client-certificate auth
that build imports a PKCS#12 and touches the keychain, which can outlast a LAN
connect. `didConnectToHost:` runs on the delegate queue (the main queue) and is
gated on `if (!_tls)`, so a connect that completed first announced a plain
`connect` for a socket whose handshake had not begun. `connectTLS` wires
`secureConnect` to that event, so callers were told the socket was secure and
anything written from the callback went out ahead of the TLS packet.

Build the settings and set `_tls` before `connectToHost:`, then queue
`startTLS:` on the socket right behind the connect; GCDAsyncSocket starts the
handshake once connected. `_tls` is cleared again if the connect never started.

Also clear `_connecting` on the plain-connect path, from Rapsssito#217: without it a
later `new TLSSocket(socket)` upgrade announces a second `connect` from
`socketDidSecure`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

[iOS] connectTLS can emit a plain connect before the TLS handshake starts

1 participant