diff --git a/cmd/beacon/main.go b/cmd/beacon/main.go index 6c909a4..c69b1e7 100644 --- a/cmd/beacon/main.go +++ b/cmd/beacon/main.go @@ -47,7 +47,7 @@ var version = "dev" // @title MeshCore Beacon API // @version 2.0.7 // @description MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket. -// @description REST requests share a configurable per-client rate limit (default 300/minute and a 300-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget. +// @description REST requests share a configurable per-client rate limit (default 600/minute and a 600-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget. // @description WebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open. // @termsOfService https://github.com/MeshCore-Beacon/beacon-server diff --git a/config.yaml.example b/config.yaml.example index cc19991..43c107c 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -40,11 +40,11 @@ server: # the proxy's budget. IPv6 clients share a /64 budget to prevent address rotation. ratelimit: enabled: true - requests_per_minute: 300 + requests_per_minute: 600 # Maximum requests in a one-second sliding window, not token-bucket capacity. # Omitted/zero defaults to requests_per_minute. Negative limits prevent startup. # A 429 returns error.code=rate_limited and Retry-After (1 or 60 seconds). - burst: 300 + burst: 600 # MeshMapper integrations (optional, require a regional or grouped-region API key). # Your local MeshMapper regional or grouped-region admin can generate the key. diff --git a/docs/docs.go b/docs/docs.go index b4a5a5f..1ad9249 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -6276,7 +6276,7 @@ var SwaggerInfo = &swag.Spec{ BasePath: "/api/v1", Schemes: []string{"http", "https"}, Title: "MeshCore Beacon API", - Description: "MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket.\nREST requests share a configurable per-client rate limit (default 300/minute and a 300-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.\nWebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open.", + Description: "MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket.\nREST requests share a configurable per-client rate limit (default 600/minute and a 600-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.\nWebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open.", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/docs/swagger.json b/docs/swagger.json index d557e25..6ba981a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -5,7 +5,7 @@ ], "swagger": "2.0", "info": { - "description": "MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket.\nREST requests share a configurable per-client rate limit (default 300/minute and a 300-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.\nWebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open.", + "description": "MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket.\nREST requests share a configurable per-client rate limit (default 600/minute and a 600-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.\nWebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open.", "title": "MeshCore Beacon API", "termsOfService": "https://github.com/MeshCore-Beacon/beacon-server", "contact": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 93dd713..c837842 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1819,7 +1819,7 @@ info: url: https://github.com/MeshCore-Beacon/beacon-server description: |- MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket. - REST requests share a configurable per-client rate limit (default 300/minute and a 300-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget. + REST requests share a configurable per-client rate limit (default 600/minute and a 600-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget. WebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open. license: name: AGPL-3-or-later diff --git a/internal/api/router/rate_limit_test.go b/internal/api/router/rate_limit_test.go index b0e1cd8..96e1184 100644 --- a/internal/api/router/rate_limit_test.go +++ b/internal/api/router/rate_limit_test.go @@ -23,13 +23,13 @@ import ( func TestDefaultAPIRateLimit(t *testing.T) { handler := New(nil, nil, nil, Options{MaxConnsPerIP: 5, MaxConnectsPerMinute: 1000, RateLimit: config.Resolve(&config.Config{}).RateLimit}) - for i := 0; i <= 300; i++ { + for i := 0; i <= 600; i++ { request := httptest.NewRequest(http.MethodGet, "/api/v1/brokers", nil) request.RemoteAddr = "198.51.100.1:1234" response := httptest.NewRecorder() handler.ServeHTTP(response, request) want := http.StatusOK - if i == 300 { + if i == 600 { want = http.StatusTooManyRequests } if response.Code != want { diff --git a/internal/config/config.go b/internal/config/config.go index 70985d3..9065a65 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -600,7 +600,7 @@ func Resolve(cfg *Config) ResolvedConfig { ObserverDeleteAfter: cfg.Observers.DeleteAfter.Duration, } if r.RateLimit.RequestsPerMinute == 0 { - r.RateLimit.RequestsPerMinute = 300 + r.RateLimit.RequestsPerMinute = 600 } if r.RateLimit.Burst == 0 { r.RateLimit.Burst = r.RateLimit.RequestsPerMinute diff --git a/internal/config/rate_limit_test.go b/internal/config/rate_limit_test.go index 9e4e135..7b9c4f3 100644 --- a/internal/config/rate_limit_test.go +++ b/internal/config/rate_limit_test.go @@ -15,11 +15,11 @@ func TestRateLimitConfig(t *testing.T) { want ResolvedRateLimitConfig wantError bool }{ - {"omitted", "{}", ResolvedRateLimitConfig{true, 300, 300}, false}, - {"disabled", "ratelimit: {enabled: false}", ResolvedRateLimitConfig{false, 300, 300}, false}, + {"omitted", "{}", ResolvedRateLimitConfig{true, 600, 600}, false}, + {"disabled", "ratelimit: {enabled: false}", ResolvedRateLimitConfig{false, 600, 600}, false}, {"custom", "ratelimit: {enabled: true, requests_per_minute: 900, burst: 100}", ResolvedRateLimitConfig{true, 900, 100}, false}, - {"burst default follows budget", "ratelimit: {requests_per_minute: 600}", ResolvedRateLimitConfig{true, 600, 600}, false}, - {"zero defaults", "ratelimit: {requests_per_minute: 0, burst: 0}", ResolvedRateLimitConfig{true, 300, 300}, false}, + {"burst default follows budget", "ratelimit: {requests_per_minute: 1200}", ResolvedRateLimitConfig{true, 1200, 1200}, false}, + {"zero defaults", "ratelimit: {requests_per_minute: 0, burst: 0}", ResolvedRateLimitConfig{true, 600, 600}, false}, {"negative minute", "ratelimit: {requests_per_minute: -1}", ResolvedRateLimitConfig{}, true}, {"negative burst", "ratelimit: {burst: -1}", ResolvedRateLimitConfig{}, true}, {"invalid disabled config", "ratelimit: {enabled: false, burst: -1}", ResolvedRateLimitConfig{}, true}, @@ -39,7 +39,7 @@ func TestRateLimitConfig(t *testing.T) { }) } cfg, err := Load(filepath.Join(t.TempDir(), "missing.yaml")) - if err != nil || Resolve(cfg).RateLimit != (ResolvedRateLimitConfig{true, 300, 300}) { + if err != nil || Resolve(cfg).RateLimit != (ResolvedRateLimitConfig{true, 600, 600}) { t.Fatalf("missing config did not enable defaults: %v", err) } }