-
Notifications
You must be signed in to change notification settings - Fork 887
Add per-IP rate limiting to CometBFT RPC HTTP #3911
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
958d873
1d9d63e
a59e2ec
11b2a8c
2e8fcc7
5dd7838
47e4f2e
24e706f
5368fe0
edbdc18
c35b819
b5beb3d
4645d42
b4df254
27ed5a8
1a9d586
ff787ad
c7948b4
6922f76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package rpc | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/sei-protocol/sei-chain/sei-tendermint/config" | ||
| "github.com/sei-protocol/sei-chain/sei-tendermint/internal/rpc/core" | ||
| ) | ||
|
|
||
| func TestHandler_InvalidTrustedProxyCIDRsDoesNotPanic(t *testing.T) { | ||
| cfg := config.DefaultRPCConfig() | ||
| cfg.RateLimitingEnabled = true | ||
| cfg.TrustedProxyCIDRs = []string{"not-a-cidr"} | ||
|
|
||
| require.NotPanics(t, func() { | ||
| h := Handler(cfg, core.RoutesMap{}) | ||
| req := httptest.NewRequest(http.MethodGet, "/status", nil) | ||
| rec := httptest.NewRecorder() | ||
| h.ServeHTTP(rec, req) | ||
| }) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| package server | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "io" | ||
| "math" | ||
| "strings" | ||
|
|
||
| "github.com/sei-protocol/sei-chain/ratelimiter" | ||
| ) | ||
|
|
||
| const cometbftRateLimitPlane = "cometbft" | ||
|
|
||
| var errInvalidURIMethod = errors.New("invalid URI method") | ||
|
|
||
| // RateLimitGate applies per-IP token-bucket rate limiting for CometBFT RPC HTTP | ||
| // requests. POST JSON-RPC bodies are parsed with MethodParser before full decode; | ||
| // GET URI routes are accounted by path-derived method names. | ||
| type RateLimitGate struct { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] This file is a near-verbatim copy of Both packages already import |
||
| registry *ratelimiter.Registry | ||
| parser *ratelimiter.MethodParser | ||
| maxBodyBytes int64 | ||
| enabled bool | ||
| plane string | ||
| } | ||
|
|
||
| // NewRateLimitGate returns a gate for CometBFT RPC HTTP (plane "cometbft"). | ||
| // registry must be non-nil. maxBodyBytes should match max-body-bytes; non-positive | ||
| // values use DefaultConfig().MaxBodyBytes. | ||
| func NewRateLimitGate(registry *ratelimiter.Registry, maxBodyBytes int64, enabled bool) *RateLimitGate { | ||
| if maxBodyBytes <= 0 { | ||
| maxBodyBytes = DefaultConfig().MaxBodyBytes | ||
|
amir-deris marked this conversation as resolved.
Outdated
|
||
| } | ||
| if maxBodyBytes == math.MaxInt64 { | ||
| maxBodyBytes = math.MaxInt64 - 1 | ||
| } | ||
| return &RateLimitGate{ | ||
| registry: registry, | ||
| parser: ratelimiter.NewMethodParser(maxBodyBytes), | ||
| maxBodyBytes: maxBodyBytes, | ||
| enabled: enabled, | ||
| plane: cometbftRateLimitPlane, | ||
| } | ||
| } | ||
|
|
||
| // chargeAdmissionRejection consumes one token for a fail-closed rejection that | ||
| // never reaches method parsing (oversize body, read error). Returns true when | ||
| // the bucket is exhausted and the caller should respond with HTTP 429. | ||
| func (g *RateLimitGate) chargeAdmissionRejection(ctx context.Context, ip string) bool { | ||
| if !g.enabled { | ||
| return false | ||
| } | ||
| return !g.registry.Allow(ctx, ip, g.plane, ratelimiter.MethodInvalid) | ||
| } | ||
|
|
||
| // CheckPOST parses body for JSON-RPC method names and applies per-IP rate limits. | ||
| // Parse errors still charge the bucket under ratelimiter.MethodInvalid so | ||
| // malformed bodies can't bypass rate limiting. | ||
| func (g *RateLimitGate) CheckPOST(ctx context.Context, ip string, body io.Reader) (allowed bool, rejectMethod string, err error) { | ||
| if !g.enabled { | ||
| return true, "", nil | ||
| } | ||
|
|
||
| methods, _, parseErr := g.parser.Parse(body) | ||
| if parseErr != nil { | ||
| if !g.registry.Allow(ctx, ip, g.plane, ratelimiter.MethodInvalid) { | ||
| return false, ratelimiter.MethodInvalid, nil | ||
| } | ||
| return false, "", parseErr | ||
| } | ||
|
|
||
| if n := len(methods); n > 0 && !g.registry.AllowN(ctx, ip, g.plane, methods[0], n) { | ||
| return false, methods[0], nil | ||
| } | ||
| return true, "", nil | ||
| } | ||
|
|
||
| // CheckURI applies per-IP rate limits for REST-style GET/HEAD RPC routes. | ||
| func (g *RateLimitGate) CheckURI(ctx context.Context, ip, path string) (allowed bool, rejectMethod string, err error) { | ||
| if !g.enabled { | ||
| return true, "", nil | ||
| } | ||
| method := strings.TrimPrefix(path, "/") | ||
| if method == "" { | ||
| if !g.registry.Allow(ctx, ip, g.plane, ratelimiter.MethodInvalid) { | ||
| return false, ratelimiter.MethodInvalid, nil | ||
| } | ||
| return false, "", errInvalidURIMethod | ||
| } | ||
| if !g.registry.Allow(ctx, ip, g.plane, method) { | ||
| return false, method, nil | ||
| } | ||
| return true, "", nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.