223 lines
6.6 KiB
Go
223 lines
6.6 KiB
Go
package ratelimits
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"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 a new limit Name, you MUST add it to the 'nameToString'
|
|
// mapping and idValidForName function below.
|
|
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
|
|
|
|
// FailedAuthorizationsPerAccount uses bucket key 'enum:regId', where regId
|
|
// is the registration id of the account.
|
|
FailedAuthorizationsPerAccount
|
|
|
|
// CertificatesPerDomainPerAccount uses bucket key 'enum:regId:domain',
|
|
// where name is the a name in a certificate issued to the account matching
|
|
// regId.
|
|
CertificatesPerDomainPerAccount
|
|
|
|
// CertificatesPerFQDNSetPerAccount uses bucket key 'enum:regId:fqdnSet',
|
|
// where nameSet is a set of names in a certificate issued to the account
|
|
// matching regId.
|
|
CertificatesPerFQDNSetPerAccount
|
|
)
|
|
|
|
// 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]
|
|
}
|
|
|
|
// nameToString is a map of Name values to string names.
|
|
var nameToString = map[Name]string{
|
|
Unknown: "Unknown",
|
|
NewRegistrationsPerIPAddress: "NewRegistrationsPerIPAddress",
|
|
NewRegistrationsPerIPv6Range: "NewRegistrationsPerIPv6Range",
|
|
NewOrdersPerAccount: "NewOrdersPerAccount",
|
|
FailedAuthorizationsPerAccount: "FailedAuthorizationsPerAccount",
|
|
CertificatesPerDomainPerAccount: "CertificatesPerDomainPerAccount",
|
|
CertificatesPerFQDNSetPerAccount: "CertificatesPerFQDNSetPerAccount",
|
|
}
|
|
|
|
// validIPAddress validates that the provided string is a valid IP address.
|
|
func validIPAddress(id string) error {
|
|
ip := net.ParseIP(id)
|
|
if ip == 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
|
|
}
|
|
|
|
// validateRegIdDomain validates that the provided string is formatted
|
|
// 'regId:domain', where regId is an ACME registration Id and domain is a single
|
|
// domain name.
|
|
func validateRegIdDomain(id string) error {
|
|
parts := strings.SplitN(id, ":", 2)
|
|
if len(parts) != 2 {
|
|
return fmt.Errorf(
|
|
"invalid regId:domain, %q must be formatted 'regId:domain'", id)
|
|
}
|
|
if validateRegId(parts[0]) != nil {
|
|
return fmt.Errorf(
|
|
"invalid regId, %q must be formatted 'regId:domain'", id)
|
|
}
|
|
if policy.ValidDomain(parts[1]) != nil {
|
|
return fmt.Errorf(
|
|
"invalid domain, %q must be formatted 'regId:domain'", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateRegIdFQDNSet validates that the provided string is formatted
|
|
// 'regId:fqdnSet', where regId is an ACME registration Id and fqdnSet is a
|
|
// comma-separated list of domain names.
|
|
func validateRegIdFQDNSet(id string) error {
|
|
parts := strings.SplitN(id, ":", 2)
|
|
if len(parts) != 2 {
|
|
return fmt.Errorf(
|
|
"invalid regId:fqdnSet, %q must be formatted 'regId:fqdnSet'", id)
|
|
}
|
|
if validateRegId(parts[0]) != nil {
|
|
return fmt.Errorf(
|
|
"invalid regId, %q must be formatted 'regId:fqdnSet'", id)
|
|
}
|
|
domains := strings.Split(parts[1], ",")
|
|
if len(domains) == 0 {
|
|
return fmt.Errorf(
|
|
"invalid fqdnSet, %q must be formatted 'regId:fqdnSet'", id)
|
|
}
|
|
for _, domain := range domains {
|
|
if policy.ValidDomain(domain) != nil {
|
|
return fmt.Errorf(
|
|
"invalid domain, %q must be formatted 'regId:fqdnSet'", id)
|
|
}
|
|
}
|
|
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, FailedAuthorizationsPerAccount:
|
|
// 'enum:regId'
|
|
return validateRegId(id)
|
|
|
|
case CertificatesPerDomainPerAccount:
|
|
// 'enum:regId:domain'
|
|
return validateRegIdDomain(id)
|
|
|
|
case CertificatesPerFQDNSetPerAccount:
|
|
// 'enum:regId:fqdnSet'
|
|
return validateRegIdFQDNSet(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, len(nameToString))
|
|
for _, v := range nameToString {
|
|
names = append(names, v)
|
|
}
|
|
return names
|
|
}()
|
|
|
|
// nameToEnumString converts the integer value of the Name enumeration to its
|
|
// string representation.
|
|
func nameToEnumString(s Name) string {
|
|
return strconv.Itoa(int(s))
|
|
}
|
|
|
|
// bucketKey returns the key used to store a rate limit bucket.
|
|
func bucketKey(name Name, id string) string {
|
|
return nameToEnumString(name) + ":" + id
|
|
}
|