Skip to content

Add WS bridge over DAP TCP server#4328

Open
rentziass wants to merge 12 commits intomainfrom
rentziass/debugger-ws-bridge-fix
Open

Add WS bridge over DAP TCP server#4328
rentziass wants to merge 12 commits intomainfrom
rentziass/debugger-ws-bridge-fix

Conversation

@rentziass
Copy link
Copy Markdown
Member

@rentziass rentziass commented Apr 8, 2026

This adds a bridge converting messages to/from TCP <-> WS so that we can connect to the DAP server through wss:// directly and using GitHub credentials out of the box with Dev Tunnels.

https://github.com/github/c2c-actions/issues/9831

@rentziass rentziass marked this pull request as ready for review April 8, 2026 16:43
@rentziass rentziass requested a review from a team as a code owner April 8, 2026 16:43
Copilot AI review requested due to automatic review settings April 8, 2026 16:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a WebSocket-to-TCP bridge in the DAP subsystem so remote debugging can connect via ws:///wss:// to the runner’s DAP server (e.g., through Dev Tunnels using GitHub auth), while the internal DAP server continues to speak DAP-over-TCP.

Changes:

  • Add WebSocketDapBridge to translate WebSocket text frames ↔ DAP TCP (Content-Length framed) messages.
  • Update DapDebugger startup/shutdown to expose the tunnel port via the WebSocket bridge and move the internal DAP listener to an ephemeral local port.
  • Add L0 coverage for the bridge and for DapDebugger WebSocket connectivity (including “pre-upgraded” WebSocket streams).
Show a summary per file
File Description
src/Runner.Worker/Dap/WebSocketDapBridge.cs New bridge implementation handling upgrade detection, handshake, and bidirectional message pumping.
src/Runner.Worker/Dap/DapDebugger.cs Starts/stops the bridge and uses an internal ephemeral TCP port for the actual DAP server when the bridge is enabled.
src/Test/L0/Worker/WebSocketDapBridgeL0.cs New L0 tests validating forwarding, rejection paths, message size limits, and disposal behavior.
src/Test/L0/Worker/DapDebuggerL0.cs Extends L0 tests to validate DapDebugger can accept DAP initialize via WebSocket and pre-upgraded WebSocket streams.

Copilot's findings

  • Files reviewed: 4/4 changed files
  • Comments generated: 7

rentziass and others added 3 commits April 9, 2026 17:58
Co-authored-by: Tingluo Huang <tingluohuang@github.com>
@rentziass rentziass requested a review from TingluoHuang April 10, 2026 08:58
else
{
Trace.Info($"Internal DAP debugger listening on {_listener.LocalEndpoint}");
_webSocketBridge = new WebSocketDapBridge();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can do HostContext.CreateService<IWebSocketDapBridge>() which will create you new instance everyone instead of returned the signleton created via HostContext.GetService

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any existing library doing this?
it feels like we are writing too much code. 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the reply i got from copilot. the watsonwebserver and System.IO.Pipelines are interesting points.

Here's a thorough analysis of the WebSocketDapBridge.cs implementation and what public libraries could replace parts of it.


What the File Does

WebSocketDapBridge.cs implements a WebSocket ↔ TCP proxy specifically for the Debug Adapter Protocol (DAP). It:

  1. Listens on a TCP port for incoming connections
  2. Performs the WebSocket HTTP Upgrade handshake manually (RFC 6455)
  3. Detects pre-upgraded WebSocket streams (e.g. from Dev Tunnels that already stripped the HTTP layer)
  4. Pumps frames bidirectionally: WS frames → DAP Content-Length-framed TCP messages, and vice versa

What Could Be Replaced by Public Libraries

WebSocket Accept Handshake (the largest custom section)

The most substantial custom code is the HTTP Upgrade handshake (~150 lines): header parsing, Sec-WebSocket-Accept computation, rejection responses.

This can be fully replaced by Microsoft.AspNetCore.WebSockets, specifically via a minimal Kestrel host or even via HttpListener-based upgrade:

// With a Kestrel/minimal API host — the handshake becomes 1 line:
await context.WebSockets.AcceptWebSocketAsync();

However, the project intentionally avoids ASP.NET Core (this is Runner.Worker, a lean console process), and WebSocket.CreateFromStream (which they already use at the bottom of the handshake) is BCL, not ASP.NET. So the handshake must be done manually — or via a library.

Best drop-in alternative: WatsonWebsocket or Fleck — both handle the server-side WS handshake without requiring ASP.NET Core.


ReplayableStream (prefix-replay buffer)

This is a small custom Stream subclass that replays the first 4 bytes after they've been peeked to detect the protocol. This pattern is sometimes called a "peekable" or "pre-read" stream.

Libraries with this built-in:

  • System.IO.Pipelines (BCL, .NET 6+) — PipeReader supports peeking/examining bytes before consuming, eliminating the need for a custom ReplayableStream entirely. This is likely the most idiomatic .NET replacement.

DAP Content-Length Message Framing (TryParseDapMessage)

The DAP header parsing loop is straightforward but custom. There is no widely-used dedicated NuGet package for DAP framing, but:


WS→TCP Pumping (PumpWebSocketToTcpAsync / PumpTcpToWebSocketAsync)

This is generic bidirectional stream proxying. The current implementation uses List<byte> as a buffer, which is allocation-heavy.

Better alternatives:

  • System.IO.Pipelines — avoids List<byte> and ToArray() allocations entirely; this is what ASP.NET Core itself uses internally for WebSocket proxying.
  • YARP (Yet Another Reverse Proxy) — overkill here, but it does contain robust WebSocket forwarding middleware.

Summary Table

Section of WebSocketDapBridge.cs Publicly Available Alternative
HTTP Upgrade handshake parsing Fleck, WatsonWebsocket, or Microsoft.AspNetCore.WebSockets (needs Kestrel)
WebSocket.CreateFromStream ✅ Already uses BCL (built into .NET 6+, no library needed)
ReplayableStream (prefix peek) System.IO.Pipelines PipeReader.AdvanceTo
DAP Content-Length framing System.IO.Pipelines + SequenceReader<byte>
Bidirectional WS↔TCP pump System.IO.Pipelines, or YARP for a full proxy scenario
Protocol sniffing (ClassifyIncomingStreamPrefix) No public library — keep as-is, this is custom to the Dev Tunnels use case

Practical Recommendation

The most impactful and well-scoped replacement would be:

  1. Replace ReplayableStream + the pump's List<byte> buffer with System.IO.Pipelines — it's a BCL package, zero external dependency risk, and would make the DAP framing and WS pumping both faster and allocation-free.

  2. Keep the handshake manual OR adopt Fleck/WatsonWebsocket — but adopting a third-party WS library adds a dependency to a security-sensitive component (a runner handling GitHub credentials), so the manual implementation is arguably the safer call here.

  3. The protocol sniffing logic (ClassifyIncomingStreamPrefix) is unique to the Dev Tunnels pre-upgraded stream scenario and has no public library equivalent — it should stay custom.

rentziass and others added 3 commits April 13, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants