From: Evan Hunt Date: Fri, 7 Jun 2013 19:47:11 +0000 (-0700) Subject: [v9_9] backport RRL to 9.9.x X-Git-Tag: v9.9.4b1~35 X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=6260eef2be9923e5dd046e634a087189cfae0f43;p=thirdparty%2Fbind9.git [v9_9] backport RRL to 9.9.x This incorporates the following changes, plus a new configure option "--enable-rrl" to turn them on: 3575. [func] Changed the logging category for RRL events from 'queries' to 'query-errors'. [RT #33540] 3554. [bug] RRL failed to correctly rate-limit upward referrals and failed to count dropped error responses in the statistics. [RT #33225] 3545. [bug] RRL slip behavior was incorrect when set to 1. [RT #33111] 3518. [bug] Increase the size of dns_rrl_key.s.rtype by one bit so that all dns_rrl_rtype_t enum values fit regardless of whether it is teated as signed or unsigned by the compiler. [RT #32792] 3494. [func] DNS RRL: Blunt the impact of DNS reflection and amplification attacks by rate-limiting substantially- identical responses. To enable, use "configure --enable-rrl". [RT #28130] --- diff --git a/CHANGES b/CHANGES index 4b2c8fcad9e..027447fde84 100644 --- a/CHANGES +++ b/CHANGES @@ -28,6 +28,9 @@ 3576. [bug] Address a shutdown race when validating. [RT #33573] +3575. [func] Changed the logging category for RRL events from + 'queries' to 'query-errors'. [RT #33540] + 3574. [doc] The 'hostname' keyword was missing from server-id description in the named.conf man page. [RT #33476] @@ -40,6 +43,23 @@ 3566. [func] Log when forwarding updates to master. [RT #33240] +3554. [bug] RRL failed to correctly rate-limit upward + referrals and failed to count dropped error + responses in the statistics. [RT #33225] + +3545. [bug] RRL slip behavior was incorrect when set to 1. + [RT #33111] + +3518. [bug] Increase the size of dns_rrl_key.s.rtype by one bit + so that all dns_rrl_rtype_t enum values fit regardless + of whether it is teated as signed or unsigned by + the compiler. [RT #32792] + +3494. [func] DNS RRL: Blunt the impact of DNS reflection and + amplification attacks by rate-limiting substantially- + identical responses. To enable, use "configure + --enable-rrl". [RT #28130] + --- 9.9.3 released --- 3568. [cleanup] Add a product description line to the version file, diff --git a/bin/named/client.c b/bin/named/client.c index 933abc7631e..35f59c53e5c 100644 --- a/bin/named/client.c +++ b/bin/named/client.c @@ -994,6 +994,13 @@ ns_client_send(ns_client_t *client) { } if (result != ISC_R_SUCCESS) goto done; +#ifdef USE_RRL + /* + * Stop after the question if TC was set for rate limiting. + */ + if ((client->message->flags & DNS_MESSAGEFLAG_TC) != 0) + goto renderend; +#endif /* USE_RRL */ result = dns_message_rendersection(client->message, DNS_SECTION_ANSWER, DNS_MESSAGERENDER_PARTIAL | @@ -1133,6 +1140,53 @@ ns_client_error(ns_client_t *client, isc_result_t result) { } #endif +#ifdef USE_RRL + /* + * Try to rate limit error responses. + */ + if (client->view != NULL && client->view->rrl != NULL) { + isc_boolean_t wouldlog; + char log_buf[DNS_RRL_LOG_BUF_LEN]; + dns_rrl_result_t rrl_result; + + INSIST(rcode != dns_rcode_noerror && + rcode != dns_rcode_nxdomain); + wouldlog = isc_log_wouldlog(ns_g_lctx, DNS_RRL_LOG_DROP); + rrl_result = dns_rrl(client->view, &client->peeraddr, + TCP_CLIENT(client), + dns_rdataclass_in, dns_rdatatype_none, + NULL, result, client->now, + wouldlog, log_buf, sizeof(log_buf)); + if (rrl_result != DNS_RRL_RESULT_OK) { + /* + * Log dropped errors in the query category + * so that they are not lost in silence. + * Starts of rate-limited bursts are logged in + * NS_LOGCATEGORY_RRL. + */ + if (wouldlog) { + ns_client_log(client, + NS_LOGCATEGORY_QUERY_EERRORS, + NS_LOGMODULE_CLIENT, + DNS_RRL_LOG_DROP, + "%s", log_buf); + } + /* + * Some error responses cannot be 'slipped', + * so don't try to slip any error responses. + */ + if (!client->view->rrl->log_only) { + isc_stats_increment(ns_g_server->nsstats, + dns_nsstatscounter_ratedropped); + isc_stats_increment(ns_g_server->nsstats, + dns_nsstatscounter_dropped); + ns_client_next(client, DNS_R_DROP); + return; + } + } + } +#endif /* USE_RRL */ + /* * Message may be an in-progress reply that we had trouble * with, in which case QR will be set. We need to clear QR before diff --git a/bin/named/config.c b/bin/named/config.c index fd136b18167..92cf7bf7537 100644 --- a/bin/named/config.c +++ b/bin/named/config.c @@ -227,8 +227,17 @@ view \"_bind\" chaos {\n\ recursion no;\n\ notify no;\n\ allow-new-zones no;\n\ -\n\ - zone \"version.bind\" chaos {\n\ +" +#ifdef USE_RRL +" # Prevent use of this zone in DNS amplified reflection DoS attacks\n\ + rate-limit {\n\ + responses-per-second 3;\n\ + slip 0;\n\ + min-table-size 10;\n\ + };\n\ +" +#endif /* USE_RRL */ +" zone \"version.bind\" chaos {\n\ type master;\n\ database \"_builtin version\";\n\ };\n\ diff --git a/bin/named/include/named/query.h b/bin/named/include/named/query.h index 6dfe96bc9d4..38ae081f71e 100644 --- a/bin/named/include/named/query.h +++ b/bin/named/include/named/query.h @@ -86,6 +86,10 @@ struct ns_query { #define NS_QUERYATTR_DNS64 0x4000 #define NS_QUERYATTR_DNS64EXCLUDE 0x8000 +#ifdef USE_RRL +#define NS_QUERYATTR_RRL_CHECKED 0x10000 +#endif /* USE_RRL */ + isc_result_t ns_query_init(ns_client_t *client); diff --git a/bin/named/include/named/server.h b/bin/named/include/named/server.h index 3ba0c64a712..ff0bfd3bef6 100644 --- a/bin/named/include/named/server.h +++ b/bin/named/include/named/server.h @@ -167,7 +167,14 @@ enum { dns_nsstatscounter_rpz_rewrites = 36, +#ifdef USE_RRL + dns_nsstatscounter_ratedropped = 37, + dns_nsstatscounter_rateslipped = 38, + + dns_nsstatscounter_max = 39 +#else /* USE_RRL */ dns_nsstatscounter_max = 37 +#endif /* USE_RRL */ }; void diff --git a/bin/named/query.c b/bin/named/query.c index 5093cb243da..6917f076fe1 100644 --- a/bin/named/query.c +++ b/bin/named/query.c @@ -193,7 +193,7 @@ inc_stats(ns_client_t *client, isc_statscounter_t counter) { #ifdef NEWSTATS /* Do query type statistics * - * We only increment per-type if we're using the authoriative + * We only increment per-type if we're using the authoritative * answer counter, preventing double-counting. */ if (counter == dns_nsstatscounter_authans) { @@ -5865,6 +5865,120 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) resume: CTRACE("query_find: resume"); +#ifdef USE_RRL + /* + * Rate limit these responses to this client. + */ + if (client->view->rrl != NULL && + ((fname != NULL && dns_name_isabsolute(fname)) || + (result == ISC_R_NOTFOUND && !RECURSIONOK(client))) && + (client->query.attributes & NS_QUERYATTR_RRL_CHECKED) == 0) { + dns_rdataset_t nc_rdataset; + isc_boolean_t wouldlog; + char log_buf[DNS_RRL_LOG_BUF_LEN]; + isc_result_t nc_result; + dns_rrl_result_t rrl_result; + + client->query.attributes |= NS_QUERYATTR_RRL_CHECKED; + + wouldlog = isc_log_wouldlog(ns_g_lctx, DNS_RRL_LOG_DROP); + tname = fname; + if (result == DNS_R_NXDOMAIN) { + /* + * Use the database origin name to rate limit NXDOMAIN + */ + if (db != NULL) + tname = dns_db_origin(db); + rrl_result = result; + } else if (result == DNS_R_NCACHENXDOMAIN && + rdataset != NULL && + dns_rdataset_isassociated(rdataset) && + (rdataset->attributes & + DNS_RDATASETATTR_NEGATIVE) != 0) { + /* + * Try to use owner name in the negative cache SOA. + */ + dns_fixedname_init(&fixed); + dns_rdataset_init(&nc_rdataset); + for (nc_result = dns_rdataset_first(rdataset); + nc_result == ISC_R_SUCCESS; + nc_result = dns_rdataset_next(rdataset)) { + dns_ncache_current(rdataset, + dns_fixedname_name(&fixed), + &nc_rdataset); + if (nc_rdataset.type == dns_rdatatype_soa) { + dns_rdataset_disassociate(&nc_rdataset); + tname = dns_fixedname_name(&fixed); + break; + } + dns_rdataset_disassociate(&nc_rdataset); + } + rrl_result = DNS_R_NXDOMAIN; + } else if (result == DNS_R_NXRRSET || + result == DNS_R_EMPTYNAME) { + rrl_result = DNS_R_NXRRSET; + } else if (result == DNS_R_DELEGATION) { + rrl_result = result; + } else if (result == ISC_R_NOTFOUND) { + /* + * Handle referral to ".", including when recursion + * is off or not requested and the hints have not + * been loaded or we have "additional-from-cache no". + */ + tname = dns_rootname; + rrl_result = DNS_R_DELEGATION; + } else { + rrl_result = ISC_R_SUCCESS; + } + rrl_result = dns_rrl(client->view, &client->peeraddr, + ISC_TF((client->attributes + & NS_CLIENTATTR_TCP) != 0), + client->message->rdclass, qtype, tname, + rrl_result, client->now, + wouldlog, log_buf, sizeof(log_buf)); + if (rrl_result != DNS_RRL_RESULT_OK) { + /* + * Log dropped or slipped responses in the query + * category so that requests are not silently lost. + * Starts of rate-limited bursts are logged in + * DNS_LOGCATEGORY_RRL. + * + * Dropped responses are counted with dropped queries + * in QryDropped while slipped responses are counted + * with other truncated responses in RespTruncated. + */ + if (wouldlog) { + ns_client_log(client, + NS_LOGCATEGORY_QUERY_EERRORS, + NS_LOGMODULE_QUERY, + DNS_RRL_LOG_DROP, + "%s", log_buf); + } + if (!client->view->rrl->log_only) { + if (rrl_result == DNS_RRL_RESULT_DROP) { + /* + * These will also be counted in + * dns_nsstatscounter_dropped + */ + inc_stats(client, + dns_nsstatscounter_ratedropped); + QUERY_ERROR(DNS_R_DROP); + } else { + /* + * These will also be counted in + * dns_nsstatscounter_truncatedresp + */ + inc_stats(client, + dns_nsstatscounter_rateslipped); + client->message->flags |= + DNS_MESSAGEFLAG_TC; + } + goto cleanup; + } + } + } +#endif /* USE_RRL */ + if (!ISC_LIST_EMPTY(client->view->rpz_zones) && (RECURSIONOK(client) || !client->view->rpz_recursive_only) && rpz_ck_dnssec(client, result, rdataset, sigrdataset) && @@ -7317,13 +7431,21 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) goto restart; } +#ifdef USE_RRL if (eresult != ISC_R_SUCCESS && - (!PARTIALANSWER(client) || WANTRECURSION(client))) { + (!PARTIALANSWER(client) || WANTRECURSION(client) + || eresult == DNS_R_DROP)) +#else /* USE_RRL */ + if (eresult != ISC_R_SUCCESS && + (!PARTIALANSWER(client) || WANTRECURSION(client))) +#endif /* USE_RRL */ + { if (eresult == DNS_R_DUPLICATE || eresult == DNS_R_DROP) { /* * This was a duplicate query that we are - * recursing on. Don't send a response now. - * The original query will still cause a response. + * recursing on or the result of rate limiting. + * Don't send a response now for a duplicate query, + * because the original will still cause a response. */ query_next(client, eresult); } else { diff --git a/bin/named/server.c b/bin/named/server.c index a770742a3eb..f28504758af 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -1639,6 +1639,170 @@ configure_rpz(dns_view_t *view, const cfg_listelt_t *element, return (ISC_R_SUCCESS); } +#ifdef USE_RRL +#define CHECK_RRL(cond, pat, val1, val2) \ + do { \ + if (!(cond)) { \ + cfg_obj_log(obj, ns_g_lctx, ISC_LOG_ERROR, \ + pat, val1, val2); \ + result = ISC_R_RANGE; \ + goto cleanup; \ + } \ + } while (0) + +#define CHECK_RRL_RATE(rate, def, max_rate, name) \ + do { \ + obj = NULL; \ + rrl->rate.str = name; \ + result = cfg_map_get(map, name, &obj); \ + if (result == ISC_R_SUCCESS) { \ + rrl->rate.r = cfg_obj_asuint32(obj); \ + CHECK_RRL(rrl->rate.r <= max_rate, \ + name" %d > %d", \ + rrl->rate.r, max_rate); \ + } else { \ + rrl->rate.r = def; \ + } \ + rrl->rate.scaled = rrl->rate.r; \ + } while (0) + +static isc_result_t +configure_rrl(dns_view_t *view, const cfg_obj_t *config, const cfg_obj_t *map) { + const cfg_obj_t *obj; + dns_rrl_t *rrl; + isc_result_t result; + int min_entries, i, j; + + /* + * Most DNS servers have few clients, but intentinally open + * recursive and authoritative servers often have many. + * So start with a small number of entries unless told otherwise + * to reduce cold-start costs. + */ + min_entries = 500; + obj = NULL; + result = cfg_map_get(map, "min-table-size", &obj); + if (result == ISC_R_SUCCESS) { + min_entries = cfg_obj_asuint32(obj); + if (min_entries < 1) + min_entries = 1; + } + result = dns_rrl_init(&rrl, view, min_entries); + if (result != ISC_R_SUCCESS) + return (result); + + i = ISC_MAX(20000, min_entries); + obj = NULL; + result = cfg_map_get(map, "max-table-size", &obj); + if (result == ISC_R_SUCCESS) { + i = cfg_obj_asuint32(obj); + CHECK_RRL(i >= min_entries, + "max-table-size %d < min-table-size %d", + i, min_entries); + } + rrl->max_entries = i; + + CHECK_RRL_RATE(responses_per_second, 0, DNS_RRL_MAX_RATE, + "responses-per-second"); + CHECK_RRL_RATE(referrals_per_second, + rrl->responses_per_second.r, DNS_RRL_MAX_RATE, + "referrals-per-second"); + CHECK_RRL_RATE(nodata_per_second, + rrl->responses_per_second.r, DNS_RRL_MAX_RATE, + "nodata-per-second"); + CHECK_RRL_RATE(nxdomains_per_second, + rrl->responses_per_second.r, DNS_RRL_MAX_RATE, + "nxdomains-per-second"); + CHECK_RRL_RATE(errors_per_second, + rrl->responses_per_second.r, DNS_RRL_MAX_RATE, + "errors-per-second"); + + CHECK_RRL_RATE(all_per_second, 0, DNS_RRL_MAX_RATE, + "all-per-second"); + + CHECK_RRL_RATE(slip, 2, DNS_RRL_MAX_SLIP, + "slip"); + + i = 15; + obj = NULL; + result = cfg_map_get(map, "window", &obj); + if (result == ISC_R_SUCCESS) { + i = cfg_obj_asuint32(obj); + CHECK_RRL(i >= 1 && i <= DNS_RRL_MAX_WINDOW, + "window %d < 1 or > %d", i, DNS_RRL_MAX_WINDOW); + } + rrl->window = i; + + i = 0; + obj = NULL; + result = cfg_map_get(map, "qps-scale", &obj); + if (result == ISC_R_SUCCESS) { + i = cfg_obj_asuint32(obj); + CHECK_RRL(i >= 1, "invalid 'qps-scale %d'%s", i, ""); + } + rrl->qps_scale = i; + rrl->qps = 1.0; + + i = 24; + obj = NULL; + result = cfg_map_get(map, "ipv4-prefix-length", &obj); + if (result == ISC_R_SUCCESS) { + i = cfg_obj_asuint32(obj); + CHECK_RRL(i >= 8 && i <= 32, + "invalid 'ipv4-prefix-length %d'%s", i, ""); + } + rrl->ipv4_prefixlen = i; + if (i == 32) + rrl->ipv4_mask = 0xffffffff; + else + rrl->ipv4_mask = htonl(0xffffffff << (32-i)); + + i = 56; + obj = NULL; + result = cfg_map_get(map, "ipv6-prefix-length", &obj); + if (result == ISC_R_SUCCESS) { + i = cfg_obj_asuint32(obj); + CHECK_RRL(i >= 16 && i <= DNS_RRL_MAX_PREFIX, + "ipv6-prefix-length %d < 16 or > %d", + i, DNS_RRL_MAX_PREFIX); + } + rrl->ipv6_prefixlen = i; + for (j = 0; j < 4; ++j) { + if (i <= 0) { + rrl->ipv6_mask[j] = 0; + } else if (i < 32) { + rrl->ipv6_mask[j] = htonl(0xffffffff << (32-i)); + } else { + rrl->ipv6_mask[j] = 0xffffffff; + } + i -= 32; + } + + obj = NULL; + result = cfg_map_get(map, "exempt-clients", &obj); + if (result == ISC_R_SUCCESS) { + result = cfg_acl_fromconfig(obj, config, ns_g_lctx, + ns_g_aclconfctx, ns_g_mctx, + 0, &rrl->exempt); + CHECK_RRL(result == ISC_R_SUCCESS, + "invalid %s%s", "address match list", ""); + } + + obj = NULL; + result = cfg_map_get(map, "log-only", &obj); + if (result == ISC_R_SUCCESS && cfg_obj_asboolean(obj)) + rrl->log_only = ISC_TRUE; + else + rrl->log_only = ISC_FALSE; + + return (ISC_R_SUCCESS); + + cleanup: + dns_rrl_view_destroy(view); + return (result); +} +#endif /* USE_RRL */ + /* * Configure 'view' according to 'vconfig', taking defaults from 'config' * where values are missing in 'vconfig'. @@ -3043,6 +3207,16 @@ configure_view(dns_view_t *view, cfg_obj_t *config, cfg_obj_t *vconfig, } } +#ifdef USE_RRL + obj = NULL; + result = ns_config_get(maps, "rate-limit", &obj); + if (result == ISC_R_SUCCESS) { + result = configure_rrl(view, config, obj); + if (result != ISC_R_SUCCESS) + goto cleanup; + } +#endif /* USE_RRL */ + result = ISC_R_SUCCESS; cleanup: diff --git a/bin/named/statschannel.c b/bin/named/statschannel.c index c78f78044c0..b1372e30a67 100644 --- a/bin/named/statschannel.c +++ b/bin/named/statschannel.c @@ -208,6 +208,12 @@ init_desc(void) { "UpdateBadPrereq"); SET_NSSTATDESC(rpz_rewrites, "response policy zone rewrites", "RPZRewrites"); +#ifdef USE_RRL + SET_NSSTATDESC(ratedropped, "responses dropped for rate limits", + "RateDropped"); + SET_NSSTATDESC(rateslipped, "responses truncated for rate limits", + "RateSlipped"); +#endif /* USE_RRL */ INSIST(i == dns_nsstatscounter_max); /* Initialize resolver statistics */ diff --git a/bin/tests/system/Makefile.in b/bin/tests/system/Makefile.in index 1f9f41fdd02..724db3f26dc 100644 --- a/bin/tests/system/Makefile.in +++ b/bin/tests/system/Makefile.in @@ -21,7 +21,8 @@ top_srcdir = @top_srcdir@ @BIND9_MAKE_INCLUDES@ -SUBDIRS = dlzexternal filter-aaaa lwresd rpz rsabigexponent tkey tsiggss +SUBDIRS = dlzexternal filter-aaaa lwresd rpz rrl \ + rsabigexponent tkey tsiggss TARGETS = @BIND9_MAKE_RULES@ diff --git a/bin/tests/system/README b/bin/tests/system/README index dbca6083ded..229c887abb7 100644 --- a/bin/tests/system/README +++ b/bin/tests/system/README @@ -17,6 +17,7 @@ involving a different DNS setup. They are: nsupdate/ Dynamic update and IXFR tests resolver/ Regression tests for resolver bugs that have been fixed (not a complete resolver test suite) + rrl/ query rate limiting rpz/ Tests of response policy zone (RPZ) rewriting stub/ Tests of stub zone functionality unknown/ Unknown type and class tests diff --git a/bin/tests/system/conf.sh.in b/bin/tests/system/conf.sh.in index 9246b52f096..447aaa645c7 100644 --- a/bin/tests/system/conf.sh.in +++ b/bin/tests/system/conf.sh.in @@ -62,7 +62,7 @@ SUBDIRS="acl additional allow_query addzone autosign builtin database dlv dlvauto dlz dlzexternal dname dns64 dnssec ecdsa formerr forward glue gost ixfr inline limits logfileconfig lwresd masterfile masterformat metadata notify nsupdate pending - pkcs11 redirect resolver rndc rpz rrsetorder rsabigexponent + pkcs11 redirect resolver rndc rpz rrl rrsetorder rsabigexponent smartsign sortlist spf staticstub stub tkey tsig tsiggss unknown upforwd verify views wildcard xfer xferquota zero zonechecks" diff --git a/bin/tests/system/rrl/.gitignore b/bin/tests/system/rrl/.gitignore new file mode 100644 index 00000000000..16b57968275 --- /dev/null +++ b/bin/tests/system/rrl/.gitignore @@ -0,0 +1 @@ +flood diff --git a/bin/tests/system/rrl/Makefile.in b/bin/tests/system/rrl/Makefile.in new file mode 100644 index 00000000000..1c737dda726 --- /dev/null +++ b/bin/tests/system/rrl/Makefile.in @@ -0,0 +1,53 @@ +# Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +srcdir = @srcdir@ +VPATH = @srcdir@ +top_srcdir = @top_srcdir@ + +@BIND9_VERSION@ + +@BIND9_MAKE_INCLUDES@ + +CINCLUDES = ${ISC_INCLUDES} + +CDEFINES = +CWARNINGS = + +DNSLIBS = +ISCLIBS = . + +DNSDEPLIBS = +ISCDEPLIBS = + +DEPLIBS = + +LIBS = @LIBS@ + +TARGETS = rrl@EXEEXT@ + +FILTEROBJS = rrl.@O@ + +SRCS = rrl.c + +@BIND9_MAKE_RULES@ + +all: rrl@EXEEXT@ + +rrl@EXEEXT@: ${FILTEROBJS} + ${LIBTOOL_MODE_LINK} ${PURIFY} ${CC} ${CFLAGS} ${LDFLAGS} -o $@ ${FILTEROBJS} ${LIBS} + +clean distclean:: + rm -f ${TARGETS} + diff --git a/bin/tests/system/rrl/clean.sh b/bin/tests/system/rrl/clean.sh new file mode 100644 index 00000000000..59a94038fee --- /dev/null +++ b/bin/tests/system/rrl/clean.sh @@ -0,0 +1,21 @@ +# Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + + + +# Clean up after rrl tests. + +rm -f dig.out* +rm -f */named.memstats */named.run */named.stats */log-* */session.key +rm -f ns3/bl*.db */*.jnl */*.core */*.pid diff --git a/bin/tests/system/rrl/ns1/named.conf b/bin/tests/system/rrl/ns1/named.conf new file mode 100644 index 00000000000..e0091d794ba --- /dev/null +++ b/bin/tests/system/rrl/ns1/named.conf @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + + +controls { /* empty */ }; + +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port 5300; + session-keyfile "session.key"; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + listen-on-v6 { none; }; + notify no; +}; + +zone "." {type master; file "root.db";}; diff --git a/bin/tests/system/rrl/ns1/root.db b/bin/tests/system/rrl/ns1/root.db new file mode 100644 index 00000000000..9b648a5596c --- /dev/null +++ b/bin/tests/system/rrl/ns1/root.db @@ -0,0 +1,31 @@ +; Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +; +; Permission to use, copy, modify, and/or distribute this software for any +; purpose with or without fee is hereby granted, provided that the above +; copyright notice and this permission notice appear in all copies. +; +; THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +; REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +; AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +; INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +; LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +; OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +; PERFORMANCE OF THIS SOFTWARE. + + +$TTL 120 +@ SOA ns. hostmaster.ns. ( 1 3600 1200 604800 60 ) +@ NS ns. +ns. A 10.53.0.1 +. A 10.53.0.1 + +; limit responses from here +tld2. NS ns.tld2. +ns.tld2. A 10.53.0.2 + +; limit recursion to here +tld3. NS ns.tld3. +ns.tld3. A 10.53.0.3 + +; generate SERVFAIL +tld4. NS ns.tld3. diff --git a/bin/tests/system/rrl/ns2/hints b/bin/tests/system/rrl/ns2/hints new file mode 100644 index 00000000000..d5b3a457979 --- /dev/null +++ b/bin/tests/system/rrl/ns2/hints @@ -0,0 +1,18 @@ +; Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +; +; Permission to use, copy, modify, and/or distribute this software for any +; purpose with or without fee is hereby granted, provided that the above +; copyright notice and this permission notice appear in all copies. +; +; THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +; REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +; AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +; INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +; LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +; OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +; PERFORMANCE OF THIS SOFTWARE. + + + +. 0 NS ns1. +ns1. 0 A 10.53.0.1 diff --git a/bin/tests/system/rrl/ns2/named.conf b/bin/tests/system/rrl/ns2/named.conf new file mode 100644 index 00000000000..969532556a6 --- /dev/null +++ b/bin/tests/system/rrl/ns2/named.conf @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + + +controls { /* empty */ }; + +options { + query-source address 10.53.0.2; + notify-source 10.53.0.2; + transfer-source 10.53.0.2; + port 5300; + session-keyfile "session.key"; + pid-file "named.pid"; + statistics-file "named.stats"; + listen-on { 10.53.0.2; }; + listen-on-v6 { none; }; + notify no; + + rate-limit { + responses-per-second 2; + all-per-second 70; + slip 3; + exempt-clients { 10.53.0.7; }; + + // small enough to force a table expansion + min-table-size 75; + }; + + additional-from-cache no; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm hmac-md5; +}; +controls { + inet 10.53.0.2 port 9953 allow { any; } keys { rndc_key; }; +}; + +/* + * These log settings have no effect unless "-g" is removed from ../../start.pl + */ +logging { + channel debug { + file "log-debug"; + print-category yes; print-severity yes; severity debug 10; + }; + channel queries { + file "log-queries"; + print-category yes; print-severity yes; severity info; + }; + category rate-limit { debug; queries; }; + category queries { debug; queries; }; +}; + +zone "." { type hint; file "hints"; }; + +zone "tld2."{ type master; file "tld2.db"; }; diff --git a/bin/tests/system/rrl/ns2/tld2.db b/bin/tests/system/rrl/ns2/tld2.db new file mode 100644 index 00000000000..20930edbade --- /dev/null +++ b/bin/tests/system/rrl/ns2/tld2.db @@ -0,0 +1,47 @@ +; Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +; +; Permission to use, copy, modify, and/or distribute this software for any +; purpose with or without fee is hereby granted, provided that the above +; copyright notice and this permission notice appear in all copies. +; +; THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +; REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +; AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +; INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +; LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +; OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +; PERFORMANCE OF THIS SOFTWARE. + + + +; rate limit response from this zone + +$TTL 120 +@ SOA tld2. hostmaster.ns.tld2. ( 1 3600 1200 604800 60 ) + NS ns + NS . +ns A 10.53.0.2 + +; basic rate limiting +a1 A 192.0.2.1 + +; wildcards +*.a2 A 192.0.2.2 + +; a3 is in tld3 + +; a4 does not exist to give NXDOMAIN + +; a5 for TCP requests +a5 A 192.0.2.5 + +; a6 for whitelisted clients +a6 A 192.0.2.6 + +; a7 for SERVFAIL + +; a8 for NODATA +a8 A 192.0.2.8 + +; a9 for all-per-second limit +$GENERATE 101-180 all$.a9 A 192.0.2.8 diff --git a/bin/tests/system/rrl/ns3/hints b/bin/tests/system/rrl/ns3/hints new file mode 100644 index 00000000000..d5b3a457979 --- /dev/null +++ b/bin/tests/system/rrl/ns3/hints @@ -0,0 +1,18 @@ +; Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +; +; Permission to use, copy, modify, and/or distribute this software for any +; purpose with or without fee is hereby granted, provided that the above +; copyright notice and this permission notice appear in all copies. +; +; THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +; REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +; AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +; INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +; LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +; OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +; PERFORMANCE OF THIS SOFTWARE. + + + +. 0 NS ns1. +ns1. 0 A 10.53.0.1 diff --git a/bin/tests/system/rrl/ns3/named.conf b/bin/tests/system/rrl/ns3/named.conf new file mode 100644 index 00000000000..a3c31e0b0ad --- /dev/null +++ b/bin/tests/system/rrl/ns3/named.conf @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + + +controls { /* empty */ }; + +options { + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port 5300; + session-keyfile "session.key"; + pid-file "named.pid"; + listen-on { 10.53.0.3; }; + listen-on-v6 { none; }; + notify no; + + // check that all of the options are parsed without limiting anything + rate-limit { + responses-per-second 200; + referrals-per-second 220; + nodata-per-second 230; + nxdomains-per-second 240; + errors-per-second 250; + all-per-second 700; + ipv4-prefix-length 24; + ipv6-prefix-length 64; + qps-scale 10; + window 1; + max-table-size 1000; + }; + +}; + +zone "." { type hint; file "hints"; }; + +zone "tld3."{ type master; file "tld3.db"; }; diff --git a/bin/tests/system/rrl/ns3/tld3.db b/bin/tests/system/rrl/ns3/tld3.db new file mode 100644 index 00000000000..870fc2a3005 --- /dev/null +++ b/bin/tests/system/rrl/ns3/tld3.db @@ -0,0 +1,25 @@ +; Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +; +; Permission to use, copy, modify, and/or distribute this software for any +; purpose with or without fee is hereby granted, provided that the above +; copyright notice and this permission notice appear in all copies. +; +; THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +; REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +; AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +; INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +; LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +; OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +; PERFORMANCE OF THIS SOFTWARE. + + + +; rate limit response from this zone + +$TTL 120 +@ SOA tld3. hostmaster.ns.tld3. ( 1 3600 1200 604800 60 ) + NS ns + NS . +ns A 10.53.0.3 + +*.a3 A 192.0.3.3 diff --git a/bin/tests/system/rrl/prereq.sh b/bin/tests/system/rrl/prereq.sh new file mode 100644 index 00000000000..f23d48861ce --- /dev/null +++ b/bin/tests/system/rrl/prereq.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# +# Copyright (C) 2010, 2012 Internet Systems Consortium, Inc. ("ISC") +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +if ./rrl +then + : +else + echo "I:This test requires --enable-rrl at compile time." >&2 + exit 1 +fi diff --git a/bin/tests/system/rrl/rrl.c b/bin/tests/system/rrl/rrl.c new file mode 100644 index 00000000000..6e98e8dc89f --- /dev/null +++ b/bin/tests/system/rrl/rrl.c @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +int +main(int argc, char **argv) { + + UNUSED(argc); + UNUSED(argv); + +#ifdef USE_RRL + return (0); +#else + return (1); +#endif +} diff --git a/bin/tests/system/rrl/setup.sh b/bin/tests/system/rrl/setup.sh new file mode 100644 index 00000000000..8eb1a70e4df --- /dev/null +++ b/bin/tests/system/rrl/setup.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# +# Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + + +SYSTEMTESTTOP=.. +. $SYSTEMTESTTOP/conf.sh +. ./clean.sh + diff --git a/bin/tests/system/rrl/tests.sh b/bin/tests/system/rrl/tests.sh new file mode 100644 index 00000000000..6d77480739b --- /dev/null +++ b/bin/tests/system/rrl/tests.sh @@ -0,0 +1,258 @@ +# Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + + +# test response rate limiting + +SYSTEMTESTTOP=.. +. $SYSTEMTESTTOP/conf.sh + +#set -x +#set -o noclobber + +ns1=10.53.0.1 # root, defining the others +ns2=10.53.0.2 # test server +ns3=10.53.0.3 # secondary test server +ns7=10.53.0.7 # whitelisted client + +USAGE="$0: [-x]" +while getopts "x" c; do + case $c in + x) set -x;; + *) echo "$USAGE" 1>&2; exit 1;; + esac +done +shift `expr $OPTIND - 1 || true` +if test "$#" -ne 0; then + echo "$USAGE" 1>&2 + exit 1 +fi +# really quit on control-C +trap 'exit 1' 1 2 15 + + +ret=0 +setret () { + ret=1 + echo "$*" +} + + +# Wait until soon after the start of a second to make results consistent. +# The start of a second credits a rate limit. +# This would be far easier in C or by assuming a modern version of perl. +sec_start () { + START=`date` + while true; do + NOW=`date` + if test "$START" != "$NOW"; then + return + fi + $PERL -e 'select(undef, undef, undef, 0.05)' || true + done +} + + +# turn off ${HOME}/.digrc +HOME=/dev/null; export HOME + +# $1=result name $2=domain name $3=dig options +digcmd () { + OFILE=$1; shift + DIG_DOM=$1; shift + ARGS="+nosearch +time=1 +tries=1 +ignore -p 5300 $* $DIG_DOM @$ns2" + #echo I:dig $ARGS 1>&2 + START=`date +%y%m%d%H%M.%S` + RESULT=`$DIG $ARGS 2>&1 | tee $OFILE=TEMP \ + | sed -n -e '/^;; AUTHORITY/,/^$/d' \ + -e '/^;; ADDITIONAL/,/^$/d' \ + -e 's/^[^;].* \([^ ]\{1,\}\)$/\1/p' \ + -e 's/;; flags.* tc .*/TC/p' \ + -e 's/;; .* status: NXDOMAIN.*/NXDOMAIN/p' \ + -e 's/;; .* status: SERVFAIL.*/SERVFAIL/p' \ + -e 's/;; connection timed out.*/drop/p' \ + -e 's/;; communications error to.*/drop/p' \ + | tr -d '\n'` + mv "$OFILE=TEMP" "$OFILE=$RESULT" + touch -t $START "$OFILE=$RESULT" +} + + +# $1=number of tests $2=target domain $3=dig options +CNT=1 +burst () { + BURST_LIMIT=$1; shift + BURST_DOM_BASE="$1"; shift + while test "$BURST_LIMIT" -ge 1; do + if test $CNT -lt 10; then + CNT="00$CNT" + else + if test $CNT -lt 100; then + CNT="0$CNT" + fi + fi + eval BURST_DOM="$BURST_DOM_BASE" + FILE="dig.out-$BURST_DOM-$CNT" + digcmd $FILE $BURST_DOM $* & + CNT=`expr $CNT + 1` + BURST_LIMIT=`expr "$BURST_LIMIT" - 1` + done +} + + +# $1=domain $2=IP address $3=# of IP addresses $4=TC $5=drop +# $6=NXDOMAIN $7=SERVFAIL or other errors +ck_result() { + BAD= + wait + ADDRS=`ls dig.out-$1-*=$2 2>/dev/null | wc -l | tr -d ' '` + TC=`ls dig.out-$1-*=TC 2>/dev/null | wc -l | tr -d ' '` + DROP=`ls dig.out-$1-*=drop 2>/dev/null | wc -l | tr -d ' '` + NXDOMAIN=`ls dig.out-$1-*=NXDOMAIN 2>/dev/null | wc -l | tr -d ' '` + SERVFAIL=`ls dig.out-$1-*=SERVFAIL 2>/dev/null | wc -l | tr -d ' '` + if test $ADDRS -ne "$3"; then + setret "I:$ADDRS instead of $3 '$2' responses for $1" + BAD=yes + fi + if test $TC -ne "$4"; then + setret "I:$TC instead of $4 truncation responses for $1" + BAD=yes + fi + if test $DROP -ne "$5"; then + setret "I:$DROP instead of $5 dropped responses for $1" + BAD=yes + fi + if test $NXDOMAIN -ne "$6"; then + setret "I:$NXDOMAIN instead of $6 NXDOMAIN responses for $1" + BAD=yes + fi + if test $SERVFAIL -ne "$7"; then + setret "I:$SERVFAIL instead of $7 error responses for $1" + BAD=yes + fi + if test -z "$BAD"; then + rm -f dig.out-$1-* + fi +} + + +ckstats () { + LABEL="$1"; shift + TYPE="$1"; shift + EXPECTED="$1"; shift + CNT=`sed -n -e "s/[ ]*\([0-9]*\).responses $TYPE for rate limits.*/\1/p" \ + ns2/named.stats | tail -1` + CNT=`expr 0$CNT + 0` + if test "$CNT" -ne $EXPECTED; then + setret "I:wrong $LABEL $TYPE statistics of $CNT instead of $EXPECTED" + fi +} + + +######### +sec_start + +# Tests of referrals to "." must be done before the hints are loaded +# or with "additional-from-cache no" +burst 5 a1.tld3 +norec +# basic rate limiting +burst 3 a1.tld2 +# 1 second delay allows an additional response. +sleep 1 +burst 21 a1.tld2 +# Request 30 different qnames to try a wildcard. +burst 30 'x$CNT.a2.tld2' +# These should be counted and limited but are not. See RT33138. +burst 10 'y.x$CNT.a2.tld2' + +# IP TC drop NXDOMAIN SERVFAIL +# referrals to "." +ck_result a1.tld3 '' 2 1 2 0 0 +# check 24 results including 1 second delay that allows an additional response +ck_result a1.tld2 192.0.2.1 3 7 14 0 0 + +# Check the wild card answers. +# The parent name of the 30 requests is counted. +ck_result 'x*.a2.tld2' 192.0.2.2 2 10 18 0 0 + +# These should be limited but are not. See RT33138. +ck_result 'y.x*.a2.tld2' 192.0.2.2 10 0 0 0 0 + +######### +sec_start + +burst 1 'y$CNT.a3.tld3'; wait; burst 20 'y$CNT.a3.tld3' +burst 20 'z$CNT.a4.tld2' + +# Recursion. +# The first answer is counted separately because it is counted against +# the rate limit on recursing to the server for a3.tld3. The remaining 20 +# are counted as local responses from the cache. +ck_result 'y*.a3.tld3' 192.0.3.3 3 6 12 0 0 + +# NXDOMAIN responses are also limited based on the parent name. +ck_result 'z*.a4.tld2' x 0 6 12 2 0 + +$RNDC -c $SYSTEMTESTTOP/common/rndc.conf -p 9953 -s $ns2 stats +ckstats first dropped 58 +ckstats first truncated 30 + + +######### +sec_start + +burst 20 a5.tld2 +tcp +burst 20 a6.tld2 -b $ns7 +burst 20 a7.tld4 +burst 2 a8.tld2 AAAA +burst 2 a8.tld2 TXT +burst 2 a8.tld2 SPF + +# TCP responses are not rate limited +ck_result a5.tld2 192.0.2.5 20 0 0 0 0 + +# whitelisted client is not rate limited +ck_result a6.tld2 192.0.2.6 20 0 0 0 0 + +# Errors such as SERVFAIL are rate limited. The numbers are confusing, because +# other rate limiting can be triggered before the SERVFAIL limit is reached. +ck_result a7.tld4 192.0.2.1 0 6 12 0 2 + +# NODATA responses are counted as the same regardless of qtype. +ck_result a8.tld2 '' 2 2 2 0 0 + +$RNDC -c $SYSTEMTESTTOP/common/rndc.conf -p 9953 -s $ns2 stats +ckstats second dropped 72 +ckstats second truncated 38 + + +######### +sec_start + +# all-per-second +# The qnames are all unique but the client IP address is constant. +CNT=101 +burst 80 'all$CNT.a9.tld2' + +ck_result 'a*.a9.tld2' 192.0.2.8 70 0 10 0 0 + +$RNDC -c $SYSTEMTESTTOP/common/rndc.conf -p 9953 -s $ns2 stats +ckstats final dropped 82 +ckstats final truncated 38 + + +echo "I:exit status: $ret" +# exit $ret +[ $ret -ne 0 ] && echo "I:test failure overridden" +exit 0 diff --git a/config.h.in b/config.h.in index a6ddcb1ed55..85a43815d19 100644 --- a/config.h.in +++ b/config.h.in @@ -428,6 +428,9 @@ int sigwait(const unsigned int *set, int *sig); non-blocking. */ #undef USE_FIONBIO_IOCTL +/* Enable DNS Response Rate Limiting */ +#undef USE_RRL + /* define if idnkit support is to be included. */ #undef WITH_IDN diff --git a/configure b/configure index 726ee6c24b8..8c1e044b079 100755 --- a/configure +++ b/configure @@ -1331,6 +1331,8 @@ ISC_PLATFORM_NEEDSYSSELECTH ISC_PLATFORM_HAVEDEVPOLL ISC_PLATFORM_HAVEEPOLL ISC_PLATFORM_HAVEKQUEUE +RRLLINKSRCS +RRLLINKOBJS ISC_PLATFORM_HAVELIFCONF ISC_PLATFORM_NORETURN_POST ISC_PLATFORM_NORETURN_PRE @@ -1452,6 +1454,7 @@ enable_libbind enable_developer with_python enable_newstats +enable_rrl enable_kqueue enable_epoll enable_devpoll @@ -2129,7 +2132,8 @@ Optional Features: --disable-libtool-lock avoid locking (might break parallel builds) --enable-libbind deprecated --enable-developer enable developer build settings - --enable-newstats use the new statistics + --enable-newstats use the new statistics + --enable-rrl use DNS Response Rate Limiting --enable-kqueue use BSD kqueue when available [default=yes] --enable-epoll use Linux epoll when available [default=auto] --enable-devpoll use /dev/poll when available [default=yes] @@ -11865,6 +11869,7 @@ yes) test "${enable_fixed_rrset+set}" = set || enable_fixed_rrset=yes test "${with_atf+set}" = set || with_atf=yes test "${enable_filter_aaaa+set}" = set || enable_filter_aaaa=yes + test "${enable_rrl+set}" = set || enable_rrl=yes test "${with_dlz_filesystem+set}" = set || with_dlz_filesystem=yes case "$host" in *-darwin*) @@ -13544,6 +13549,30 @@ $as_echo "#define NEWSTATS 1" >>confdefs.h ;; esac +# +# check if we want DNS RRL +# +# Check whether --enable-rrl was given. +if test "${enable_rrl+set}" = set; then : + enableval=$enable_rrl; +fi + +case "$enable_rrl" in +yes) + +$as_echo "#define USE_RRL 1" >>confdefs.h + + RRLLINKOBJS='${RRLOBJS}' + RRLLINKSRCS='${RRLSRCS}' + ;; +*) + RRLLINKOBJS="" + RRLLINKSRCS="" + ;; +esac + + + # # check if we have kqueue # @@ -20526,7 +20555,7 @@ ac_config_commands="$ac_config_commands chmod" # elsewhere if there's a good reason for doing so. # -ac_config_files="$ac_config_files make/Makefile make/mkdep Makefile bin/Makefile bin/check/Makefile bin/confgen/Makefile bin/confgen/unix/Makefile bin/dig/Makefile bin/dnssec/Makefile bin/named/Makefile bin/named/unix/Makefile bin/nsupdate/Makefile bin/pkcs11/Makefile bin/python/Makefile bin/python/dnssec-checkds.py bin/python/dnssec-coverage.py bin/rndc/Makefile bin/tests/Makefile bin/tests/atomic/Makefile bin/tests/db/Makefile bin/tests/dst/Makefile bin/tests/dst/Kdh.+002+18602.key bin/tests/dst/Kdh.+002+18602.private bin/tests/dst/Kdh.+002+48957.key bin/tests/dst/Kdh.+002+48957.private bin/tests/dst/Ktest.+001+00002.key bin/tests/dst/Ktest.+001+54622.key bin/tests/dst/Ktest.+001+54622.private bin/tests/dst/Ktest.+003+23616.key bin/tests/dst/Ktest.+003+23616.private bin/tests/dst/Ktest.+003+49667.key bin/tests/dst/dst_2_data bin/tests/dst/t2_data_1 bin/tests/dst/t2_data_2 bin/tests/dst/t2_dsasig bin/tests/dst/t2_rsasig bin/tests/hashes/Makefile bin/tests/headerdep_test.sh bin/tests/master/Makefile bin/tests/mem/Makefile bin/tests/names/Makefile bin/tests/net/Makefile bin/tests/rbt/Makefile bin/tests/resolver/Makefile bin/tests/sockaddr/Makefile bin/tests/system/Makefile bin/tests/system/conf.sh bin/tests/system/dlz/prereq.sh bin/tests/system/dlzexternal/Makefile bin/tests/system/dlzexternal/ns1/named.conf bin/tests/system/ecdsa/prereq.sh bin/tests/system/filter-aaaa/Makefile bin/tests/system/gost/prereq.sh bin/tests/system/lwresd/Makefile bin/tests/system/rpz/Makefile bin/tests/system/rsabigexponent/Makefile bin/tests/system/tkey/Makefile bin/tests/system/tsiggss/Makefile bin/tests/tasks/Makefile bin/tests/timers/Makefile bin/tests/virtual-time/Makefile bin/tests/virtual-time/conf.sh bin/tools/Makefile contrib/check-secure-delegation.pl contrib/zone-edit.sh doc/Makefile doc/arm/Makefile doc/doxygen/Doxyfile doc/doxygen/Makefile doc/doxygen/doxygen-input-filter doc/misc/Makefile doc/xsl/Makefile doc/xsl/isc-docbook-chunk.xsl doc/xsl/isc-docbook-html.xsl doc/xsl/isc-docbook-latex.xsl doc/xsl/isc-manpage.xsl isc-config.sh lib/Makefile lib/bind9/Makefile lib/bind9/include/Makefile lib/bind9/include/bind9/Makefile lib/dns/Makefile lib/dns/include/Makefile lib/dns/include/dns/Makefile lib/dns/include/dst/Makefile lib/dns/tests/Makefile lib/export/Makefile lib/export/dns/Makefile lib/export/dns/include/Makefile lib/export/dns/include/dns/Makefile lib/export/dns/include/dst/Makefile lib/export/irs/Makefile lib/export/irs/include/Makefile lib/export/irs/include/irs/Makefile lib/export/isc/$thread_dir/Makefile lib/export/isc/$thread_dir/include/Makefile lib/export/isc/$thread_dir/include/isc/Makefile lib/export/isc/Makefile lib/export/isc/include/Makefile lib/export/isc/include/isc/Makefile lib/export/isc/nls/Makefile lib/export/isc/unix/Makefile lib/export/isc/unix/include/Makefile lib/export/isc/unix/include/isc/Makefile lib/export/isccfg/Makefile lib/export/isccfg/include/Makefile lib/export/isccfg/include/isccfg/Makefile lib/export/samples/Makefile lib/export/samples/Makefile-postinstall lib/irs/Makefile lib/irs/include/Makefile lib/irs/include/irs/Makefile lib/irs/include/irs/netdb.h lib/irs/include/irs/platform.h lib/isc/$arch/Makefile lib/isc/$arch/include/Makefile lib/isc/$arch/include/isc/Makefile lib/isc/$thread_dir/Makefile lib/isc/$thread_dir/include/Makefile lib/isc/$thread_dir/include/isc/Makefile lib/isc/Makefile lib/isc/include/Makefile lib/isc/include/isc/Makefile lib/isc/include/isc/platform.h lib/isc/tests/Makefile lib/isc/nls/Makefile lib/isc/unix/Makefile lib/isc/unix/include/Makefile lib/isc/unix/include/isc/Makefile lib/isccc/Makefile lib/isccc/include/Makefile lib/isccc/include/isccc/Makefile lib/isccfg/Makefile lib/isccfg/include/Makefile lib/isccfg/include/isccfg/Makefile lib/lwres/Makefile lib/lwres/include/Makefile lib/lwres/include/lwres/Makefile lib/lwres/include/lwres/netdb.h lib/lwres/include/lwres/platform.h lib/lwres/man/Makefile lib/lwres/unix/Makefile lib/lwres/unix/include/Makefile lib/lwres/unix/include/lwres/Makefile lib/tests/Makefile lib/tests/include/Makefile lib/tests/include/tests/Makefile unit/Makefile unit/unittest.sh" +ac_config_files="$ac_config_files make/Makefile make/mkdep Makefile bin/Makefile bin/check/Makefile bin/confgen/Makefile bin/confgen/unix/Makefile bin/dig/Makefile bin/dnssec/Makefile bin/named/Makefile bin/named/unix/Makefile bin/nsupdate/Makefile bin/pkcs11/Makefile bin/python/Makefile bin/python/dnssec-checkds.py bin/python/dnssec-coverage.py bin/rndc/Makefile bin/tests/Makefile bin/tests/atomic/Makefile bin/tests/db/Makefile bin/tests/dst/Makefile bin/tests/dst/Kdh.+002+18602.key bin/tests/dst/Kdh.+002+18602.private bin/tests/dst/Kdh.+002+48957.key bin/tests/dst/Kdh.+002+48957.private bin/tests/dst/Ktest.+001+00002.key bin/tests/dst/Ktest.+001+54622.key bin/tests/dst/Ktest.+001+54622.private bin/tests/dst/Ktest.+003+23616.key bin/tests/dst/Ktest.+003+23616.private bin/tests/dst/Ktest.+003+49667.key bin/tests/dst/dst_2_data bin/tests/dst/t2_data_1 bin/tests/dst/t2_data_2 bin/tests/dst/t2_dsasig bin/tests/dst/t2_rsasig bin/tests/hashes/Makefile bin/tests/headerdep_test.sh bin/tests/master/Makefile bin/tests/mem/Makefile bin/tests/names/Makefile bin/tests/net/Makefile bin/tests/rbt/Makefile bin/tests/resolver/Makefile bin/tests/sockaddr/Makefile bin/tests/system/Makefile bin/tests/system/conf.sh bin/tests/system/dlz/prereq.sh bin/tests/system/dlzexternal/Makefile bin/tests/system/dlzexternal/ns1/named.conf bin/tests/system/ecdsa/prereq.sh bin/tests/system/filter-aaaa/Makefile bin/tests/system/gost/prereq.sh bin/tests/system/lwresd/Makefile bin/tests/system/rpz/Makefile bin/tests/system/rrl/Makefile bin/tests/system/rsabigexponent/Makefile bin/tests/system/tkey/Makefile bin/tests/system/tsiggss/Makefile bin/tests/tasks/Makefile bin/tests/timers/Makefile bin/tests/virtual-time/Makefile bin/tests/virtual-time/conf.sh bin/tools/Makefile contrib/check-secure-delegation.pl contrib/zone-edit.sh doc/Makefile doc/arm/Makefile doc/doxygen/Doxyfile doc/doxygen/Makefile doc/doxygen/doxygen-input-filter doc/misc/Makefile doc/xsl/Makefile doc/xsl/isc-docbook-chunk.xsl doc/xsl/isc-docbook-html.xsl doc/xsl/isc-docbook-latex.xsl doc/xsl/isc-manpage.xsl isc-config.sh lib/Makefile lib/bind9/Makefile lib/bind9/include/Makefile lib/bind9/include/bind9/Makefile lib/dns/Makefile lib/dns/include/Makefile lib/dns/include/dns/Makefile lib/dns/include/dst/Makefile lib/dns/tests/Makefile lib/export/Makefile lib/export/dns/Makefile lib/export/dns/include/Makefile lib/export/dns/include/dns/Makefile lib/export/dns/include/dst/Makefile lib/export/irs/Makefile lib/export/irs/include/Makefile lib/export/irs/include/irs/Makefile lib/export/isc/$thread_dir/Makefile lib/export/isc/$thread_dir/include/Makefile lib/export/isc/$thread_dir/include/isc/Makefile lib/export/isc/Makefile lib/export/isc/include/Makefile lib/export/isc/include/isc/Makefile lib/export/isc/nls/Makefile lib/export/isc/unix/Makefile lib/export/isc/unix/include/Makefile lib/export/isc/unix/include/isc/Makefile lib/export/isccfg/Makefile lib/export/isccfg/include/Makefile lib/export/isccfg/include/isccfg/Makefile lib/export/samples/Makefile lib/export/samples/Makefile-postinstall lib/irs/Makefile lib/irs/include/Makefile lib/irs/include/irs/Makefile lib/irs/include/irs/netdb.h lib/irs/include/irs/platform.h lib/isc/$arch/Makefile lib/isc/$arch/include/Makefile lib/isc/$arch/include/isc/Makefile lib/isc/$thread_dir/Makefile lib/isc/$thread_dir/include/Makefile lib/isc/$thread_dir/include/isc/Makefile lib/isc/Makefile lib/isc/include/Makefile lib/isc/include/isc/Makefile lib/isc/include/isc/platform.h lib/isc/tests/Makefile lib/isc/nls/Makefile lib/isc/unix/Makefile lib/isc/unix/include/Makefile lib/isc/unix/include/isc/Makefile lib/isccc/Makefile lib/isccc/include/Makefile lib/isccc/include/isccc/Makefile lib/isccfg/Makefile lib/isccfg/include/Makefile lib/isccfg/include/isccfg/Makefile lib/lwres/Makefile lib/lwres/include/Makefile lib/lwres/include/lwres/Makefile lib/lwres/include/lwres/netdb.h lib/lwres/include/lwres/platform.h lib/lwres/man/Makefile lib/lwres/unix/Makefile lib/lwres/unix/include/Makefile lib/lwres/unix/include/lwres/Makefile lib/tests/Makefile lib/tests/include/Makefile lib/tests/include/tests/Makefile unit/Makefile unit/unittest.sh" # @@ -21574,6 +21603,7 @@ do "bin/tests/system/gost/prereq.sh") CONFIG_FILES="$CONFIG_FILES bin/tests/system/gost/prereq.sh" ;; "bin/tests/system/lwresd/Makefile") CONFIG_FILES="$CONFIG_FILES bin/tests/system/lwresd/Makefile" ;; "bin/tests/system/rpz/Makefile") CONFIG_FILES="$CONFIG_FILES bin/tests/system/rpz/Makefile" ;; + "bin/tests/system/rrl/Makefile") CONFIG_FILES="$CONFIG_FILES bin/tests/system/rrl/Makefile" ;; "bin/tests/system/rsabigexponent/Makefile") CONFIG_FILES="$CONFIG_FILES bin/tests/system/rsabigexponent/Makefile" ;; "bin/tests/system/tkey/Makefile") CONFIG_FILES="$CONFIG_FILES bin/tests/system/tkey/Makefile" ;; "bin/tests/system/tsiggss/Makefile") CONFIG_FILES="$CONFIG_FILES bin/tests/system/tsiggss/Makefile" ;; diff --git a/configure.in b/configure.in index 8e543d85fdd..2bdf91be5ca 100644 --- a/configure.in +++ b/configure.in @@ -70,6 +70,7 @@ yes) test "${enable_fixed_rrset+set}" = set || enable_fixed_rrset=yes test "${with_atf+set}" = set || with_atf=yes test "${enable_filter_aaaa+set}" = set || enable_filter_aaaa=yes + test "${enable_rrl+set}" = set || enable_rrl=yes test "${with_dlz_filesystem+set}" = set || with_dlz_filesystem=yes case "$host" in *-darwin*) @@ -469,7 +470,7 @@ AC_SUBST(ISC_PLATFORM_HAVELIFCONF) # check if we want the new statistics # AC_ARG_ENABLE(newstats, - [ --enable-newstats use the new statistics]) + [ --enable-newstats use the new statistics]) case "$enable_newstats" in yes) AC_DEFINE(NEWSTATS, 1, [Use the new XML schema for statistics]) @@ -478,6 +479,25 @@ yes) ;; esac +# +# check if we want DNS RRL +# +AC_ARG_ENABLE(rrl, + [ --enable-rrl use DNS Response Rate Limiting]) +case "$enable_rrl" in +yes) + AC_DEFINE(USE_RRL, 1, [Enable DNS Response Rate Limiting]) + RRLLINKOBJS='${RRLOBJS}' + RRLLINKSRCS='${RRLSRCS}' + ;; +*) + RRLLINKOBJS="" + RRLLINKSRCS="" + ;; +esac +AC_SUBST(RRLLINKOBJS) +AC_SUBST(RRLLINKSRCS) + # # check if we have kqueue # @@ -3690,6 +3710,7 @@ AC_CONFIG_FILES([ bin/tests/system/gost/prereq.sh bin/tests/system/lwresd/Makefile bin/tests/system/rpz/Makefile + bin/tests/system/rrl/Makefile bin/tests/system/rsabigexponent/Makefile bin/tests/system/tkey/Makefile bin/tests/system/tsiggss/Makefile diff --git a/doc/arm/Bv9ARM-book.xml b/doc/arm/Bv9ARM-book.xml index 47e0067c67a..13173f3f50b 100644 --- a/doc/arm/Bv9ARM-book.xml +++ b/doc/arm/Bv9ARM-book.xml @@ -4818,6 +4818,37 @@ category notify { null; }; + + + rate-limit + + + + (Only available when BIND 9 is + configured with the --enable-rrl + option at compile time.) + + + The start, periodic, and final notices of the + rate limiting of a stream of responses are logged at + info severity in this category. + These messages include a hash value of the domain name + of the response and the name itself, + except when there is insufficient memory to record + the name for the final notice + The final notice is normally delayed until about one + minute after rate limit stops. + A lack of memory can hurry the final notice, + in which case it starts with an asterisk (*). + Various internal events are logged at debug 1 level + and higher. + + + Rate limiting of individual requests + is logged in the query-errors category. + + + @@ -5318,7 +5349,7 @@ badresp:1,adberr:0,findfail:0,valfail:0] match-mapped-addresses yes_or_no; filter-aaaa-on-v4 ( yes_or_no | break-dnssec ); filter-aaaa { address_match_list }; - dns64 IPv6-prefix { + dns64 ipv6-prefix { clients { address_match_list }; mapped { address_match_list }; exclude { address_match_list }; @@ -5351,6 +5382,23 @@ badresp:1,adberr:0,findfail:0,valfail:0] resolver-query-timeout number ; deny-answer-addresses { address_match_list } except-from { namelist } ; deny-answer-aliases { namelist } except-from { namelist } ; + rate-limit { + responses-per-second number ; + referrals-per-second number ; + nodata-per-second number ; + nxdomains-per-second number ; + errors-per-second number ; + all-per-second number ; + window number ; + log-only yes_or_no ; + qps-scale number ; + ipv4-prefix-length number ; + ipv6-prefix-length number ; + slip number ; + exempt-clients { address_match_list } ; + max-table-size number ; + min-table-size number ; + } ; response-policy { zone_name policy given | disabled | passthru | nxdomain | nodata | cname domain recursive-only yes_or_no max-policy-ttl number ; @@ -9898,6 +9946,228 @@ ns.domain.com.rpz-nsdname CNAME . RPZRewrites statistics. + + + Response Rate Limiting + + This feature is only available when BIND 9 + is compiled with the --enable-rrl + option on the "configure" command line. + + + Excessive almost identical UDP responses + can be controlled by configuring a + rate-limit clause in an + options or view statement. + This mechanism keeps authoritative BIND 9 from being used + in amplifying reflection denial of service (DoS) attacks. + Short truncated (TC=1) responses can be sent to provide + rate-limited responses to legitimate clients within + a range of forged, attacked IP addresses. + Legitimate clients react to dropped or truncated response + by retrying with UDP or with TCP respectively. + + + + This mechanism is intended for authoritative DNS servers. + It can be used on recursive servers but can slow + applications such as SMTP servers (mail receivers) and + HTTP clients (web browsers) that repeatedly request the + same domains. + When possible, closing "open" recursive servers is better. + + + + Response rate limiting uses a "credit" or "token bucket" scheme. + Each combination of identical response and client + has a conceptual account that earns a specified number + of credits every second. + A prospective response debits its account by one. + Responses are dropped or truncated + while the account is negative. + Responses are tracked within a rolling window of time + which defaults to 15 seconds, but can be configured with + the window option to any value from + 1 to 3600 seconds (1 hour). + The account cannot become more positive than + the per-second limit + or more negative than window + times the per-second limit. + When the specified number of credits for a class of + responses is set to 0, those responses are not rate limited. + + + + The notions of "identical response" and "DNS client" + for rate limiting are not simplistic. + All responses to an address block are counted as if to a + single client. + The prefix lengths of addresses blocks are + specified with ipv4-prefix-length (default 24) + and ipv6-prefix-length (default 56). + + + + All non-empty responses for a valid domain name (qname) + and record type (qtype) are identical and have a limit specified + with responses-per-second + (default 0 or no limit). + All empty (NODATA) responses for a valid domain, + regardless of query type, are identical. + Responses in the NODATA class are limited by + nodata-per-second + (default responses-per-second). + Requests for any and all undefined subdomains of a given + valid domain result in NXDOMAIN errors, and are identical + regardless of query type. + They are limited by nxdomain-per-second + (default responses-per-second). + This controls some attacks using random names, but + can be relaxed or turned off (set to 0) + on servers that expect many legitimate + NXDOMAIN responses, such as from anti-spam blacklists. + Referrals or delegations to the server of a given + domain are identical and are limited by + referrals-per-second + (default responses-per-second). + + + + Responses generated from local wildcards are counted and limited + as if they were for the parent domain name. + This controls flooding using random.wild.example.com. + + + + All requests that result in DNS errors other + than NXDOMAIN, such as SERVFAIL and FORMERR, are identical + regardless of requested name (qname) or record type (qtype). + This controls attacks using invalid requests or distant, + broken authoritative servers. + By default the limit on errors is the same as the + responses-per-second value, + but it can be set separately with + errors-per-second. + + + + Many attacks using DNS involve UDP requests with forged source + addresses. + Rate limiting prevents the use of BIND 9 to flood a network + with responses to requests with forged source addresses, + but could let a third party block responses to legitimate requests. + There is a mechanism that can answer some legitimate + requests from a client whose address is being forged in a flood. + Setting slip to 2 (its default) causes every + other UDP request to be answered with a small truncated (TC=1) + response. + The small size and reduced frequency, and so lack of + amplification, of "slipped" responses make them unattractive + for reflection DoS attacks. + slip must be between 0 and 10. + A value of 0 does not "slip"; + no truncated responses are sent due to rate limiting. + Some error responses including REFUSED and SERVFAIL + cannot be replaced with truncated responses and are instead + leaked at the slip rate. + + + + When the approximate query per second rate exceeds + the qps-scale value, + then the responses-per-second, + errors-per-second, + nxdomains-per-second and + all-per-second values are reduced by the + ratio of the current rate to the qps-scale value. + This feature can tighten defenses during attacks. + For example, with + qps-scale 250; responses-per-second 20; and + a total query rate of 1000 queries/second for all queries from + all DNS clients including via TCP, + then the effective responses/second limit changes to + (250/1000)*20 or 5. + Responses sent via TCP are not limited + but are counted to compute the query per second rate. + + + + Communities of DNS clients can be given their own parameters or no + rate limiting by putting + rate-limit statements in view + statements instead of the global option + statement. + A rate-limit statement in a view replaces, + rather than supplementing, a rate-limit + statement among the main options. + DNS clients within a view can be exempted from rate limits + with the exempt-clients clause. + + + + UDP responses of all kinds can be limited with the + all-per-second phrase. + This rate limiting is unlike the rate limiting provided by + responses-per-second, + errors-per-second, and + nxdomains-per-second on a DNS server + which are often invisible to the victim of a DNS reflection attack. + Unless the forged requests of the attack are the same as the + legitimate requests of the victim, the victim's requests are + not affected. + Responses affected by an all-per-second limit + are always dropped; the slip value has no + effect. + An all-per-second limit should be + at least 4 times as large as the other limits, + because single DNS clients often send bursts of legitimate + requests. + For example, the receipt of a single mail message can prompt + requests from an SMTP server for NS, PTR, A, and AAAA records + as the incoming SMTP/TCP/IP connection is considered. + The SMTP server can need additional NS, A, AAAA, MX, TXT, and SPF + records as it considers the STMP Mail From + command. + Web browsers often repeatedly resolve the same names that + are repeated in HTML <IMG> tags in a page. + All-per-second is similar to the + rate limiting offered by firewalls but often inferior. + Attacks that justify ignoring the + contents of DNS responses are likely to be attacks on the + DNS server itself. + They usually should be discarded before the DNS server + spends resources make TCP connections or parsing DNS requesets, + but that rate limiting must be done before the + DNS server sees the requests. + + + + The maximum size of the table used to track requests and + rate limit responses is set with max-table-size. + Each entry in the table is between 40 and 80 bytes. + The table needs approximately as many entries as the number + of requests received per second. + The default is 20,000. + To reduce the cold start of growing the table, + min-table-size (default 500) + can set the minimum table size. + Enable rate-limit category logging to monitor + expansions of the table and inform + choices for the initial and maximum table size. + + + + Use log-only yes to test rate limiting parameters + without actually dropping any requests. + + + + Responses dropped by rate limits are included in the + RateDropped and QryDropped + statistics. + Responses that truncated by rate limits are included in + RateSlipped and RespTruncated. + @@ -14650,6 +14920,32 @@ HOST-127.EXAMPLE. MX 0 . + + + RateDropped + + + + + + + Responses dropped by rate limits. + + + + + + RateSlipped + + + + + + + Responses truncated by rate limits. + + + diff --git a/lib/dns/Makefile.in b/lib/dns/Makefile.in index b712ab1cd87..a3e3e9a4e86 100644 --- a/lib/dns/Makefile.in +++ b/lib/dns/Makefile.in @@ -55,6 +55,8 @@ DSTOBJS = @DST_EXTRA_OBJS@ @OPENSSLLINKOBJS@ \ dst_api.@O@ dst_lib.@O@ dst_parse.@O@ dst_result.@O@ \ gssapi_link.@O@ gssapictx.@O@ hmac_link.@O@ key.@O@ +RRLOBJS = rrl.@O@ + # Alphabetically DNSOBJS = acache.@O@ acl.@O@ adb.@O@ byaddr.@O@ \ cache.@O@ callbacks.@O@ clientinfo.@O@ compress.@O@ \ @@ -67,14 +69,14 @@ DNSOBJS = acache.@O@ acl.@O@ adb.@O@ byaddr.@O@ \ portlist.@O@ private.@O@ \ rbt.@O@ rbtdb.@O@ rbtdb64.@O@ rcode.@O@ rdata.@O@ \ rdatalist.@O@ rdataset.@O@ rdatasetiter.@O@ rdataslab.@O@ \ - request.@O@ resolver.@O@ result.@O@ rootns.@O@ rpz.@O@ \ - rriterator.@O@ sdb.@O@ \ + request.@O@ resolver.@O@ result.@O@ rootns.@O@ \ + rpz.@O@ rriterator.@O@ sdb.@O@ \ sdlz.@O@ soa.@O@ ssu.@O@ ssu_external.@O@ \ stats.@O@ tcpmsg.@O@ time.@O@ timer.@O@ tkey.@O@ \ tsec.@O@ tsig.@O@ ttl.@O@ update.@O@ validator.@O@ \ version.@O@ view.@O@ xfrin.@O@ zone.@O@ zonekey.@O@ zt.@O@ -OBJS= ${DNSOBJS} ${OTHEROBJS} ${DSTOBJS} +OBJS= ${DNSOBJS} ${OTHEROBJS} ${DSTOBJS} @RRLLINKOBJS@ # Alphabetically OPENSSLGOSTLINKSRCS = opensslgost_link.c @@ -101,7 +103,9 @@ DNSSRCS = acache.c acl.c adb.c byaddr.c \ tsec.c tsig.c ttl.c update.c validator.c \ version.c view.c xfrin.c zone.c zonekey.c zt.c ${OTHERSRCS} -SRCS = ${DSTSRCS} ${DNSSRCS} +RRLSRCS = rrl.c + +SRCS = ${DSTSRCS} ${DNSSRCS} @RRLLINKSRCS@ SUBDIRS = include TARGETS = include/dns/enumtype.h include/dns/enumclass.h \ diff --git a/lib/dns/include/dns/log.h b/lib/dns/include/dns/log.h index 3c4df8a4500..f89ae2502fa 100644 --- a/lib/dns/include/dns/log.h +++ b/lib/dns/include/dns/log.h @@ -43,6 +43,7 @@ LIBDNS_EXTERNAL_DATA extern isc_logmodule_t dns_modules[]; #define DNS_LOGCATEGORY_DELEGATION_ONLY (&dns_categories[10]) #define DNS_LOGCATEGORY_EDNS_DISABLED (&dns_categories[11]) #define DNS_LOGCATEGORY_RPZ (&dns_categories[12]) +#define DNS_LOGCATEGORY_RRL (&dns_categories[13]) /* Backwards compatibility. */ #define DNS_LOGCATEGORY_GENERAL ISC_LOGCATEGORY_GENERAL diff --git a/lib/dns/include/dns/rrl.h b/lib/dns/include/dns/rrl.h new file mode 100644 index 00000000000..ef6b72b4280 --- /dev/null +++ b/lib/dns/include/dns/rrl.h @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + + +#ifndef DNS_RRL_H +#define DNS_RRL_H 1 + +/* + * Rate limit DNS responses. + */ + +#include + +#include +#include +#include + +ISC_LANG_BEGINDECLS + + +/* + * Memory allocation or other failures. + */ +#define DNS_RRL_LOG_FAIL ISC_LOG_WARNING +/* + * dropped or slipped responses. + */ +#define DNS_RRL_LOG_DROP ISC_LOG_INFO +/* + * Major events in dropping or slipping. + */ +#define DNS_RRL_LOG_DEBUG1 ISC_LOG_DEBUG(3) +/* + * Limit computations. + */ +#define DNS_RRL_LOG_DEBUG2 ISC_LOG_DEBUG(4) +/* + * Even less interesting. + */ +#define DNS_RRL_LOG_DEBUG3 ISC_LOG_DEBUG(9) + + +#define DNS_RRL_LOG_ERR_LEN 64 +#define DNS_RRL_LOG_BUF_LEN (sizeof("would continue limiting") + \ + DNS_RRL_LOG_ERR_LEN + \ + sizeof(" responses to ") + \ + ISC_NETADDR_FORMATSIZE + \ + sizeof("/128 for IN ") + \ + DNS_RDATATYPE_FORMATSIZE + \ + DNS_NAME_FORMATSIZE) + + +typedef struct dns_rrl_hash dns_rrl_hash_t; + +/* + * Response types. + */ +typedef enum { + DNS_RRL_RTYPE_FREE = 0, + DNS_RRL_RTYPE_QUERY, + DNS_RRL_RTYPE_REFERRAL, + DNS_RRL_RTYPE_NODATA, + DNS_RRL_RTYPE_NXDOMAIN, + DNS_RRL_RTYPE_ERROR, + DNS_RRL_RTYPE_ALL, + DNS_RRL_RTYPE_TCP, +} dns_rrl_rtype_t; + +/* + * A rate limit bucket key. + * This should be small to limit the total size of the database. + * The hash of the qname should be wide enough to make the probability + * of collisions among requests from a single IP address block less than 50%. + * We need a 32-bit hash value for 10000 qps (e.g. random qnames forged + * by attacker) to collide with legitimate qnames from the target with + * probability at most 1%. + */ +#define DNS_RRL_MAX_PREFIX 64 +typedef union dns_rrl_key dns_rrl_key_t; +union dns_rrl_key { + struct { + isc_uint32_t ip[DNS_RRL_MAX_PREFIX/32]; + isc_uint32_t qname_hash; + dns_rdatatype_t qtype; + isc_uint8_t qclass; + dns_rrl_rtype_t rtype :4; /* 3 bits + sign bit */ + isc_boolean_t ipv6 :1; + } s; + isc_uint16_t w[1]; +}; + +/* + * A rate-limit entry. + * This should be small to limit the total size of the table of entries. + */ +typedef struct dns_rrl_entry dns_rrl_entry_t; +typedef ISC_LIST(dns_rrl_entry_t) dns_rrl_bin_t; +struct dns_rrl_entry { + ISC_LINK(dns_rrl_entry_t) lru; + ISC_LINK(dns_rrl_entry_t) hlink; + dns_rrl_key_t key; +# define DNS_RRL_RESPONSE_BITS 24 + signed int responses :DNS_RRL_RESPONSE_BITS; +# define DNS_RRL_QNAMES_BITS 8 + unsigned int log_qname :DNS_RRL_QNAMES_BITS; + +# define DNS_RRL_TS_GEN_BITS 2 + unsigned int ts_gen :DNS_RRL_TS_GEN_BITS; + isc_boolean_t ts_valid :1; +# define DNS_RRL_HASH_GEN_BITS 1 + unsigned int hash_gen :DNS_RRL_HASH_GEN_BITS; + isc_boolean_t logged :1; +# define DNS_RRL_LOG_BITS 11 + unsigned int log_secs :DNS_RRL_LOG_BITS; + +# define DNS_RRL_TS_BITS 12 + unsigned int ts :DNS_RRL_TS_BITS; + +# define DNS_RRL_MAX_SLIP 10 + unsigned int slip_cnt :4; +}; + +#define DNS_RRL_MAX_TIME_TRAVEL 5 +#define DNS_RRL_FOREVER (1<= DNS_RRL_MAX_TS +#error "DNS_RRL_MAX_WINDOW is too large" +#endif +#define DNS_RRL_MAX_RATE 1000 +#if DNS_RRL_MAX_RATE >= (DNS_RRL_MAX_RESPONSES / DNS_RRL_MAX_WINDOW) +#error "DNS_RRL_MAX_rate is too large" +#endif + +#if (1<= DNS_RRL_FOREVER +#error DNS_RRL_LOG_BITS is too big +#endif +#define DNS_RRL_MAX_LOG_SECS 1800 +#if DNS_RRL_MAX_LOG_SECS >= (1<= (1< #include +#include #include #include #include @@ -142,6 +143,7 @@ struct dns_view { dns_rbt_t * answeracl_exclude; dns_rbt_t * denyanswernames; dns_rbt_t * answernames_exclude; + dns_rrl_t * rrl; isc_boolean_t provideixfr; isc_boolean_t requestnsid; dns_ttl_t maxcachettl; diff --git a/lib/dns/log.c b/lib/dns/log.c index c4d644e3899..0620ba30aba 100644 --- a/lib/dns/log.c +++ b/lib/dns/log.c @@ -45,6 +45,7 @@ LIBDNS_EXTERNAL_DATA isc_logcategory_t dns_categories[] = { { "delegation-only", 0 }, { "edns-disabled", 0 }, { "rpz", 0 }, + { "rate-limit", 0 }, { NULL, 0 } }; diff --git a/lib/dns/rrl.c b/lib/dns/rrl.c new file mode 100644 index 00000000000..96c55fef4b8 --- /dev/null +++ b/lib/dns/rrl.c @@ -0,0 +1,1324 @@ +/* + * Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +/*! \file */ + +/* + * Rate limit DNS responses. + */ + +/* #define ISC_LIST_CHECKINIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +static void +log_end(dns_rrl_t *rrl, dns_rrl_entry_t *e, isc_boolean_t early, + char *log_buf, unsigned int log_buf_len); + +/* + * Get a modulus for a hash function that is tolerably likely to be + * relatively prime to most inputs. Of course, we get a prime for for initial + * values not larger than the square of the last prime. We often get a prime + * after that. + * This works well in practice for hash tables up to at least 100 + * times the square of the last prime and better than a multiplicative hash. + */ +static int +hash_divisor(unsigned int initial) { + static isc_uint16_t primes[] = { + 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, + 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, +#if 0 + 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, + 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, + 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, + 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, + 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, + 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, + 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, + 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, + 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, + 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, + 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, + 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997,1009, +#endif + }; + int divisions, tries; + unsigned int result; + isc_uint16_t *pp, p; + + result = initial; + + if (primes[sizeof(primes)/sizeof(primes[0])-1] >= result) { + pp = primes; + while (*pp < result) + ++pp; + return (*pp); + } + + if ((result & 1) == 0) + ++result; + + divisions = 0; + tries = 1; + pp = primes; + do { + p = *pp++; + ++divisions; + if ((result % p) == 0) { + ++tries; + result += 2; + pp = primes; + } + } while (pp < &primes[sizeof(primes) / sizeof(primes[0])]); + + if (isc_log_wouldlog(dns_lctx, DNS_RRL_LOG_DEBUG3)) + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, DNS_RRL_LOG_DEBUG3, + "%d hash_divisor() divisions in %d tries" + " to get %d from %d", + divisions, tries, result, initial); + + return (result); +} + +/* + * Convert a timestamp to a number of seconds in the past. + */ +static inline int +delta_rrl_time(isc_stdtime_t ts, isc_stdtime_t now) { + int delta; + + delta = now - ts; + if (delta >= 0) + return (delta); + + /* + * The timestamp is in the future. That future might result from + * re-ordered requests, because we use timestamps on requests + * instead of consulting a clock. Timestamps in the distant future are + * assumed to result from clock changes. When the clock changes to + * the past, make existing timestamps appear to be in the past. + */ + if (delta < -DNS_RRL_MAX_TIME_TRAVEL) + return (DNS_RRL_FOREVER); + return (0); +} + +static inline int +get_age(const dns_rrl_t *rrl, const dns_rrl_entry_t *e, isc_stdtime_t now) { + if (!e->ts_valid) + return (DNS_RRL_FOREVER); + return (delta_rrl_time(e->ts + rrl->ts_bases[e->ts_gen], now)); +} + +static inline void +set_age(dns_rrl_t *rrl, dns_rrl_entry_t *e, isc_stdtime_t now) { + dns_rrl_entry_t *e_old; + unsigned int ts_gen; + int i, ts; + + ts_gen = rrl->ts_gen; + ts = now - rrl->ts_bases[ts_gen]; + if (ts < 0) { + if (ts < -DNS_RRL_MAX_TIME_TRAVEL) + ts = DNS_RRL_FOREVER; + else + ts = 0; + } + + /* + * Make a new timestamp base if the current base is too old. + * All entries older than DNS_RRL_MAX_WINDOW seconds are ancient, + * useless history. Their timestamps can be treated as if they are + * all the same. + * We only do arithmetic on more recent timestamps, so bases for + * older timestamps can be recycled provided the old timestamps are + * marked as ancient history. + * This loop is almost always very short because most entries are + * recycled after one second and any entries that need to be marked + * are older than (DNS_RRL_TS_BASES)*DNS_RRL_MAX_TS seconds. + */ + if (ts >= DNS_RRL_MAX_TS) { + ts_gen = (ts_gen + 1) % DNS_RRL_TS_BASES; + for (e_old = ISC_LIST_TAIL(rrl->lru), i = 0; + e_old != NULL && (e_old->ts_gen == ts_gen || + !ISC_LINK_LINKED(e_old, hlink)); + e_old = ISC_LIST_PREV(e_old, lru), ++i) + { + e_old->ts_valid = ISC_FALSE; + } + if (i != 0) + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, DNS_RRL_LOG_DEBUG1, + "rrl new time base scanned %d entries" + " at %d for %d %d %d %d", + i, now, rrl->ts_bases[ts_gen], + rrl->ts_bases[(ts_gen + 1) % + DNS_RRL_TS_BASES], + rrl->ts_bases[(ts_gen + 2) % + DNS_RRL_TS_BASES], + rrl->ts_bases[(ts_gen + 3) % + DNS_RRL_TS_BASES]); + rrl->ts_gen = ts_gen; + rrl->ts_bases[ts_gen] = now; + ts = 0; + } + + e->ts_gen = ts_gen; + e->ts = ts; + e->ts_valid = ISC_TRUE; +} + +static isc_result_t +expand_entries(dns_rrl_t *rrl, int new) { + unsigned int bsize; + dns_rrl_block_t *b; + dns_rrl_entry_t *e; + double rate; + int i; + + if (rrl->num_entries + new >= rrl->max_entries && + rrl->max_entries != 0) + { + new = rrl->max_entries - rrl->num_entries; + if (new <= 0) + return (ISC_R_SUCCESS); + } + + /* + * Log expansions so that the user can tune max-table-size + * and min-table-size. + */ + if (isc_log_wouldlog(dns_lctx, DNS_RRL_LOG_DROP) && + rrl->hash != NULL) { + rate = rrl->probes; + if (rrl->searches != 0) + rate /= rrl->searches; + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, DNS_RRL_LOG_DROP, + "increase from %d to %d RRL entries with" + " %d bins; average search length %.1f", + rrl->num_entries, rrl->num_entries+new, + rrl->hash->length, rate); + } + + bsize = sizeof(dns_rrl_block_t) + (new-1)*sizeof(dns_rrl_entry_t); + b = isc_mem_get(rrl->mctx, bsize); + if (b == NULL) { + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, DNS_RRL_LOG_FAIL, + "isc_mem_get(%d) failed for RRL entries", + bsize); + return (ISC_R_NOMEMORY); + } + memset(b, 0, bsize); + b->size = bsize; + + e = b->entries; + for (i = 0; i < new; ++i, ++e) { + ISC_LINK_INIT(e, hlink); + ISC_LIST_INITANDAPPEND(rrl->lru, e, lru); + } + rrl->num_entries += new; + ISC_LIST_INITANDAPPEND(rrl->blocks, b, link); + + return (ISC_R_SUCCESS); +} + +static inline dns_rrl_bin_t * +get_bin(dns_rrl_hash_t *hash, unsigned int hval) { + return (&hash->bins[hval % hash->length]); +} + +static void +free_old_hash(dns_rrl_t *rrl) { + dns_rrl_hash_t *old_hash; + dns_rrl_bin_t *old_bin; + dns_rrl_entry_t *e, *e_next; + + old_hash = rrl->old_hash; + for (old_bin = &old_hash->bins[0]; + old_bin < &old_hash->bins[old_hash->length]; + ++old_bin) + { + for (e = ISC_LIST_HEAD(*old_bin); e != NULL; e = e_next) { + e_next = ISC_LIST_NEXT(e, hlink); + ISC_LINK_INIT(e, hlink); + } + } + + isc_mem_put(rrl->mctx, old_hash, + sizeof(*old_hash) + + (old_hash->length - 1) * sizeof(old_hash->bins[0])); + rrl->old_hash = NULL; +} + +static isc_result_t +expand_rrl_hash(dns_rrl_t *rrl, isc_stdtime_t now) { + dns_rrl_hash_t *hash; + int old_bins, new_bins, hsize; + double rate; + + if (rrl->old_hash != NULL) + free_old_hash(rrl); + + /* + * Most searches fail and so go to the end of the chain. + * Use a small hash table load factor. + */ + old_bins = (rrl->hash == NULL) ? 0 : rrl->hash->length; + new_bins = old_bins/8 + old_bins; + if (new_bins < rrl->num_entries) + new_bins = rrl->num_entries; + new_bins = hash_divisor(new_bins); + + hsize = sizeof(dns_rrl_hash_t) + (new_bins-1)*sizeof(hash->bins[0]); + hash = isc_mem_get(rrl->mctx, hsize); + if (hash == NULL) { + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, DNS_RRL_LOG_FAIL, + "isc_mem_get(%d) failed for" + " RRL hash table", + hsize); + return (ISC_R_NOMEMORY); + } + memset(hash, 0, hsize); + hash->length = new_bins; + rrl->hash_gen ^= 1; + hash->gen = rrl->hash_gen; + + if (isc_log_wouldlog(dns_lctx, DNS_RRL_LOG_DROP) && old_bins != 0) { + rate = rrl->probes; + if (rrl->searches != 0) + rate /= rrl->searches; + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, DNS_RRL_LOG_DROP, + "increase from %d to %d RRL bins for" + " %d entries; average search length %.1f", + old_bins, new_bins, rrl->num_entries, rate); + } + + rrl->old_hash = rrl->hash; + if (rrl->old_hash != NULL) + rrl->old_hash->check_time = now; + rrl->hash = hash; + + return (ISC_R_SUCCESS); +} + +static void +ref_entry(dns_rrl_t *rrl, dns_rrl_entry_t *e, int probes, isc_stdtime_t now) { + /* + * Make the entry most recently used. + */ + if (ISC_LIST_HEAD(rrl->lru) != e) { + if (e == rrl->last_logged) + rrl->last_logged = ISC_LIST_PREV(e, lru); + ISC_LIST_UNLINK(rrl->lru, e, lru); + ISC_LIST_PREPEND(rrl->lru, e, lru); + } + + /* + * Expand the hash table if it is time and necessary. + * This will leave the newly referenced entry in a chain in the + * old hash table. It will migrate to the new hash table the next + * time it is used or be cut loose when the old hash table is destroyed. + */ + rrl->probes += probes; + ++rrl->searches; + if (rrl->searches > 100 && + delta_rrl_time(rrl->hash->check_time, now) > 1) { + if (rrl->probes/rrl->searches > 2) + expand_rrl_hash(rrl, now); + rrl->hash->check_time = now; + rrl->probes = 0; + rrl->searches = 0; + } +} + +static inline isc_boolean_t +key_cmp(const dns_rrl_key_t *a, const dns_rrl_key_t *b) { + if (memcmp(a, b, sizeof(dns_rrl_key_t)) == 0) + return (ISC_TRUE); + return (ISC_FALSE); +} + +static inline isc_uint32_t +hash_key(const dns_rrl_key_t *key) { + isc_uint32_t hval; + int i; + + hval = key->w[0]; + for (i = sizeof(*key) / sizeof(key->w[0]) - 1; i >= 0; --i) { + hval = key->w[i] + (hval<<1); + } + return (hval); +} + +/* + * Construct the hash table key. + * Use a hash of the DNS query name to save space in the database. + * Collisions result in legitimate rate limiting responses for one + * query name also limiting responses for other names to the + * same client. This is rare and benign enough given the large + * space costs compared to keeping the entire name in the database + * entry or the time costs of dynamic allocation. + */ +static void +make_key(const dns_rrl_t *rrl, dns_rrl_key_t *key, + const isc_sockaddr_t *client_addr, + dns_rdatatype_t qtype, dns_name_t *qname, dns_rdataclass_t qclass, + dns_rrl_rtype_t rtype) +{ + dns_name_t base; + dns_offsets_t base_offsets; + int labels, i; + + memset(key, 0, sizeof(*key)); + + key->s.rtype = rtype; + if (rtype == DNS_RRL_RTYPE_QUERY) { + key->s.qtype = qtype; + key->s.qclass = qclass & 0xff; + } else if (rtype == DNS_RRL_RTYPE_REFERRAL || + rtype == DNS_RRL_RTYPE_NODATA) { + /* + * Because there is no qtype in the empty answer sections of + * referral and NODATA responses, count them as the same. + */ + key->s.qclass = qclass & 0xff; + } + + if (qname != NULL && qname->labels != 0) { + /* + * Ignore the first label of wildcards. + */ + if ((qname->attributes & DNS_NAMEATTR_WILDCARD) != 0 && + (labels = dns_name_countlabels(qname)) > 1) + { + dns_name_init(&base, base_offsets); + dns_name_getlabelsequence(qname, 1, labels-1, &base); + key->s.qname_hash = dns_name_hashbylabel(&base, + ISC_FALSE); + } else { + key->s.qname_hash = dns_name_hashbylabel(qname, + ISC_FALSE); + } + } + + switch (client_addr->type.sa.sa_family) { + case AF_INET: + key->s.ip[0] = (client_addr->type.sin.sin_addr.s_addr & + rrl->ipv4_mask); + break; + case AF_INET6: + key->s.ipv6 = ISC_TRUE; + memcpy(key->s.ip, &client_addr->type.sin6.sin6_addr, + sizeof(key->s.ip)); + for (i = 0; i < DNS_RRL_MAX_PREFIX/32; ++i) + key->s.ip[i] &= rrl->ipv6_mask[i]; + break; + } +} + +static inline dns_rrl_rate_t * +get_rate(dns_rrl_t *rrl, dns_rrl_rtype_t rtype) { + switch (rtype) { + case DNS_RRL_RTYPE_QUERY: + return (&rrl->responses_per_second); + case DNS_RRL_RTYPE_REFERRAL: + return (&rrl->referrals_per_second); + case DNS_RRL_RTYPE_NODATA: + return (&rrl->nodata_per_second); + case DNS_RRL_RTYPE_NXDOMAIN: + return (&rrl->nxdomains_per_second); + case DNS_RRL_RTYPE_ERROR: + return (&rrl->errors_per_second); + case DNS_RRL_RTYPE_ALL: + return (&rrl->all_per_second); + default: + INSIST(0); + } + return (NULL); +} + +static int +response_balance(dns_rrl_t *rrl, const dns_rrl_entry_t *e, int age) { + dns_rrl_rate_t *ratep; + int balance, rate; + + if (e->key.s.rtype == DNS_RRL_RTYPE_TCP) { + rate = 1; + } else { + ratep = get_rate(rrl, e->key.s.rtype); + rate = ratep->scaled; + } + + balance = e->responses + age * rate; + if (balance > rate) + balance = rate; + return (balance); +} + +/* + * Search for an entry for a response and optionally create it. + */ +static dns_rrl_entry_t * +get_entry(dns_rrl_t *rrl, const isc_sockaddr_t *client_addr, + dns_rdataclass_t qclass, dns_rdatatype_t qtype, dns_name_t *qname, + dns_rrl_rtype_t rtype, isc_stdtime_t now, isc_boolean_t create, + char *log_buf, unsigned int log_buf_len) +{ + dns_rrl_key_t key; + isc_uint32_t hval; + dns_rrl_entry_t *e; + dns_rrl_hash_t *hash; + dns_rrl_bin_t *new_bin, *old_bin; + int probes, age; + + make_key(rrl, &key, client_addr, qtype, qname, qclass, rtype); + hval = hash_key(&key); + + /* + * Look for the entry in the current hash table. + */ + new_bin = get_bin(rrl->hash, hval); + probes = 1; + e = ISC_LIST_HEAD(*new_bin); + while (e != NULL) { + if (key_cmp(&e->key, &key)) { + ref_entry(rrl, e, probes, now); + return (e); + } + ++probes; + e = ISC_LIST_NEXT(e, hlink); + } + + /* + * Look in the old hash table. + */ + if (rrl->old_hash != NULL) { + old_bin = get_bin(rrl->old_hash, hval); + e = ISC_LIST_HEAD(*old_bin); + while (e != NULL) { + if (key_cmp(&e->key, &key)) { + ISC_LIST_UNLINK(*old_bin, e, hlink); + ISC_LIST_PREPEND(*new_bin, e, hlink); + e->hash_gen = rrl->hash_gen; + ref_entry(rrl, e, probes, now); + return (e); + } + e = ISC_LIST_NEXT(e, hlink); + } + + /* + * Discard prevous hash table when all of its entries are old. + */ + age = delta_rrl_time(rrl->old_hash->check_time, now); + if (age > rrl->window) + free_old_hash(rrl); + } + + if (!create) + return (NULL); + + /* + * The entry does not exist, so create it by finding a free entry. + * Keep currently penalized and logged entries. + * Try to make more entries if none are idle. + * Steal the oldest entry if we cannot create more. + */ + for (e = ISC_LIST_TAIL(rrl->lru); + e != NULL; + e = ISC_LIST_PREV(e, lru)) + { + if (!ISC_LINK_LINKED(e, hlink)) + break; + age = get_age(rrl, e, now); + if (age <= 1) { + e = NULL; + break; + } + if (!e->logged && response_balance(rrl, e, age) > 0) + break; + } + if (e == NULL) { + expand_entries(rrl, ISC_MIN((rrl->num_entries+1)/2, 1000)); + e = ISC_LIST_TAIL(rrl->lru); + } + if (e->logged) + log_end(rrl, e, ISC_TRUE, log_buf, log_buf_len); + if (ISC_LINK_LINKED(e, hlink)) { + if (e->hash_gen == rrl->hash_gen) + hash = rrl->hash; + else + hash = rrl->old_hash; + old_bin = get_bin(hash, hash_key(&e->key)); + ISC_LIST_UNLINK(*old_bin, e, hlink); + } + ISC_LIST_PREPEND(*new_bin, e, hlink); + e->hash_gen = rrl->hash_gen; + e->key = key; + e->ts_valid = ISC_FALSE; + ref_entry(rrl, e, probes, now); + return (e); +} + +static void +debit_log(const dns_rrl_entry_t *e, int age, const char *action) { + char buf[sizeof("age=12345678")]; + const char *age_str; + + if (age == DNS_RRL_FOREVER) { + age_str = ""; + } else { + snprintf(buf, sizeof(buf), "age=%d", age); + age_str = buf; + } + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, DNS_RRL_LOG_DEBUG3, + "rrl %08x %6s responses=%-3d %s", + hash_key(&e->key), age_str, e->responses, action); +} + +static inline dns_rrl_result_t +debit_rrl_entry(dns_rrl_t *rrl, dns_rrl_entry_t *e, double qps, double scale, + const isc_sockaddr_t *client_addr, isc_stdtime_t now, + char *log_buf, unsigned int log_buf_len) +{ + int rate, new_rate, slip, new_slip, age, log_secs, min; + dns_rrl_rate_t *ratep; + dns_rrl_entry_t const *credit_e; + + /* + * Pick the rate counter. + * Optionally adjust the rate by the estimated query/second rate. + */ + ratep = get_rate(rrl, e->key.s.rtype); + rate = ratep->r; + if (rate == 0) + return (DNS_RRL_RESULT_OK); + + if (scale < 1.0) { + /* + * The limit for clients that have used TCP is not scaled. + */ + credit_e = get_entry(rrl, client_addr, + 0, dns_rdatatype_none, NULL, + DNS_RRL_RTYPE_TCP, now, ISC_FALSE, + log_buf, log_buf_len); + if (credit_e != NULL) { + age = get_age(rrl, e, now); + if (age < rrl->window) + scale = 1.0; + } + } + if (scale < 1.0) { + new_rate = (int) (rate * scale); + if (new_rate < 1) + new_rate = 1; + if (ratep->scaled != new_rate) { + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, + DNS_RRL_LOG_DEBUG1, + "%d qps scaled %s by %.2f" + " from %d to %d", + (int)qps, ratep->str, scale, + rate, new_rate); + rate = new_rate; + ratep->scaled = rate; + } + } + + min = -rrl->window * rate; + + /* + * Treat time jumps into the recent past as no time. + * Treat entries older than the window as if they were just created + * Credit other entries. + */ + age = get_age(rrl, e, now); + if (age > 0) { + /* + * Credit tokens earned during elapsed time. + */ + if (age > rrl->window) { + e->responses = rate; + e->slip_cnt = 0; + } else { + e->responses += rate*age; + if (e->responses > rate) { + e->responses = rate; + e->slip_cnt = 0; + } + } + /* + * Find the seconds since last log message without overflowing + * small counter. This counter is reset when an entry is + * created. It is not necessarily reset when some requests + * are answered provided other requests continue to be dropped + * or slipped. This can happen when the request rate is just + * at the limit. + */ + if (e->logged) { + log_secs = e->log_secs; + log_secs += age; + if (log_secs > DNS_RRL_MAX_LOG_SECS || log_secs < 0) + log_secs = DNS_RRL_MAX_LOG_SECS; + e->log_secs = log_secs; + } + } + set_age(rrl, e, now); + + /* + * Debit the entry for this response. + */ + if (--e->responses >= 0) { + if (isc_log_wouldlog(dns_lctx, DNS_RRL_LOG_DEBUG3)) + debit_log(e, age, ""); + return (DNS_RRL_RESULT_OK); + } + + if (e->responses < min) + e->responses = min; + + /* + * Drop this response unless it should slip or leak. + */ + slip = rrl->slip.r; + if (slip > 2 && scale < 1.0) { + new_slip = (int) (slip * scale); + if (new_slip < 2) + new_slip = 2; + if (rrl->slip.scaled != new_slip) { + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, + DNS_RRL_LOG_DEBUG1, + "%d qps scaled slip" + " by %.2f from %d to %d", + (int)qps, scale, + slip, new_slip); + slip = new_slip; + rrl->slip.scaled = slip; + } + } + if (slip != 0 && e->key.s.rtype != DNS_RRL_RTYPE_ALL) { + if (e->slip_cnt++ == 0) { + if ((int) e->slip_cnt >= slip) + e->slip_cnt = 0; + if (isc_log_wouldlog(dns_lctx, DNS_RRL_LOG_DEBUG3)) + debit_log(e, age, "slip"); + return (DNS_RRL_RESULT_SLIP); + } else if ((int) e->slip_cnt >= slip) { + e->slip_cnt = 0; + } + } + + if (isc_log_wouldlog(dns_lctx, DNS_RRL_LOG_DEBUG3)) + debit_log(e, age, "drop"); + return (DNS_RRL_RESULT_DROP); +} + +static inline dns_rrl_qname_buf_t * +get_qname(dns_rrl_t *rrl, const dns_rrl_entry_t *e) { + dns_rrl_qname_buf_t *qbuf; + + qbuf = rrl->qnames[e->log_qname]; + if (qbuf == NULL || qbuf->e != e) + return (NULL); + return (qbuf); +} + +static inline void +free_qname(dns_rrl_t *rrl, dns_rrl_entry_t *e) { + dns_rrl_qname_buf_t *qbuf; + + qbuf = get_qname(rrl, e); + if (qbuf != NULL) { + qbuf->e = NULL; + ISC_LIST_APPEND(rrl->qname_free, qbuf, link); + } +} + +static void +add_log_str(isc_buffer_t *lb, const char *str, unsigned int str_len) { + isc_region_t region; + + isc_buffer_availableregion(lb, ®ion); + if (str_len >= region.length) { + if (region.length <= 0) + return; + str_len = region.length; + } + memcpy(region.base, str, str_len); + isc_buffer_add(lb, str_len); +} + +#define ADD_LOG_CSTR(eb, s) add_log_str(eb, s, sizeof(s)-1) + +/* + * Build strings for the logs + */ +static void +make_log_buf(dns_rrl_t *rrl, dns_rrl_entry_t *e, + const char *str1, const char *str2, isc_boolean_t plural, + dns_name_t *qname, isc_boolean_t save_qname, + dns_rrl_result_t rrl_result, isc_result_t resp_result, + char *log_buf, unsigned int log_buf_len) +{ + isc_buffer_t lb; + dns_rrl_qname_buf_t *qbuf; + isc_netaddr_t cidr; + char strbuf[ISC_MAX(sizeof("/123"), sizeof(" (12345678)"))]; + const char *rstr; + isc_result_t msg_result; + + if (log_buf_len <= 1) { + if (log_buf_len == 1) + log_buf[0] = '\0'; + return; + } + isc_buffer_init(&lb, log_buf, log_buf_len-1); + + if (str1 != NULL) + add_log_str(&lb, str1, strlen(str1)); + if (str2 != NULL) + add_log_str(&lb, str2, strlen(str2)); + + switch (rrl_result) { + case DNS_RRL_RESULT_OK: + break; + case DNS_RRL_RESULT_DROP: + ADD_LOG_CSTR(&lb, "drop "); + break; + case DNS_RRL_RESULT_SLIP: + ADD_LOG_CSTR(&lb, "slip "); + break; + default: + INSIST(0); + break; + } + + switch (e->key.s.rtype) { + case DNS_RRL_RTYPE_QUERY: + break; + case DNS_RRL_RTYPE_REFERRAL: + ADD_LOG_CSTR(&lb, "referral "); + break; + case DNS_RRL_RTYPE_NODATA: + ADD_LOG_CSTR(&lb, "NODATA "); + break; + case DNS_RRL_RTYPE_NXDOMAIN: + ADD_LOG_CSTR(&lb, "NXDOMAIN "); + break; + case DNS_RRL_RTYPE_ERROR: + if (resp_result == ISC_R_SUCCESS) { + ADD_LOG_CSTR(&lb, "error "); + } else { + rstr = isc_result_totext(resp_result); + add_log_str(&lb, rstr, strlen(rstr)); + ADD_LOG_CSTR(&lb, " error "); + } + break; + case DNS_RRL_RTYPE_ALL: + ADD_LOG_CSTR(&lb, "all "); + break; + default: + INSIST(0); + } + + if (plural) + ADD_LOG_CSTR(&lb, "responses to "); + else + ADD_LOG_CSTR(&lb, "response to "); + + memset(&cidr, 0, sizeof(cidr)); + if (e->key.s.ipv6) { + snprintf(strbuf, sizeof(strbuf), "/%d", rrl->ipv6_prefixlen); + cidr.family = AF_INET6; + memset(&cidr.type.in6, 0, sizeof(cidr.type.in6)); + memcpy(&cidr.type.in6, e->key.s.ip, sizeof(e->key.s.ip)); + } else { + snprintf(strbuf, sizeof(strbuf), "/%d", rrl->ipv4_prefixlen); + cidr.family = AF_INET; + cidr.type.in.s_addr = e->key.s.ip[0]; + } + msg_result = isc_netaddr_totext(&cidr, &lb); + if (msg_result != ISC_R_SUCCESS) + ADD_LOG_CSTR(&lb, "?"); + add_log_str(&lb, strbuf, strlen(strbuf)); + + if (e->key.s.rtype == DNS_RRL_RTYPE_QUERY || + e->key.s.rtype == DNS_RRL_RTYPE_REFERRAL || + e->key.s.rtype == DNS_RRL_RTYPE_NODATA || + e->key.s.rtype == DNS_RRL_RTYPE_NXDOMAIN) { + qbuf = get_qname(rrl, e); + if (save_qname && qbuf == NULL && + qname != NULL && dns_name_isabsolute(qname)) { + /* + * Capture the qname for the "stop limiting" message. + */ + qbuf = ISC_LIST_TAIL(rrl->qname_free); + if (qbuf != NULL) { + ISC_LIST_UNLINK(rrl->qname_free, qbuf, link); + } else if (rrl->num_qnames < DNS_RRL_QNAMES) { + qbuf = isc_mem_get(rrl->mctx, sizeof(*qbuf)); + if (qbuf != NULL) { + memset(qbuf, 0, sizeof(*qbuf)); + ISC_LINK_INIT(qbuf, link); + qbuf->index = rrl->num_qnames; + rrl->qnames[rrl->num_qnames++] = qbuf; + } else { + isc_log_write(dns_lctx, + DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, + DNS_RRL_LOG_FAIL, + "isc_mem_get(%d)" + " failed for RRL qname", + (int)sizeof(*qbuf)); + } + } + if (qbuf != NULL) { + e->log_qname = qbuf->index; + qbuf->e = e; + dns_fixedname_init(&qbuf->qname); + dns_name_copy(qname, + dns_fixedname_name(&qbuf->qname), + NULL); + } + } + if (qbuf != NULL) + qname = dns_fixedname_name(&qbuf->qname); + if (qname != NULL) { + ADD_LOG_CSTR(&lb, " for "); + (void)dns_name_totext(qname, ISC_TRUE, &lb); + } else { + ADD_LOG_CSTR(&lb, " for (?)"); + } + if (e->key.s.rtype != DNS_RRL_RTYPE_NXDOMAIN) { + ADD_LOG_CSTR(&lb, " "); + (void)dns_rdataclass_totext(e->key.s.qclass, &lb); + if (e->key.s.rtype == DNS_RRL_RTYPE_QUERY) { + ADD_LOG_CSTR(&lb, " "); + (void)dns_rdatatype_totext(e->key.s.qtype, &lb); + } + } + snprintf(strbuf, sizeof(strbuf), " (%08x)", + e->key.s.qname_hash); + add_log_str(&lb, strbuf, strlen(strbuf)); + } + + /* + * We saved room for '\0'. + */ + log_buf[isc_buffer_usedlength(&lb)] = '\0'; +} + +static void +log_end(dns_rrl_t *rrl, dns_rrl_entry_t *e, isc_boolean_t early, + char *log_buf, unsigned int log_buf_len) +{ + if (e->logged) { + make_log_buf(rrl, e, + early ? "*" : NULL, + rrl->log_only ? "would stop limiting " + : "stop limiting ", + ISC_TRUE, NULL, ISC_FALSE, + DNS_RRL_RESULT_OK, ISC_R_SUCCESS, + log_buf, log_buf_len); + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, DNS_RRL_LOG_DROP, + "%s", log_buf); + free_qname(rrl, e); + e->logged = ISC_FALSE; + --rrl->num_logged; + } +} + +/* + * Log messages for streams that have stopped being rate limited. + */ +static void +log_stops(dns_rrl_t *rrl, isc_stdtime_t now, int limit, + char *log_buf, unsigned int log_buf_len) +{ + dns_rrl_entry_t *e; + int age; + + for (e = rrl->last_logged; e != NULL; e = ISC_LIST_PREV(e, lru)) { + if (!e->logged) + continue; + if (now != 0) { + age = get_age(rrl, e, now); + if (age < DNS_RRL_STOP_LOG_SECS || + response_balance(rrl, e, age) < 0) + break; + } + + log_end(rrl, e, now == 0, log_buf, log_buf_len); + if (rrl->num_logged <= 0) + break; + + /* + * Too many messages could stall real work. + */ + if (--limit < 0) { + rrl->last_logged = ISC_LIST_PREV(e, lru); + return; + } + } + if (e == NULL) { + INSIST(rrl->num_logged == 0); + rrl->log_stops_time = now; + } + rrl->last_logged = e; +} + +/* + * Main rate limit interface. + */ +dns_rrl_result_t +dns_rrl(dns_view_t *view, + const isc_sockaddr_t *client_addr, isc_boolean_t is_tcp, + dns_rdataclass_t qclass, dns_rdatatype_t qtype, + dns_name_t *qname, isc_result_t resp_result, isc_stdtime_t now, + isc_boolean_t wouldlog, char *log_buf, unsigned int log_buf_len) +{ + dns_rrl_t *rrl; + dns_rrl_rtype_t rtype; + dns_rrl_entry_t *e; + isc_netaddr_t netclient; + int secs; + double qps, scale; + int exempt_match; + isc_result_t result; + dns_rrl_result_t rrl_result; + + INSIST(log_buf != NULL && log_buf_len > 0); + + rrl = view->rrl; + if (rrl->exempt != NULL) { + isc_netaddr_fromsockaddr(&netclient, client_addr); + result = dns_acl_match(&netclient, NULL, rrl->exempt, + &view->aclenv, &exempt_match, NULL); + if (result == ISC_R_SUCCESS && exempt_match > 0) + return (DNS_RRL_RESULT_OK); + } + + LOCK(&rrl->lock); + + /* + * Estimate total query per second rate when scaling by qps. + */ + if (rrl->qps_scale == 0) { + qps = 0.0; + scale = 1.0; + } else { + ++rrl->qps_responses; + secs = delta_rrl_time(rrl->qps_time, now); + if (secs <= 0) { + qps = rrl->qps; + } else { + qps = (1.0*rrl->qps_responses) / secs; + if (secs >= rrl->window) { + if (isc_log_wouldlog(dns_lctx, + DNS_RRL_LOG_DEBUG3)) + isc_log_write(dns_lctx, + DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, + DNS_RRL_LOG_DEBUG3, + "%d responses/%d seconds" + " = %d qps", + rrl->qps_responses, secs, + (int)qps); + rrl->qps = qps; + rrl->qps_responses = 0; + rrl->qps_time = now; + } else if (qps < rrl->qps) { + qps = rrl->qps; + } + } + scale = rrl->qps_scale / qps; + } + + /* + * Do maintenance once per second. + */ + if (rrl->num_logged > 0 && rrl->log_stops_time != now) + log_stops(rrl, now, 8, log_buf, log_buf_len); + + /* + * Notice TCP responses when scaling limits by qps. + * Do not try to rate limit TCP responses. + */ + if (is_tcp) { + if (scale < 1.0) { + e = get_entry(rrl, client_addr, + 0, dns_rdatatype_none, NULL, + DNS_RRL_RTYPE_TCP, now, ISC_TRUE, + log_buf, log_buf_len); + if (e != NULL) { + e->responses = -(rrl->window+1); + set_age(rrl, e, now); + } + } + UNLOCK(&rrl->lock); + return (ISC_R_SUCCESS); + } + + /* + * Find the right kind of entry, creating it if necessary. + * If that is impossible, then nothing more can be done + */ + switch (resp_result) { + case ISC_R_SUCCESS: + rtype = DNS_RRL_RTYPE_QUERY; + break; + case DNS_R_DELEGATION: + rtype = DNS_RRL_RTYPE_REFERRAL; + break; + case DNS_R_NXRRSET: + rtype = DNS_RRL_RTYPE_NODATA; + break; + case DNS_R_NXDOMAIN: + rtype = DNS_RRL_RTYPE_NXDOMAIN; + break; + default: + rtype = DNS_RRL_RTYPE_ERROR; + break; + } + e = get_entry(rrl, client_addr, qclass, qtype, qname, rtype, + now, ISC_TRUE, log_buf, log_buf_len); + if (e == NULL) { + UNLOCK(&rrl->lock); + return (DNS_RRL_RESULT_OK); + } + + if (isc_log_wouldlog(dns_lctx, DNS_RRL_LOG_DEBUG1)) { + /* + * Do not worry about speed or releasing the lock. + * This message appears before messages from debit_rrl_entry(). + */ + make_log_buf(rrl, e, "consider limiting ", NULL, ISC_FALSE, + qname, ISC_FALSE, DNS_RRL_RESULT_OK, resp_result, + log_buf, log_buf_len); + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, DNS_RRL_LOG_DEBUG1, + "%s", log_buf); + } + + rrl_result = debit_rrl_entry(rrl, e, qps, scale, client_addr, now, + log_buf, log_buf_len); + + if (rrl->all_per_second.r != 0) { + /* + * We must debit the all-per-second token bucket if we have + * an all-per-second limit for the IP address. + * The all-per-second limit determines the log message + * when both limits are hit. + * The response limiting must continue if the + * all-per-second limiting lapses. + */ + dns_rrl_entry_t *e_all; + dns_rrl_result_t rrl_all_result; + + e_all = get_entry(rrl, client_addr, + 0, dns_rdatatype_none, NULL, + DNS_RRL_RTYPE_ALL, now, ISC_TRUE, + log_buf, log_buf_len); + if (e_all == NULL) { + UNLOCK(&rrl->lock); + return (DNS_RRL_RESULT_OK); + } + rrl_all_result = debit_rrl_entry(rrl, e_all, qps, scale, + client_addr, now, + log_buf, log_buf_len); + if (rrl_all_result != DNS_RRL_RESULT_OK) { + int level; + + e = e_all; + rrl_result = rrl_all_result; + if (rrl_result == DNS_RRL_RESULT_OK) + level = DNS_RRL_LOG_DEBUG2; + else + level = DNS_RRL_LOG_DEBUG1; + if (isc_log_wouldlog(dns_lctx, level)) { + make_log_buf(rrl, e, + "prefer all-per-second limiting ", + NULL, ISC_TRUE, qname, ISC_FALSE, + DNS_RRL_RESULT_OK, resp_result, + log_buf, log_buf_len); + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, level, + "%s", log_buf); + } + } + } + + if (rrl_result == DNS_RRL_RESULT_OK) { + UNLOCK(&rrl->lock); + return (DNS_RRL_RESULT_OK); + } + + /* + * Log occassionally in the rate-limit category. + */ + if ((!e->logged || e->log_secs >= DNS_RRL_MAX_LOG_SECS) && + isc_log_wouldlog(dns_lctx, DNS_RRL_LOG_DROP)) { + make_log_buf(rrl, e, rrl->log_only ? "would " : NULL, + e->logged ? "continue limiting " : "limit ", + ISC_TRUE, qname, ISC_TRUE, + DNS_RRL_RESULT_OK, resp_result, + log_buf, log_buf_len); + if (!e->logged) { + e->logged = ISC_TRUE; + if (++rrl->num_logged <= 1) + rrl->last_logged = e; + } + e->log_secs = 0; + + /* + * Avoid holding the lock. + */ + if (!wouldlog) { + UNLOCK(&rrl->lock); + e = NULL; + } + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RRL, + DNS_LOGMODULE_REQUEST, DNS_RRL_LOG_DROP, + "%s", log_buf); + } + + /* + * Make a log message for the caller. + */ + if (wouldlog) + make_log_buf(rrl, e, + rrl->log_only ? "would rate limit " : "rate limit ", + NULL, ISC_FALSE, qname, ISC_FALSE, + rrl_result, resp_result, log_buf, log_buf_len); + + if (e != NULL) { + /* + * Do not save the qname unless we might need it for + * the ending log message. + */ + if (!e->logged) + free_qname(rrl, e); + UNLOCK(&rrl->lock); + } + + return (rrl_result); +} + +void +dns_rrl_view_destroy(dns_view_t *view) { + dns_rrl_t *rrl; + dns_rrl_block_t *b; + dns_rrl_hash_t *h; + char log_buf[DNS_RRL_LOG_BUF_LEN]; + int i; + + rrl = view->rrl; + if (rrl == NULL) + return; + view->rrl = NULL; + + /* + * Assume the caller takes care of locking the view and anything else. + */ + + if (rrl->num_logged > 0) + log_stops(rrl, 0, ISC_INT32_MAX, log_buf, sizeof(log_buf)); + + for (i = 0; i < DNS_RRL_QNAMES; ++i) { + if (rrl->qnames[i] == NULL) + break; + isc_mem_put(rrl->mctx, rrl->qnames[i], sizeof(*rrl->qnames[i])); + } + + if (rrl->exempt != NULL) + dns_acl_detach(&rrl->exempt); + + DESTROYLOCK(&rrl->lock); + + while (!ISC_LIST_EMPTY(rrl->blocks)) { + b = ISC_LIST_HEAD(rrl->blocks); + ISC_LIST_UNLINK(rrl->blocks, b, link); + isc_mem_put(rrl->mctx, b, b->size); + } + + h = rrl->hash; + if (h != NULL) + isc_mem_put(rrl->mctx, h, + sizeof(*h) + (h->length - 1) * sizeof(h->bins[0])); + + h = rrl->old_hash; + if (h != NULL) + isc_mem_put(rrl->mctx, h, + sizeof(*h) + (h->length - 1) * sizeof(h->bins[0])); + + isc_mem_putanddetach(&rrl->mctx, rrl, sizeof(*rrl)); +} + +isc_result_t +dns_rrl_init(dns_rrl_t **rrlp, dns_view_t *view, int min_entries) { + dns_rrl_t *rrl; + isc_result_t result; + + *rrlp = NULL; + + rrl = isc_mem_get(view->mctx, sizeof(*rrl)); + if (rrl == NULL) + return (ISC_R_NOMEMORY); + memset(rrl, 0, sizeof(*rrl)); + isc_mem_attach(view->mctx, &rrl->mctx); + result = isc_mutex_init(&rrl->lock); + if (result != ISC_R_SUCCESS) { + isc_mem_putanddetach(&rrl->mctx, rrl, sizeof(*rrl)); + return (result); + } + isc_stdtime_get(&rrl->ts_bases[0]); + + view->rrl = rrl; + + result = expand_entries(rrl, min_entries); + if (result != ISC_R_SUCCESS) { + dns_rrl_view_destroy(view); + return (result); + } + result = expand_rrl_hash(rrl, 0); + if (result != ISC_R_SUCCESS) { + dns_rrl_view_destroy(view); + return (result); + } + + *rrlp = rrl; + return (ISC_R_SUCCESS); +} diff --git a/lib/dns/view.c b/lib/dns/view.c index 021198fa70f..7daf64fae79 100644 --- a/lib/dns/view.c +++ b/lib/dns/view.c @@ -49,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -184,6 +185,7 @@ dns_view_create(isc_mem_t *mctx, dns_rdataclass_t rdclass, view->answeracl_exclude = NULL; view->denyanswernames = NULL; view->answernames_exclude = NULL; + view->rrl = NULL; view->provideixfr = ISC_TRUE; view->maxcachettl = 7 * 24 * 3600; view->maxncachettl = 3 * 3600; @@ -335,10 +337,16 @@ destroy(dns_view_t *view) { dns_acache_detach(&view->acache); } dns_rpz_view_destroy(view); -#else +#ifdef USE_RRL + dns_rrl_view_destroy(view); +#else /* USE_RRL */ + INSIST(view->rrl == NULL); +#endif /* USE_RRL */ +#else /* BIND9 */ INSIST(view->acache == NULL); INSIST(ISC_LIST_EMPTY(view->rpz_zones)); -#endif + INSIST(view->rrl == NULL); +#endif /* BIND9 */ if (view->requestmgr != NULL) dns_requestmgr_detach(&view->requestmgr); if (view->task != NULL) diff --git a/lib/dns/win32/libdns.def b/lib/dns/win32/libdns.def index 3446d1d49e2..4632763568b 100644 --- a/lib/dns/win32/libdns.def +++ b/lib/dns/win32/libdns.def @@ -657,6 +657,9 @@ dns_rriterator_init dns_rriterator_next dns_rriterator_nextrrset dns_rriterator_pause +dns_rrl +dns_rrl_init +dns_rrl_view_destroy dns_sdb_putnamedrr dns_sdb_putrdata dns_sdb_putrr diff --git a/lib/dns/win32/libdns.dsp b/lib/dns/win32/libdns.dsp index a4120ac84f6..a9f8fdad50e 100644 --- a/lib/dns/win32/libdns.dsp +++ b/lib/dns/win32/libdns.dsp @@ -346,6 +346,10 @@ SOURCE=..\include\dns\rpz.h # End Source File # Begin Source File +SOURCE=..\include\dns\rrl.h +# End Source File +# Begin Source File + SOURCE=..\include\dns\rriterator.h # End Source File # Begin Source File @@ -650,6 +654,10 @@ SOURCE=..\rpz.c # End Source File # Begin Source File +SOURCE=..\rrl.c +# End Source File +# Begin Source File + SOURCE=..\rriterator.c # End Source File # Begin Source File diff --git a/lib/dns/win32/libdns.mak b/lib/dns/win32/libdns.mak index ead7f828ff3..cbdf8585f0a 100644 --- a/lib/dns/win32/libdns.mak +++ b/lib/dns/win32/libdns.mak @@ -184,6 +184,7 @@ CLEAN : -@erase "$(INTDIR)\result.obj" -@erase "$(INTDIR)\rootns.obj" -@erase "$(INTDIR)\rpz.obj" + -@erase "$(INTDIR)\rrl.obj" -@erase "$(INTDIR)\sdb.obj" -@erase "$(INTDIR)\sdlz.obj" -@erase "$(INTDIR)\soa.obj" @@ -309,6 +310,7 @@ LINK32_OBJS= \ "$(INTDIR)\result.obj" \ "$(INTDIR)\rootns.obj" \ "$(INTDIR)\rpz.obj" \ + "$(INTDIR)\rrl.obj" \ "$(INTDIR)\rriterator.obj" \ "$(INTDIR)\sdb.obj" \ "$(INTDIR)\sdlz.obj" \ @@ -505,6 +507,8 @@ CLEAN : -@erase "$(INTDIR)\rootns.sbr" -@erase "$(INTDIR)\rpz.obj" -@erase "$(INTDIR)\rpz.sbr" + -@erase "$(INTDIR)\rrl.obj" + -@erase "$(INTDIR)\rrl.sbr" -@erase "$(INTDIR)\rriterator.obj" -@erase "$(INTDIR)\rriterator.sbr" -@erase "$(INTDIR)\sdb.obj" @@ -651,6 +655,7 @@ BSC32_SBRS= \ "$(INTDIR)\result.sbr" \ "$(INTDIR)\rootns.sbr" \ "$(INTDIR)\rpz.sbr" \ + "$(INTDIR)\rrl.sbr" \ "$(INTDIR)\rriterator.sbr" \ "$(INTDIR)\sdb.sbr" \ "$(INTDIR)\sdlz.sbr" \ @@ -748,6 +753,7 @@ LINK32_OBJS= \ "$(INTDIR)\result.obj" \ "$(INTDIR)\rootns.obj" \ "$(INTDIR)\rpz.obj" \ + "$(INTDIR)\rrl.obj" \ "$(INTDIR)\rriterator.obj" \ "$(INTDIR)\sdb.obj" \ "$(INTDIR)\sdlz.obj" \ @@ -1724,6 +1730,24 @@ SOURCE=..\rpz.c $(CPP) $(CPP_PROJ) $(SOURCE) +!ENDIF + +SOURCE=..\rrl.c + +!IF "$(CFG)" == "libdns - Win32 Release" + + +"$(INTDIR)\rrl.obj" : $(SOURCE) "$(INTDIR)" + $(CPP) $(CPP_PROJ) $(SOURCE) + + +!ELSEIF "$(CFG)" == "libdns - Win32 Debug" + + +"$(INTDIR)\rrl.obj" "$(INTDIR)\rrl.sbr" : $(SOURCE) "$(INTDIR)" + $(CPP) $(CPP_PROJ) $(SOURCE) + + !ENDIF SOURCE=..\rriterator.c diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c index 431af746338..15fd55499d3 100644 --- a/lib/isccfg/namedconf.c +++ b/lib/isccfg/namedconf.c @@ -1269,6 +1269,39 @@ static cfg_type_t cfg_type_rpz = { rpz_fields }; +#ifdef USE_RRL +/* + * rate-limit + */ +static cfg_clausedef_t rrl_clauses[] = { + { "responses-per-second", &cfg_type_uint32, 0 }, + { "referrals-per-second", &cfg_type_uint32, 0 }, + { "nodata-per-second", &cfg_type_uint32, 0 }, + { "nxdomains-per-second", &cfg_type_uint32, 0 }, + { "errors-per-second", &cfg_type_uint32, 0 }, + { "all-per-second", &cfg_type_uint32, 0 }, + { "slip", &cfg_type_uint32, 0 }, + { "window", &cfg_type_uint32, 0 }, + { "log-only", &cfg_type_boolean, 0 }, + { "qps-scale", &cfg_type_uint32, 0 }, + { "ipv4-prefix-length", &cfg_type_uint32, 0 }, + { "ipv6-prefix-length", &cfg_type_uint32, 0 }, + { "exempt-clients", &cfg_type_bracketed_aml, 0 }, + { "max-table-size", &cfg_type_uint32, 0 }, + { "min-table-size", &cfg_type_uint32, 0 }, + { NULL, NULL, 0 } +}; + +static cfg_clausedef_t *rrl_clausesets[] = { + rrl_clauses, + NULL +}; + +static cfg_type_t cfg_type_rrl = { + "rate-limit", cfg_parse_map, cfg_print_map, cfg_doc_map, + &cfg_rep_map, rrl_clausesets +}; +#endif /* USE_RRL */ /*% * dnssec-lookaside @@ -1423,6 +1456,9 @@ view_clauses[] = { CFG_CLAUSEFLAG_NOTCONFIGURED }, #endif { "response-policy", &cfg_type_rpz, 0 }, +#ifdef USE_RRL + { "rate-limit", &cfg_type_rrl, 0 }, +#endif /* USE_RRL */ { NULL, NULL, 0 } };