cmd/derper,derp: add --rate-config file with SIGHUP reload (#19314)

Add a --rate-config flag pointing to a JSON file for per-client receive
rate limits (bytes/sec and burst bytes). The config is reloaded on SIGHUP,
updating all existing client connections live. The --per-client-rate-limit
and --per-client-rate-burst flags are removed in favor of the config file.

In derpserver, rate limiting uses an atomic.Pointer[xrate.Limiter] per
client: nil when unlimited or mesh (zero overhead), non-nil when
rate-limited.

Document that clientSet.activeClient Store operations require Server.mu.

Updates tailscale/corp#38509

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
This commit is contained in:
Mike O'Driscoll
2026-04-10 18:37:54 -04:00
committed by GitHub
parent b4c0d67f8b
commit ca5db865b4
3 changed files with 414 additions and 54 deletions
+26 -7
View File
@@ -87,8 +87,7 @@ var (
acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection")
acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection")
perClientRateLimit = flag.Uint("per-client-rate-limit", 0, "per-client receive rate limit in bytes/sec; 0 means unlimited. Mesh peers are exempt.")
perClientRateBurst = flag.Uint("per-client-rate-burst", 0, "per-client receive rate burst in bytes; 0 defaults to 2x the rate limit (only relevant when using nonzero --per-client-rate-limit)")
rateConfigPath = flag.String("rate-config", "", "path to JSON rate limit config file; reloaded on SIGHUP")
// tcpKeepAlive is intentionally long, to reduce battery cost. There is an L7 keepalive on a higher frequency schedule.
tcpKeepAlive = flag.Duration("tcp-keepalive-time", 10*time.Minute, "TCP keepalive time")
@@ -195,12 +194,11 @@ func main() {
s.SetVerifyClientURL(*verifyClientURL)
s.SetVerifyClientURLFailOpen(*verifyFailOpen)
s.SetTCPWriteTimeout(*tcpWriteTimeout)
if *perClientRateLimit > 0 {
burst := *perClientRateBurst
if burst < 1 {
burst = *perClientRateLimit * 2
if *rateConfigPath != "" {
if err := s.LoadAndApplyRateConfig(*rateConfigPath); err != nil {
log.Fatalf("derper: loading rate config: %v", err)
}
s.SetPerClientRateLimit(*perClientRateLimit, burst)
go watchRateConfig(ctx, s, *rateConfigPath)
}
var meshKey string
@@ -436,6 +434,27 @@ func main() {
}
}
// watchRateConfig listens for SIGHUP signals and reloads the rate config
// file on each signal, applying it to the server. It returns when ctx is done.
func watchRateConfig(ctx context.Context, s *derpserver.Server, path string) {
sighup := make(chan os.Signal, 1)
signal.Notify(sighup, syscall.SIGHUP)
defer signal.Stop(sighup)
for {
select {
case <-ctx.Done():
return
case <-sighup:
log.Printf("derper: received SIGHUP, reloading rate config from %s", path)
if err := s.LoadAndApplyRateConfig(path); err != nil {
log.Printf("derper: rate config reload failed: %v", err)
continue
}
log.Printf("derper: rate config reloaded successfully")
}
}
}
var validProdHostname = regexp.MustCompile(`^derp([^.]*)\.tailscale\.com\.?$`)
func prodAutocertHostPolicy(_ context.Context, host string) error {