Skip to content

Commit d93f820

Browse files
yzang2019fjl
andauthored
rpc: add SetWebsocketReadLimit in Server (#32279)
Exposing the public method to setReadLimits for Websocket RPC to prevent OOM. Current, Geth Server is using a default 32MB max read limit (message size) for websocket, which is prune to being attacked for OOM. Any one can easily launch a client to send a bunch of concurrent large request to cause the node to crash for OOM. One example of such script that can easily crash a Geth node running websocket server is like this: https://gist.githubusercontent.com/DeltaXV/b64d221e342e9c1ec6c99c1ab8201544/raw/ec830979ac9a707d98f40dfcc0ce918fc8fb9057/poc.go --------- Co-authored-by: Felix Lange <fjl@twurst.com>
1 parent 42bf484 commit d93f820

File tree

3 files changed

+98
-1
lines changed

3 files changed

+98
-1
lines changed

rpc/server.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type Server struct {
5454
batchItemLimit int
5555
batchResponseLimit int
5656
httpBodyLimit int
57+
wsReadLimit int64
5758
}
5859

5960
// NewServer creates a new server instance with no registered handlers.
@@ -62,6 +63,7 @@ func NewServer() *Server {
6263
idgen: randomIDGenerator(),
6364
codecs: make(map[ServerCodec]struct{}),
6465
httpBodyLimit: defaultBodyLimit,
66+
wsReadLimit: wsDefaultReadLimit,
6567
}
6668
server.run.Store(true)
6769
// Register the default service providing meta information about the RPC service such
@@ -89,6 +91,13 @@ func (s *Server) SetHTTPBodyLimit(limit int) {
8991
s.httpBodyLimit = limit
9092
}
9193

94+
// SetWebsocketReadLimit sets the limit for max message size for Websocket requests.
95+
//
96+
// This method should be called before processing any requests via Websocket server.
97+
func (s *Server) SetWebsocketReadLimit(limit int64) {
98+
s.wsReadLimit = limit
99+
}
100+
92101
// RegisterName creates a service for the given receiver type under the given name. When no
93102
// methods on the given receiver match the criteria to be either an RPC method or a
94103
// subscription an error is returned. Otherwise a new service is created and added to the

rpc/server_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@ package rpc
1919
import (
2020
"bufio"
2121
"bytes"
22+
"context"
23+
"errors"
2224
"io"
2325
"net"
26+
"net/http/httptest"
2427
"os"
2528
"path/filepath"
2629
"strings"
2730
"testing"
2831
"time"
32+
33+
"github.com/gorilla/websocket"
2934
)
3035

3136
func TestServerRegisterName(t *testing.T) {
@@ -202,3 +207,86 @@ func TestServerBatchResponseSizeLimit(t *testing.T) {
202207
}
203208
}
204209
}
210+
211+
func TestServerWebsocketReadLimit(t *testing.T) {
212+
t.Parallel()
213+
214+
// Test different read limits
215+
testCases := []struct {
216+
name string
217+
readLimit int64
218+
testSize int
219+
shouldFail bool
220+
}{
221+
{
222+
name: "limit with small request - should succeed",
223+
readLimit: 4096, // generous limit to comfortably allow JSON overhead
224+
testSize: 256, // reasonably small payload
225+
shouldFail: false,
226+
},
227+
{
228+
name: "limit with large request - should fail",
229+
readLimit: 256, // tight limit to trigger server-side read limit
230+
testSize: 1024, // payload that will exceed the limit including JSON overhead
231+
shouldFail: true,
232+
},
233+
}
234+
235+
for _, tc := range testCases {
236+
t.Run(tc.name, func(t *testing.T) {
237+
// Create server and set read limits
238+
srv := newTestServer()
239+
srv.SetWebsocketReadLimit(tc.readLimit)
240+
defer srv.Stop()
241+
242+
// Start HTTP server with WebSocket handler
243+
httpsrv := httptest.NewServer(srv.WebsocketHandler([]string{"*"}))
244+
defer httpsrv.Close()
245+
246+
wsURL := "ws:" + strings.TrimPrefix(httpsrv.URL, "http:")
247+
248+
// Connect WebSocket client
249+
client, err := DialOptions(context.Background(), wsURL)
250+
if err != nil {
251+
t.Fatalf("can't dial: %v", err)
252+
}
253+
defer client.Close()
254+
255+
// Create large request data - this is what will be limited
256+
largeString := strings.Repeat("A", tc.testSize)
257+
258+
// Send the large string as a parameter in the request
259+
var result echoResult
260+
err = client.Call(&result, "test_echo", largeString, 42, &echoArgs{S: "test"})
261+
262+
if tc.shouldFail {
263+
// Expecting an error due to read limit exceeded
264+
if err == nil {
265+
t.Fatalf("expected error for request size %d with limit %d, but got none", tc.testSize, tc.readLimit)
266+
}
267+
// Be tolerant about the exact error surfaced by gorilla/websocket.
268+
// Prefer a CloseError with code 1009, but accept ErrReadLimit or an error string containing 1009/message too big.
269+
var cerr *websocket.CloseError
270+
if errors.As(err, &cerr) {
271+
if cerr.Code != websocket.CloseMessageTooBig {
272+
t.Fatalf("unexpected websocket close code: have %d want %d (err=%v)", cerr.Code, websocket.CloseMessageTooBig, err)
273+
}
274+
} else if !errors.Is(err, websocket.ErrReadLimit) &&
275+
!strings.Contains(strings.ToLower(err.Error()), "1009") &&
276+
!strings.Contains(strings.ToLower(err.Error()), "message too big") {
277+
// Not the error we expect from exceeding the message size limit.
278+
t.Fatalf("unexpected error for read limit violation: %v", err)
279+
}
280+
} else {
281+
// Expecting success
282+
if err != nil {
283+
t.Fatalf("unexpected error for request size %d with limit %d: %v", tc.testSize, tc.readLimit, err)
284+
}
285+
// Verify the response is correct - the echo should return our string
286+
if result.String != largeString {
287+
t.Fatalf("expected echo result to match input")
288+
}
289+
}
290+
})
291+
}
292+
}

rpc/websocket.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func (s *Server) WebsocketHandler(allowedOrigins []string) http.Handler {
6060
log.Debug("WebSocket upgrade failed", "err", err)
6161
return
6262
}
63-
codec := newWebsocketCodec(conn, r.Host, r.Header, wsDefaultReadLimit)
63+
codec := newWebsocketCodec(conn, r.Host, r.Header, s.wsReadLimit)
6464
s.ServeCodec(codec, 0)
6565
})
6666
}

0 commit comments

Comments
 (0)