diff options
| author | bndw <ben@bdw.to> | 2026-02-14 21:53:14 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-14 21:53:14 -0800 |
| commit | 32ca0fba5108d0dc2c7415f36e55f031d5a0562e (patch) | |
| tree | ff91309ef9af7d0ec8c9b5bd5b6f39f073d4be55 /cmd | |
| parent | e647880669b79cd968231cf85dc037a18e8bfd9c (diff) | |
feat: add rate limiting to WebSocket connections
WebSocket clients were completely unprotected from abuse. Add RateLimiter
interface to WebSocket handler and enforce limits on EVENT and REQ messages.
- Add RateLimiter interface with Allow(identifier, method) method
- Track client IP in connState (proxy-aware via X-Forwarded-For)
- Check rate limits in handleEvent and handleReq
- Use authenticated pubkey as identifier, fallback to IP
- Share same rate limiter instance with gRPC
- Add getClientIP() helper that checks proxy headers first
Critical security fix for production deployment. Without this, any client
could spam unlimited events/subscriptions via WebSocket.
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/relay/main.go | 7 |
1 files changed, 6 insertions, 1 deletions
diff --git a/cmd/relay/main.go b/cmd/relay/main.go index e4afec8..86a29cb 100644 --- a/cmd/relay/main.go +++ b/cmd/relay/main.go | |||
| @@ -79,8 +79,9 @@ func main() { | |||
| 79 | streamInterceptors = append(streamInterceptors, auth.NostrStreamInterceptor(authOpts)) | 79 | streamInterceptors = append(streamInterceptors, auth.NostrStreamInterceptor(authOpts)) |
| 80 | } | 80 | } |
| 81 | 81 | ||
| 82 | var limiter *ratelimit.Limiter | ||
| 82 | if cfg.RateLimit.Enabled { | 83 | if cfg.RateLimit.Enabled { |
| 83 | limiter := ratelimit.New(cfg.RateLimit.ToRateLimiter()) | 84 | limiter = ratelimit.New(cfg.RateLimit.ToRateLimiter()) |
| 84 | unaryInterceptors = append(unaryInterceptors, ratelimit.UnaryInterceptor(limiter)) | 85 | unaryInterceptors = append(unaryInterceptors, ratelimit.UnaryInterceptor(limiter)) |
| 85 | streamInterceptors = append(streamInterceptors, ratelimit.StreamInterceptor(limiter)) | 86 | streamInterceptors = append(streamInterceptors, ratelimit.StreamInterceptor(limiter)) |
| 86 | } | 87 | } |
| @@ -125,6 +126,10 @@ func main() { | |||
| 125 | }) | 126 | }) |
| 126 | } | 127 | } |
| 127 | 128 | ||
| 129 | if limiter != nil { | ||
| 130 | wsHandler.SetRateLimiter(limiter) | ||
| 131 | } | ||
| 132 | |||
| 128 | var grpcDisplay, httpDisplay, wsDisplay string | 133 | var grpcDisplay, httpDisplay, wsDisplay string |
| 129 | if cfg.Server.PublicURL != "" { | 134 | if cfg.Server.PublicURL != "" { |
| 130 | grpcDisplay = cfg.Server.PublicURL + ":443" | 135 | grpcDisplay = cfg.Server.PublicURL + ":443" |
