schollz/croc · error · Error

Sender did not secure the channel

Error message

Sender did not secure the channel

What it means

Thrown by the recipient when the first message received after key establishment (sent encrypted with the derived key) is not 'externalip'. Both peers exchange external IP announcements as the first encrypted control messages to confirm the channel works end-to-end; receiving anything else — typically a decrypt failure artifact, an error frame, or a desynchronized peer — means the encrypted control channel is not functioning as expected.

Source

Thrown at web/src/protocol/client.ts:728

    }
    requirePakeVersion(confirmationB.v);
    if (!(await wasm().confirmPeerKey(peerKeys.confirmationB, confirmationB.b))) {
      throw new Error("Sender PAKE confirmation failed");
    }
    key = peerKeys.key;
    data = await openDataConnections(
      settings,
      room,
      dataPorts(relay.banner),
      signal,
    );
    await sendControl(control, {
      t: "externalip",
      m: relay.externalIP,
      b: peerPake.b,
    }, key);
    const peerIP = await receiveControl(control, key);
    if (peerIP.t !== "externalip") throw new Error("Sender did not secure the channel");

    const fileInfo = await receiveControl(control, key);
    if (fileInfo.t === "error") throw new Error(fileInfo.m || "Sender cancelled");
    if (fileInfo.t !== "fileinfo" || !fileInfo.b) {
      throw new Error("Sender did not provide file metadata");
    }
    const sender = JSON.parse(textDecoder.decode(fileInfo.b)) as SenderInfoWire;
    const offer = validateSenderInfo(sender);
    callbacks.onStatus?.("Review the incoming files");
    const destination = await callbacks.onOffer(offer);
    if (!destination) {
      await sendControl(control, { t: "error", m: "refusing files" }, key);
      throw new Error("Transfer refused");
    }

    for (const folder of offer.emptyFolders) {
      await destination.createEmptyFolder(folder);
    }

View on GitHub (pinned to e25f1bdc04)

Solutions

  1. Check the sender side — a data-channel setup failure is the usual cause and the peer error tells you which port/relay failed
  2. Verify the relay banner port list is valid and those ports are reachable by both peers
  3. Upgrade both peers so the post-handshake message order (externalip <-> externalip) matches
  4. Retry with a different relay to rule out relay-level frame injection or connection resets
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: verify the relay's data ports are parseable and reachable before handshake
const ports = dataPorts(relay.banner);
if (ports.length === 0) throw new Error("relay banner has no data ports");

Type guard

function isExternalIP(m: { t: string }): boolean { return m.t === "externalip"; }

Try / catch

catch (e) {
  if (e instanceof Error && e.message === "Sender did not secure the channel") {
    // peer failed post-handshake (usually data-channel setup): check peer logs / relay ports, retry
  }
  throw e;
}

Prevention

When it happens

Trigger: Peer sends an 'error' control message instead (it aborted during data-connection setup); decrypt fails or produces a message whose type decodes to something unexpected because keys differ (should have failed at pake-confirm, but partial divergence is possible); relay injects a banner or close frame; peer implementation skips the externalip exchange.

Common situations: Sender fails at openDataConnections (relay port list invalid, data ports unreachable) and reports an error; version-skewed peer with a different post-handshake choreography; the control socket receives a relay-level close frame mistaken for a peer message.

Related errors


AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15). Data as JSON: /api/errors/6fe1cdcba167e7f7. Report an issue: GitHub.