Skip to content

Commit 84cf6fe

Browse files
feat: add rate limit exempt IPs
Signed-off-by: simpliq-marvin <[email protected]>
1 parent cf82b94 commit 84cf6fe

9 files changed

Lines changed: 160 additions & 2 deletions

File tree

src/api/docs/content/specs/config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,11 @@ components:
340340
type: integer
341341
interval:
342342
type: integer
343+
exemptIPs:
344+
type: array
345+
items:
346+
type: string
347+
x-format: ipv4|ipv6
343348
dhcp:
344349
type: object
345350
properties:
@@ -789,6 +794,9 @@ components:
789794
rateLimit:
790795
count: 0
791796
interval: 0
797+
exemptIPs:
798+
- "192.168.1.10"
799+
- "fd00::10"
792800
dhcp:
793801
active: false
794802
start: "192.168.0.10"

src/config/config.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "signals.h"
3232
// validation functions
3333
#include "config/validator.h"
34+
#include "datastructure.h"
3435
// getEnvVars()
3536
#include "config/env.h"
3637
// sha256sum()
@@ -770,6 +771,13 @@ void initConfig(struct config *conf)
770771
conf->dns.rateLimit.interval.d.ui = 60;
771772
conf->dns.rateLimit.interval.c = validate_stub; // Only type-based checking
772773

774+
conf->dns.rateLimit.exemptIPs.k = "dns.rateLimit.exemptIPs";
775+
conf->dns.rateLimit.exemptIPs.h = "IP addresses that should bypass per-client DNS rate limiting. Matching is exact by IP address and applies only when rate limiting is enabled. This is intended for trusted clients that may generate short legitimate DNS bursts.\n\n Example: [ \"192.168.1.10\", \"fd00::10\" ]";
776+
conf->dns.rateLimit.exemptIPs.a = cJSON_CreateStringReference("Array of valid IPv4 and/or IPv6 addresses");
777+
conf->dns.rateLimit.exemptIPs.t = CONF_JSON_STRING_ARRAY;
778+
conf->dns.rateLimit.exemptIPs.d.json = cJSON_CreateArray();
779+
conf->dns.rateLimit.exemptIPs.c = validate_ip_array;
780+
773781
// sub-struct dhcp
774782
conf->dhcp.active.k = "dhcp.active";
775783
conf->dhcp.active.h = "Is the embedded DHCP server enabled?";
@@ -2040,6 +2048,7 @@ void replace_config(struct config *newconf)
20402048

20412049
// Replace old config struct by changed one atomically
20422050
memcpy(&config, newconf, sizeof(struct config));
2051+
reload_all_per_client_rate_limit_exemption();
20432052

20442053
// Free old backup struct
20452054
free_config(&old_conf, false);

src/config/config.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ struct config {
189189
struct {
190190
struct conf_item count;
191191
struct conf_item interval;
192+
struct conf_item exemptIPs;
192193
} rateLimit;
193194
} dns;
194195

