Commit 492f8714 authored by vicotor's avatar vicotor

add ratelimit

parent c304ff5a
...@@ -4,6 +4,7 @@ go 1.24.4 ...@@ -4,6 +4,7 @@ go 1.24.4
require ( require (
github.com/ethereum/go-ethereum v1.15.10 github.com/ethereum/go-ethereum v1.15.10
github.com/fsnotify/fsnotify v1.6.0
github.com/go-sql-driver/mysql v1.9.3 github.com/go-sql-driver/mysql v1.9.3
) )
...@@ -12,17 +13,13 @@ require ( ...@@ -12,17 +13,13 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect github.com/StackExchange/wmi v1.2.1 // indirect
github.com/bits-and-blooms/bitset v1.20.0 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/consensys/gnark-crypto v0.18.0 // indirect github.com/consensys/gnark-crypto v0.18.0 // indirect
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.3 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect
...@@ -34,4 +31,5 @@ require ( ...@@ -34,4 +31,5 @@ require (
golang.org/x/crypto v0.36.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/sync v0.12.0 // indirect golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.36.0 // indirect
golang.org/x/time v0.14.0 // indirect
) )
This diff is collapsed.
...@@ -6,8 +6,10 @@ import ( ...@@ -6,8 +6,10 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"log" "log"
"net"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
...@@ -18,6 +20,8 @@ import ( ...@@ -18,6 +20,8 @@ import (
"regexp" "regexp"
"sync" "sync"
"golang.org/x/time/rate"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
...@@ -74,8 +78,19 @@ var ( ...@@ -74,8 +78,19 @@ var (
whitelist map[string]struct{} whitelist map[string]struct{}
whitelistPatterns []*regexp.Regexp whitelistPatterns []*regexp.Regexp
whitelistMu sync.RWMutex whitelistMu sync.RWMutex
// Rate limiting
visitors = make(map[string]*visitor)
visitorsMu sync.Mutex
rateLimit rate.Limit
rateBurst int
) )
type visitor struct {
limiter *rate.Limiter
lastSeen time.Time
}
func main() { func main() {
// Initialize database connection // Initialize database connection
var err error var err error
...@@ -136,6 +151,26 @@ func main() { ...@@ -136,6 +151,26 @@ func main() {
} }
startBlacklistCacheJanitor(cleanupInterval) startBlacklistCacheJanitor(cleanupInterval)
// Initialize rate limiter settings
// Default: 500 requests per 15 minutes
// rate.Every calculates the Limit (events/sec) for a given interval between events.
// Interval = 15 minutes / 500 requests
limit := rate.Every(15 * time.Minute / 500)
if s := os.Getenv("RATE_LIMIT"); s != "" {
if val, err := strconv.ParseFloat(s, 64); err == nil {
limit = rate.Limit(val)
}
}
rateLimit = limit
rateBurst = 50 // default burst
if s := os.Getenv("RATE_BURST"); s != "" {
if val, err := strconv.Atoi(s); err == nil {
rateBurst = val
}
}
startRateLimitCleaner()
http.HandleFunc("/", proxyHandler) http.HandleFunc("/", proxyHandler)
log.Println("RPC proxy service started, listening on port: 8545") log.Println("RPC proxy service started, listening on port: 8545")
log.Fatal(http.ListenAndServe(":8545", nil)) log.Fatal(http.ListenAndServe(":8545", nil))
...@@ -165,12 +200,21 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) { ...@@ -165,12 +200,21 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
// Whitelist short-circuit: if Origin or Referer matches whitelist, forward immediately // Whitelist short-circuit: if Origin or Referer matches whitelist, forward immediately
origin := r.Header.Get("Origin") origin := r.Header.Get("Origin")
referer := r.Header.Get("Referer") // typical header key referer := r.Header.Get("Referer") // typical header key
if isWhitelisted(origin) || isWhitelisted(referer) { ip := getClientIP(r)
log.Printf("whitelist matched (origin=%s referer=%s), forwarding directly", origin, referer) if isWhitelisted(origin) || isWhitelisted(referer) || isWhitelisted(ip) {
log.Printf("whitelist matched (origin=%s referer=%s ip=%s), forwarding directly", origin, referer, ip)
forwardToBackend(w, body) forwardToBackend(w, body)
return return
} }
// Rate limiting for non-whitelisted requests
limiter := getVisitor(ip)
if !limiter.Allow() {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
log.Printf("rate limit exceeded for IP: %s", ip)
return
}
var reqs []RPCRequest var reqs []RPCRequest
// Try to parse as batch request first // Try to parse as batch request first
if err := json.Unmarshal(body, &reqs); err == nil { if err := json.Unmarshal(body, &reqs); err == nil {
...@@ -560,3 +604,52 @@ func startWhitelistWatcher(path string) { ...@@ -560,3 +604,52 @@ func startWhitelistWatcher(path string) {
} }
}() }()
} }
func getVisitor(ip string) *rate.Limiter {
visitorsMu.Lock()
defer visitorsMu.Unlock()
v, exists := visitors[ip]
if !exists {
limiter := rate.NewLimiter(rateLimit, rateBurst)
v = &visitor{limiter, time.Now()}
visitors[ip] = v
}
v.lastSeen = time.Now()
return v.limiter
}
func startRateLimitCleaner() {
go func() {
for {
time.Sleep(5 * time.Minute)
visitorsMu.Lock()
for ip, v := range visitors {
if time.Since(v.lastSeen) > 15*time.Minute {
delete(visitors, ip)
}
}
visitorsMu.Unlock()
}
}()
}
func getClientIP(r *http.Request) string {
// Try X-Forwarded-For
xff := r.Header.Get("X-Forwarded-For")
if xff != "" {
parts := strings.Split(xff, ",")
return strings.TrimSpace(parts[0])
}
// Try X-Real-IP
xrip := r.Header.Get("X-Real-IP")
if xrip != "" {
return strings.TrimSpace(xrip)
}
// Fallback to RemoteAddr
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
return host
}
return r.RemoteAddr
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment