Skip to content

Commit

Permalink
Add blockRangeLimit and batchRequestLimit JSON-RPC flags to help prev…
Browse files Browse the repository at this point in the history
…ent node DDoS (#638)

* add batchRequestLimit and blockRangeLimit flags for json-rpc ddos protection

* addressed PR comments

* improve tests

* fix typo
  • Loading branch information
dankostiuk committed Jul 28, 2022
1 parent f786241 commit 8502125
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 96 deletions.
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.JSONRPCBlockRangeLimit,
"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 @@ -253,6 +262,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
112 changes: 91 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,95 @@ 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
batchResponse []*SuccessResponse
}{
{
"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,
[]*SuccessResponse{
{Error: &ObjectError{Code: -32602, Message: "Invalid Params"}},
{Error: nil},
{Error: nil},
{Error: 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,
[]*SuccessResponse{
{Error: nil},
{Error: nil},
{Error: nil},
{Error: nil},
{Error: nil},
{Error: 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"},
nil,
},
}

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 batchResp []SuccessResponse
assert.NoError(t, expectBatchJSONResult(res, &batchResp))

if c.name == "leading-whitespace" {
assert.Len(t, batchResp, 4)
for index, resp := range batchResp {
assert.Equal(t, resp.Error, c.batchResponse[index].Error)
}
} else if c.name == "valid-batch-req" {
assert.Len(t, batchResp, 6)
for index, resp := range batchResp {
assert.Equal(t, resp.Error, c.batchResponse[index].Error)
}
}
}
}
}
Loading

0 comments on commit 8502125

Please sign in to comment.