rrl

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 14, 2023 License: BSD-2-Clause Imports: 8 Imported by: 2

README

rrl - Response Rate Limiting for DNS Servers

Introduction

rrl is a standalone go package which implements the ISC Response Rate Limiting algorithms as originally implemented in Bind 9. The goal of "Response Rate Limiting" is to help authoritative DNS servers mitigate against being used as part of an amplification attack. Such attacks are very easy to orchestrate since most authoritative DNS servers respond to UDP queries from any putative source address.

If you are the developer of an authoritative DNS server then in the interest of Internet hygiene you really should incorporate a "Response Rate Limiting" capability - whether with this package or some other. If you don't, your server is more vulnerable to being used as part of an amplification attack which will not be regarded highly by other DNS operators.

This 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.

If you use miekg/dns you might find it convenient to use markdingo/miekgrrl which provides an adaptor function for passing miekg.dns.Msg attributes to this package.

Genesis

This package is derived from coredns/rrl which mimics the ISC algorithms.

The main differences between this package and coredns/rrl is that all coredns dependencies and external interfaces have been removed so that this package can be used by programs unrelated to coredns. For example the external logging and statistics functions have been removed and are now the responsibility of the caller. In short, all external interactions and dependencies have been eliminated, but otherwise the underlying implementation is largely unchanged.

(Needless to say, this package only exists because of the efforts of the coredns/rrl developers. A big "thank you" to them.)

Project Status

Build Status codecov CodeQL Go Report Card Go Reference

Description

rrl is called by an authoritative DNS server prior to sending each response to a query. rrl tracks the query-per-second rate in a unique "account" assigned to each "Response Tuple" destined for a particular Client Network.

"Accounts" are credited each second with a configured amount and debited once for each call to [Debit]. At most an "account" can gain up to one second of credits or up to a configurable 15 seconds of debits. While the "account" is in credit rrl indicates that the caller should send their planned response. Otherwise rrl indicates that the caller should Drop or 'Slip' their response.

The "Response Tuple" is formulated from the most salient features of the response message. This formulation is somewhat convoluted because DNS responses are somewhat convoluted.

'Slip' is ISC terminology which means to respond with a BADCOOKIE response or a truncated response depending on whether the query contained 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.

rrl plays no part in processing DNS messages or modifying them for output - it solely tracks rate-limiting "accounts" and returns a recommended course of action. All DNS actions, statistics gathering and logging are the responsibility of the caller.

"Response Tuple" and Client Network

"Response Tuple" and Client Network are used to uniquely identity rate-limiting "accounts". In effect they form keys to an internal rrl "accounts" cache.

A "Response Tuple" is formulated from various features of the response message - the exact details depend on the nature of the response (NXDomain, Error, referral, etc). To paraphrase ISC, the formulation of the "Response Tuple" is not simplistic. 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 package documentation describes how to formulate a "Response Tuple".

A Client Network is the putative source address of the request masked by the configured size of the "network". The default configured sizes being 24 for ipv4 and 56 for ipv6.

ISC Terminology

As a general rule, this documentation uses ISC terminology, such as "accounts" and "debits" and so on. The one exception being "Response Tuple" which is used in preference to "Identical Response", or "Token" in coredns/rrl parlance. While there is obvious merit in common terminology, "Response Tuple" seem to better convey intent and outcome.

Sample Code

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

      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.

Installation

rrl requires go version 1.19 or later.

Once your application imports "github.com/markdingo/rrl", then "go build" or "go mod tidy" in your application directory should download and compile rrl automatically.

Further Reading

The rrl API is described in the package documentation which is mirrored online at pkg.go.dev. Other background material can be found at the coredns rrl plugin home page.

Community

If you have any problems using rrl or suggestions on how it can do a better job, don't hesitate to create an issue on the project home page. This package can only improve with your feedback.

Motivation

rrl was originally created for autoreverse, the no muss, no fuss reverse DNS server; check it out if you want an example of how rrl is used in the wild.

rrl is Copyright © 2023 Mark Delany and is licensed under the BSD 2-Clause "Simplified" License.

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)

const (
	Send Action = iota // Send the planned response
	Drop               // Do not send the planned response
	Slip               // Send a truncated response (if able) or a BADCOOKIE error
	ActionLast
)

func (Action) String

func (act Action) String() string

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

func (c *Config) IsActive() bool

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

func (c *Config) SetNowFunc(fn func() time.Time)

SetNowFunc is intended for testing purposes only. It replaces the time.Now() function used in the cache eviction logic.

func (*Config) SetValue

func (c *Config) SetValue(keyword string, arg string) error

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

func (c *Config) String() 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.

const (
	IPOk            IPReason = iota // IP CIDR is within rate limits
	IPNotConfigured                 // Config entry is zero
	IPNotReached                    // Not possible at this stage, but allow for possibility
	IPRateLimit                     // Ran out of credits
	IPCacheFull                     // RRL cache failed to create a new account
	IPLast
)

func (IPReason) String

func (ipr IPReason) String() string

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

func NewRRL(cfg *Config) *RRL

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

func (rrl *RRL) Debit(src net.Addr, tuple *ResponseTuple) (act Action, ipr IPReason, rtr RTReason)

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.

func (*RRL) GetStats

func (rrl *RRL) GetStats(zeroAfter bool) (c Stats)

GetStats returns the internal stats accumulated by the Debit call. The caller can optionally request that the stats be zeroed after the copy.

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
)

func (RTReason) String

func (rtr RTReason) String() string

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.

  1. 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.

  2. 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".

  3. 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.

func (*Stats) Add

func (c *Stats) Add(from *Stats)

Add assumes any concurrency protection required is managed by the caller.

func (*Stats) Copy

func (c *Stats) Copy(zeroAfter bool) (ret Stats)

Copy returns a copy of the current stats and optionally zeroes the source afterwards.

func (*Stats) String

func (c *Stats) String() string

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL