package server import ( "errors" "net" "strings" "sync" "time" "golang.org/x/crypto/acme" ) // cooldown pauses new ACME orders after repeated CA-side failures. Five // consecutive failures trigger a one-minute pause, doubling up to an hour; a // success clears the state. type cooldown struct { mu sync.Mutex failures int until time.Time now func() time.Time } func newCooldown() cooldown { return cooldown{now: time.Now} } func (c *cooldown) remaining() time.Duration { c.mu.Lock() defer c.mu.Unlock() if c.until.IsZero() { return 0 } now := c.now() if !now.Before(c.until) { return 0 } return c.until.Sub(now) } func (c *cooldown) success() { c.mu.Lock() c.failures = 0 c.until = time.Time{} c.mu.Unlock() } // failure records a CA-side failure and returns the new pause, if one began. func (c *cooldown) failure() time.Duration { c.mu.Lock() defer c.mu.Unlock() c.failures++ if c.failures < 5 { return 0 } exponent := c.failures - 5 if exponent > 6 { exponent = 6 // cap at one hour before shifting } delay := time.Minute << exponent if delay > time.Hour { delay = time.Hour } c.until = c.now().Add(delay) return delay } // caFailure distinguishes failures that point at the CA (network, 5xx, rate // limits) from local DNS/CSR problems and validation errors, which should not // pause issuance. func caFailure(err error) bool { if err == nil { return false } var hostErr errCSRHostname if errors.As(err, &hostErr) { return false } text := err.Error() if strings.Contains(text, "dns:") || strings.Contains(text, "not a PEM") || strings.Contains(text, "CSR") { return false } if strings.Contains(text, "authorization") || strings.Contains(text, "dns-01") { return false } var netErr net.Error if errors.As(err, &netErr) { return true } var acmeErr *acme.Error if errors.As(err, &acmeErr) { return acmeErr.StatusCode == 429 || acmeErr.StatusCode >= 500 } return true }