Skip to content

Commit

Permalink
Add per-client rate-limiting. The default limit is 1000 queries in 60…
Browse files Browse the repository at this point in the history
… seconds.

Signed-off-by: DL6ER <dl6er@dl6er.de>
  • Loading branch information
DL6ER committed Feb 3, 2021
1 parent c355e85 commit bf52156
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 46 deletions.
21 changes: 20 additions & 1 deletion src/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down
6 changes: 5 additions & 1 deletion src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/database/query-table.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/datastructure.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
92 changes: 57 additions & 35 deletions src/dnsmasq_interface.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -761,15 +775,23 @@ 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;
// Reset DNS reply forcing
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
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
Expand Down
31 changes: 24 additions & 7 deletions src/gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit bf52156

Please sign in to comment.