WFE: Two changes to NewRegistration key-value rate limits (#7258)

Make NewRegistration more consistent with the implementation in NewOrder
(#7201):
- Construct transactions just once,
- use batched spending instead of multiple spend calls, and
- do not attempt a refund for requests that fail due to RateLimit
errors.

Part of #5545
This commit is contained in:
Samantha 2024-01-23 12:09:20 -05:00 committed by GitHub
parent ce5632b480
commit 21044c5236
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 48 additions and 62 deletions

View File

@ -619,55 +619,55 @@ func link(url, relation string) string {
return fmt.Sprintf("<%s>;rel=\"%s\"", url, relation)
}
// checkNewAccountLimits checks whether sufficient limit quota exists for the
// creation of a new account from the given IP address. If so, that quota is
// spent. If an error is encountered during the check, it is logged but not
// returned.
//
// TODO(#5545): For now we're simply exercising the new rate limiter codepath.
// This should eventually return a berrors.RateLimit error containing the retry
// after duration among other information available in the ratelimits.Decision.
func (wfe *WebFrontEndImpl) checkNewAccountLimits(ctx context.Context, ip net.IP) {
func (wfe *WebFrontEndImpl) newNewAccountLimitTransactions(ip net.IP) []ratelimits.Transaction {
if wfe.limiter == nil && wfe.txnBuilder == nil {
// Limiter is disabled.
return
return nil
}
warn := func(err error, limit ratelimits.Name) {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return
}
// TODO(#5545): Once key-value rate limits are authoritative this log
// line should be removed in favor of returning the error.
wfe.log.Warningf("checking %s rate limit: %s", limit, err)
}
var transactions []ratelimits.Transaction
txn, err := wfe.txnBuilder.RegistrationsPerIPAddressTransaction(ip)
if err != nil {
warn(err, ratelimits.NewRegistrationsPerIPAddress)
return
return nil
}
transactions = append(transactions, txn)
decision, err := wfe.limiter.Spend(ctx, txn)
if err != nil {
warn(err, ratelimits.NewRegistrationsPerIPAddress)
return
}
if !decision.Allowed || ip.To4() != nil {
// This requester is being limited or the request was made from an IPv4
// address.
return
if ip.To4() != nil {
// This request was made from an IPv4 address.
return transactions
}
txn, err = wfe.txnBuilder.RegistrationsPerIPv6RangeTransaction(ip)
if err != nil {
warn(err, ratelimits.NewRegistrationsPerIPv6Range)
return nil
}
return append(transactions, txn)
}
// checkNewAccountLimits checks whether sufficient limit quota exists for the
// creation of a new account. If so, that quota is spent. If an error is
// encountered during the check, it is logged but not returned.
//
// TODO(#5545): For now we're simply exercising the new rate limiter codepath.
// This should eventually return a berrors.RateLimit error containing the retry
// after duration among other information available in the ratelimits.Decision.
func (wfe *WebFrontEndImpl) checkNewAccountLimits(ctx context.Context, transactions []ratelimits.Transaction) {
if wfe.limiter == nil && wfe.txnBuilder == nil {
// Limiter is disabled.
return
}
_, err = wfe.limiter.Spend(ctx, txn)
_, err := wfe.limiter.BatchSpend(ctx, transactions)
if err != nil {
warn(err, ratelimits.NewRegistrationsPerIPv6Range)
wfe.log.Errf("checking newAccount limits: %s", err)
}
}
@ -675,46 +675,15 @@ func (wfe *WebFrontEndImpl) checkNewAccountLimits(ctx context.Context, ip net.IP
// It refunds the limit quota consumed by the request, allowing the caller to
// retry immediately. If an error is encountered during the refund, it is logged
// but not returned.
func (wfe *WebFrontEndImpl) refundNewAccountLimits(ctx context.Context, ip net.IP) {
func (wfe *WebFrontEndImpl) refundNewAccountLimits(ctx context.Context, transactions []ratelimits.Transaction) {
if wfe.limiter == nil && wfe.txnBuilder == nil {
// Limiter is disabled.
return
}
warn := func(err error, limit ratelimits.Name) {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return
}
// TODO(#5545): Once key-value rate limits are authoritative this log
// line should be removed in favor of returning the error.
wfe.log.Warningf("refunding %s rate limit: %s", limit, err)
}
txn, err := wfe.txnBuilder.RegistrationsPerIPAddressTransaction(ip)
_, err := wfe.limiter.BatchRefund(ctx, transactions)
if err != nil {
warn(err, ratelimits.NewRegistrationsPerIPAddress)
return
}
_, err = wfe.limiter.Refund(ctx, txn)
if err != nil {
warn(err, ratelimits.NewRegistrationsPerIPAddress)
return
}
if ip.To4() != nil {
// Request was made from an IPv4 address.
return
}
txn, err = wfe.txnBuilder.RegistrationsPerIPv6RangeTransaction(ip)
if err != nil {
warn(err, ratelimits.NewRegistrationsPerIPv6Range)
return
}
_, err = wfe.limiter.Refund(ctx, txn)
if err != nil {
warn(err, ratelimits.NewRegistrationsPerIPv6Range)
wfe.log.Errf("refunding newAccount limits: %s", err)
}
}
@ -842,17 +811,34 @@ func (wfe *WebFrontEndImpl) NewAccount(
// TODO(#5545): Spending and Refunding can be async until these rate limits
// are authoritative. This saves us from adding latency to each request.
go wfe.checkNewAccountLimits(ctx, ip)
// Goroutines spun out below will respect a context deadline set by the
// ratelimits package and cannot be prematurely canceled by the requester.
txns := wfe.newNewAccountLimitTransactions(ip)
go wfe.checkNewAccountLimits(ctx, txns)
var newRegistrationSuccessful bool
var errIsRateLimit bool
defer func() {
if !newRegistrationSuccessful {
go wfe.refundNewAccountLimits(ctx, ip)
if !newRegistrationSuccessful && !errIsRateLimit {
// This can be a little racy, but we're not going to worry about it
// for now. If the check hasn't completed yet, we can pretty safely
// assume that the refund will be similarly delayed.
go wfe.refundNewAccountLimits(ctx, txns)
}
}()
// Send the registration to the RA via grpc
acctPB, err := wfe.ra.NewRegistration(ctx, &reg)
if err != nil {
if errors.Is(err, berrors.RateLimit) {
// Request was denied by a legacy rate limit. In this error case we
// do not want to refund the quota consumed by the request because
// repeated requests would result in unearned refunds.
//
// TODO(#5545): Once key-value rate limits are authoritative this
// can be removed.
errIsRateLimit = true
}
if errors.Is(err, berrors.Duplicate) {
existingAcct, err := wfe.sa.GetRegistrationByKey(ctx, &sapb.JSONWebKey{Jwk: keyBytes})
if err == nil {