352 lines
12 KiB
Go
352 lines
12 KiB
Go
package ratelimits
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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:identValue', where regId is the ACME registration Id of
|
|
// the account and identValue is the value of an identifier in the
|
|
// certificate.
|
|
FailedAuthorizationsPerDomainPerAccount
|
|
|
|
// CertificatesPerDomain uses bucket key 'enum:domainOrCIDR', where
|
|
// domainOrCIDR is a domain name or IP address in the certificate. It uses
|
|
// two different IP address formats depending on the context:
|
|
// - When referenced in an overrides file: uses a single IP address.
|
|
// - When referenced in a transaction: uses an IP address prefix in CIDR
|
|
// notation. IPv4 prefixes must be /32, and IPv6 prefixes must be /64.
|
|
// In both cases, IPv6 addresses must be the lowest address in their /64;
|
|
// i.e. their last 64 bits must be zero.
|
|
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:domainOrCIDR', where regId is the ACME registration Id of
|
|
// the account and domainOrCIDR is either a domain name in the
|
|
// certificate or an IP prefix in CIDR notation.
|
|
// - IP address formats vary by context, as for CertificatesPerDomain.
|
|
//
|
|
// 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 identifier values in the certificate.
|
|
//
|
|
// Note: When this is referenced in an overrides file, the fqdnSet MUST be
|
|
// passed as a comma-separated list of identifier values.
|
|
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:identValue', where regId is the ACME registration Id of
|
|
// the account and identValue is the value of an identifier 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 {
|
|
ip, err := netip.ParseAddr(id)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid IP address, %q must be an IP address", id)
|
|
}
|
|
canon := ip.String()
|
|
if canon != id {
|
|
return fmt.Errorf(
|
|
"invalid IP address, %q must be in canonical form (%q)", id, canon)
|
|
}
|
|
return policy.IsReservedIP(ip)
|
|
}
|
|
|
|
// validIPv6RangeCIDR validates that the provided string is formatted as an IPv6
|
|
// prefix in CIDR notation, with a /48 mask.
|
|
func validIPv6RangeCIDR(id string) error {
|
|
prefix, err := netip.ParsePrefix(id)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"invalid CIDR, %q must be an IPv6 CIDR range", id)
|
|
}
|
|
if prefix.Bits() != 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)
|
|
}
|
|
canon := prefix.Masked().String()
|
|
if canon != id {
|
|
return fmt.Errorf(
|
|
"invalid CIDR, %q must be in canonical form (%q)", id, canon)
|
|
}
|
|
return policy.IsReservedPrefix(prefix)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// validateRegIdIdentValue validates that the provided string is formatted
|
|
// 'regId:identValue', where regId is an ACME registration Id and identValue is
|
|
// a valid identifier value.
|
|
func validateRegIdIdentValue(id string) error {
|
|
regIdIdentValue := strings.Split(id, ":")
|
|
if len(regIdIdentValue) != 2 {
|
|
return fmt.Errorf(
|
|
"invalid regId:identValue, %q must be formatted 'regId:identValue'", id)
|
|
}
|
|
err := validateRegId(regIdIdentValue[0])
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"invalid regId, %q must be formatted 'regId:identValue'", id)
|
|
}
|
|
domainErr := policy.ValidDomain(regIdIdentValue[1])
|
|
if domainErr != nil {
|
|
ipErr := policy.ValidIP(regIdIdentValue[1])
|
|
if ipErr != nil {
|
|
return fmt.Errorf("invalid identValue, %q must be formatted 'regId:identValue': %w as domain, %w as IP", id, domainErr, ipErr)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateDomainOrCIDR validates that the provided string is either a domain
|
|
// name or an IP address. IPv6 addresses must be the lowest address in their
|
|
// /64, i.e. their last 64 bits must be zero.
|
|
func validateDomainOrCIDR(id string) error {
|
|
domainErr := policy.ValidDomain(id)
|
|
if domainErr == nil {
|
|
// This is a valid domain.
|
|
return nil
|
|
}
|
|
|
|
ip, ipErr := netip.ParseAddr(id)
|
|
if ipErr != nil {
|
|
return fmt.Errorf("%q is neither a domain (%w) nor an IP address (%w)", id, domainErr, ipErr)
|
|
}
|
|
|
|
if ip.String() != id {
|
|
return fmt.Errorf("invalid IP address %q, must be in canonical form (%q)", id, ip.String())
|
|
}
|
|
|
|
prefix, prefixErr := coveringPrefix(ip)
|
|
if prefixErr != nil {
|
|
return fmt.Errorf("invalid IP address %q, couldn't determine prefix: %w", id, prefixErr)
|
|
}
|
|
if prefix.Addr() != ip {
|
|
return fmt.Errorf("invalid IP address %q, must be the lowest address in its prefix (%q)", id, prefix.Addr().String())
|
|
}
|
|
|
|
return policy.IsReservedPrefix(prefix)
|
|
}
|
|
|
|
// validateRegIdDomainOrCIDR validates that the provided string is formatted
|
|
// 'regId:domainOrCIDR', where domainOrCIDR is either a domain name or an IP
|
|
// address. IPv6 addresses must be the lowest address in their /64, i.e. their
|
|
// last 64 bits must be zero.
|
|
func validateRegIdDomainOrCIDR(id string) error {
|
|
regIdDomainOrCIDR := strings.Split(id, ":")
|
|
if len(regIdDomainOrCIDR) != 2 {
|
|
return fmt.Errorf(
|
|
"invalid regId:domainOrCIDR, %q must be formatted 'regId:domainOrCIDR'", id)
|
|
}
|
|
err := validateRegId(regIdDomainOrCIDR[0])
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"invalid regId, %q must be formatted 'regId:domainOrCIDR'", id)
|
|
}
|
|
err = validateDomainOrCIDR(regIdDomainOrCIDR[1])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid domainOrCIDR, %q must be formatted 'regId:domainOrCIDR': %w", id, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateFQDNSet validates that the provided string is formatted 'fqdnSet',
|
|
// where fqdnSet is a comma-separated list of identifier values.
|
|
func validateFQDNSet(id string) error {
|
|
values := strings.Split(id, ",")
|
|
if len(values) == 0 {
|
|
return fmt.Errorf(
|
|
"invalid fqdnSet, %q must be formatted 'fqdnSet'", id)
|
|
}
|
|
for _, value := range values {
|
|
domainErr := policy.ValidDomain(value)
|
|
if domainErr != nil {
|
|
ipErr := policy.ValidIP(value)
|
|
if ipErr != nil {
|
|
return fmt.Errorf("invalid fqdnSet member %q: %w as domain, %w as IP", id, domainErr, ipErr)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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:identValue' for transaction
|
|
return validateRegIdIdentValue(id)
|
|
} else {
|
|
// 'enum:regId' for overrides
|
|
return validateRegId(id)
|
|
}
|
|
|
|
case CertificatesPerDomainPerAccount:
|
|
if strings.Contains(id, ":") {
|
|
// 'enum:regId:domainOrCIDR' for transaction
|
|
return validateRegIdDomainOrCIDR(id)
|
|
} else {
|
|
// 'enum:regId' for overrides
|
|
return validateRegId(id)
|
|
}
|
|
|
|
case CertificatesPerDomain:
|
|
// 'enum:domainOrCIDR'
|
|
return validateDomainOrCIDR(id)
|
|
|
|
case CertificatesPerFQDNSet:
|
|
// 'enum:fqdnSet'
|
|
return validateFQDNSet(id)
|
|
|
|
case FailedAuthorizationsForPausingPerDomainPerAccount:
|
|
if strings.Contains(id, ":") {
|
|
// 'enum:regId:identValue' for transaction
|
|
return validateRegIdIdentValue(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
|
|
}()
|