Skip to content

cli: hangs before sending request on networks that drop fragmented TLS ClientHello (post-quantum key share) #60399

Closed

Description

@tweenfoldkay-sudo

Summary

On a network where the ISP's DPI drops/resets fragmented TLS ClientHellos, opencode hangs before it ever sends the inference request: no session, no stream, no error for 120+ seconds. The same request from curl on the same machine and same IP succeeds in ~0.2s. Works instantly when routed through a VPN.

Environment

  • opencode version: 2.2.2 (latest channel)
  • OS: Windows 10 Pro 26H2, build 26300.9818 (win32 x64)
  • Terminal: xterm-256color (TERM set; TERM_PROGRAM/COLORTERM unset)
  • Shell: C:\WINDOWS\system32\cmd.exe
  • Install/channel: npm global install (@opencode/cli), channel latest
  • Active plugins: none (~/.config/opencode/opencode.jsonc contains only {"autoupdate": false})

Reproduction

  1. Use a network where TLS ClientHellos split across two TCP segments are dropped by DPI (observed on Rostelecom, Irkutsk, Russia; home IP, no VPN, no local proxy/TUN active).
  2. Ensure no VPN/proxy is running (Get-NetAdapter shows only the physical Ethernet adapter; public IP is the ISP's).
  3. Run:
    opencode run --model opencode/deepseek-v4.1-flash-free "hi"
    
  4. Observe: command produces no output and never completes (still running after 120s).
  5. Repeat the same run with a VPN enabled: it answers within a couple of seconds.

Expected Behavior

Either the request is sent and a response (or a normal HTTP error such as 401/429) is returned, or a clear network error is reported quickly. It should not hang indefinitely without any log line indicating where it stopped.

Actual Behavior

The CLI starts, spawns its local server, the server boots and emits provider.updated / model.updated / agent.updated events, and then nothing at all. There is no session line, no stream line and no error — for 120+ seconds:

23:37:16.915Z level=INFO run=e33c85a4 message="cli starting" version=2.2.2 channel=latest local=false args="[\"run\",\"--model\",\"opencode/deepseek-v4.1-flash-free\",\"hi\"]" role=cli
23:37:22.001Z level=INFO run=7c383666 message="cli starting" version=2.2.2 channel=latest local=false args="[\"serve\",\"--service\"]" role=cli
23:37:22.601Z level=INFO run=7c383666 message=event event.type=provider.updated ...
23:37:22.606Z level=INFO run=7c383666 message=event event.type=model.updated ...
23:37:22.607Z level=INFO run=7c383666 message=event event.type=agent.updated ...
(no further entries for 120s)

In other sessions the same environment instead produces, after ~19-20 seconds:

level=ERROR message="stream error" providerID=opencode modelID=deepseek-v4.1-flash-free
  error.error="AI_APICallError: Cannot connect to API: ECONNRESET: The socket connection was closed unexpectedly."

Notably, curl on the very same machine, same IP, same moment:

$ curl -s -D - -o NUL -X POST -H "Content-Type: application/json" \
    -d '{"model":"deepseek-v4.1-flash-free","messages":[{"role":"user","content":"hi"}]}' \
    https://opencode.ai/inference/openai/v1/chat/completions
HTTP/1.1 400 Bad Request
Content-Length: 0
Server: cloudflare
x-opencode-log-id: 01a18a01-9bf6-7360-b0a1-61ec0c3cd719

$ curl -s -o NUL -w "%{http_code}" https://opencode.ai/inference/v1/models
200

So DNS, TCP and TLS from this host are fine, and the gateway responds normally. Only opencode's own fetch path fails.

Additional Context

Suspected cause: Bun's TLS stack sends a post-quantum hybrid key share (X25519MLKEM768), which makes the ClientHello roughly 1.5-1.9 KB and therefore spans two TCP segments. Networks with DPI that mishandles fragmented ClientHellos drop or reset it, so the handshake never completes. curl on Windows uses schannel with a small (~300 byte) ClientHello and is unaffected. Routing through a VPN hides the ClientHello inside the tunnel, which is why it works.

This is consistent with all observations:

  • curl (small ClientHello) works without VPN; opencode (large, fragmented ClientHello) does not.
  • opencode works immediately over VPN.
  • Not model-specific: deepseek-v4.1-flash-free and beam-free fail identically.
  • npm installs performed by opencode also fail with read ECONNRESET against https://registry.npmjs.org in the same conditions (NpmInstallFailedError), i.e. the failure is in Bun's fetch, not in the inference client.
  • IPv6 is not involved (no global IPv6 address, no AAAA records).
  • The endpoint is not blocked and not rate limited: unauthenticated curl POSTs return 400, GET /inference/v1/models returns 200. An earlier 429 with Retry-After was transient and later cleared.

Workarounds tried:

  • VPN: works, but is the thing we want to avoid.
  • Setting HTTPS_PROXY / HTTP_PROXY to a local proxy: had no effect on the inference request (opencode still answered while the proxy pointed at a dead port), so it is not a usable workaround.
  • BUN_CONFIG_VERBOSE_FETCH=1: only local 127.0.0.1 API calls are logged; the failing external fetch is not shown, which makes the hang hard to diagnose.

Suggestion: if possible, expose a way to disable the post-quantum key share (e.g. a TLS curves option or an env var), or add a connect/handshake timeout that surfaces a clear error instead of hanging silently. Other users behind DPI-heavy networks are likely affected.

Frequency: 100% reproducible on this network without a VPN; 0% with a VPN.

Happy to run any additional diagnostics you need.

Activity

  1. opencodejr commented on Oct 30, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report and the curl comparison. I could reproduce the silent hang, and it still happens on the latest dev (237a847).

    I can't recreate your ISP's DPI here, so I simulated it with a local server. It accepts the TCP connection, receives opencode's TLS ClientHello (about 1.48 KB) and never answers. A custom OpenAI-compatible provider points at that server, and the command is opencode run --standalone --print-logs --model blackhole/m "hi".

    On 2.2.2 and on dev:

    • Nothing is printed, and the logs stop after provider.updated / model.updated, with no session or stream line, just like your log.
    • After about 5 minutes (the default headerTimeout), opencode quietly opens a new connection and keeps waiting, still with no output.
    • The ECONNRESET error only appears when the process is killed.

    So a stalled TLS handshake is treated like a slow model response and waited on silently, instead of failing quickly with a clear network error. The script below reproduces it.

    This only confirms the hang. It doesn't confirm that your network drops the larger ClientHello. Your suggestions (an option to turn off the post-quantum key share, and logging of failed external fetches) are separate improvements for the maintainers to consider.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #60399: `opencode run` hangs silently when the TLS handshake stalls
    # (server accepts TCP, receives the ClientHello, never answers - like DPI dropping it).
    # Usage: ./repro.sh                 -> uses `opencode` on PATH
    #        OC_DEV=/path/to/repo ./repro.sh -> runs from a source checkout
    set -u
    DIR=$(mktemp -d)
    cat > "$DIR/blackhole.mjs" <<'EOF'
    import net from "node:net";
    net.createServer(s => {
      console.log("tcp connection accepted");
      s.on("data", d => console.log(`received ${d.length} bytes (TLS ClientHello), never answering`));
      s.on("error", () => {});
    }).listen(9443, "127.0.0.1", () => console.log("blackhole listening on 9443"));
    EOF
    mkdir -p "$DIR/proj" "$DIR/home"
    cat > "$DIR/proj/opencode.json" <<'EOF'
    {
      "provider": {
        "blackhole": {
          "npm": "@ai-sdk/openai-compatible",
          "name": "Blackhole",
          "options": { "baseURL": "https://127.0.0.1:9443/v1", "apiKey": "x" },
          "models": { "m": { "name": "m" } }
        }
      }
    }
    EOF
    node "$DIR/blackhole.mjs" & BH=$!
    sleep 1
    cd "$DIR/proj"
    # from source, the spawned server process needs the opentui preload too
    [ -n "${OC_DEV:-}" ] && echo "preload = [\"$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js\"]" > bunfig.toml
    if [ -n "${OC_DEV:-}" ]; then
      OC=(bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts")
    else
      OC=(opencode)
    fi
    start=$(date +%s)
    HOME="$DIR/home" timeout ${RUN_TIMEOUT:-120} "${OC[@]}" run --standalone --print-logs --model blackhole/m "hi" </dev/null >"$DIR/out" 2>"$DIR/err"
    code=$?
    echo "exit=$code after $(( $(date +%s) - start ))s (124 = killed by timeout: hung with no error)"
    echo "--- stdout:"; cat "$DIR/out"
    echo "--- session/stream/error log lines:"; grep -i -E "session|stream|error" "$DIR/err" | grep -v "watcher" | head
    kill $BH

    Run it: bash repro.sh

  2. MLGSW4G commented on Oct 30, 2026

    @MLGSW4G

    Same failure confirmed on several lines in Russia, starting Oct 29–30 UTC.

    To be explicit up front: this is not an OpenCode defect. The connection is dropped during the TLS handshake, before any HTTP request is sent, so nothing in the client's request handling is involved — the same failure hits any client that offers a post-quantum key share, including Chrome and Edge.

    Every affected line reported so far is Rostelecom: Irkutsk ×2 (this report, and the reply in #60455), Perm ×1, and Ulyanovsk in #60518. That is a sample artefact as much as a finding — Rostelecom is the largest provider, and DPI filtering of this kind is mandatory across Russian operators, so other ISPs may simply not have this rule deployed yet. If you are on another provider and see the same split (browser and Bun failing while curl.exe returns 200 on the same host at the same moment), please add your ISP and city here.

    On all affected lines the browser cannot load opencode.ai and opencode (Bun fetch) times out or returns ECONNRESET, while curl.exe against the same host from the same machine at the same moment returns 200 — so the host is reachable and the reset happens inside the TLS handshake.

    Hello-level measurement. On one affected line (Perm), with a host that showed the identical failure temporarily removed from the DPI-bypass list, using openssl s_client -tls1_3 -servername <host> -groups <group>:

    ClientHello key share Result
    X25519MLKEM768 (post-quantum, ~1.5 KB) TIMEOUT 3/3
    X25519 (classical, ~300 B) OK 3/3

    Same host, same edge IP, same second, three rounds each. opencode.ai shows the same split (Bun times out, curl returns 200) but was not A/B'd at hello level. This varies the key share and the hello size together, so it does not separate "PQ key share present" from "hello exceeds one TCP segment" — but it does rule out the server, the IP and plain SNI matching, since a classical hello with the same SNI passes.

    That is consistent with the simulation above, and it also explains why the browser is affected: Chrome and Edge offer the PQ key share by default, while Windows curl (Schannel) does not. On the same line, other hosts behaved the same way, while api.opencode.ai and api.github.com were fine.

    Workaround. Add the affected hostnames to the DPI-bypass hostlist. With opencode.ai listed, it returns 200 through Bun on a direct connection with no VPN.

    Not the same as the earlier reports. #59291 (resets on long or complex responses), #59786 and #54160 (stale keep-alive reuse) all fail after the connection is established; this one fails during the handshake. #58854 ("TLS handshake error", Oct 25) is closest in phase but has no ISP or geolocation data, so it cannot be classified either way.

    The only client-side item here is failure reporting, not connectivity: the 120 s silent hang with no log line is worth fixing regardless of this network condition, because failing fast and naming the host would have made this a one-line diagnosis instead of a day of guessing.

  3. MLGSW4G commented on Oct 30, 2026

    @MLGSW4G

    Follow-up: hello size, the version split, and a client-side escape hatch.

    Additional per-name evidence on the same line (Rostelecom, Perm). With the host removed from the DPI bypass, so the rule is active for it:

    host, not in the bypass list curl.exe Bun 1.4.4 default
    models.dev 200 ERR 23 timeout
    registry.npmjs.org 200 ERR 23 timeout

    With fetch(url, { tls: { ecdhCurve: 'X25519:P-256' } }) the same host returns 200 on the same line, same minute, no VPN. So the rule is per name, and it is the handshake shape that decides.

    Where the shape changes. ClientHello sizes measured locally against openssl s_server -trace, i.e. bytes on the wire:

    client ClientHello key share
    curl.exe (Schannel) 438 B x25519
    Bun 1.3.14 508 B x25519
    Bun 1.4.0 508 B x25519
    Bun 1.4.1 1487 B X25519MLKEM768 first
    Bun 1.4.4 1491 B X25519MLKEM768
    Bun 1.4.4 + ecdhCurve: 'X25519' 265 B x25519
    Bun 1.4.4 + ecdhCurve: 'X25519:P-256' 267 B x25519

    That explains the version split in #60518: 1.18.12 pins [email protected], while 1.19.5 and 2.2.2 both pin [email protected]. The default changed in Bun 1.4.1 — the same release rewrote its usockets TLS layer (packages/bun-usockets/src/crypto/openssl.c), and the only SSL_CTX_set1_groups_list call there is the one behind ecdhCurve, so the default comes from the bundled BoringSSL. The boundary is precise: 1.19.3 is the last release on [email protected], 1.19.4 was the first on [email protected], and every v2 release is on 1.4.2 or newer — so 1.19.3 is the newest build that works on an affected line. Those runtime strings were read from the published binaries, not just from the build pin.

    Using the escape hatch. tls.groups is ignored by Bun's fetch; ecdhCurve is what reaches the group list. The names must be BoringSSL's (P-256, not secp256r1), and a single group is not safe: registry.npmjs.org fails with X25519 alone and works with X25519:P-256.

    I would not make that the default. It drops the post-quantum half of the handshake for every connection, which is a poor trade for a coding agent. A retry with the classical group list on handshake failure or timeout — sticky per host, logged — or an explicit opt-in setting keeps PQ wherever it works and degrades only the path that needs it.

    To be explicit: this is a mitigation for a network rule, not a fix. If the rule starts matching small hellos as well, it stops working.

    Happy to send a PR for the retry or the knob if you agree on the shape — I would rather not guess at provider-wide TLS defaults.

  4. MLGSW4G commented on Oct 30, 2026

    @MLGSW4G

    Confirming the proxy note in the report, with one version split that matters:

    client HTTPS_PROXY set to a dead port result
    Bun 1.4.4 fetch yes fails ECONNREFUSED — honoured
    opencode 1.19.3 (v1) yes Cannot connect to API: Unable to connect — honoured
    opencode 2.2.2 (v2) yes completed a free-tier request — ignored for inference

    So the observation that a proxy has no effect holds for v2, but not for v1 — plain Bun and v1 both honour the variable. One related trap: in v2, setting HTTPS_PROXY without NO_PROXY=localhost,127.0.0.1 also routes the CLI's own local server calls through the proxy, which hangs the run.

    That means a proxy is not a workaround for 2.2.x today; only a local DPI bypass (admin) or a client-side change works there.

  5. b0dhi88 commented on Oct 30, 2026

    @b0dhi88

    Same failure from a non-Bun stack, if useful as another data point: pi 1.4.0 on Node 24.20.0 (OpenSSL 3.5.7, undici 7.29.0), provider opencode-go (deepseek-v4.1-flash), OpenAI-compatible endpoint on opencode.ai.

    Symptom in the same window (Oct 30, ~08:27–08:37 UTC): Request timed out. after a constant 10.0–10.5 s, usage: 0, 3 retries. No request ever reaches the provider.

    Where it stalls:

    • TCP connect: 14–55 ms.
    • TLS 1.3 handshake: never completes; undici aborts at its default 10 s connectTimeout.
    • tls.connect against the same host: 5–11 of 16 attempts succeed, the rest time out at 10 s. TLS 1.2: 48/48. TLS 1.3 with ecdhCurve: 'X25519' / 'P-256': 100%.
    • curl and openssl s_client -tls1_3 from the same machine at the same moment: 20/20, ~0.25 s.
    • node --trace-tls: ClientHello 1579 B by default vs 347 B with classical groups; key_share alone is 1258 B (X25519MLKEM768). So this is not Bun-specific — Node 24 / OpenSSL 3.5 has the same default.

    A/B through undici (fresh connection per request): 13/30 successes by default, 25/25 with tls.DEFAULT_ECDH_CURVE = 'X25519:P-256' in the same process.

    Relevant for the proposed fix shape: pi has no connect-timeout knob and declined to add one (#11115, PR #11515 — "waiting longer does not fix that path"), so a client-side retry with classical groups (sticky per host) or an explicit opt-in would cover more than Bun. I currently use the one-line tls.DEFAULT_ECDH_CURVE workaround. Happy to test a patch.

  6. MLGSW4G commented on Oct 30, 2026

    @MLGSW4G

    This is the most useful data point here — it removes the "Bun-specific" framing and quantifies the intermittency.

    1. Scope: it isn't Bun. Node 24 / OpenSSL 3.5 enables X25519MLKEM768 by default too (1579 B vs 347 B), so any modern client — Chrome, Bun, Node 24+, OpenSSL 3.5 — hits this, while Schannel curl (438 B) does not. The 13/30 also explains the "retrying fixed it" and "another client fixed it" reports: that's a biased coin flip, not a workaround.
    2. Fix shape: tls.DEFAULT_ECDH_CURVE = 'X25519:P-256' being one line and process-wide means an opencode-side change does not have to wait on Bun or Node — it can set the classical group list for provider fetches, or on a retry, and cover both runtimes.

    Agreed on sticky retry over a longer timeout: a longer timeout only makes the coin flip slower. The smallest useful patch is: on a handshake timeout, retry once per host with classical groups, and remember the result for that host.

  7. zugmaster commented on Oct 30, 2026

    @zugmaster

    Coming from #60568 (same symptom class, different ISP): I can confirm the "larger ClientHello" part of this hypothesis with a controlled test, so it doesn't have to stay an assumption.

    On the affected machine the trigger is exactly the post-quantum key share, and it only matters for destination SNIs that are not on the carrier's pass-through list.

    Fingerprint boundary

    Same request, same machine, same minute, only the emulated TLS fingerprint changes (curl_cffi):

    host chrome123 (no PQ key share) chrome124 (X25519Kyber768)
    github.com OK 309 ms OK 286 ms
    registry.npmjs.org OK 89 ms OK 82 ms
    ollama.com OK 1791 ms OK 883 ms
    opencode.ai OK 517 ms timeout
    models.dev OK 232 ms timeout

    Stepwise scan on opencode.ai: chrome99, 100, 101, 104, 107, 110, 116, 119, 120, 123 all succeed; chrome124, 131, 136 all time out. Safari profile succeeds. curl and Node succeed. Chrome 124 is the release that enabled the PQ key share by default.

    Hello size

    Captured with strace from the real request, parsed per extension:

    • Bun: 1502 bytes total (TLS record 1497). key_share is 1258 bytes of it — group 0x11ec (X25519MLKEM768) with a 1216-byte key, plus 32 bytes for X25519. So 1216 of 1502 bytes, ~81% of the handshake, is the PQ key.
    • curl: 517 bytes total. key_share is only X25519, 32 bytes.

    With a 1500-byte MTU (1460 MSS) the 1497-byte record cannot fit one segment and goes out as two.

    Vantage point

    Same Bun.connect({tls:true}) script:

    • affected machine: never completes, 8 s timeout, no open, no error
    • VPS outside the country: handshake OK in 214 ms

    Plain TCP to the same host opens in 45 ms, so it's not routing or DNS.

    What this means

    The block is content-dependent and destination-dependent: a PQ ClientHello passes to well-known hosts and is silently dropped for others. It is not specific to Bun — a current Chrome or Firefox profile fails the same way, Bun is just the transport that opencode happens to use.

    That reinforces the suggestions already made here: an option to disable the post-quantum key share (or a non-PQ transport for internal requests) would actually work on such networks, while retries would not — in a sweep of 8 attempts the same Bun fetch to opencode.ai failed 8 times, while the non-PQ profile succeeded every time.

  8. taishi-sama commented on Oct 30, 2026

    @taishi-sama

    As (temporary) alternative solution it also possible to MITM opencode, so it would use OpenSSL cypher+crypto not blocked by DPI:
    Be warn that opencode is pretty strict and use even cypher differences to detect free models abuse, so it is gonna work only with Opencode Go/Opencode Zen

    1. Create the CA + the classical-groups config

    mkdir -p "$HOME/.mitmproxy" && \
    openssl req -x509 -newkey rsa:2048 -sha256 -days 30 -nodes \
      -keyout /tmp/mitm-key.pem -out /tmp/mitm-cert.pem \
      -subj "/CN=mitmproxy" \
      -addext "basicConstraints=critical,CA:TRUE" \
      -addext "keyUsage=critical,keyCertSign,cRLSign" && \
    cat /tmp/mitm-key.pem /tmp/mitm-cert.pem > "$HOME/.mitmproxy/mitmproxy-ca.pem" && \
    cp /tmp/mitm-cert.pem "$HOME/.mitmproxy/mitmproxy-ca-cert.pem" && \
    chmod 600 "$HOME/.mitmproxy/mitmproxy-ca.pem" && \
    printf 'openssl_conf = default_conf\n[default_conf]\nssl_conf = ssl_sect\n[ssl_sect]\nsystem_default = system_default_sect\n[system_default_sect]\nGroups = X25519:P-256\n' \
      > "$HOME/.mitmproxy/openssl-classical.cnf" && \
    rm -f /tmp/mitm-key.pem /tmp/mitm-cert.pem

    2. Spawn the proxy

    OPENSSL_CONF="$HOME/.mitmproxy/openssl-classical.cnf" \
      uvx --from mitmproxy mitmdump --mode regular@8080

    OPENSSL_CONF must be set on this process: the proxy's upstream handshake is
    what the DPI sees now. --from mitmproxy is required because the mitmproxy
    binary itself is the TUI.

    3. Point opencode at it

    HTTPS_PROXY=http://127.0.0.1:8080 NO_PROXY= \
      NODE_EXTRA_CA_CERTS="$HOME/.mitmproxy/mitmproxy-ca-cert.pem" \
      opencode serve
  9. Rive116 commented on Oct 30, 2026

    @Rive116

    help me please, i dont know what to do.NoDPI didn't help; what should I try next? Checking for updates didn't yield any results.

  10. MLGSW4G commented on Oct 30, 2026

    @MLGSW4G
  11. Rive116 commented on Oct 30, 2026

    @Rive116

    Thank you very much, that helped. @MLGSW4G

  12. tweenfoldkay-sudo commented on Oct 30, 2026

    @tweenfoldkay-sudo
    Author

    Field follow-up from an affected line (Rostelecom, Irkutsk, RU) — root-cause validation, a no-VPN workaround, and one confusing error to not conflate

    Confirming the diagnosis of this thread from a real affected line, plus two things that may help other users and the maintainers.

    Environment

    • opencode v2.2.2 (npm @opencode/cli, Bun-compiled binary, Bun 1.4.4 runtime)
    • Windows, ISP: Rostelecom (Irkutsk, Eastern Siberia) — DPI drops the fragmented post-quantum ClientHello for the SNIs opencode.ai (and models.dev is not filtered on this line)

    Measurements on the same machine / same line (today):

    client → path TLS ClientHello plain line (no bypass) with DPI-desync bypass, still no VPN
    curl.exe (Windows Schannel) → opencode.ai ~440 bytes, no PQ key share HTTP 400 in 0.65 s ✅ HTTP 400 in 0.65 s ✅
    Bun 1.4.4 default fetch (= opencode's stack, X25519MLKEM768 → CHLO ≈ 1.5 KB, split into 2 TCP segments) large CHLO stalls, server never responds ❌ HTTP 500 in 1.8 s ✅ (server answered — body was {}, so 4xx/5xx is expected; the point is the handshake + HTTP round-trip completed)
    Bun fetch(url, { tls: { ecdhCurve: "X25519:P-256" } }) classical groups only answers instantly ✅ ✅
    opencode itself (TUI / run) same Bun stack hangs before sending the request, no output for minutes ❌ works ✅

    So the client-side escape hatch everyone proposed earlier (retry/downgrade to classical groups) works on the real affected line, and the proxy path (HTTPS_PROXY) also fixes it — both without touching the network.

    The end-user workaround that worked end-to-end today (no VPN):

    • Flowseal/zapret-discord-youtube (WinDivert DPI-desync, winws.exe).
    • Added to the user hostlist lists/list-general-user.txt:
      opencode.ai
      models.dev
      
    • Result on the same line, everything direct: Bun default fetch to opencode.ai/inference/openai/v1/chat/completions → HTTP 500 in 1.8 s (handshake passed); TUI and opencode run respond normally. Everything else on the machine stays untouched (only the SNIs from the list are desynchronized).
    • Caveat for users: the desync tool must run permanently (install it as a Windows service), otherwise the stall returns after reboot.

    One misleading error that is NOT this bug — worth documenting:
    On the same line, opencode run with the default model can fail instantly (~2 s, no hang) with:

    Error: Access denied by security policy.
    

    Do not confuse it with the 2-minute stall: that message is a fast server-side reject from the provider edge (in our case the default run model resolved to an OpenRouter-hosted model, and OpenRouter refuses RU-region IPs by policy — their site is also blocked in a browser from a RU IP; a TLS bypass cannot help, only a proxy can). Pinning a non-openrouter model fixed it instantly:

    // opencode.jsonc
    { "model": "opencode/glm-5.3-flash-free" }

    after which opencode run answers in ~4 s. Rule of thumb for affected users: minutes-long silence ≈ fragmented PQ ClientHello DPI; instant reject ≈ provider geo/WAF policy.

    Suggestion for maintainers (in line with what others proposed):
    Since opencode ships Bun's stack, a config-exposed TLS override or an automatic stall-fallback would rescue every affected user at once, e.g. an "inference": { "tls": { "ecdhCurve": "X25519:P-256" } } option passed to the underlying Bun fetch, or a per-host retry that downgrades the key-share to classical groups after N seconds with no server response. The stall currently looks exactly like a "slow model", which makes it extra confusing in the UI.

    Happy to re-run any measurements on this line if it helps. Huge thanks to everyone in this thread — the diagnosis and the workaround saved several people here an RDDT/MW ticket today.

  13. fylat commented on Oct 30, 2026

    @fylat

    Дисклеймер: Делюсь решением на свой страх и риск. Я не несу ответственности за вашу ОС, безопасность или возможные сбои. Отключение проверки сертификатов снижает защиту — не используйте в публичных сетях.

    Проблема: Ошибка ECONNRESET из-за DPI Ростелекома. Провайдер режет TLS-рукопожатие, а OpenCode (Bun) не доверяет сертификату VPN.

    Мое решение (работает без отката на 1.19.3 и без Zapret):

    Включаю VPN (Happ) в режиме «Смешанный» (TUN/Mixed).

    Создаю .bat файл ровно с таким содержимым:

    bat
    set HTTPS_PROXY=http://127.0.0.1:10809
    set NODE_TLS_REJECT_UNAUTHORIZED=0
    start "" "C:\opencode\OpenCode.exe"
    Запускаю OpenCode только через этот .bat.

    Результат: Ошибка ECONNRESET пропадает, бесплатные модели (DeepSeek) работают.
    Это временный костыль, но он помогает.

  14. ailanatoliicomsupermega99-byte commented on Oct 30, 2026

    @ailanatoliicomsupermega99-byte

    я жру трупы

  15. AlexRicS commented on Oct 30, 2026

    @AlexRicS

    Теперь при использовании ВПН или Запрета появляется эта ошибка... ( если это ошибка вообще )
    Без ВПН и Запрета такого не было, всё работало нормально. Кто-то сталкивался с этим ?
    (Не обращайте внимание на дату, скрин не мой, но ошибка точно такая же, только дата 30.10)
    Image

  16. YourReflection commented on Oct 31, 2026

    @YourReflection

    "Workaround published: zapret as a Windows service, scoped to a hostlist\n\nInstead of waiting for a client-side fix, I packaged the approach from this thread as a one-click installer for Windows:\n\n**https://github.com/YourReflection/zapret-opencode-bypass**\n\nWhat it does:\n- wraps winws (bol-van/zapret, official zapret-win-bundle binaries — downloaded at install time, nothing bundled) into a Windows service winws-opencode, start type auto, so the bypass survives reboots;\n- installs a 5-min watchdog scheduled task (runs as SYSTEM, restarts the service if it dies, e.g. after AV removes WinDivert64.sys);\n- uses the strategy tested earlier in this thread: --dpi-desync=fake,fakedsplit --dpi-desync-split-pos=1 --dpi-desync-fooling=badseq for TLS + fake QUIC initial, hostlist = opencode.ai, models.dev, registry.npmjs.org (all subdomains match automatically, list is editable and preserved across reinstalls).\n\nVerified on an affected line (Rostelecom): TUI and opencode run answer, models.dev and registry.npmjs.org reachable through Bun\u0027\u0027s default PQ ClientHello, no VPN.\n\nNotes: it is not a VPN — no re-encryption, no third-party hops, only the listed SNIs are desynchronized. Full removal is one script (service_opencode_delete.cmd).\n\nStill hoping for the proper fix discussed above (classical-groups retry / ecdhCurve option) — this is just the mitigation until then."

  17. opencode-agent commented on Oct 31, 2026

    @opencode-agent
    Contributor

    Impact from our gateway telemetry. Because the connection breaks during the TLS handshake, the gateway logs no errors for these users. What it shows instead is Russian traffic disappearing. The figures below count distinct IPs that completed at least one generation, 12:00–24:00 UTC each day:

    Oct 28 Oct 29 Oct 30 Oct 30 vs Oct 29
    Russia, all tiers 31,743 32,710 13,662 −58%
    Global, all tiers 852,277 903,897 812,635 −10%
    Russia, Go 4,100 4,207 2,420 −42%
    Global, Go 182,561 200,902 179,853 −10%

    Go is paid and our free-tier policy changes don't touch it, so the Russia-only Go drop is the cleanest view of this connectivity break.

    User feedback shows the same thing: reports of "ECONNRESET / socket closed unexpectedly" went from a 7-day median of about 1 a day to 24 in the last 24 h. Almost all of them are Windows Desktop users on Russian home ISPs, on 2.1.4 through 2.2.3, across Free, Go and Zen. Reports started at about 23:48 UTC on Oct 29 and jumped on the morning of Oct 30, Moscow time (#60846 and #60661 are the same issue). No OpenCode or Console deploy lines up with the start.

    Possible fixes on the client side: send a smaller ClientHello (no post-quantum key share by default), or retry with a classic handshake after a reset during the handshake.

  18. alexeyfadeev commented on Oct 31, 2026

    @alexeyfadeev

    This morning the issue was still reproducible for me, but now opencode-go is accessible again from Russia (both, web-portal and LLM endpoint).

  19. MLGSW4G commented on Oct 31, 2026

    @MLGSW4G

    Confirming, opencode.ai is accessible for me now — default PQ ClientHello,
    no VPN, no desync (Bun 1.4.4, opencode 2.2.5).

    But the DPI rule is still deployed. At the same time, models.dev on the same
    machine and minute still shows the original signature: curl.exe 200 in 0.69 s,
    Bun default hello (X25519MLKEM768) times out at 19 s, same request with
    ecdhCurve: 'X25519:P-256' returns 200 in 0.22 s.

    So only the opencode.ai SNI stopped matching, not the rule. Model catalogue
    refreshes can still stall silently, and this can flip back without notice —
    today's reachability isn't evidence of a fix.

  20. Samo-samo commented on Nov 1, 2026

    @Samo-samo

    Note: this report was drafted with the help of an AI coding agent (OpenCode) from local log analysis and live tests on my machine; the tests, timings, and conclusions were reviewed by me before posting.

    Data point from Turkey: bursty EPROTO/ECONNRESET on Bun/BoringSSL while curl and Node stay clean

    Follow-up on this issue's topic, posted here as a general data point. My original report was #57875 ("Constant ECONNRESET error"), closed as not planned after one round of follow-up. It still reproduces as of Nov 1, 2026. I believe it belongs to this issue's class (DPI/middlebox interference with the large PQ ClientHello), but with an important twist: the failures arrive in bursts, affect the Bun stack while curl/Node stay clean, and the connection recovers completely between bursts.

    Environment

    • OpenCode 2.2.2 CLI (also bundled copy inside OpenChamber 2.5.0, same behavior), Windows 10
    • Provider: OpenCode Zen free tier (beam-free, deepseek-v4.1-flash-free, glm-5.3-flash-free) and Google AI Studio (Gemini) — all affected identically
    • Network: residential Turkish ISP (no VPN, no corporate network), Windows Defender only
    • GoodbyeDPI (DPI-desync utility popular in Turkey) tested on and off — see below

    Error signature by surface

    • OpenCode CLI (v2.2.2) — including this very diagnostic session, run from the CLI: fails with EPROTO: The TLS handshake failed.
    • OpenChamber 2.5.0 (an Electron UI that bundles and runs its own OpenCode CLI copy) — fails with ECONNRESET: The socket connection was closed unexpectedly during the same time windows.

    Both are transport-level failures of the bundled Bun runtime; the difference in message is consistent per surface.

    Server side checked and clean: status.opencode.ai showed "All systems operational" (Free inference 1.9 s · 174 tok/s at check time) while these client-side bursts were happening, and none of the recent server-side incidents in the past 90 days overlap the burst windows listed in the timeline below (closest: a Gateway/Free-inference "slow responses" Nov 1 13:07–13:14 UTC vs. our Nov 1 burst at 14:27; a "slow responses" Oct 30 22:28–22:35 vs. our Oct 30 burst 21:13–22:21). Also, a handshake that never completes is invisible to the gateway's own metrics — which is why bursts like this will never show up on the status page even while many users are affected.

    Within-session pattern and retries: even the diagnostic CLI session producing this report hit EPROTO intermittently between turns; after a few manual retries it works again — for a while (minutes to hours), then the next burst arrives. During a burst, built-in retries eat multiple EPROTOs before a turn gets through: some turns recover only on the 7th–8th attempt, others never complete and surface in opencode.log as Failed to drain Session. On top of that there are failures that get retried silently and never reach the log — a turn that normally takes ~1 minute can burn 8 minutes and still end in error.

    Timeline from opencode.log (UTC)

    All failures are SessionRunner.drain → SessionStep.attempt → AI.Error.Transport; the connection dies at the provider call, on every model/provider tried.

    Burst window Errors
    Oct 19 14:35 – 18:04 ECONNRESET ×8
    Oct 20 13:48 – 16:26 ECONNRESET ×6 + one UNKNOWN_CERTIFICATE_VERIFICATION_ERROR (14:00)
    Oct 21 16:53 – 17:03 ECONNRESET ×2
    Oct 25 10:49 – 11:16 ECONNRESET ×5
    Oct 30 21:13 – 22:21 EPROTO ×6
    Nov 1 14:27 – 14:59 EPROTO ×7 (6 in-session, 1 update check at 14:33) + ECONNRESET ×1 (14:27:54), GoodbyeDPI off

    Outside these windows the same machine opens sessions and completes turns normally, sometimes for hours. This matches the "API call failed after 3 retries: Connection error... works after a few retries, then fails again" pattern from #57875.

    A/B tests (Windows, same machine, same minutes)

    Stack comparison:

    Client ClientHello Result
    curl.exe (Schannel) small, classical 8/8 HTTP 200 to opencode.ai and models.dev
    Node v24.16.0 / OpenSSL default (sends X25519MLKEM768 too) large PQ intermittent record corruption: ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC (Oct 30) and ERR_SSL_TLSV1_ALERT_DECODE_ERROR (Nov 1) on models.dev; otherwise OK
    Node with ecdhCurve: 'X25519:P-256' classical 0 failures in ~30 attempts (including during an active OpenCode burst)
    OpenCode CLI (Bun 1.4.4) large PQ (BoringSSL) EPROTO/ECONNRESET exactly during the burst windows above

    Two moments worth highlighting:

    1. Nov 1, 14:27:41–14:27:54 (burst active, GoodbyeDPI off): OpenCode got EPROTO and ECONNRESET in the same minute. A Node PQ-default sweep to opencode.ai and models.dev run seconds later was 24/24 clean, and curl was 8/8 clean. So at that moment Bun's fetch was failing against a path that OpenSSL- and Schannel-based clients passed — consistent with the middlebox matching on ClientHello shape/fingerprint rather than pure size, since Node's PQ hello (~1.5 KB) also passed.
    2. Quiet window seconds later: opencode run --model opencode/glm-5.3-flash-free "hi" ×4 → 4/4 OK in 2–6 s each. Nothing changed locally between the burst and this test.

    GoodbyeDPI A/B: Turkish users commonly run GoodbyeDPI. Observed both corruption errors (BAD_RECORD_MAC, TLSV1_ALERT_DECODE_ERROR) while GoodbyeDPI was running; one clean 24/24 sweep while it was off. Burst failures also occurred while it was fully off (Nov 1, 14:27). Net: it neither fixes nor clearly worsens the bursts, and its current strategy does not help the fragmented PQ hello get through. If it matters: sample sizes are small and the process may have restarted between rounds, so treat this as weak evidence.

    What this adds to the analysis here

    • The per-window (bursty) behavior suggests ISP-side filtering rules that get deployed/toggled rather than a constant drop — consistent with earlier reports in this thread that other (non-OpenRouter) hosts behave differently on affected lines, and that the onset of the problem starting Oct 29–30 was rule deployment, not user-local misconfiguration.
    • This also extends the affected geography beyond Russia: every affected line in this thread so far has been Rostelecom (RU), while this data point is a Turkish residential ISP with the same signature — so the rule is not unique to one operator, and other countries' DPI deployments can produce the same client-side symptom even while the gateway telemetry only shows one country "disappearing" (per the gateway-telemetry comment above, a handshake-level break also happens to be invisible to the gateway's own metrics).
    • Because Schannel and OpenSSL-with-PQ can pass while BoringSSL/Bun fails in the same minute, a per-host retry that downgrades TLS groups, or a config-exposed TLS override, would likely keep clients working through bursts like these — my +1 to that suggestion in this thread.
    • The UNKNOWN_CERTIFICATE_VERIFICATION_ERROR seen mid-burst (Oct 20 14:00) matches the finding that handshake resets surface as certificate errors in Bun.

    Happy to run further diagnostics during the next burst window (e.g. openssl s_client A/B with X25519MLKEM768 vs X25519 on opencode.ai, or a targeted repeated-opencode run loop) — the bursts are frequent enough that it's easy to catch one.

    Re the latest update in this thread (some lines in Russia regaining access to opencode.ai while models.dev still shows the original signature): from this line, the block is still active — the Nov 1 14:27–14:59 burst above happened on the default PQ ClientHello with no VPN or bypass running, and opencode.ai itself failed while Schannel curl and classical-groups Node passed from the same machine in the same minutes. So I'd read the reachability change there as an SNI-level rule edit, not a rollback — worth keeping the classical-groups retry/mitigation on the roadmap either way.

  21. marked error #60636 as a duplicate of this issue on Nov 4, 2026
  22. marked Not usable #60739 as a duplicate of this issue on Nov 4, 2026
  23. galimzyanov-danil commented on Nov 5, 2026

    @galimzyanov-danil

    I have same issue with opencode go plus plan. How or when fix this? Downgrade to 1.19.3 not fix problem

  24. neriousy commented on Nov 12, 2026

    @neriousy
    Member

    This should be fixed in the latest version. If you still see ECONNRESET after updating, please open a new issue with your opencode --version, OS, provider/model and any logs.

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