@@ -3,8 +3,10 @@ package config
33
44import (
55 "fmt"
6+ "net"
67 "net/http"
78 "os"
9+ "regexp"
810 "time"
911
1012 "github.com/ethpandaops/lab-backend/internal/cartographoor"
@@ -20,6 +22,7 @@ type Config struct {
2022 Experiments map [string ]ExperimentSettings `yaml:"experiments"`
2123 Cartographoor cartographoor.Config `yaml:"cartographoor"`
2224 Bounds BoundsConfig `yaml:"bounds"`
25+ RateLimiting RateLimitingConfig `yaml:"rate_limiting"`
2326}
2427
2528// ServerConfig contains HTTP server settings.
@@ -58,6 +61,22 @@ type BoundsConfig struct {
5861 BoundsTTL time.Duration `yaml:"bounds_ttl"` // Redis TTL for bounds data (0 = no expiration)
5962}
6063
64+ // RateLimitingConfig holds rate limiting configuration.
65+ type RateLimitingConfig struct {
66+ Enabled bool `yaml:"enabled"`
67+ FailureMode string `yaml:"failure_mode"` // "fail_open" or "fail_closed"
68+ ExemptIPs []string `yaml:"exempt_ips"` // CIDR ranges to whitelist
69+ Rules []RateLimitRule `yaml:"rules"`
70+ }
71+
72+ // RateLimitRule defines a single rate limit rule.
73+ type RateLimitRule struct {
74+ Name string `yaml:"name"`
75+ PathPattern string `yaml:"path_pattern"` // Regex pattern
76+ Limit int `yaml:"limit"` // Max requests
77+ Window time.Duration `yaml:"window"` // Time window
78+ }
79+
6180// Validate validates the configuration and sets defaults.
6281func (c * BoundsConfig ) Validate () error {
6382 // Set defaults
@@ -199,5 +218,57 @@ func (c *Config) Validate() error {
199218 return fmt .Errorf ("bounds: %w" , err )
200219 }
201220
221+ // Validate rate limiting config
222+ if c .RateLimiting .Enabled {
223+ if err := c .validateRateLimiting (); err != nil {
224+ return fmt .Errorf ("rate_limiting: %w" , err )
225+ }
226+ }
227+
228+ return nil
229+ }
230+
231+ func (c * Config ) validateRateLimiting () error {
232+ if c .RateLimiting .FailureMode != "fail_open" && c .RateLimiting .FailureMode != "fail_closed" {
233+ return fmt .Errorf ("failure_mode must be 'fail_open' or 'fail_closed'" )
234+ }
235+
236+ if len (c .RateLimiting .Rules ) == 0 {
237+ return fmt .Errorf ("rules must have at least one rule" )
238+ }
239+
240+ for i , rule := range c .RateLimiting .Rules {
241+ if rule .Name == "" {
242+ return fmt .Errorf ("rules[%d].name is required" , i )
243+ }
244+
245+ if rule .PathPattern == "" {
246+ return fmt .Errorf ("rules[%d].path_pattern is required" , i )
247+ }
248+
249+ if rule .Limit <= 0 {
250+ return fmt .Errorf ("rules[%d].limit must be positive" , i )
251+ }
252+
253+ if rule .Window <= 0 {
254+ return fmt .Errorf ("rules[%d].window must be positive" , i )
255+ }
256+
257+ // Validate regex pattern compiles
258+ if _ , err := regexp .Compile (rule .PathPattern ); err != nil {
259+ return fmt .Errorf ("rules[%d].path_pattern invalid regex: %w" , i , err )
260+ }
261+ }
262+
263+ // Validate CIDR ranges
264+ for i , cidr := range c .RateLimiting .ExemptIPs {
265+ if _ , _ , err := net .ParseCIDR (cidr ); err != nil {
266+ // Try parsing as single IP
267+ if net .ParseIP (cidr ) == nil {
268+ return fmt .Errorf ("exempt_ips[%d] invalid IP or CIDR: %s" , i , cidr )
269+ }
270+ }
271+ }
272+
202273 return nil
203274}
0 commit comments