src/config/validator.c

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,60 @@ bool validate_dns_domain(union conf_value *val, const char *key, char err[VALIDA
206206
return true;
207207
}
208208

209+
// Validate arrays of IP addresses (IPv4 and/or IPv6)
210+
bool validate_ip_array(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
211+
{
212+
if(!cJSON_IsArray(val->json))
213+
{
214+
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not an array", key);
215+
return false;
216+
}
217+
218+
for(int i = 0; i < cJSON_GetArraySize(val->json); i++)
219+
{
220+
cJSON *item = cJSON_GetArrayItem(val->json, i);
221+
222+
if(!cJSON_IsString(item) || item->valuestring == NULL)
223+
{
224+
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a string", key, i);
225+
return false;
226+
}
227+
228+
const char *raw = item->valuestring;
229+
while(isspace((unsigned char)*raw))
230+
raw++;
231+
232+
size_t len = strlen(raw);
233+
while(len > 0 && isspace((unsigned char)raw[len-1]))
234+
len--;
235+
236+
if(len == 0)
237+
{
238+
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: empty string", key, i);
239+
return false;
240+
}
241+
242+
if(len > INET6_ADDRSTRLEN)
243+
{
244+
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: address too long (\"%s\")", key, i, item->valuestring);
245+
return false;
246+
}
247+
248+
char ip[INET6_ADDRSTRLEN + 1] = { 0 };
249+
memcpy(ip, raw, len);
250+
251+
struct in_addr addr4;
252+
struct in6_addr addr6;
253+
if(inet_pton(AF_INET, ip, &addr4) != 1 && inet_pton(AF_INET6, ip, &addr6) != 1)
254+
{
255+
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: neither a valid IPv4 nor IPv6 address (\"%s\")", key, i, item->valuestring);
256+
return false;
257+
}
258+
}
259+
260+
return true;
261+
}
262+
209263
// Validate IPs in CIDR notation
210264
bool validate_cidr(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
211265
{

src/config/validator.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ bool validate_stub(union conf_value *val, const char *key, char err[VALIDATOR_ER
1818
bool validate_dns_hosts(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
1919
bool validate_dns_cnames(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
2020
bool validate_dns_domain(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
21+
bool validate_ip_array(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
2122
bool validate_cidr(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
2223
bool validate_domain(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
2324
bool validate_filepath(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);

src/datastructure.c

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,78 @@ static uint32_t __attribute__ ((pure)) hashStr(const char *s)
7272
return hash;
7373
}
7474

75+
static bool client_matches_rate_limit_exempt_ip(const char *clientIP)
76+
{
77+
if(clientIP == NULL || config.dns.rateLimit.exemptIPs.v.json == NULL)
78+
return false;
79+
80+
struct in_addr client4;
81+
struct in6_addr client6;
82+
const sa_family_t family = inet_pton(AF_INET, clientIP, &client4) == 1 ? AF_INET :
83+
inet_pton(AF_INET6, clientIP, &client6) == 1 ? AF_INET6 : AF_UNSPEC;
84+
if(family == AF_UNSPEC)
85+
return false;
86+
87+
cJSON *item = NULL;
88+
cJSON_ArrayForEach(item, config.dns.rateLimit.exemptIPs.v.json)
89+
{
90+
if(!cJSON_IsString(item) || item->valuestring == NULL)
91+
continue;
92+
93+
const char *raw = item->valuestring;
94+
while(isspace((unsigned char)*raw))
95+
raw++;
96+
97+
size_t len = strlen(raw);
98+
while(len > 0 && isspace((unsigned char)raw[len-1]))
99+
len--;
100+
101+
if(len == 0 || len > ADDRSTRLEN)
102+
continue;
103+
104+
char exemptIP[ADDRSTRLEN + 1] = { 0 };
105+
memcpy(exemptIP, raw, len);
106+
107+
if(family == AF_INET)
108+
{
109+
struct in_addr exempt4;
110+
if(inet_pton(AF_INET, exemptIP, &exempt4) == 1 && memcmp(&client4, &exempt4, sizeof(exempt4)) == 0)
111+
return true;
112+
}
113+
else
114+
{
115+
struct in6_addr exempt6;
116+
if(inet_pton(AF_INET6, exemptIP, &exempt6) == 1 && memcmp(&client6, &exempt6, sizeof(exempt6)) == 0)
117+
return true;
118+
}
119+
}
120+
121+
return false;
122+
}
123+
124+
void reload_per_client_rate_limit_exemption(clientsData *client)
125+
{
126+
if(client == NULL || client->flags.aliasclient)
127+
return;
128+
129+
client->flags.rate_limit_exempt = client_matches_rate_limit_exempt_ip(getstr(client->ippos));
130+
}
131+
132+
void reload_all_per_client_rate_limit_exemption(void)
133+
{
134+
if(counters == NULL)
135+
return;
136+
137+
for(unsigned int clientID = 0; clientID < counters->clients; clientID++)
138+
{
139+
clientsData *client = getClient(clientID, true);
140+
if(client == NULL)
141+
continue;
142+
143+
reload_per_client_rate_limit_exemption(client);
144+
}
145+
}
146+
75147
/**
76148
* @brief Computes a hash value for the cache IDs using a simple XOR operation.
77149
*
@@ -356,6 +428,7 @@ int _findClientID(const char *clientIP, const bool count, const bool aliasclient
356428
client->blockedcount = 0;
357429
// Store client IP - no need to check for NULL here as it doesn't harm
358430
client->ippos = addstr(clientIP);
431+
reload_per_client_rate_limit_exemption(client);
359432
// Store pre-computed hash for faster lookups later on
360433
client->hash = hash;
361434
// Initialize client hostname

src/datastructure.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ typedef struct {
8181
bool found_group:1;
8282
bool aliasclient:1;
8383
bool rate_limited:1;
84+
bool rate_limit_exempt:1;
8485
} flags;
8586
int count;
8687
int blockedcount;
@@ -160,6 +161,8 @@ void _query_set_status(queriesData *query, const enum query_status new_status, c
160161

161162
void FTL_reload_all_domainlists(void);
162163
void FTL_reset_per_client_domain_data(void);
164+
void reload_per_client_rate_limit_exemption(clientsData *client);
165+
void reload_all_per_client_rate_limit_exemption(void);
163166

164167
const char *getDomainString(const queriesData *query);
165168
const char *getCNAMEDomainString(const queriesData *query);

src/dnsmasq_interface.c

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -794,9 +794,8 @@ bool _FTL_new_query(const unsigned int flags, const char *name,
794794
// Interface name is only available for regular queries, not for
795795
// automatically generated DNSSEC queries
796796
const char *interface = internal_query ? "-" : next_iface.name;
797-
798797
// Check rate-limit for this client
799-
if(!internal_query && config.dns.rateLimit.count.v.ui > 0 &&
798+
if(!internal_query && !client->flags.rate_limit_exempt && config.dns.rateLimit.count.v.ui > 0 &&
800799
(++client->rate_limit > config.dns.rateLimit.count.v.ui || client->flags.rate_limited))
801800
{
802801
if(!client->flags.rate_limited)

test/pihole.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,16 @@
553553
# A positive integer value in seconds
554554
interval = 0 ### CHANGED, default = 60
555555

556+
# IP addresses that should bypass per-client DNS rate limiting. Matching is exact by
557+
# IP address and applies only when rate limiting is enabled. This is intended for
558+
# trusted clients that may generate short legitimate DNS bursts.
559+
#
560+
# Example: [ "192.168.1.10", "fd00::10" ]
561+
#
562+
# Allowed values are:
563+
# An array of valid IPv4 and/or IPv6 addresses
564+
exemptIPs = ["192.168.1.10", "fd00::10"] ### CHANGED, default = []
565+
556566
[dhcp]
557567
# Is the embedded DHCP server enabled?
558568
#

0 commit comments

Comments
 (0)