boulder/ratelimits/names.go

284 lines
9.3 KiB
Go

package ratelimits
import (
"fmt"
"net"
"net/netip"
"strconv"
"strings"
"github.com/letsencrypt/boulder/identifier"
"github.com/letsencrypt/boulder/policy"
)
// Name is an enumeration of all rate limit names. It is used to intern rate
// limit names as strings and to provide a type-safe way to refer to rate
// limits.
//
// IMPORTANT: If you add or remove a limit Name, you MUST update:
// - the string representation of the Name in nameToString,
// - the validators for that name in validateIdForName(),
// - the transaction constructors for that name in bucket.go, and
// - the Subscriber facing error message in ErrForDecision().
type Name int
const (
// Unknown is the zero value of Name and is used to indicate an unknown
// limit name.
Unknown Name = iota
// NewRegistrationsPerIPAddress uses bucket key 'enum:ipAddress'.
NewRegistrationsPerIPAddress
// NewRegistrationsPerIPv6Range uses bucket key 'enum:ipv6rangeCIDR'. The
// address range must be a /48. RFC 3177, which was published in 2001,
// advised operators to allocate a /48 block of IPv6 addresses for most end
// sites. RFC 6177, which was published in 2011 and obsoletes RFC 3177,
// advises allocating a smaller /56 block. We've chosen to use the larger
// /48 block for our IPv6 rate limiting. See:
// 1. https://tools.ietf.org/html/rfc3177#section-3
// 2. https://datatracker.ietf.org/doc/html/rfc6177#section-2
NewRegistrationsPerIPv6Range
// NewOrdersPerAccount uses bucket key 'enum:regId'.
NewOrdersPerAccount
// FailedAuthorizationsPerDomainPerAccount uses two different bucket keys
// depending on the context:
// - When referenced in an overrides file: uses bucket key 'enum:regId',
// where regId is the ACME registration Id of the account.
// - When referenced in a transaction: uses bucket key 'enum:regId:domain',
// where regId is the ACME registration Id of the account and domain is a
// domain name in the certificate.
FailedAuthorizationsPerDomainPerAccount
// CertificatesPerDomain uses bucket key 'enum:domain', where domain is a
// domain name in the certificate.
CertificatesPerDomain
// CertificatesPerDomainPerAccount is only used for per-account overrides to
// the CertificatesPerDomain rate limit. If this limit is referenced in the
// default limits file, it will be ignored. It uses two different bucket
// keys depending on the context:
// - When referenced in an overrides file: uses bucket key 'enum:regId',
// where regId is the ACME registration Id of the account.
// - When referenced in a transaction: uses bucket key 'enum:regId:domain',
// where regId is the ACME registration Id of the account and domain is a
// domain name in the certificate.
//
// When overrides to the CertificatesPerDomainPerAccount are configured for a
// subscriber, the cost:
// - MUST be consumed from each CertificatesPerDomainPerAccount bucket and
// - SHOULD be consumed from each CertificatesPerDomain bucket, if possible.
CertificatesPerDomainPerAccount
// CertificatesPerFQDNSet uses bucket key 'enum:fqdnSet', where fqdnSet is a
// hashed set of unique eTLD+1 domain names in the certificate.
//
// Note: When this is referenced in an overrides file, the fqdnSet MUST be
// passed as a comma-separated list of domain names.
CertificatesPerFQDNSet
// FailedAuthorizationsForPausingPerDomainPerAccount is similar to
// FailedAuthorizationsPerDomainPerAccount in that it uses two different
// bucket keys depending on the context:
// - When referenced in an overrides file: uses bucket key 'enum:regId',
// where regId is the ACME registration Id of the account.
// - When referenced in a transaction: uses bucket key 'enum:regId:domain',
// where regId is the ACME registration Id of the account and domain is a
// domain name in the certificate.
FailedAuthorizationsForPausingPerDomainPerAccount
)
// nameToString is a map of Name values to string names.
var nameToString = map[Name]string{
Unknown: "Unknown",
NewRegistrationsPerIPAddress: "NewRegistrationsPerIPAddress",
NewRegistrationsPerIPv6Range: "NewRegistrationsPerIPv6Range",
NewOrdersPerAccount: "NewOrdersPerAccount",
FailedAuthorizationsPerDomainPerAccount: "FailedAuthorizationsPerDomainPerAccount",
CertificatesPerDomain: "CertificatesPerDomain",
CertificatesPerDomainPerAccount: "CertificatesPerDomainPerAccount",
CertificatesPerFQDNSet: "CertificatesPerFQDNSet",
FailedAuthorizationsForPausingPerDomainPerAccount: "FailedAuthorizationsForPausingPerDomainPerAccount",
}
// isValid returns true if the Name is a valid rate limit name.
func (n Name) isValid() bool {
return n > Unknown && n < Name(len(nameToString))
}
// String returns the string representation of the Name. It allows Name to
// satisfy the fmt.Stringer interface.
func (n Name) String() string {
if !n.isValid() {
return nameToString[Unknown]
}
return nameToString[n]
}
// EnumString returns the string representation of the Name enumeration.
func (n Name) EnumString() string {
if !n.isValid() {
return nameToString[Unknown]
}
return strconv.Itoa(int(n))
}
// validIPAddress validates that the provided string is a valid IP address.
func validIPAddress(id string) error {
_, err := netip.ParseAddr(id)
if err != nil {
return fmt.Errorf("invalid IP address, %q must be an IP address", id)
}
return nil
}
// validIPv6RangeCIDR validates that the provided string is formatted is an IPv6
// CIDR range with a /48 mask.
func validIPv6RangeCIDR(id string) error {
_, ipNet, err := net.ParseCIDR(id)
if err != nil {
return fmt.Errorf(
"invalid CIDR, %q must be an IPv6 CIDR range", id)
}
ones, _ := ipNet.Mask.Size()
if ones != 48 {
// This also catches the case where the range is an IPv4 CIDR, since an
// IPv4 CIDR can't have a /48 subnet mask - the maximum is /32.
return fmt.Errorf(
"invalid CIDR, %q must be /48", id)
}
return nil
}
// validateRegId validates that the provided string is a valid ACME regId.
func validateRegId(id string) error {
_, err := strconv.ParseUint(id, 10, 64)
if err != nil {
return fmt.Errorf("invalid regId, %q must be an ACME registration Id", id)
}
return nil
}
// validateDomain validates that the provided string is formatted 'domain',
// where domain is a domain name.
func validateDomain(id string) error {
err := policy.ValidDomain(id)
if err != nil {
return fmt.Errorf("invalid domain, %q must be formatted 'domain': %w", id, err)
}
return nil
}
// validateRegIdDomain validates that the provided string is formatted
// 'regId:domain', where regId is an ACME registration Id and domain is a domain
// name.
func validateRegIdDomain(id string) error {
regIdDomain := strings.Split(id, ":")
if len(regIdDomain) != 2 {
return fmt.Errorf(
"invalid regId:domain, %q must be formatted 'regId:domain'", id)
}
err := validateRegId(regIdDomain[0])
if err != nil {
return fmt.Errorf(
"invalid regId, %q must be formatted 'regId:domain'", id)
}
err = policy.ValidDomain(regIdDomain[1])
if err != nil {
return fmt.Errorf(
"invalid domain, %q must be formatted 'regId:domain': %w", id, err)
}
return nil
}
// validateFQDNSet validates that the provided string is formatted 'fqdnSet',
// where fqdnSet is a comma-separated list of domain names.
//
// TODO(#7311): Support non-DNS identifiers.
func validateFQDNSet(id string) error {
domains := strings.Split(id, ",")
if len(domains) == 0 {
return fmt.Errorf(
"invalid fqdnSet, %q must be formatted 'fqdnSet'", id)
}
return policy.WellFormedIdentifiers(identifier.NewDNSSlice(domains))
}
func validateIdForName(name Name, id string) error {
switch name {
case NewRegistrationsPerIPAddress:
// 'enum:ipaddress'
return validIPAddress(id)
case NewRegistrationsPerIPv6Range:
// 'enum:ipv6rangeCIDR'
return validIPv6RangeCIDR(id)
case NewOrdersPerAccount:
// 'enum:regId'
return validateRegId(id)
case FailedAuthorizationsPerDomainPerAccount:
if strings.Contains(id, ":") {
// 'enum:regId:domain' for transaction
return validateRegIdDomain(id)
} else {
// 'enum:regId' for overrides
return validateRegId(id)
}
case CertificatesPerDomainPerAccount:
if strings.Contains(id, ":") {
// 'enum:regId:domain' for transaction
return validateRegIdDomain(id)
} else {
// 'enum:regId' for overrides
return validateRegId(id)
}
case CertificatesPerDomain:
// 'enum:domain'
return validateDomain(id)
case CertificatesPerFQDNSet:
// 'enum:fqdnSet'
return validateFQDNSet(id)
case FailedAuthorizationsForPausingPerDomainPerAccount:
if strings.Contains(id, ":") {
// 'enum:regId:domain' for transaction
return validateRegIdDomain(id)
} else {
// 'enum:regId' for overrides
return validateRegId(id)
}
case Unknown:
fallthrough
default:
// This should never happen.
return fmt.Errorf("unknown limit enum %q", name)
}
}
// stringToName is a map of string names to Name values.
var stringToName = func() map[string]Name {
m := make(map[string]Name, len(nameToString))
for k, v := range nameToString {
m[v] = k
}
return m
}()
// limitNames is a slice of all rate limit names.
var limitNames = func() []string {
names := make([]string, 0, len(nameToString))
for _, v := range nameToString {
names = append(names, v)
}
return names
}()