Skip to content

bug(sdk/go): TCP().Listen sets up tunnels serially in the accept loop #4512

Open

Description

@CloneOfAlex

User Story

I use OpenShell and build automation against its SDKs. While reviewing sdk/ I hit this behavior and reproduced it with the minimal test below against main.

Problem Statement

tunnelListener.acceptAndBridge calls Forward/SSH().Tunnel (a GetSandbox RPC, stream open and init frame) synchronously before returning to Accept, so each new connection waits for every earlier connection's tunnel setup.

Impact / Why This Matters

One slow or hung tunnel setup blocks all other local clients of the listener; connection latency grows linearly with concurrent connections (browsers, connection pools).

Acceptance Criteria

  • Concurrent connections to a Listen port establish tunnels in parallel; N connections with ~500ms setup each complete in roughly 500ms, not N×500ms.
  • A hung setup for one connection does not block others.

Reproduction Steps

Stub SandboxInterface.Get to sleep 500ms; open 4 concurrent connections to a Listen port, each writes 1 byte and reads the echo. Total time: 2.01s.

Suggested UX (if applicable)

No response

Environment

  • OpenShell: main @ 82977e2 (2026-10-13)
  • SDK: Go (sdk/go/openshell/v1/tcp_client.go)
  • Toolchain: Go 1.25.4 linux/amd64, Node 22

Logs

BUG: 4 concurrent connections took 2.01s (each tunnel setup ~500ms); accept loop serializes setup

Activity

  1. added theissue type on Oct 13, 2026
  2. rhuss commented on Oct 14, 2026

    @rhuss
    Contributor

    📋 triage-agent

    Thanks for the report and the timing reproduction.

    Confirmed on main. tunnelListener.acceptAndBridge (sdk/go/openshell/v1/tcp_client.go) calls Forward or SSH().Tunnel inside the accept loop and only starts the bridge goroutine after that returns. Each connection waits for the previous one's tunnel setup. There is no timeout on setup either, so one hung setup stalls the listener until someone closes it. The TypeScript SDK doesn't have this problem because net.createServer runs each connection's setup asynchronously.

    The fix is to move tunnel setup into the per-connection goroutine, add that goroutine to the WaitGroup, and check closing before spawning it.

    Setup also needs a deadline, and that part is easy to get wrong. Forward and SSH().Tunnel derive the stream context from the context they're given, so wrapping it in context.WithTimeout would kill established connections once the timeout expires. The deadline should cover only the GetSandbox call, opening the stream, and sending the init frame, for example with a timer that cancels on expiry and is stopped once the init frame is sent.

    Separately, setup failures are dropped without a trace today (the loop just continues), which makes problems like this one hard to diagnose. Logging them through the SDK logger would help once #4510 makes that logger do something.

    The evidence and acceptance criteria are complete. Next step: maintainer acceptance decision.

  3. d0w commented on Oct 15, 2026

    @d0w

    Hey @CloneOfAlex, I can take a look at this one if you aren't already working on a fix for it. Please let me know!
    CC: @rhuss

  4. added
    state:acceptedA maintainer decided OpenShell should pursue this issue
    and removed
    state:triage-neededOpened without agent diagnostics and needs triage
    on Nov 2, 2026
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

    area:sdk:goGo SDKstate:acceptedA maintainer decided OpenShell should pursue this issue

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions