Skip to content

Commit

Permalink
ratelimits: Implement batched Spends and Refunds (#7143)
Browse files Browse the repository at this point in the history
- Move default and override limits, and associated methods, out of the
Limiter to new limitRegistry struct, embedded in a new public
TransactionBuilder.
- Export Transaction and add corresponding Transaction constructor
methods for each limit Name, making Limiter and TransactionBuilder the
API for interacting with the ratelimits package.
- Implement batched Spends and Refunds on the Limiter, the new methods
accept a slice of Transactions.
- Add new boolean fields check and spend to Transaction to support more
complicated cases that can arise in batches:
1. the InvalidAuthorizations limit is checked at New Order time in a
batch with many other limits, but should only be spent when an
Authorization is first considered invalid.
2. the CertificatesPerDomain limit is overridden by
CertficatesPerDomainPerAccount, when this is the case, spends of the
CertificatesPerDomain limit should be "best-effort" but NOT deny the
request if capacity is lacking.
- Modify the existing Spend/Refund methods to support
Transaction.check/spend and 0 cost Transactions.
- Make bucketId private and add a constructor for each bucket key format
supported by ratelimits.
- Move domainsForRateLimiting() from the ra.go to ratelimits. This
avoids a circular import issue in ra.go.

Part of #5545
  • Loading branch information
beautifulentropy authored Dec 7, 2023
1 parent 3366be5 commit eb49d44
Show file tree
Hide file tree
Showing 18 changed files with 1,031 additions and 394 deletions.
6 changes: 5 additions & 1 deletion cmd/boulder-wfe2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,15 +340,18 @@ func main() {
pendingAuthorizationLifetime := time.Duration(c.WFE.PendingAuthorizationLifetimeDays) * 24 * time.Hour

var limiter *ratelimits.Limiter
var txnBuilder *ratelimits.TransactionBuilder
var limiterRedis *bredis.Ring
if c.WFE.Limiter.Defaults != "" {
// Setup rate limiting.
limiterRedis, err = bredis.NewRingFromConfig(*c.WFE.Limiter.Redis, stats, logger)
cmd.FailOnError(err, "Failed to create Redis ring")

source := ratelimits.NewRedisSource(limiterRedis.Ring, clk, stats)
limiter, err = ratelimits.NewLimiter(clk, source, c.WFE.Limiter.Defaults, c.WFE.Limiter.Overrides, stats)
limiter, err = ratelimits.NewLimiter(clk, source, stats)
cmd.FailOnError(err, "Failed to create rate limiter")
txnBuilder, err = ratelimits.NewTransactionBuilder(c.WFE.Limiter.Defaults, c.WFE.Limiter.Overrides)
cmd.FailOnError(err, "Failed to create rate limits transaction builder")
}

var accountGetter wfe2.AccountGetter
Expand Down Expand Up @@ -380,6 +383,7 @@ func main() {
npKey,
accountGetter,
limiter,
txnBuilder,
)
cmd.FailOnError(err, "Unable to create WFE")

Expand Down
23 changes: 1 addition & 22 deletions ra/ra.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (

"github.com/jmhodges/clock"
"github.com/prometheus/client_golang/prometheus"
"github.com/weppos/publicsuffix-go/publicsuffix"
"golang.org/x/crypto/ocsp"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
Expand Down Expand Up @@ -1369,26 +1368,6 @@ func (ra *RegistrationAuthorityImpl) getSCTs(ctx context.Context, cert []byte, e
return scts, nil
}

// domainsForRateLimiting transforms a list of FQDNs into a list of eTLD+1's
// for the purpose of rate limiting. It also de-duplicates the output
// domains. Exact public suffix matches are included.
func domainsForRateLimiting(names []string) []string {
var domains []string
for _, name := range names {
domain, err := publicsuffix.Domain(name)
if err != nil {
// The only possible errors are:
// (1) publicsuffix.Domain is giving garbage values
// (2) the public suffix is the domain itself
// We assume 2 and include the original name in the result.
domains = append(domains, name)
} else {
domains = append(domains, domain)
}
}
return core.UniqueLowerNames(domains)
}

// enforceNameCounts uses the provided count RPC to find a count of certificates
// for each of the names. If the count for any of the names exceeds the limit
// for the given registration then the names out of policy are returned to be
Expand Down Expand Up @@ -1457,7 +1436,7 @@ func (ra *RegistrationAuthorityImpl) checkCertificatesPerNameLimit(ctx context.C
return nil
}

tldNames := domainsForRateLimiting(names)
tldNames := ratelimits.DomainsForRateLimiting(names)
namesOutOfLimit, earliest, err := ra.enforceNameCounts(ctx, tldNames, limit, regID)
if err != nil {
return fmt.Errorf("checking certificates per name limit for %q: %s",
Expand Down
20 changes: 0 additions & 20 deletions ra/ra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1109,26 +1109,6 @@ func TestAuthzFailedRateLimitingNewOrder(t *testing.T) {
testcase()
}

func TestDomainsForRateLimiting(t *testing.T) {
domains := domainsForRateLimiting([]string{})
test.AssertEquals(t, len(domains), 0)

domains = domainsForRateLimiting([]string{"www.example.com", "example.com"})
test.AssertDeepEquals(t, domains, []string{"example.com"})

domains = domainsForRateLimiting([]string{"www.example.com", "example.com", "www.example.co.uk"})
test.AssertDeepEquals(t, domains, []string{"example.co.uk", "example.com"})

domains = domainsForRateLimiting([]string{"www.example.com", "example.com", "www.example.co.uk", "co.uk"})
test.AssertDeepEquals(t, domains, []string{"co.uk", "example.co.uk", "example.com"})

domains = domainsForRateLimiting([]string{"foo.bar.baz.www.example.com", "baz.example.com"})
test.AssertDeepEquals(t, domains, []string{"example.com"})

domains = domainsForRateLimiting([]string{"github.io", "foo.github.io", "bar.github.io"})
test.AssertDeepEquals(t, domains, []string{"bar.github.io", "foo.github.io", "github.io"})
}

type mockSAWithNameCounts struct {
mocks.StorageAuthority
nameCounts *sapb.CountByNames
Expand Down
Loading

0 comments on commit eb49d44

Please sign in to comment.