derp/derpserver: support global rate limiting independent of per-client

This commit enables the operator to set a global rate limit without any
per-client.

Updates tailscale/corp#40962

Signed-off-by: Jordan Whited <jordan@tailscale.com>
This commit is contained in:
Jordan Whited 2026-04-29 10:16:31 -07:00 committed by Jordan Whited
parent 15cba0a3f6
commit 0e9f9e2bd8

View File

@ -519,7 +519,8 @@ const minRateLimitTokenBucketSize = derp.MaxPacketSize + derp.KeyLen
// in bytes.
type RateConfig struct {
// PerClientRateLimitBytesPerSec represents the per-client
// rate limit in bytes per second. A zero value disables rate limiting.
// rate limit in bytes per second. A zero value disables per-client rate limiting,
// but global (GlobalRate...) configuration may still apply.
PerClientRateLimitBytesPerSec uint64 `json:",omitzero"`
// PerClientRateBurstBytes represents the per-client token bucket depth,
// or burst, in bytes. Any value lower than [minRateLimitTokenBucketSize]
@ -528,15 +529,14 @@ type RateConfig struct {
PerClientRateBurstBytes uint64 `json:",omitzero"`
// GlobalRateLimitBytesPerSec represents the global rate limit in bytes per
// second. A zero value disables global rate limiting, but per-client (PerClient...)
// configuration may still apply. Only relevant if PerClientRateLimitBytesPerSec
// is nonzero. If GlobalRateLimitBytesPerSec is nonzero and less than
// configuration may still apply. If GlobalRateLimitBytesPerSec is nonzero and less than
// PerClientRateLimitBytesPerSec, then GlobalRateLimitBytesPerSec will be set
// equal to PerClientRateLimitBytesPerSec before application.
GlobalRateLimitBytesPerSec uint64 `json:",omitzero"`
// GlobalRateBurstBytes represents the global token bucket depth, or burst,
// in bytes. Any value lower than [minRateLimitTokenBucketSize] will be increased to
// [minRateLimitTokenBucketSize] before application. Only relevant if
// PerClientRateLimitBytesPerSec and GlobalRateLimitBytesPerSec are nonzero.
// GlobalRateLimitBytesPerSec is nonzero.
GlobalRateBurstBytes uint64 `json:",omitzero"`
}
@ -576,8 +576,8 @@ func (s *Server) LoadAndApplyRateConfig(path string) error {
func (s *Server) UpdateRateLimits(rc RateConfig) (applied RateConfig) {
s.mu.Lock()
defer s.mu.Unlock()
if rc.PerClientRateLimitBytesPerSec == 0 {
// if per-client is disabled, all rate limiting is disabled
if rc.PerClientRateLimitBytesPerSec == 0 && rc.GlobalRateLimitBytesPerSec == 0 {
// if per-client and global are disabled, all rate limiting is disabled
rc = RateConfig{}
}
if rc.PerClientRateLimitBytesPerSec != 0 {
@ -1336,14 +1336,17 @@ func (c *sclient) handleFrameSendPacket(_ derp.FrameType, fl uint32) error {
return c.sendPkt(dst, p)
}
// setRateLimit updates the receive rate limiter. When bytesPerSec is 0 or the
// setRateLimit updates the receive rate limiter. When bytesPerSec is 0 and parent is nil, or the
// client is a mesh peer, the limiter is set to nil so that [sclient.rateLimit] is a no-op.
func (c *sclient) setRateLimit(bytesPerSec, burst uint64, parent *xrate.Limiter) {
if bytesPerSec == 0 || c.canMesh {
if c.canMesh || (bytesPerSec == 0 && parent == nil) {
c.recvLim.Store(nil)
return
}
child := xrate.NewLimiter(xrate.Limit(bytesPerSec), int(burst))
var child *xrate.Limiter
if bytesPerSec != 0 {
child = xrate.NewLimiter(xrate.Limit(bytesPerSec), int(burst))
}
lim := &parentChildTokenBuckets{
parent: parent,
child: child,
@ -1402,12 +1405,18 @@ func (c *sclient) rateLimit(n int) error {
// 3. would cause the connection to close shortly after rate limiting, anyway.
clampedN := min(n, minRateLimitTokenBucketSize)
now := c.s.clock.Now()
durationWaited, err := rateLimitWait(c.ctx, lim.child, clampedN, now, newTimer)
if err != nil {
return err
}
if durationWaited > 0 {
c.s.rateLimitPerClientWaited.Add(1)
var (
durationWaited time.Duration
err error
)
if lim.child != nil {
durationWaited, err = rateLimitWait(c.ctx, lim.child, clampedN, now, newTimer)
if err != nil {
return err
}
if durationWaited > 0 {
c.s.rateLimitPerClientWaited.Add(1)
}
}
if lim.parent != nil {
if durationWaited > 0 {
@ -1872,7 +1881,7 @@ type sclient struct {
// which is already optimized to use [mono.Time].
type parentChildTokenBuckets struct {
parent *xrate.Limiter // parent may be nil
child *xrate.Limiter // child is always non-nil
child *xrate.Limiter // child may be nil
}
func (c *sclient) presentFlags() derp.PeerPresentFlags {