Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add per-client rate-limiting #1052

Merged
merged 1 commit into from
Feb 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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