Skip to content

api.telegram.org backends reset TLS ClientHellos that offer X25519MLKEM768 (~17% of connections; breaks Node 24 bots by default) #896

Description

@antialias

Some — not all — of the backends behind api.telegram.org abort the TLS handshake with a TCP RST when the ClientHello offers the post-quantum hybrid group X25519MLKEM768.

That group is enabled by default in OpenSSL 3.5.x, which is what Node.js 24 bundles. So any Node 24 bot hits this without configuring anything, and will keep hitting it as more runtimes pick up OpenSSL 3.5 / PQ-by-default.

The failure surfaces to the application as a generic network error on sendMessage — indistinguishable from ordinary flakiness — so bots retry blindly or drop the message. It cost us a silently lost reply before we found it.

Measurement

Interleaved A/B (alternating one connection per arm, so a transient outage can't be attributed to one side). 125 fresh TLS connections per arm to api.telegram.org:443, resolving to 149.154.166.110:

ClientHello groups ECONNRESET
OpenSSL 3.5.7 default (includes X25519MLKEM768) 21 / 125 (16.8%)
X25519:prime256v1 (classical only) 0 / 125 (0.0%)

Negative controls, same process, same interleaving, 25 connections per arm: api.anthropic.com, api.openai.com, www.googleapis.com, oauth2.googleapis.com, graph.microsoft.com0 resets on both arms. So this is specific to api.telegram.org, not to the client, the network, or the PQ group as such.

Ruling out the local network: curl from inside the same container (system OpenSSL 3.0.20, which has no MLKEM support and so never offers it) 0/70; from the Docker host 20/20; from a laptop on a different network 20/20. Only the process offering the PQ group fails.

Likely cause

An MLKEM768 key share is ~1.2 KB, which pushes the ClientHello past a single TCP segment. This looks like a load balancer or TLS terminator in part of the pool that can't reassemble a multi-segment ClientHello. The inconsistency across connections to the same IP suggests it is per-backend rather than per-edge, which would also explain why it reads as intermittent flakiness rather than a hard failure.

Reproducer

Node 24, no dependencies. Expect a nonzero count on the first arm and zero on the second:

const tls = require("node:tls");
const N = 125;
const connect = (curve) => new Promise((res) => {
  const opts = { host: "api.telegram.org", port: 443, servername: "api.telegram.org" };
  if (curve) opts.ecdhCurve = curve;
  const s = tls.connect(opts, () => { s.destroy(); res(null); });
  s.on("error", (e) => res(e.code));
});
(async () => {
  let a = 0, b = 0;
  for (let i = 0; i < N; i++) {            // interleaved, not batched
    if (await connect(undefined) === "ECONNRESET") a++;
    if (await connect("X25519:prime256v1") === "ECONNRESET") b++;
  }
  console.log({ defaultGroups: a, classicalOnly: b, of: N });
})();

Environment

  • Node v24.18.0, OpenSSL 3.5.7 (Node's bundled build)
  • Debian 12 bookworm, linux/x64, in Docker
  • Observed continuously; the A/B above is from 2026-09-01

Client-side workaround

For anyone else landing here: set tls.DEFAULT_ECDH_CURVE = "X25519:prime256v1" before any TLS connection is opened. Two gotchas — it is per-JS-context, so worker threads need it set again (NODE_OPTIONS reaches child processes but is not in process.execArgv, so it never reaches worker threads); and setting Groups via OPENSSL_CONF does not work, because Node ignores it.

I appreciate this repo is the self-hosted Bot API server rather than the hosted infrastructure — filing here because it's the closest public tracker for api.telegram.org behaviour. Happy to move it if there's a better venue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions