Skip to content
This repository has been archived by the owner on Dec 4, 2024. It is now read-only.

Add blockRangeLimit and batchRequestLimit JSON-RPC flags to help prevent node DDoS #638

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 27 additions & 17 deletions command/server/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,24 @@ import (

// Config defines the server configuration params
type Config struct {
GenesisPath string `json:"chain_config" yaml:"chain_config"`
SecretsConfigPath string `json:"secrets_config" yaml:"secrets_config"`
DataDir string `json:"data_dir" yaml:"data_dir"`
BlockGasTarget string `json:"block_gas_target" yaml:"block_gas_target"`
GRPCAddr string `json:"grpc_addr" yaml:"grpc_addr"`
JSONRPCAddr string `json:"jsonrpc_addr" yaml:"jsonrpc_addr"`
Telemetry *Telemetry `json:"telemetry" yaml:"telemetry"`
Network *Network `json:"network" yaml:"network"`
ShouldSeal bool `json:"seal" yaml:"seal"`
TxPool *TxPool `json:"tx_pool" yaml:"tx_pool"`
LogLevel string `json:"log_level" yaml:"log_level"`
RestoreFile string `json:"restore_file" yaml:"restore_file"`
BlockTime uint64 `json:"block_time_s" yaml:"block_time_s"`
IBFTBaseTimeout uint64 `json:"ibft_base_time_s" yaml:"ibft_base_time_s"`
Headers *Headers `json:"headers" yaml:"headers"`
LogFilePath string `json:"log_to" yaml:"log_to"`
GenesisPath string `json:"chain_config" yaml:"chain_config"`
SecretsConfigPath string `json:"secrets_config" yaml:"secrets_config"`
DataDir string `json:"data_dir" yaml:"data_dir"`
BlockGasTarget string `json:"block_gas_target" yaml:"block_gas_target"`
GRPCAddr string `json:"grpc_addr" yaml:"grpc_addr"`
JSONRPCAddr string `json:"jsonrpc_addr" yaml:"jsonrpc_addr"`
Telemetry *Telemetry `json:"telemetry" yaml:"telemetry"`
Network *Network `json:"network" yaml:"network"`
ShouldSeal bool `json:"seal" yaml:"seal"`
TxPool *TxPool `json:"tx_pool" yaml:"tx_pool"`
LogLevel string `json:"log_level" yaml:"log_level"`
RestoreFile string `json:"restore_file" yaml:"restore_file"`
BlockTime uint64 `json:"block_time_s" yaml:"block_time_s"`
IBFTBaseTimeout uint64 `json:"ibft_base_time_s" yaml:"ibft_base_time_s"`
Headers *Headers `json:"headers" yaml:"headers"`
LogFilePath string `json:"log_to" yaml:"log_to"`
JSONRPCBatchRequestLimit uint64 `json:"json_rpc_batch_request_limit" yaml:"json_rpc_batch_request_limit"`
JSONRPCBlockRangeLimit uint64 `json:"json_rpc_block_range_limit" yaml:"json_rpc_block_range_limit"`
}

// Telemetry holds the config details for metric services.
Expand Down Expand Up @@ -69,6 +71,12 @@ const (
// Multiplier to get IBFT timeout from block time
// timeout is calculated when IBFT timeout is not specified
BlockTimeMultiplierForTimeout uint64 = 5

// maximum length allowed for json_rpc batch requests
DefaultJSONRPCBatchRequestLimit uint64 = 20

// maximum block range allowed for json_rpc requests with fromBlock/toBlock values (e.g. eth_getLogs)
DefaultJSONRPCBlockRangeLimit uint64 = 1000
)

// DefaultConfig returns the default server configuration
Expand Down Expand Up @@ -102,7 +110,9 @@ func DefaultConfig() *Config {
Headers: &Headers{
AccessControlAllowOrigins: []string{"*"},
},
LogFilePath: "",
LogFilePath: "",
JSONRPCBatchRequestLimit: DefaultJSONRPCBatchRequestLimit,
JSONRPCBlockRangeLimit: DefaultJSONRPCBlockRangeLimit,
}
}

Expand Down
51 changes: 29 additions & 22 deletions command/server/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,30 @@ import (
)

const (
configFlag = "config"
genesisPathFlag = "chain"
dataDirFlag = "data-dir"
libp2pAddressFlag = "libp2p"
prometheusAddressFlag = "prometheus"
natFlag = "nat"
dnsFlag = "dns"
sealFlag = "seal"
maxPeersFlag = "max-peers"
maxInboundPeersFlag = "max-inbound-peers"
maxOutboundPeersFlag = "max-outbound-peers"
priceLimitFlag = "price-limit"
maxSlotsFlag = "max-slots"
blockGasTargetFlag = "block-gas-target"
secretsConfigFlag = "secrets-config"
restoreFlag = "restore"
blockTimeFlag = "block-time"
ibftBaseTimeoutFlag = "ibft-base-timeout"
devIntervalFlag = "dev-interval"
devFlag = "dev"
corsOriginFlag = "access-control-allow-origins"
logFileLocationFlag = "log-to"
configFlag = "config"
genesisPathFlag = "chain"
dataDirFlag = "data-dir"
libp2pAddressFlag = "libp2p"
prometheusAddressFlag = "prometheus"
natFlag = "nat"
dnsFlag = "dns"
sealFlag = "seal"
maxPeersFlag = "max-peers"
maxInboundPeersFlag = "max-inbound-peers"
maxOutboundPeersFlag = "max-outbound-peers"
priceLimitFlag = "price-limit"
jsonRPCBatchRequestLimitFlag = "json-rpc-batch-request-limit"
jsonRPCBlockRangeLimitFlag = "json-rpc-block-range-limit"
maxSlotsFlag = "max-slots"
blockGasTargetFlag = "block-gas-target"
secretsConfigFlag = "secrets-config"
restoreFlag = "restore"
blockTimeFlag = "block-time"
ibftBaseTimeoutFlag = "ibft-base-timeout"
devIntervalFlag = "dev-interval"
devFlag = "dev"
corsOriginFlag = "access-control-allow-origins"
logFileLocationFlag = "log-to"
)

