Skip to content

desktop: ECONNRESET on networks that drop fragmented TLS ClientHello (Bun sends 1741-byte PQ hello) #60661

Closed as duplicate

Description

@bobro99

Summary

On networks where an in-path device drops TLS ClientHello messages that span more than one TCP segment (observed on a residential ISP toward Cloudflare-fronted destinations), every model request fails with ECONNRESET. The bundled runtime sends a 1741-byte ClientHello because of the default X25519MLKEM768 post-quantum key share, which cannot fit into a single segment at MSS 1360, so the connection dies before the handshake completes. Other TLS clients on the same host and network work fine.

Environment

  • opencode version: v2.2.3 (desktop app, bundled opencode-cli 2.2.3-x64)
  • OS: Windows 11, Windows_NT 10.0.26200.9457 (win32 x64)
  • Terminal: desktop app (no TERM_PROGRAM); TERM=xterm-256color
  • Shell: C:\WINDOWS\system32\cmd.exe
  • Install/channel: release (%LOCALAPPDATA%\Programs\ai.opencode.desktop, CLI in %APPDATA%\ai.opencode.desktop\cli\2.2.3-x64)
  • Active plugins: none (no plugin directory, ~/.config/opencode/opencode.json has providers only)

Reproduction

  1. Use a network path where a middlebox drops a ClientHello that is split across two or more TCP segments, while accepting a single-segment one. In my case this happens for Cloudflare ranges only — 172.65.90.20-23, 172.66.173.149, 104.16.132.229 — and it is independent of the destination port (443, 8443, 2053, 2083, 2087, 2096 all behave the same).
  2. Start the desktop app with that path active (no VPN/proxy) and send any prompt to a provider hosted behind those addresses (opencode Zen). No special configuration.
  3. Observe the TCP connection to :443: the client's first data flight is 1360 + 381 bytes; the server acknowledges only the first segment; the client retransmits the tail at 0.17 / 0.48 / 1.08 / 2.30 / 4.70 / 9.50 s; no ACK ever arrives; the client sends RST at ~19 s.

Deterministic for me: it failed on every attempt (10+ times) over several days, while the same provider works instantly through a VPN.

Expected Behavior

The request should go through, as it does for other TLS clients on the same machine and network (Python, curl, browsers) — and as it does for the same app over a VPN.

Actual Behavior

Every request fails with:

AI.Error: ECONNRESET: The socket connection was closed unexpectedly.
For more information, pass `verbose: true` in the second argument to fetch()
    at i (B:/~BUN/root/chunk-fz8jgc59.js:3:4247)

Additional Context

What the capture shows

Packet capture (pktmon) of the app's own traffic, no VPN, Wi-Fi interface, MTU 1400:

192.168.2.94:10388 -> 172.65.90.21:443
0.0000 cli S    seq=X
0.0505 srv SA   ack=X+1                     MSS=1400 (client SYN offered MSS=1360)
0.0508 cli A    seq=X+1
0.0529 cli PA   pay=1360 seq=X+1            <- ClientHello, part 1
0.0529 cli PA   pay= 381 seq=X+1361         <- ClientHello, part 2
0.1656 cli PA   pay=1360 seq=X+382          <- retransmit of the tail, coalesced to MSS
0.4812 / 1.08 / 2.30 / 4.70 / 9.50          <- RTO backoff, still no ACK
19.11  cli RST                              -> ECONNRESET in the app

The first flight is 1741 bytes (16 03 01 06 c8 ..., TLS record length 1736, handshake length 1732).

Why the flight is that large

The runtime is Bun with BoringSSL, and the default group list includes X25519MLKEM768 (strings on the binary shows the group name and the BoringSSL group table). The hybrid post-quantum key share occupies ~1.2 KB, which pushes the ClientHello past the MSS (1360 with a 1400 path MTU). So the message is always split: there is no configuration that avoids it.

Why this is not MTU, LSO or a local stack problem

  • Path MTU measured with ICMP DF from the same interface: IP 1400 → OK, IP 1420 → "needs fragment". MTU 1400 is correct.
  • Disabling LSO/TaskOffload globally and per-adapter did not change anything; frames are now legally segmented (1414/94/694/334 bytes).
  • Real TLS traffic over the same sockets and the same destination works: a full handshake plus POST bodies of 1000 / 3000 / 8000 bytes (≈9 segments) completes in <1 s. Only the fragmented ClientHello is dropped.
  • Same 1300-byte ClientHello, split into 2 segments, sent 3× per destination:
destination 1 segment 2 segments
api.timeweb.ai (37.220.83.96) server answered 3/3 server answered 3/3
github.com (20.250.119.67) 3/3 3/3
1.1.1.1 3/3 3/3
172.65.90.21 (opencode.ai) 3/3 0/3
172.66.173.149 (opencode.ai) 3/3 0/3
104.16.132.229 (opencode.ai) 3/3 0/3

So: bytes, port, SNI and MTU are identical; only the destination range decides, and only when the ClientHello is fragmented.

Why it matters beyond one ISP

The app is effectively unusable on any network with anti-fragmentation DPI in front of Cloudflare, which is a common middlebox policy. Because the ClientHello size is fixed by the runtime's default group list, the user has no workaround other than routing all traffic through a VPN.

Possible directions

  • Expose a way to restrict TLS groups / disable the post-quantum key share (env var or config), which would bring the ClientHello back under ~1 KB and into a single segment.
  • Or clamp the ClientHello to the connection MSS.
  • Or document the VPN/proxy workaround in the FAQ until then.

Workarounds tried

  • MTU 1400 + LSO off: no effect (the drop is content-based, not size-based).
  • IPv6: opencode.ai publishes AAAA records, but this host has no global IPv6 address on that network.
  • VPN: works, but is the thing we are trying to avoid.

Activity

  1. opencodejr commented on Oct 30, 2026

    @opencodejr
    Contributor

    Thanks for the very thorough report. The packet capture and the per-destination table make the cause clear.

    I think this is a duplicate of #60399. It is the same failure: OpenCode 2.x's bundled runtime (Bun) adds an X25519MLKEM768 post-quantum key share to the TLS ClientHello. That makes the ClientHello large enough to be split across two TCP segments, and network filtering in front of opencode.ai/Cloudflare drops the split handshake. Requests then fail with ECONNRESET or hang without a VPN and work with one, while curl and other clients on the same machine succeed. That issue has been reproduced, and the fix you suggest (limiting the TLS groups so the ClientHello fits in one segment) is already discussed there. A commenter there also confirmed that a request with ecdhCurve: 'X25519:P-256' succeeds on an affected line.

    Possible duplicates and related issues, ordered by likelihood:

    Your capture is a useful extra data point for the desktop app, so please consider adding it to #60399. The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.

  2. opencodejr commented on Oct 31, 2026

    @opencodejr
    Contributor

    Closing this as a duplicate of #60399, since there's been no reply in the past day to say it's a different problem. Both have the same cause: the bundled runtime's large post-quantum TLS ClientHello is split across TCP segments and dropped by network filtering in front of opencode.ai. Please follow #60399 for updates; adding your desktop pktmon capture there would be a useful extra data point. If your problem turns out to be different, reply here and we can reopen.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions