schollz/croc · error · Error
Peer sent an unexpected control handshake
Error message
Peer sent an unexpected control handshake
What it means
While probing for local-IP connectivity (the handshake/probe exchange on the control socket), the client received bytes that decrypted and decoded but failed JSON.parse. The peer was expected to send a JSON 'pake1' probe object; anything that is not valid JSON at this point is treated as an unexpected handshake. It indicates protocol divergence or garbage on the control channel, not a wrong passphrase (decryption already succeeded).
Source
Thrown at web/src/protocol/client.ts:190
}
if (bytesEqual(plain, IP_REQUEST) && localKey) {
await socket.send(
await engine.encrypt(textEncoder.encode(JSON.stringify([])), localKey),
);
continue;
}
let probe: {
Bytes?: string;
Bytes2?: string;
Kind?: string;
Version?: number;
Curve?: string;
};
try {
probe = JSON.parse(textDecoder.decode(plain)) as typeof probe;
} catch {
throw new Error("Peer sent an unexpected control handshake");
}
if (probe.Kind !== "pake1" || !probe.Bytes || !probe.Curve) {
throw new Error("Peer sent an unexpected control handshake");
}
requirePakeVersion(probe.Version);
const initiator = base64ToBytes(probe.Bytes);
const pake = await engine.pakeInitWithIdentities(
textEncoder.encode(passphrase),
1,
probe.Curve,
PAKE_PURPOSE_LOCAL_PROBE,
room,
);
const finished = await engine.pakeUpdate(pake.handle, initiator);
const salt = randomBytes(PAKE_SALT_SIZE);
const keys = await engine.derivePeerKeys(
finished.key,
salt,View on GitHub (pinned to e25f1bdc04)
Solutions
- Ensure both peers run compatible croc builds (same PAKE protocol version)
- Retry with a fresh connection; do not reuse a control socket after a failed handshake
- If behind a custom gateway, verify it does not inject frames into the relay data stream
Example fix
// before: retrying on a consumed socket
try { await waitForHandshake(control, ...); } catch { /* control buffer is dirty */ }
await waitForHandshake(control, ...); // JSON.parse of leftover bytes fails
// after: reconnect from scratch on failure
control?.close();
const relay = await connectRelay(settings, room, port, signal);
control = relay.socket; Defensive patterns
Strategy: try-catch
Try / catch
try {
await waitForHandshake(control, room, passphrase, signal);
} catch (e) {
if (/unexpected control handshake/.test(e.message)) {
control.close();
const relay = await connectRelay(settings, room, port, signal); // fresh socket
return waitForHandshake(relay.socket, room, passphrase, signal);
}
throw e;
} Prevention
- Never reuse a control socket after a failed handshake attempt
- Keep both clients on the same protocol build
- Log the raw decrypted bytes on parse failure to spot gateway-injected frames
When it happens
Trigger: A peer or middlebox sending a raw binary banner where the JSON probe was expected; a croc version that emits a different probe framing; leftover buffered bytes from a previous failed handshake being read as the probe.
Common situations: Mixing client protocol versions; connecting through a gateway that injects its own keepalive frames; retry logic that reuses a half-consumed socket.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- Recipient did not start a croc PAKE handshake
- Sender did not complete the croc PAKE handshake
- Sender did not secure the channel
- Peer uses unsupported PAKE protocol version ${version ?? 0};
- Recipient did not confirm the croc PAKE handshake
AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15).
Data as JSON: /api/errors/2538aacbac90c06e.
Report an issue: GitHub.