Conversation
… buffer Conn.Read returned copy(reader, data) after reading a whole WebSocket message, dropping every byte past len(reader). stream.Pipe feeds it a 16 KiB buffer via cfio.Copy, so client messages larger than 16 KiB reached tcp://, ssh://, rdp://, smb://, bastion and socks ingress origins with only their first 16 KiB. Keep the unread remainder in a bytes.Buffer and drain it on the next Read, as GorillaConn.Read already does. Add tests for a 100 KiB message and for a mix of sizes around the 16 KiB boundary.
There was a problem hiding this comment.
🟡 Changes recommended
The retained buffer allocation can unnecessarily increase memory usage for long-lived connections.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Fixes truncation of oversized WebSocket messages when proxying stream-based ingress traffic.
Changes:
- Buffers unread message tails across successive
Readcalls. - Adds regression tests for large and mixed-size messages.
File summaries
| File | Description |
|---|---|
websocket/connection.go |
Preserves unread WebSocket message data. |
websocket/connection_test.go |
Tests message integrity across buffer boundaries. |
Review details
Suppressed comments (1)
websocket/connection_test.go:77
- This test is isolated and parallel-safe, so it should call
t.Parallel()as required by the repository's testing guidelines.
func TestConnReadMixedMessageSizes(t *testing.T) {
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
jcsf
reviewed
Sep 16, 2026
jcsf
reviewed
Sep 16, 2026
jcsf
reviewed
Sep 16, 2026
Holding the remainder as a subslice of the message wsutil returned avoids a second copy and releases the memory once the tail is drained, instead of a bytes.Buffer pinning its peak allocation for the life of the connection. The two new tests are parallel-safe, so they call t.Parallel.
Author
|
@jcsf it looks like the reader accepts messages of any length (with or without this patch) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
For
tcp://,ssh://,rdp://,smb://, bastion and socks-over-WebSocket ingress services, cloudflared unwraps the client's WebSocket frames itself via the server-sidewebsocket.Conn. ItsRead(websocket/connection.go) calledwsutil.ReadClientBinary, which returns one complete WebSocket message, and then returnedcopy(reader, data). Any bytes of the message pastlen(reader)were silently discarded.stream.Pipedrives thatReadthroughcfio.Copy, which uses a pooled 16 KiB buffer (cfio/copy.go). So every client message larger than 16 KiB reached the origin truncated to its first 16 KiB, while the origin-to-client direction was unaffected. The origin then saw a corrupted byte stream (in our case a broken TLS record) and reset the connection, which shows up in debug logs asdownstream->upstream copy: stream error: stream ID N; NO_ERROR.The code has been this way since e226208 (TUN-3617, 2020).
cloudflared access tcpnever triggers it because it also copies throughcfio.Copyand therefore never emits messages larger than 16 KiB, but any other WebSocket client that writes larger binary messages does.Reproduction
tcp://<host>:443behind Cloudflare Access.wss://endpoint and forwards a TCP stream as one binary message perWrite(for exampleio.Copyfrom a*net.TCPConn, which produces 32 KiB messages).Fix
Keep the unread remainder of a message in a
bytes.BufferonConnand drain it on subsequentReadcalls before reading the next message. This mirrors whatGorillaConn.Readin the same file already does.Write, ping/pong handling, control-frame handling and error behaviour are unchanged.Tests
websocket/connection_test.goadds:TestConnReadMessageLargerThanBuffer: a single 100 KiB client binary message read through 16 KiBReads arrives byte for byte.TestConnReadMixedMessageSizes: messages of 1, 16383, 16384, 16385, 40000, 49152, 102400, 7 and 16384 bytes sent back to back arrive concatenated in order.Both tests fail on the previous code (the reader times out waiting for the dropped tail) and pass with the fix.
go test ./websocket/andgo vet ./websocket/are clean. The change applies unmodified on 2025.5.0, 2026.8.2 and master.C✳