M8: per-key UPDATE rate limiting (token bucket)
Hamilton M8: a compromised TSIG key — or a misconfigured client retrying forever — must not be able to drive unbounded UPDATE traffic. Each UPDATE costs disk IOPS, a git commit, and a slot in the SOA serial counter (now 9999/day per zone). Without a cap, a few hours of runaway traffic could exhaust the SOA serial counter and brick the zone for the day. Implementation: per-key token bucket in ratelimit.go. Default 100 tokens / 60 seconds. New keys start full so legitimate clients see no delay at boot. Refill is continuous, capped at the burst value. Configurable in Corefile: rate-limit off # disable entirely rate-limit <burst> <period-secs> # e.g., rate-limit 200 60 Enforcement runs in ServeDNS after TSIG verification — a request that fails auth doesn't consume a token (and a forged TSIG can't be used to deny service to a real key holder, since we never reached the rate check). 100/min is well above ACME's needs: a worst-case full-renewal storm across our ~84 zones emits maybe 200 UPDATEs total over several minutes. Anything beyond is suspicious by definition. New tests covering: first-call allowed, burst exhaustion, refill behavior, per-key isolation, refill-cap (no idle-accumulation overflow).
This commit is contained in:
parent
6ab2b6af6d
commit
8d1477350a
4 changed files with 222 additions and 0 deletions
37
setup.go
37
setup.go
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
|
|
@ -164,6 +165,13 @@ func parse(c *caddy.Controller) (*RFC2136, error) {
|
|||
// Per-zone git author overrides. Defaults are applied later.
|
||||
var gitAuthorName, gitAuthorEmail string
|
||||
|
||||
// Rate-limit config (Hamilton M8). Defaults are
|
||||
// defaultRateBurst/defaultRatePeriod from ratelimit.go; an explicit
|
||||
// `rate-limit <burst> <period-seconds>` directive overrides.
|
||||
rateBurst := defaultRateBurst
|
||||
ratePeriod := defaultRatePeriod
|
||||
rateLimitEnabled := true
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
if len(args) < 1 {
|
||||
|
|
@ -235,6 +243,30 @@ func parse(c *caddy.Controller) (*RFC2136, error) {
|
|||
gitAuthorName = gArgs[0]
|
||||
gitAuthorEmail = gArgs[1]
|
||||
|
||||
case "rate-limit":
|
||||
rArgs := c.RemainingArgs()
|
||||
switch len(rArgs) {
|
||||
case 1:
|
||||
if rArgs[0] == "off" || rArgs[0] == "false" || rArgs[0] == "no" {
|
||||
rateLimitEnabled = false
|
||||
break
|
||||
}
|
||||
return nil, c.Errf("rate-limit single-arg form must be 'off'; for limits use 'rate-limit <burst> <period-seconds>'")
|
||||
case 2:
|
||||
b, err := strconv.ParseUint(rArgs[0], 10, 31)
|
||||
if err != nil || b < 1 {
|
||||
return nil, c.Errf("rate-limit burst must be positive integer, got %q", rArgs[0])
|
||||
}
|
||||
pSec, err := strconv.ParseUint(rArgs[1], 10, 31)
|
||||
if err != nil || pSec < 1 {
|
||||
return nil, c.Errf("rate-limit period must be positive integer seconds, got %q", rArgs[1])
|
||||
}
|
||||
rateBurst = int(b)
|
||||
ratePeriod = time.Duration(pSec) * time.Second
|
||||
default:
|
||||
return nil, c.Errf("rate-limit takes 'off' OR '<burst> <period-seconds>', got %d args", len(rArgs))
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, c.Errf("unknown directive: %s", c.Val())
|
||||
}
|
||||
|
|
@ -248,6 +280,11 @@ func parse(c *caddy.Controller) (*RFC2136, error) {
|
|||
return nil, c.Err("zones-dir is required")
|
||||
}
|
||||
|
||||
// Construct rate limiter if enabled.
|
||||
if rateLimitEnabled {
|
||||
p.rateLimit = newRateLimiter(rateBurst, ratePeriod)
|
||||
}
|
||||
|
||||
// Build zoneFile handles for each declared zone.
|
||||
p.zones = make(map[string]*zoneFile, len(p.Zones))
|
||||
for _, z := range p.Zones {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue