Documentation ¶
Overview ¶
Package rrl is a stand-alone implementation of “Response Rate Limiting” which helps protect authoritative DNS servers from being used as a vehicle for amplification attacks. In addition to “Response Rate Limiting”, rrl provides a configurable source address rate limiter.
The rrl package is designed to be very easy to use. It consists of a configuration mechanism and a single public function to check limits. That's it; that's the interface.
“Response Rate Limiting“ was original devised by ISC and this implementation is heavily derived from COREDNSRRL which mimics the ISC algorithms.
Usage ¶
The general pattern of use is to create a one-time RRL object with NewRRL using a deployment-specific Config, then call [Debit] prior to sending each response back to a client. [Debit] returns one of the following recommended actions: “Send”, “Drop” or “Slip”.
While the meaning of “Send” and “Drop” are self-evident, “Slip” is more complicated.
“Slip” is ISC terminology which means to respond with a BADCOOKIE response or a truncated response depending on whether the query included a valid client cookie or not. The goal of a “Slip” response is to give genuine clients a small chance of getting a response even when their source addresses are in a range being used as part of an amplification attack.
The “Slip” response is one of a number of differences between a regular rate-limiting system and the DNS-specific rrl.
Note that requests with valid server cookies are never rate-limited so a BADCOOKIE response is always valid in the presence of a client cookie.
Sample Code ¶
The follow example demonstrates the expected pattern of use. It introduces terms such as “Response Tuple”, “account” and “debit” which are explained in subsequent sections. For now, the logic flow is of most relevance.
package main import "github.com/markdingo/rrl" func main() { server:= dnsListenSocket() db := myDatabase() cfg := rrl.NewConfig() cfg.SetValue(...) // Configure limits relevant to our deployment R := NewRRL(cfg) // Create our `rrl` instance for { srcIP, request := server.GetRequest() // Accept a query response := db.lookupResponse(request) // Create the response action := rrl.Send // Default to sending response as-is if !request.validServerCook() { // Only rate-limit if src can be spoofed tuple := makeTuple(response) // Formulate the "Response Tuple"... action, _, _ := R.Debit(srcIP, tuple) // ... and debit the corresponding accounts } switch action { // Dispatch on the recommended action case rrl.Drop: // Drop is easy, do nothing case rrl.Send: // No rate limit applies, ship it! server.Send(response) case rrl.Slip: if request.ValidClientCookie() { // Slip response varies depending on server.SendBadCookie(response) // whether the client sent a cooke or not } else { response.makeTruncatedIfAble() // No valid client cookie means server.Send(response) // send a truncated response } } } }
Note that some error responses such as REFUSED and SERVFAIL cannot be replaced with truncated responses thus the “makeTruncatedIfAble” function needs some intelligence.
Concurrency ¶
The rrl package is safe for concurrent use by multiple goroutines. Normally a single RRL object is shared amongst all goroutines across the application. However, if an application does require multiple RRL instances, they all operate completely independently of each other.
Background ¶
While rate limiting is a common strategy used to limit abusive traffic, “Response Rate Limiting” is specifically designed for UDP DNS queries (which lack a valid server cookie) received by authoritative DNS servers. The original RRL design was promulgated by ISC who have published extensive articles on the subject. A good place to start is their ISCINTRO document.
Description ¶
RRL tracks the query-per-second rate in a unique “account” assigned to each “Response Tuple“ destined for a particular Client Network.
Each “account” is credited once per second by the configured amount and debited once for each [Debit] call. At most an “account” can gain up to one second of credits or up a configurable 15 seconds of debits. While the “account” is in credit, a call to [Debit] returns a “Send” action. If the “account” is not in credit, then a “Drop” or “Slip” action is returned depending on the configured slip ratio.
## Response Tuple
A “Response Tuple” is an “account” key formulated from various features of the response message depending on the nature of the response (NXDomain, Error, referral, etc).
The intent is for responses indicative of potential abuse to be assigned to a small set of tuples whereas responses indicative of genuine requests are assigned to a large set of tuples. The goal being to cause “accounts” of suspect queries to run out of credits far sooner than the “accounts” of genuine queries.
The formulation of a “Response Tuple” is somewhat convoluted. Suffice to say that it varies considerably depending on the nature of the response - a unique feature or rrl. The ResponseTuple struct describes this formulation in detail.
## Client Network
The Client Network forms the other “account” key. It is the source address of the request masked by the configured network size. The default sizes being 24 for ipv4 and 56 for ipv6.
Genesis ¶
This package is derived from COREDNSRRL with the main differences being that all coredns dependencies and external interfaces have been removed so that this package can be used by standalone DNS implementations unrelated to coredns.
It goes without saying that this package only exists because of the efforts of the coredns/rrl authors. A big “thank you” to them.
The plan is for this project to mirror fixes and improvements to COREDNSRRL where possible.
References ¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Action ¶
type Action int
Action is the resulting recommendation returned by [Debit]. Callers should act accordingly.
Values are: Send, Drop and Slip (aka send truncated if able or BADCOOKIE response)
type AllowanceCategory ¶
type AllowanceCategory uint8
An AllowanceCategory is the distillation of the rcode and response message the caller plans to send in response to a DNS query. Each category is associated with a separately configurable allowance used to decrement the rate-limiting account.
The following table represents all categories and the selection rules which are evaluated in order from top to bottom with AllowanceError being the default if no other rules apply.
AllowanceCategory rCode len(Answers) len(Ns) +-------------------+------+--------------+---------+ | AllowanceAnswer | 0 | >0 | | | AllowanceReferral | 0 | 0 | >0 | | AllowanceNoData | 0 | 0 | 0 | | AllowanceNXDomain | 3 | | | | AllowanceError | | | | +-------------------+------+--------------+---------+
This table shows the configuration name associated with each AllowanceCategory.
AllowanceCategory Configuration Name +-------------------+----------------------+ | AllowanceAnswer | responses-per-second | | AllowanceReferral | referrals-per-second | | AllowanceNoData | nodata-per-second | | AllowanceNXDomain | nxdomains-per-second | | AllowanceError | errors-per-second | +-------------------+----------------------+
const ( AllowanceAnswer AllowanceCategory = iota AllowanceReferral AllowanceNoData AllowanceNXDomain AllowanceError AllowanceLast )
func NewAllowanceCategory ¶
func NewAllowanceCategory(rCode, answerCount, nsCount int) AllowanceCategory
NewAllowanceCategory is a helper function which creates an AllowanceCategory
func (AllowanceCategory) String ¶
func (ac AllowanceCategory) String() string
type Config ¶
type Config struct {
// contains filtered or unexported fields
}
Config provides the variable settings for an RRL. A Config should only ever be created with NewConfig as it requires non-zero default values. All Config values are set using the [SetValue] function.
A default config is effectively a no-op as most values default to responses-per-second which itself defaults to zero. The isActive() function returns true if the Config contains values which cause RRL to apply debit rules.
Unset values which default to responses-per-second are set when the Config is passed to NewRRL.
All values are either an unsigned int (as accepted by strconv.ParseUint) an unsigned float (as accepted by strconv.ParseFloat).
The following keywords are accepted:
window int SECONDS - the rolling window in SECONDS during which response rates are tracked. Default 15.
ipv4-prefix-length int LENGTH - the prefix LENGTH in bits to use for identifying a ipv4 client CIDR. Default 24.
ipv6-prefix-length int LENGTH - the prefix LENGTH in bits to use for identifying a ipv6 client CIDR. Default 56.
responses-per-second float ALLOWANCE - the number AllowanceAnswer responses allowed per second. An ALLOWANCE of 0 disables rate limiting. Default 0.
nodata-per-second float ALLOWANCE - the number of AllowanceNoData responses allowed per second. An ALLOWANCE of 0 disables rate limiting. Defaults to responses-per-second.
nxdomains-per-second float ALLOWANCE - the number of AllowanceNXDomain responses allowed per second. An ALLOWANCE of 0 disables rate limiting. Defaults to responses-per-second.
referrals-per-second float ALLOWANCE - the number of AllowanceReferral responses allowed per second. An ALLOWANCE of 0 disables rate limiting. Defaults to responses-per-second.
errors-per-second float ALLOWANCE - the number of AllowanceError allowed per second (excluding NXDOMAIN). An ALLOWANCE of 0 disables rate limiting. Defaults to responses-per-second.
requests-per-second float ALLOWANCE - the number of requests allowed per second from source IP. An ALLOWANCE of 0 disables rate limiting of requests. This value applies solely to the claimed source IP of the query whereas all other settings apply to response details. Default 0.
max-table-size int SIZE - the maximum number of responses to be tracked at one time. When exceeded, rrl stops rate limiting new responses. Defaults to 100000.
slip-ratio int RATIO - the ratio of rate-limited responses which are given a truncated response over a dropped response. A RATIO of 0 disables slip processing and thus all rate-limited responses will be dropped. A RATIO of 1 means every rate-limited response will be a truncated response and the upper limit of 10 means 1 in every 10 rate-limited responses will be a truncated with the remaining 9 being dropped. Default is 2.
For those wishing to examine the internal values, with the String() function, note that while intervals are set as per-second values they are internally converted to the number of nanoseconds to decrement per Debit call, so expect the unexpected.
ISC config values not yet supported by this package are: qps-scale and all-per-second. Maybe one day...
func NewConfig ¶
func NewConfig() *Config
NewConfig returns a new Config struct with all the default values set. This is the only way you should ever create a Config.
func (*Config) IsActive ¶
IsActive returns true if at least one of the intervals is set and thus causes Debit to evaluate accounts. IOWs it returns !no-op.
func (*Config) SetNowFunc ¶
SetNowFunc is intended for testing purposes only. It replaces the time.Now() function used in the cache eviction logic.
func (*Config) SetValue ¶
SetValue changes the configuration values for the nominated keyword Config.
SetValue is provided as a keyword-based setter to try and make it compatible with the original coredns/rrl plugin as possible. Serendipitously, this should also assist programs which use [https://pkg.go.dev/flag] with the keywords as option names such as --window xx.
Note that only keywords specific to this standalone rrl package have been carried over from coredns. For example "report-only" is not handled here as it is now expected to be handled by the caller as part of the design goal of decoupling rrl from anything specific to coredns.
See Config for a full list of valid keywords.
Example:
c := NewConfig() c.SetValue("window", "30")
func (*Config) String ¶
String is mainly intended for test code so it can verify internal values without having direct access to them. Of course the caller is free to use this printable value too.
The returned string is a single line of text containing all config values with all per-second values expressed as nanoseconds decrements.
type IPReason ¶
type IPReason int
IPReason represents the state of IP rate limiting at the time the Action was determined. It is intended for diagnostic and statistical purposes only. Callers should expect that the range of reasons may increase or change over time.
Values are: IPOk, IPNotConfigured, IPRateLimit and IPCacheFull.
type RRL ¶
type RRL struct {
// contains filtered or unexported fields
}
RRL contains the configuration and "account" database. An RRL is safe for concurrent use by multiple goroutines.
func NewRRL ¶
NewRRL creates a new RRL struct which is ready for use. The config parameter is created by the NewConfig and [SetValue] functions. All config default values are set by NewRRL and are visible in the Config on return. NewRRL takes a copy of Config so subsequent changes have no effect on the RRL.
func (*RRL) Debit ¶
Debit decrements the "account" associated with the Client Network and "Response Tuple". It returns a recommended action and reasons for recommending that action.
Debit should only be called for queries which do not contain a valid server cookie. Since Debit cannot check for a valid server cookie - the caller is responsible for this part.
src is the purported source address of the client who sent the query - this is masked by the configured network prefix lengths to determine the Client Network.
tuple is the ResponseTuple formulated from the response and related information (in particular whether the response was formulated from a wildcard).
## Returned Values
Action indicates what the caller should do with the response as a consequence of RRL processing - it can be one of Send, Drop or Slip.
IPReason and RTReason provide insights as to why the action was recommended. They may be useful details for statistics and logging purposes.
Debit is concurrency safe.
type RTReason ¶
type RTReason int
RTReason represents the state of "Response Tuple" rate limiting at the time the Action was determined. It is intended for diagnostic and statistical purposes only. Callers should expect that the range of reasons may increase or change over time.
Values are: RTOk, RTNotConfigured, RTNotReached, RTRateLimit, RTNotUDP and RTCacheFull.
const ( RTOk RTReason = iota // Account is in credit RTNotConfigured // Config entry is zero RTNotReached // An earlier condition causes Action (IPLimit most likely) RTRateLimit // Ran out of credits RTNotUDP // Debit is only applicable to UDP queries RTCacheFull // RRL cache failed to create a new account RTLast )
type ResponseTuple ¶
type ResponseTuple struct { Class uint16 Type uint16 AllowanceCategory SalientName string }
ResponseTuple is provided by the application when calling [Debit]. It is used internally as a "database key" to uniquely identify rate-limiting "accounts". [Debit] expects all fields to be filled - with the exception noted below.
To fully populate a ResponseTuple the caller needs access to the response message and whether the answer was formulated dynamically by such things as wildcards or synthetic answers (as often used in reverse serving). When dynamically generated the caller needs to know the origin name of the dynamically created resource.
Fields are:
Class - The class of the query which is highly likely to be ClassINET. This value is a direct copy of the numeric value in the DNS question RR.
Type - The type of the query such as TypeA, TypeNS, etc. This valus is a direct copy of the numeric value in the DNS Question RR.
AllowanceCategory is derived from the rtype and RR counts in the response message. It effectively collapses a wide range of rtypes and response types down to a small subset which are of most interest to rrl.
Values are: AllowanceAnswer, AllowanceReferral, AllowanceNoData, AllowanceNXDomain and the catchall AllowanceError when none of the other AllowanceCategorys apply. The AllowanceCategory type documents the rules for setting these values.
SalientName the name to use for the purpose of uniquely identifying the query. In the simplest case it is a copy of the qName from the first RR in the Question section of the response, but it varies according to the selection rules.
### SalientName Selection Rules
These rules must be evaluated in sequential order.
If AllowanceCategory is AllowanceNXDomain or AllowanceReferral then use the qName in the first RR in the Ns section of the response. If the Ns section is empty, set SalientName to an empty string.
If the response is dynamically synthesized - perhaps from a wildcard - set SalientName to the origin name prefixed with "*". E.g. "*.example.com".
The goal is to group all dynamic responses under the one "account" as otherwise the potentially huge range of responses are distributed across an equally huge number of rate-limiting "accounts" which largely defeats the purpose of rrl.
Quite often the origin name is simply all labels to the right of the leading dynamic label, but be aware that the origin name may be further up the delegation tree. The caller must be able to determine the actual origin name to reliably make this determination. By way of example, the following zone entry:
*.*.*.example.com IN TXT "Hello Worms"
should result in a SalientName of "example.com".
If neither of the previous conditions apply set SalientName to the qName from the first RR in the Question section of the response.
In the very unlikely event that the response message only contains a COOKIE OPT as allowed in RFC7873#5.4, none of the ResponseTuple fields should be populated except AllowanceCategory.
func (*ResponseTuple) String ¶
func (rt *ResponseTuple) String() string
type Stats ¶
type Stats struct { RPS [AllowanceLast]int64 // Since last zero Actions [ActionLast]int64 IPReasons [IPLast]int64 RTReasons [RTLast]int64 CacheLength int // Always current Evictions int64 // Since last zero }
Stats tracks basic statistics - mostly the results of Debit calls. Callers are responsible for concurrency protection if needed. Normal access to Stats is via [GetStats] which populates cache values and provides concurrency protection.