const (
Expand Down Expand Up @@ -73,6 +75,9 @@ type serverParams struct {

corsAllowedOrigins []string

jsonRPCBatchLengthLimit uint64
jsonRPCBlockRangeLimit uint64

genesisConfig *chain.Chain
secretsConfig *secrets.SecretsManagerConfig

Expand Down Expand Up @@ -134,6 +139,8 @@ func (p *serverParams) generateConfig() *server.Config {
JSONRPC: &server.JSONRPC{
JSONRPCAddr: p.jsonRPCAddress,
AccessControlAllowOrigin: p.corsAllowedOrigins,
BatchLengthLimit: p.jsonRPCBatchLengthLimit,
BlockRangeLimit: p.jsonRPCBlockRangeLimit,
},
GRPCAddr: p.grpcAddress,
LibP2PAddr: p.libp2pAddress,
Expand Down
15 changes: 15 additions & 0 deletions command/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,21 @@ func setFlags(cmd *cobra.Command) {
"the CORS header indicating whether any JSON-RPC response can be shared with the specified origin",
)

cmd.Flags().Uint64Var(
&params.jsonRPCBatchLengthLimit,
jsonRPCBatchRequestLimitFlag,
defaultConfig.JSONRPCBatchRequestLimit,
"the max length to be considered when handling json-rpc batch requests",
)

//nolint:lll
cmd.Flags().Uint64Var(
&params.jsonRPCBlockRangeLimit,
jsonRPCBlockRangeLimitFlag,
defaultConfig.JSONRPCBatchRequestLimit,
dankostiuk marked this conversation as resolved.
Show resolved Hide resolved
"the max block range to be considered when executing json-rpc requests that consider fromBlock/toBlock values (e.g. eth_getLogs)",
)

cmd.Flags().StringVar(
&params.rawConfig.LogFilePath,
logFileLocationFlag,
Expand Down
36 changes: 25 additions & 11 deletions jsonrpc/dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,32 @@ type endpoints struct {
// Dispatcher handles all json rpc requests by delegating
// the execution flow to the corresponding service
type Dispatcher struct {
logger hclog.Logger
serviceMap map[string]*serviceData
filterManager *FilterManager
endpoints endpoints
chainID uint64
priceLimit uint64
logger hclog.Logger
serviceMap map[string]*serviceData
filterManager *FilterManager
endpoints endpoints
chainID uint64
priceLimit uint64
jsonRPCBatchLengthLimit uint64
}

func newDispatcher(logger hclog.Logger, store JSONRPCStore, chainID uint64, priceLimit uint64) *Dispatcher {
func newDispatcher(
logger hclog.Logger,
store JSONRPCStore,
chainID uint64,
priceLimit uint64,
jsonRPCBatchLengthLimit uint64,
blockRangeLimit uint64,
) *Dispatcher {
d := &Dispatcher{
logger: logger.Named("dispatcher"),
chainID: chainID,
priceLimit: priceLimit,
logger: logger.Named("dispatcher"),
chainID: chainID,
priceLimit: priceLimit,
jsonRPCBatchLengthLimit: jsonRPCBatchLengthLimit,
}

if store != nil {
d.filterManager = NewFilterManager(logger, store)
d.filterManager = NewFilterManager(logger, store, blockRangeLimit)
go d.filterManager.Run()
}

Expand Down Expand Up @@ -247,6 +256,11 @@ func (d *Dispatcher) Handle(reqBody []byte) ([]byte, error) {
return NewRPCResponse(nil, "2.0", nil, NewInvalidRequestError("Invalid json request")).Bytes()
}

// avoid handling long batch requests
if len(requests) > int(d.jsonRPCBatchLengthLimit) {
return NewRPCResponse(nil, "2.0", nil, NewInvalidRequestError("Batch request length too long")).Bytes()
}

responses := make([]Response, 0)

for _, req := range requests {
Expand Down
103 changes: 82 additions & 21 deletions jsonrpc/dispatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func TestDispatcher_HandleWebsocketConnection_EthSubscribe(t *testing.T) {
t.Parallel()

store := newMockStore()
dispatcher := newDispatcher(hclog.NewNullLogger(), store, 0, 0)
dispatcher := newDispatcher(hclog.NewNullLogger(), store, 0, 0, 20, 1000)

mockConnection := &mockWsConn{
msgCh: make(chan []byte, 1),
Expand Down Expand Up @@ -96,7 +96,7 @@ func TestDispatcher_HandleWebsocketConnection_EthSubscribe(t *testing.T) {

func TestDispatcher_WebsocketConnection_RequestFormats(t *testing.T) {
store := newMockStore()
dispatcher := newDispatcher(hclog.NewNullLogger(), store, 0, 0)
dispatcher := newDispatcher(hclog.NewNullLogger(), store, 0, 0, 20, 1000)

mockConnection := &mockWsConn{
msgCh: make(chan []byte, 1),
Expand Down Expand Up @@ -200,7 +200,7 @@ func (m *mockService) Filter(f LogQuery) (interface{}, error) {
func TestDispatcherFuncDecode(t *testing.T) {
srv := &mockService{msgCh: make(chan interface{}, 10)}

dispatcher := newDispatcher(hclog.NewNullLogger(), newMockStore(), 0, 0)
dispatcher := newDispatcher(hclog.NewNullLogger(), newMockStore(), 0, 0, 20, 1000)
dispatcher.registerService("mock", srv)

handleReq := func(typ string, msg string) interface{} {
Expand Down Expand Up @@ -266,25 +266,86 @@ func TestDispatcherFuncDecode(t *testing.T) {
}

func TestDispatcherBatchRequest(t *testing.T) {
dispatcher := newDispatcher(hclog.NewNullLogger(), newMockStore(), 0, 0)
handle := func(dispatcher *Dispatcher, reqBody []byte) []byte {
res, _ := dispatcher.Handle(reqBody)

// test with leading whitespace (" \t\n\n\r")
leftBytes := []byte{0x20, 0x20, 0x09, 0x0A, 0x0A, 0x0D}
resp, err := dispatcher.Handle(append(leftBytes, []byte(`[
{"id":1,"jsonrpc":"2.0","method":"eth_getBalance","params":["0x1", true]},
{"id":2,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x2", true]},
{"id":3,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x3", true]},
{"id":4,"jsonrpc":"2.0","method": "web3_sha3","params": ["0x68656c6c6f20776f726c64"]}
]`)...))
assert.NoError(t, err)

var res []SuccessResponse

assert.NoError(t, expectBatchJSONResult(resp, &res))
assert.Len(t, res, 4)
return res
}

jsonerr := &ObjectError{Code: -32602, Message: "Invalid Params"}
cases := []struct {
name string
desc string
dispatcher *Dispatcher
reqBody []byte
err *ObjectError
}{
{
"leading-whitespace",
"test with leading whitespace (\" \\t\\n\\n\\r\\)",
newDispatcher(hclog.NewNullLogger(), newMockStore(), 0, 0, 20, 1000),
append([]byte{0x20, 0x20, 0x09, 0x0A, 0x0A, 0x0D}, []byte(`[
{"id":1,"jsonrpc":"2.0","method":"eth_getBalance","params":["0x1", true]},
{"id":2,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x2", true]},
{"id":3,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x3", true]},
{"id":4,"jsonrpc":"2.0","method": "web3_sha3","params": ["0x68656c6c6f20776f726c64"]}]`)...),
nil,
},
{
"valid-batch-req",
"test with batch req length within batchRequestLengthLimit",
newDispatcher(hclog.NewNullLogger(), newMockStore(), 0, 0, 10, 1000),
[]byte(`[
{"id":1,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", true]},
{"id":2,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", true]},
{"id":3,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", true]},
{"id":4,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", true]},
{"id":5,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", true]},
{"id":6,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", true]}]`),
nil,
},
{
"invalid-batch-req",
"test with batch req length exceeding batchRequestLengthLimit",
newDispatcher(hclog.NewNullLogger(), newMockStore(), 0, 0, 3, 1000),
[]byte(`[
{"id":1,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", true]},
{"id":2,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", true]},
{"id":3,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", true]},
{"id":4,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", true]},
{"id":5,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", true]},
{"id":6,"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", true]}]`),
&ObjectError{Code: -32600, Message: "Batch request length too long"},
},
}

assert.Equal(t, res[0].Error, jsonerr)
assert.Nil(t, res[3].Error)
for _, c := range cases {
res := handle(c.dispatcher, c.reqBody)

if c.err != nil {
var resp ErrorResponse

assert.NoError(t, expectBatchJSONResult(res, &resp))
assert.Equal(t, resp.Error, c.err)
} else {
var resp []SuccessResponse
assert.NoError(t, expectBatchJSONResult(res, &resp))

if c.name == "leading-whitespace" {
assert.Len(t, resp, 4)
jsonerr := &ObjectError{Code: -32602, Message: "Invalid Params"}
assert.Equal(t, resp[0].Error, jsonerr)
assert.Nil(t, resp[1].Error)
assert.Nil(t, resp[2].Error)
assert.Nil(t, resp[3].Error)
dankostiuk marked this conversation as resolved.
Show resolved Hide resolved
} else if c.name == "valid-batch-req" {
assert.Len(t, resp, 6)
assert.Nil(t, resp[0].Error)
assert.Nil(t, resp[1].Error)
assert.Nil(t, resp[2].Error)
assert.Nil(t, resp[3].Error)
assert.Nil(t, resp[4].Error)
assert.Nil(t, resp[5].Error)
dankostiuk marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
Loading