control/controlclient: handle 429 responses during node registration

If we get a 429 response during node registration, use the `Retry-After`
header for backoff instead of the regular exponential backoff.

The rate limiter error is propagated to the user, just like other
registration errors are, e.g.

```
$ tailscale up
backend error: node registration rate limited; will retry after 57s
exit status 1
```

Updates tailscale/corp#39533

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
This commit is contained in:
Anton Tolchanov
2026-04-14 13:27:06 +01:00
committed by Anton Tolchanov
parent d8190e0de5
commit 958bcda5bf
6 changed files with 282 additions and 2 deletions
@@ -80,6 +80,11 @@ type Server struct {
ExplicitBaseURL string // e.g. "http://127.0.0.1:1234" with no trailing URL
HTTPTestServer *httptest.Server // if non-nil, used to get BaseURL
// MaybeRateLimitRegister, if non-nil, is called before processing
// register requests. If it returns true, a 429 response is sent
// with the given Retry-After header value and body string.
MaybeRateLimitRegister func() (reject bool, retryAfter string, msg string)
// ModifyFirstMapResponse, if non-nil, is called exactly once per
// MapResponse stream to modify the first MapResponse sent in response to it.
ModifyFirstMapResponse func(*tailcfg.MapResponse, *tailcfg.MapRequest)
@@ -768,6 +773,16 @@ func (s *Server) CompleteDeviceApproval(controlUrl string, urlStr string, nodeKe
}
func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key.MachinePublic) {
if fn := s.MaybeRateLimitRegister; fn != nil {
if reject, retryAfter, msg := fn(); reject {
if retryAfter != "" {
w.Header().Set("Retry-After", retryAfter)
}
http.Error(w, msg, http.StatusTooManyRequests)
return
}
}
msg, err := io.ReadAll(io.LimitReader(r.Body, msgLimit))
r.Body.Close()
if err != nil {