R3: gateway edge hardening — body cap, h2c sizing, rate-limit observability
- GATEWAY_MAX_BODY_BYTES (1 MiB): connect WithReadMaxBytes + http.MaxBytesReader
on the public mux; explicit http2.Server MaxConcurrentStreams/IdleTimeout and
an http.Server ReadHeaderTimeout (R2 report follow-up).
- gateway_rate_limited_total{class} counter, Debug per rejection, a rejection
tracker drained every 30 s into a Warn summary per key and a report POST to
/api/v1/internal/ratelimit/report (feeds the admin view + auto-flag).
- The dead AdminPerMinute/AdminBurst policy now guards the /_gm mount (429),
ahead of its Basic-Auth.
- resolve() logs the cause of infra session-resolve failures at Warn (the
transient unauthenticated dips from the R2 run); unknown tokens stay silent.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package ratelimit
|
||||
|
||||
import "sync"
|
||||
|
||||
// Rejection aggregates the limiter rejections of one key within one report
|
||||
// window. Class is the limiter class (user, public, email or admin); Key is the
|
||||
// class-specific subject — an account id for the user class, a client IP for the
|
||||
// others. The JSON shape is the gateway→backend rate-limit report wire contract.
|
||||
type Rejection struct {
|
||||
Class string `json:"class"`
|
||||
Key string `json:"key"`
|
||||
Rejected int `json:"rejected"`
|
||||
}
|
||||
|
||||
// Tracker accumulates limiter rejections between drains. The gateway's periodic
|
||||
// reporter drains it to emit the per-key log summary and the backend report; the
|
||||
// per-rejection cost is one map increment under a mutex, safe on the hot path.
|
||||
type Tracker struct {
|
||||
mu sync.Mutex
|
||||
m map[trackerKey]int
|
||||
}
|
||||
|
||||
type trackerKey struct{ class, key string }
|
||||
|
||||
// NewTracker constructs an empty Tracker.
|
||||
func NewTracker() *Tracker {
|
||||
return &Tracker{m: make(map[trackerKey]int)}
|
||||
}
|
||||
|
||||
// Add counts one rejection of key under class.
|
||||
func (t *Tracker) Add(class, key string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.m[trackerKey{class: class, key: key}]++
|
||||
}
|
||||
|
||||
// Drain returns the rejections accumulated since the previous drain, in
|
||||
// unspecified order, and resets the tracker. It returns nil when nothing was
|
||||
// rejected.
|
||||
func (t *Tracker) Drain() []Rejection {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if len(t.m) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]Rejection, 0, len(t.m))
|
||||
for k, n := range t.m {
|
||||
out = append(out, Rejection{Class: k.class, Key: k.key, Rejected: n})
|
||||
}
|
||||
clear(t.m)
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user