diff options
| author | bndw <ben@bdw.to> | 2026-02-14 08:58:57 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-14 08:58:57 -0800 |
| commit | f0169fa1f9d2e2a5d1c292b9080da10ef0878953 (patch) | |
| tree | c85d31dfbf270fe4ebbe2c53bdbb96c0a0a45ace /internal/ratelimit/config.go | |
| parent | 44aa0591b0eed7851e961ea17bd1c9601570ac24 (diff) | |
feat: implement per-user rate limiting with token bucket algorithm
Add comprehensive rate limiting package that works seamlessly with
NIP-98 authentication.
Features:
- Token bucket algorithm (allows bursts, smooth average rate)
- Per-pubkey limits for authenticated users
- Per-IP limits for unauthenticated users (fallback)
- Method-specific overrides (e.g., stricter for PublishEvent)
- Per-user custom limits (VIP/admin tiers)
- Standard gRPC interceptors (chain after auth)
- Automatic cleanup of idle limiters
- Statistics tracking (allowed/denied/denial rate)
Configuration options:
- Default rate limits and burst sizes
- Method-specific overrides
- User-specific overrides (with method overrides)
- Skip methods (health checks, public endpoints)
- Skip users (admins, monitoring)
- Configurable cleanup intervals
Performance:
- In-memory (200 bytes per user)
- O(1) lookups with sync.RWMutex
- ~85ns per rate limit check
- Periodic cleanup to free memory
Returns gRPC ResourceExhausted (HTTP 429) when limits exceeded.
Includes comprehensive tests, benchmarks, and detailed README with
usage examples, configuration reference, and security considerations.
Diffstat (limited to 'internal/ratelimit/config.go')
| -rw-r--r-- | internal/ratelimit/config.go | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/internal/ratelimit/config.go b/internal/ratelimit/config.go new file mode 100644 index 0000000..132c96b --- /dev/null +++ b/internal/ratelimit/config.go | |||
| @@ -0,0 +1,153 @@ | |||
| 1 | package ratelimit | ||
| 2 | |||
| 3 | import "time" | ||
| 4 | |||
| 5 | // Config configures the rate limiter behavior. | ||
| 6 | type Config struct { | ||
| 7 | // RequestsPerSecond is the default rate limit in requests per second. | ||
| 8 | // This applies to authenticated users (identified by pubkey). | ||
| 9 | // Default: 10 | ||
| 10 | RequestsPerSecond float64 | ||
| 11 | |||
| 12 | // BurstSize is the maximum burst size (token bucket capacity). | ||
| 13 | // Allows users to make burst requests up to this limit. | ||
| 14 | // Default: 20 | ||
| 15 | BurstSize int | ||
| 16 | |||
| 17 | // IPRequestsPerSecond is the rate limit for unauthenticated users. | ||
| 18 | // These are identified by IP address. | ||
| 19 | // Typically set lower than authenticated user limits. | ||
| 20 | // Default: 5 | ||
| 21 | IPRequestsPerSecond float64 | ||
| 22 | |||
| 23 | // IPBurstSize is the burst size for IP-based rate limiting. | ||
| 24 | // Default: 10 | ||
| 25 | IPBurstSize int | ||
| 26 | |||
| 27 | // MethodLimits provides per-method rate limit overrides. | ||
| 28 | // Key is the full gRPC method name (e.g., "/nostr.v1.NostrRelay/PublishEvent") | ||
| 29 | // If not specified, uses the default RequestsPerSecond and BurstSize. | ||
| 30 | MethodLimits map[string]MethodLimit | ||
| 31 | |||
| 32 | // UserLimits provides per-user custom rate limits. | ||
| 33 | // Key is the pubkey. Useful for VIP/premium users or admins. | ||
| 34 | // If not specified, uses the default limits. | ||
| 35 | UserLimits map[string]UserLimit | ||
| 36 | |||
| 37 | // SkipMethods is a list of gRPC methods that bypass rate limiting. | ||
| 38 | // Useful for health checks or public endpoints. | ||
| 39 | // Example: []string{"/grpc.health.v1.Health/Check"} | ||
| 40 | SkipMethods []string | ||
| 41 | |||
| 42 | // SkipUsers is a list of pubkeys that bypass rate limiting. | ||
| 43 | // Useful for admins or monitoring services. | ||
| 44 | SkipUsers []string | ||
| 45 | |||
| 46 | // CleanupInterval is how often to remove idle rate limiters from memory. | ||
| 47 | // Limiters that haven't been used recently are removed to save memory. | ||
| 48 | // Default: 5 minutes | ||
| 49 | CleanupInterval time.Duration | ||
| 50 | |||
| 51 | // MaxIdleTime is how long a limiter can be idle before being cleaned up. | ||
| 52 | // Default: 10 minutes | ||
| 53 | MaxIdleTime time.Duration | ||
| 54 | } | ||
| 55 | |||
| 56 | // MethodLimit defines rate limits for a specific gRPC method. | ||
| 57 | type MethodLimit struct { | ||
| 58 | RequestsPerSecond float64 | ||
| 59 | BurstSize int | ||
| 60 | } | ||
| 61 | |||
| 62 | // UserLimit defines custom rate limits for a specific user (pubkey). | ||
| 63 | type UserLimit struct { | ||
| 64 | // RequestsPerSecond is the default rate for this user. | ||
| 65 | RequestsPerSecond float64 | ||
| 66 | |||
| 67 | // BurstSize is the burst size for this user. | ||
| 68 | BurstSize int | ||
| 69 | |||
| 70 | // MethodLimits provides per-method overrides for this user. | ||
| 71 | // Allows fine-grained control like "VIP user gets 100 req/s for queries | ||
| 72 | // but still only 5 req/s for publishes" | ||
| 73 | MethodLimits map[string]MethodLimit | ||
| 74 | } | ||
| 75 | |||
| 76 | // DefaultConfig returns the default rate limit configuration. | ||
| 77 | func DefaultConfig() *Config { | ||
| 78 | return &Config{ | ||
| 79 | RequestsPerSecond: 10, | ||
| 80 | BurstSize: 20, | ||
| 81 | IPRequestsPerSecond: 5, | ||
| 82 | IPBurstSize: 10, | ||
| 83 | CleanupInterval: 5 * time.Minute, | ||
| 84 | MaxIdleTime: 10 * time.Minute, | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | // Validate checks if the configuration is valid. | ||
| 89 | func (c *Config) Validate() error { | ||
| 90 | if c.RequestsPerSecond <= 0 { | ||
| 91 | c.RequestsPerSecond = 10 | ||
| 92 | } | ||
| 93 | if c.BurstSize <= 0 { | ||
| 94 | c.BurstSize = 20 | ||
| 95 | } | ||
| 96 | if c.IPRequestsPerSecond <= 0 { | ||
| 97 | c.IPRequestsPerSecond = 5 | ||
| 98 | } | ||
| 99 | if c.IPBurstSize <= 0 { | ||
| 100 | c.IPBurstSize = 10 | ||
| 101 | } | ||
| 102 | if c.CleanupInterval <= 0 { | ||
| 103 | c.CleanupInterval = 5 * time.Minute | ||
| 104 | } | ||
| 105 | if c.MaxIdleTime <= 0 { | ||
| 106 | c.MaxIdleTime = 10 * time.Minute | ||
| 107 | } | ||
| 108 | return nil | ||
| 109 | } | ||
| 110 | |||
| 111 | // GetLimitForMethod returns the rate limit for a specific method and user. | ||
| 112 | // Precedence: UserLimit.MethodLimit > MethodLimit > UserLimit > Default | ||
| 113 | func (c *Config) GetLimitForMethod(pubkey, method string) (requestsPerSecond float64, burstSize int) { | ||
| 114 | // Check user-specific method limit first (highest precedence) | ||
| 115 | if userLimit, ok := c.UserLimits[pubkey]; ok { | ||
| 116 | if methodLimit, ok := userLimit.MethodLimits[method]; ok { | ||
| 117 | return methodLimit.RequestsPerSecond, methodLimit.BurstSize | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | // Check global method limit | ||
| 122 | if methodLimit, ok := c.MethodLimits[method]; ok { | ||
| 123 | return methodLimit.RequestsPerSecond, methodLimit.BurstSize | ||
| 124 | } | ||
| 125 | |||
| 126 | // Check user-specific default limit | ||
| 127 | if userLimit, ok := c.UserLimits[pubkey]; ok { | ||
| 128 | return userLimit.RequestsPerSecond, userLimit.BurstSize | ||
| 129 | } | ||
| 130 | |||
| 131 | // Fall back to global default | ||
| 132 | return c.RequestsPerSecond, c.BurstSize | ||
| 133 | } | ||
| 134 | |||
| 135 | // ShouldSkipMethod returns true if the method should bypass rate limiting. | ||
| 136 | func (c *Config) ShouldSkipMethod(method string) bool { | ||
| 137 | for _, skip := range c.SkipMethods { | ||
| 138 | if skip == method { | ||
| 139 | return true | ||
| 140 | } | ||
| 141 | } | ||
| 142 | return false | ||
| 143 | } | ||
| 144 | |||
| 145 | // ShouldSkipUser returns true if the user should bypass rate limiting. | ||
| 146 | func (c *Config) ShouldSkipUser(pubkey string) bool { | ||
| 147 | for _, skip := range c.SkipUsers { | ||
| 148 | if skip == pubkey { | ||
| 149 | return true | ||
| 150 | } | ||
| 151 | } | ||
| 152 | return false | ||
| 153 | } | ||
