Skip to content

Commit 5a96c9d

Browse files
committed
TUN-10994: Fix dropping the tail of messages larger than the buffer
Integrating the changes introduced here: #1741 This fixes silent data loss in TCP/SOCKS-over-WebSocket connections when a WebSocket message exceeds the caller’s read buffer. Unread bytes are now buffered and delivered by subsequent reads, preserving the complete byte stream in order.
1 parent 8e090e5 commit 5a96c9d

2 files changed

Lines changed: 126 additions & 6 deletions

File tree

‎websocket/connection.go‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ type PingPeriodContext string
3131
// This is still used by access carrier
3232
type GorillaConn struct {
3333
*websocket.Conn
34-
log *zerolog.Logger
3534
readBuf bytes.Buffer
3635
}
3736

@@ -42,7 +41,7 @@ func (c *GorillaConn) Read(p []byte) (int, error) {
4241
return c.readBuf.Read(p)
4342
}
4443

45-
_, message, err := c.Conn.ReadMessage()
44+
_, message, err := c.ReadMessage()
4645
if err != nil {
4746
return 0, err
4847
}
@@ -58,7 +57,7 @@ func (c *GorillaConn) Read(p []byte) (int, error) {
5857

5958
// Write will write messages to the websocket connection
6059
func (c *GorillaConn) Write(p []byte) (int, error) {
61-
if err := c.Conn.WriteMessage(websocket.BinaryMessage, p); err != nil {
60+
if err := c.WriteMessage(websocket.BinaryMessage, p); err != nil {
6261
return 0, err
6362
}
6463

@@ -69,10 +68,10 @@ func (c *GorillaConn) Write(p []byte) (int, error) {
6968
// "It is equivalent to calling both SetReadDeadline and SetWriteDeadline."
7069
// Note there is no synchronization here, but the gorilla implementation isn't thread safe anyway
7170
func (c *GorillaConn) SetDeadline(t time.Time) error {
72-
if err := c.Conn.SetReadDeadline(t); err != nil {
71+
if err := c.SetReadDeadline(t); err != nil {
7372
return fmt.Errorf("error setting read deadline: %w", err)
7473
}
75-
if err := c.Conn.SetWriteDeadline(t); err != nil {
74+
if err := c.SetWriteDeadline(t); err != nil {
7675
return fmt.Errorf("error setting write deadline: %w", err)
7776
}
7877
return nil
@@ -81,6 +80,9 @@ func (c *GorillaConn) SetDeadline(t time.Time) error {
8180
type Conn struct {
8281
rw io.ReadWriter
8382
log *zerolog.Logger
83+
// readBuf holds the unread remainder of the last message when it was larger than the buffer passed to Read.
84+
// Without it, everything past len(reader) of a message would be silently dropped.
85+
readBuf []byte
8486
// writeLock makes sure
8587
// 1. Only one write at a time. The pinger and Stream function can both call write.
8688
// 2. Close only returns after in progress Write is finished, and no more Write will succeed after calling Close.
@@ -99,11 +101,26 @@ func NewConn(ctx context.Context, rw io.ReadWriter, log *zerolog.Logger) *Conn {
99101

100102
// Read will read messages from the websocket connection
101103
func (c *Conn) Read(reader []byte) (int, error) {
104+
// Intermediate buffer may contain unread bytes from the last read, start there before blocking on a new frame
105+
if len(c.readBuf) > 0 {
106+
n := copy(reader, c.readBuf)
107+
c.readBuf = c.readBuf[n:]
108+
if len(c.readBuf) == 0 {
109+
c.readBuf = nil
110+
}
111+
return n, nil
112+
}
113+
102114
data, err := wsutil.ReadClientBinary(c.rw)
103115
if err != nil {
104116
return 0, err
105117
}
106-
return copy(reader, data), nil
118+
119+
n := copy(reader, data)
120+
if n < len(data) {
121+
c.readBuf = data[n:]
122+
}
123+
return n, nil
107124
}
108125

109126
// Write will write messages to the websocket connection.

‎websocket/connection_test.go‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package websocket
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/rand"
7+
"io"
8+
"net"
9+
"testing"
10+
"time"
11+
12+
"github.com/gobwas/ws/wsutil"
13+
"github.com/rs/zerolog"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
// readThrough reads exactly want bytes from conn using buffers of size bufSize,
18+
// the way cfio.Copy feeds Conn.Read from its fixed-size pool buffer.
19+
func readThrough(t *testing.T, conn *Conn, bufSize, want int) []byte {
20+
t.Helper()
21+
var out bytes.Buffer
22+
buf := make([]byte, bufSize)
23+
for out.Len() < want {
24+
n, err := conn.Read(buf)
25+
require.NoError(t, err)
26+
require.LessOrEqual(t, n, bufSize)
27+
out.Write(buf[:n])
28+
}
29+
return out.Bytes()
30+
}
31+
32+
func randomPayload(t *testing.T, size int) []byte {
33+
t.Helper()
34+
payload := make([]byte, size)
35+
_, err := io.ReadFull(rand.Reader, payload)
36+
require.NoError(t, err)
37+
return payload
38+
}
39+
40+
func newTestConn(t *testing.T) (*Conn, net.Conn) {
41+
t.Helper()
42+
ctx, cancel := context.WithCancel(context.Background())
43+
server, client := net.Pipe()
44+
log := zerolog.Nop()
45+
// Bound the test: a Read that never sees the rest of a message would otherwise block forever
46+
require.NoError(t, server.SetReadDeadline(time.Now().Add(10*time.Second)))
47+
conn := NewConn(ctx, server, &log)
48+
t.Cleanup(func() {
49+
cancel()
50+
conn.Close()
51+
_ = server.Close()
52+
_ = client.Close()
53+
})
54+
return conn, client
55+
}
56+
57+
// TestConnReadMessageLargerThanBuffer verifies that a single client message larger
58+
// than the buffer passed to Read is delivered whole across multiple Read calls,
59+
// rather than truncated to the first len(buffer) bytes.
60+
func TestConnReadMessageLargerThanBuffer(t *testing.T) {
61+
t.Parallel()
62+
const bufSize = 16 * 1024
63+
const msgSize = 100 * 1024
64+
65+
conn, client := newTestConn(t)
66+
payload := randomPayload(t, msgSize)
67+
68+
go func() {
69+
_ = wsutil.WriteClientBinary(client, payload)
70+
}()
71+
72+
got := readThrough(t, conn, bufSize, msgSize)
73+
require.Equal(t, payload, got)
74+
}
75+
76+
// TestConnReadMixedMessageSizes sends several messages around the buffer boundary
77+
// and verifies all bytes arrive in order.
78+
func TestConnReadMixedMessageSizes(t *testing.T) {
79+
t.Parallel()
80+
const bufSize = 16 * 1024
81+
sizes := []int{1, bufSize - 1, bufSize, bufSize + 1, 40000, 3 * bufSize, 100 * 1024, 7, bufSize}
82+
83+
conn, client := newTestConn(t)
84+
85+
var expected bytes.Buffer
86+
payloads := make([][]byte, 0, len(sizes))
87+
for _, size := range sizes {
88+
payload := randomPayload(t, size)
89+
payloads = append(payloads, payload)
90+
expected.Write(payload)
91+
}
92+
93+
go func() {
94+
for _, payload := range payloads {
95+
if err := wsutil.WriteClientBinary(client, payload); err != nil {
96+
return
97+
}
98+
}
99+
}()
100+
101+
got := readThrough(t, conn, bufSize, expected.Len())
102+
require.Equal(t, expected.Bytes(), got)
103+
}

0 commit comments

Comments
 (0)