From bf52156ae0ad2755fdced005a86ea2e82f24d236 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Wed, 3 Feb 2021 16:17:21 +0100 Subject: [PATCH] Add per-client rate-limiting. The default limit is 1000 queries in 60 seconds. Signed-off-by: DL6ER --- src/config.c | 21 ++++++++- src/config.h | 6 ++- src/database/query-table.c | 2 +- src/datastructure.h | 3 +- src/dnsmasq_interface.c | 92 +++++++++++++++++++++++--------------- src/gc.c | 31 ++++++++++--- 6 files changed, 109 insertions(+), 46 deletions(-) diff --git a/src/config.c b/src/config.c index 713600cc8..eed80726b 100644 --- a/src/config.c +++ b/src/config.c @@ -461,6 +461,25 @@ void read_FTLconf(void) logg(" REFRESH_HOSTNAMES: Periodically refreshing IPv4 names"); } + // RATE_LIMIT + // defaults to: 1000 queries / 60 seconds + config.rate_limit.count = 1000; + config.rate_limit.interval = 60; + buffer = parse_FTLconf(fp, "RATE_LIMIT"); + + unsigned int count = 0, interval = 0; + if(buffer != NULL && sscanf(buffer, "%u/%u", &count, &interval) == 2) + { + config.rate_limit.count = count; + config.rate_limit.interval = interval; + } + + if(config.rate_limit.count > 0) + logg(" RATE_LIMIT: Rate-limiting client making more than %u queries in %u second%s", + config.rate_limit.count, config.rate_limit.interval, config.rate_limit.interval == 1 ? "" : "s"); + else + logg(" RATE_LIMIT: Disabled"); + // Read DEBUG_... setting from pihole-FTL.conf read_debuging_settings(fp); @@ -523,7 +542,7 @@ static char *parse_FTLconf(FILE *fp, const char * key) // Go to beginning of file fseek(fp, 0L, SEEK_SET); - + if(config.debug & DEBUG_EXTRA) logg("initial: conflinebuffer = %p, keystr = %p, size = %zu", conflinebuffer, keystr, size); diff --git a/src/config.h b/src/config.h index fb767dede..d11b72b7c 100644 --- a/src/config.h +++ b/src/config.h @@ -53,10 +53,14 @@ typedef struct { int dns_port; unsigned int delay_startup; unsigned int network_expire; + struct { + unsigned int count; + unsigned int interval; + } rate_limit; enum debug_flags debug; time_t DBinterval; } ConfigStruct; -ASSERT_SIZEOF(ConfigStruct, 56, 48, 48); +ASSERT_SIZEOF(ConfigStruct, 64, 56, 56); typedef struct { const char* conf; diff --git a/src/database/query-table.c b/src/database/query-table.c index cbbc87df7..2fa8b4276 100644 --- a/src/database/query-table.c +++ b/src/database/query-table.c @@ -432,7 +432,7 @@ void DB_read_queries(void) query->type = TYPE_OTHER; query->qtype = type - 100; } - + query->status = status; query->domainID = domainID; query->clientID = clientID; diff --git a/src/datastructure.h b/src/datastructure.h index e8e77f68a..dd482db1e 100644 --- a/src/datastructure.h +++ b/src/datastructure.h @@ -79,6 +79,7 @@ typedef struct { int blockedcount; int aliasclient_id; unsigned int id; + unsigned int rate_limit; unsigned int numQueriesARP; int overTime[OVERTIME_SLOTS]; size_t groupspos; @@ -88,7 +89,7 @@ typedef struct { time_t lastQuery; time_t firstSeen; } clientsData; -ASSERT_SIZEOF(clientsData, 688, 664, 664); +ASSERT_SIZEOF(clientsData, 696, 668, 668); typedef struct { unsigned char magic; diff --git a/src/dnsmasq_interface.c b/src/dnsmasq_interface.c index eafb37012..b76f05825 100644 --- a/src/dnsmasq_interface.c +++ b/src/dnsmasq_interface.c @@ -190,8 +190,8 @@ static bool _FTL_check_blocking(int queryID, int domainID, int clientID, const c // as something along the CNAME path hit the whitelist if(!query->flags.whitelisted) { - query_blocked(query, domain, client, QUERY_BLACKLIST); force_next_DNS_reply = dns_cache->force_reply; + query_blocked(query, domain, client, QUERY_BLACKLIST); return true; } break; @@ -210,8 +210,8 @@ static bool _FTL_check_blocking(int queryID, int domainID, int clientID, const c // as sometving along the CNAME path hit the whitelist if(!query->flags.whitelisted) { - query_blocked(query, domain, client, QUERY_GRAVITY); force_next_DNS_reply = dns_cache->force_reply; + query_blocked(query, domain, client, QUERY_GRAVITY); return true; } break; @@ -532,19 +532,9 @@ bool _FTL_new_query(const unsigned int flags, const char *name, return false; } - // Lock shared memory - lock_shm(); - - // Ensure we have enough space in the queries struct - memory_check(QUERIES); - const int queryID = counters->queries; - // If domain is "pi.hole" we skip this query if(strcasecmp(name, "pi.hole") == 0) - { - unlock_shm(); return false; - } // Convert domain to lower case char *domainString = strdup(name); @@ -574,10 +564,50 @@ bool _FTL_new_query(const unsigned int flags, const char *name, (strcmp(clientIP, "127.0.0.1") == 0 || strcmp(clientIP, "::1") == 0)) { free(domainString); + return false; + } + + // Lock shared memory + lock_shm(); + + // Find client IP + const int clientID = findClientID(clientIP, true, false); + + // Get client pointer + clientsData* client = getClient(clientID, true); + if(client == NULL) + { + // Encountered memory error, skip query + // Free allocated memory + free(domainString); + // Release thread lock unlock_shm(); return false; } + // Check rate-limit for this client + if(config.rate_limit.count > 0 && + ++client->rate_limit > config.rate_limit.count) + { + if(config.debug & DEBUG_QUERIES) + { + logg("Rate-limiting %s %s query \"%s\" from %s:%s", + proto == TCP ? "TCP" : "UDP", + types, domainString, next_iface, clientIP); + } + + // Block this query + force_next_DNS_reply = REFUSED; + + // Do not further process this query, Pi-hole has never seen it + unlock_shm(); + return true; + } + + // Ensure we have enough space in the queries struct + memory_check(QUERIES); + const int queryID = counters->queries; + // Log new query if in debug mode if(config.debug & DEBUG_QUERIES) { @@ -607,9 +637,6 @@ bool _FTL_new_query(const unsigned int flags, const char *name, // Go through already knows domains and see if it is one of them const int domainID = findDomainID(domainString, true); - // Go through already knows clients and see if it is one of them - const int clientID = findClientID(clientIP, true, false); - // Save everything queriesData* query = getQuery(queryID, false); if(query == NULL) @@ -662,19 +689,6 @@ bool _FTL_new_query(const unsigned int flags, const char *name, // Update overTime data overTime[timeidx].total++; - // Get client pointer - clientsData* client = getClient(clientID, true); - if(client == NULL) - { - // Encountered memory error, skip query - logg("WARN: No memory available, skipping query analysis"); - // Free allocated memory - free(domainString); - // Release thread lock - unlock_shm(); - return false; - } - // Update overTime data structure with the new client change_clientcount(client, 0, 0, timeidx, 1); @@ -761,8 +775,8 @@ bool _FTL_new_query(const unsigned int flags, const char *name, void _FTL_get_blocking_metadata(union all_addr **addrp, unsigned int *flags, const char* file, const int line) { // Check first if we need to force our reply to something different than the - // default/configured blocking mode For instance, we need to force NXDOMAIN - // for intercepted _esni.* queries + // default/configured blocking mode. For instance, we need to force NXDOMAIN + // for intercepted _esni.* queries. if(force_next_DNS_reply == NXDOMAIN) { *flags = F_NXDOMAIN; @@ -770,6 +784,14 @@ void _FTL_get_blocking_metadata(union all_addr **addrp, unsigned int *flags, con force_next_DNS_reply = 0u; return; } + else if(force_next_DNS_reply == REFUSED) + { + // Empty flags result in REFUSED + *flags = 0; + // Reset DNS reply forcing + force_next_DNS_reply = 0u; + return; + } // Add flags according to current blocking mode // We bit-add here as flags already contains either F_IPV4 or F_IPV6 @@ -1662,7 +1684,7 @@ static void save_reply_type(const unsigned int flags, const union all_addr *addr queriesData* query, const struct timeval response) { // Iterate through possible values - if(flags & F_NEG) + if(flags & F_NEG || force_next_DNS_reply == NXDOMAIN) { if(flags & F_NXDOMAIN) { @@ -1694,15 +1716,15 @@ static void save_reply_type(const unsigned int flags, const union all_addr *addr // TXT query query->reply = REPLY_RRNAME; } - else if(flags & F_RCODE && addr != NULL) + else if((flags & F_RCODE && addr != NULL) || force_next_DNS_reply == REFUSED) { - const unsigned int rcode = addr->log.rcode; - if(rcode == REFUSED) + if((addr != NULL && addr->log.rcode == REFUSED) + || force_next_DNS_reply == REFUSED ) { // REFUSED query query->reply = REPLY_REFUSED; } - else if(rcode == SERVFAIL) + else if(addr != NULL && addr->log.rcode == SERVFAIL) { // SERVFAIL query query->reply = REPLY_SERVFAIL; diff --git a/src/gc.c b/src/gc.c index f29409bfe..a671c19ca 100644 --- a/src/gc.c +++ b/src/gc.c @@ -23,29 +23,46 @@ bool doGC = false; -time_t lastGCrun = 0; +static void reset_rate_limiting(void) +{ + for(int clientID = 0; clientID < counters->clients; clientID++) + { + clientsData *client = getClient(clientID, true); + if(client != NULL) + client->rate_limit = 0; + } +} + void *GC_thread(void *val) { // Set thread name prctl(PR_SET_NAME,"housekeeper",0,0,0); - // Save timestamp as we do not want to store immediately - // to the database - lastGCrun = time(NULL) - time(NULL)%GCinterval; + // Remember when we last ran the actions + time_t lastGCrun = time(NULL) - time(NULL)%GCinterval; + time_t lastRateLimitCleaner = time(NULL); while(!killed) { - if(time(NULL) - GCdelay - lastGCrun >= GCinterval || doGC) + const time_t now = time(NULL); + if((unsigned int)(now - lastRateLimitCleaner) >= config.rate_limit.interval) + { + lastRateLimitCleaner = now; + lock_shm(); + reset_rate_limiting(); + unlock_shm(); + } + if(now - GCdelay - lastGCrun >= GCinterval || doGC) { doGC = false; // Update lastGCrun timer - lastGCrun = time(NULL) - GCdelay - (time(NULL) - GCdelay)%GCinterval; + lastGCrun = now - GCdelay - (now - GCdelay)%GCinterval; // Lock FTL's data structure, since it is likely that it will be changed here // Requests should not be processed/answered when data is about to change lock_shm(); // Get minimum time stamp to keep - time_t mintime = (time(NULL) - GCdelay) - MAXLOGAGE*3600; + time_t mintime = (now - GCdelay) - MAXLOGAGE*3600; // Align to the start of the next hour. This will also align with // the oldest overTime interval after GC is done.