Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
27 changes: 22 additions & 5 deletions sei-cosmos/server/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const (
// DefaultGRPCWebAddress defines the default address to bind the gRPC-web server to.
DefaultGRPCWebAddress = "0.0.0.0:9091"

// DefaultGRPCWebMaxOpenConnections defines the default maximum number of
// simultaneous open connections for the gRPC-web server.
DefaultGRPCWebMaxOpenConnections = 1000

// DefaultOccEanbled defines whether to use OCC for tx processing
DefaultOccEnabled = true
)
Expand Down Expand Up @@ -180,6 +184,9 @@ type GRPCWebConfig struct {

// EnableUnsafeCORS defines if CORS should be enabled (unsafe - use it at your own risk)
EnableUnsafeCORS bool `mapstructure:"enable-unsafe-cors"`

// MaxOpenConnections defines the maximum number of simultaneous open connections. 0 means unlimited.
MaxOpenConnections uint `mapstructure:"max-open-connections"`
}

// StateSyncConfig defines the state sync snapshot configuration.
Expand Down Expand Up @@ -292,8 +299,9 @@ func DefaultConfig() *Config {
Offline: false,
},
GRPCWeb: GRPCWebConfig{
Enable: true,
Address: DefaultGRPCWebAddress,
Enable: true,
Address: DefaultGRPCWebAddress,
MaxOpenConnections: DefaultGRPCWebMaxOpenConnections,
},
StateSync: StateSyncConfig{
SnapshotInterval: 0,
Expand Down Expand Up @@ -357,6 +365,14 @@ func GetConfig(v *viper.Viper) (Config, error) {
flatKVConfig.EnableReadWriteMetrics = v.GetBool("state-commit.flatkv.enable-read-write-metrics")
}

// Apply the in-code default when the key is absent so that nodes upgrading
// with an older app.toml (which lacks this key) are still bounded rather
// than running with unlimited connections.
grpcWebMaxOpenConnections := uint(DefaultGRPCWebMaxOpenConnections)
if v.IsSet("grpc-web.max-open-connections") {
grpcWebMaxOpenConnections = v.GetUint("grpc-web.max-open-connections")
}

return Config{
BaseConfig: BaseConfig{
MinGasPrices: v.GetString("minimum-gas-prices"),
Expand Down Expand Up @@ -404,9 +420,10 @@ func GetConfig(v *viper.Viper) (Config, error) {
Address: v.GetString("grpc.address"),
},
GRPCWeb: GRPCWebConfig{
Enable: v.GetBool("grpc-web.enable"),
Address: v.GetString("grpc-web.address"),
EnableUnsafeCORS: v.GetBool("grpc-web.enable-unsafe-cors"),
Enable: v.GetBool("grpc-web.enable"),
Address: v.GetString("grpc-web.address"),
EnableUnsafeCORS: v.GetBool("grpc-web.enable-unsafe-cors"),
MaxOpenConnections: grpcWebMaxOpenConnections,
},
StateSync: StateSyncConfig{
SnapshotInterval: v.GetUint64("state-sync.snapshot-interval"),
Expand Down
35 changes: 35 additions & 0 deletions sei-cosmos/server/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func TestDefaultGRPCWebConfig(t *testing.T) {
cfg := DefaultConfig()
require.True(t, cfg.GRPCWeb.Enable)
require.Equal(t, DefaultGRPCWebAddress, cfg.GRPCWeb.Address)
require.Equal(t, uint(DefaultGRPCWebMaxOpenConnections), cfg.GRPCWeb.MaxOpenConnections)
}

func TestDefaultRosettaConfig(t *testing.T) {
Expand Down Expand Up @@ -283,6 +284,40 @@ func TestSetAndGetMinGasPrices(t *testing.T) {
require.Equal(t, "uatom", parsed[1].Denom)
}

func TestGetConfigGRPCWebMaxOpenConnections(t *testing.T) {
baseViper := func() *viper.Viper {
v := viper.New()
v.Set("minimum-gas-prices", DefaultMinGasPrices)
v.Set("telemetry.global-labels", []interface{}{})
return v
}

t.Run("missing key falls back to the in-code default", func(t *testing.T) {
// Mirrors a node upgrading with an older app.toml that predates the
// grpc-web.max-open-connections key
cfg, err := GetConfig(baseViper())
require.NoError(t, err)
require.Equal(t, uint(DefaultGRPCWebMaxOpenConnections), cfg.GRPCWeb.MaxOpenConnections)
})

t.Run("explicit zero is preserved as unlimited", func(t *testing.T) {
v := baseViper()
v.Set("grpc-web.max-open-connections", 0)
cfg, err := GetConfig(v)
require.NoError(t, err)
require.Equal(t, uint(0), cfg.GRPCWeb.MaxOpenConnections,
"explicit 0 must remain an opt-in to unlimited connections")
})

t.Run("explicit value overrides the default", func(t *testing.T) {
v := baseViper()
v.Set("grpc-web.max-open-connections", 250)
cfg, err := GetConfig(v)
require.NoError(t, err)
require.Equal(t, uint(250), cfg.GRPCWeb.MaxOpenConnections)
})
}

func TestGetConfigStateCommit(t *testing.T) {
v := viper.New()

Expand Down
3 changes: 3 additions & 0 deletions sei-cosmos/server/config/toml.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ address = "{{ .GRPCWeb.Address }}"
# EnableUnsafeCORS defines if CORS should be enabled (unsafe - use it at your own risk).
enable-unsafe-cors = {{ .GRPCWeb.EnableUnsafeCORS }}

# MaxOpenConnections defines the number of maximum open connections. 0 means unlimited.
max-open-connections = {{ .GRPCWeb.MaxOpenConnections }}

###############################################################################
### Genesis Configuration (Auto-managed) ###
###############################################################################
Expand Down
27 changes: 22 additions & 5 deletions sei-cosmos/server/grpc/grpc_web.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package grpc

import (
"fmt"
"math"
"net"
"net/http"
"time"

"github.com/improbable-eng/grpc-web/go/grpcweb"
"golang.org/x/net/netutil"
"google.golang.org/grpc"

"github.com/sei-protocol/sei-chain/sei-cosmos/server/config"
Expand All @@ -25,20 +28,34 @@ func StartGRPCWeb(grpcSrv *grpc.Server, config config.Config) (*http.Server, err

wrappedServer := grpcweb.WrapServer(grpcSrv, options...)
grpcWebSrv := &http.Server{
Addr: config.GRPCWeb.Address,
Handler: wrappedServer,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 2 * time.Minute,
IdleTimeout: 30 * time.Second,
Comment thread
cursor[bot] marked this conversation as resolved.
}

errCh := make(chan error)
listener, err := net.Listen("tcp", config.GRPCWeb.Address)
if err != nil {
return nil, fmt.Errorf("[grpc-web] failed to listen on %s: %w", config.GRPCWeb.Address, err)
}
if config.GRPCWeb.MaxOpenConnections > 0 {
maxConn := config.GRPCWeb.MaxOpenConnections
if maxConn > math.MaxInt {
maxConn = math.MaxInt
}
listener = netutil.LimitListener(listener, int(maxConn)) //nolint:gosec // G115: clamped to math.MaxInt above
}

errCh := make(chan error, 1)
go func() {
if err := grpcWebSrv.ListenAndServe(); err != nil {
errCh <- fmt.Errorf("[grpc] failed to serve: %w", err)
if err := grpcWebSrv.Serve(listener); err != nil {
errCh <- fmt.Errorf("[grpc-web] failed to serve: %w", err)
}
}()

select {
case err := <-errCh:
case err = <-errCh:
Comment thread
cursor[bot] marked this conversation as resolved.
return nil, err
case <-time.After(types.ServerStartTime): // assume server started successfully
return grpcWebSrv, nil
Expand Down
44 changes: 44 additions & 0 deletions sei-cosmos/server/grpc/grpc_web_timeout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package grpc_test

import (
"net"
"testing"
"time"

"github.com/stretchr/testify/require"
"google.golang.org/grpc"

"github.com/sei-protocol/sei-chain/sei-cosmos/server/config"
srvgrpc "github.com/sei-protocol/sei-chain/sei-cosmos/server/grpc"
)

// TestStartGRPCWebTimeouts verifies that StartGRPCWeb sets all HTTP timeout
// fields needed to prevent body-stall DoS attacks.
func TestStartGRPCWebTimeouts(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
addr := ln.Addr().String()
err = ln.Close()
require.NoError(t, err)

grpcSrv := grpc.NewServer()
cfg := config.Config{
GRPCWeb: config.GRPCWebConfig{
Enable: true,
Address: addr,
},
}

srv, err := srvgrpc.StartGRPCWeb(grpcSrv, cfg)
require.NoError(t, err)
require.NotNil(t, srv)
defer func() {
err = srv.Shutdown(t.Context()) //nolint:errcheck
require.NoError(t, err)
}()

require.Equal(t, 10*time.Second, srv.ReadHeaderTimeout, "ReadHeaderTimeout")
require.Equal(t, 30*time.Second, srv.ReadTimeout, "ReadTimeout")
require.Equal(t, 2*time.Minute, srv.WriteTimeout, "WriteTimeout")
require.Equal(t, 30*time.Second, srv.IdleTimeout, "IdleTimeout")
}
Loading