Skip to content

Commit 4b5fd42

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

7 files changed

Lines changed: 133 additions & 1 deletion

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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,13 @@ void initConfig(struct config *conf)
770770
conf->dns.rateLimit.interval.d.ui = 60;
771771
conf->dns.rateLimit.interval.c = validate_stub; // Only type-based checking
772772

773+
conf->dns.rateLimit.exemptIPs.k = "dns.rateLimit.exemptIPs";
774+
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\" ]";
775+
conf->dns.rateLimit.exemptIPs.a = cJSON_CreateStringReference("Array of valid IPv4 and/or IPv6 addresses");
776+
conf->dns.rateLimit.exemptIPs.t = CONF_JSON_STRING_ARRAY;
777+
conf->dns.rateLimit.exemptIPs.d.json = cJSON_CreateArray();
778+
conf->dns.rateLimit.exemptIPs.c = validate_ip_array;
779+
773780
// sub-struct dhcp
774781
conf->dhcp.active.k = "dhcp.active";
775782
conf->dhcp.active.h = "Is the embedded DHCP server enabled?";

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/dnsmasq_interface.c

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ static void _query_set_dnssec(queriesData *query, const enum dnssec_status dnsse
8080
static char *get_ptrname(const struct in_addr *addr);
8181
static const char *check_dnsmasq_name(const char *name);
8282
static void get_rcode(const unsigned short rcode, const char **rcodestr, enum reply_type *reply);
83+
static bool client_matches_rate_limit_exempt_ip(const char *clientIP);
8384

8485
// Static blocking metadata
8586
static bool aabit = false, adbit = false, rabit = false;
@@ -106,6 +107,55 @@ static union mysockaddr last_server = {};
106107

107108
const char *flagnames[] = {"F_IMMORTAL ", "F_NAMEP ", "F_REVERSE ", "F_FORWARD ", "F_DHCP ", "F_NEG ", "F_HOSTS ", "F_IPV4 ", "F_IPV6 ", "F_BIGNAME ", "F_NXDOMAIN ", "F_CNAME ", "F_DNSKEY ", "F_CONFIG ", "F_DS ", "F_DNSSECOK ", "F_UPSTREAM ", "F_RRNAME ", "F_SERVER ", "F_QUERY ", "F_NOERR ", "F_AUTH ", "F_DNSSEC ", "F_KEYTAG ", "F_SECSTAT ", "F_NO_RR ", "F_IPSET ", "F_NOEXTRA ", "F_DOMAINSRV", "F_RCODE", "F_RR", "F_STALE" };
108109

110+
static bool client_matches_rate_limit_exempt_ip(const char *clientIP)
111+
{
112+
if(clientIP == NULL || config.dns.rateLimit.exemptIPs.v.json == NULL)
113+
return false;
114+
115+
struct in_addr client4;
116+
struct in6_addr client6;
117+
const sa_family_t family = inet_pton(AF_INET, clientIP, &client4) == 1 ? AF_INET :
118+
inet_pton(AF_INET6, clientIP, &client6) == 1 ? AF_INET6 : AF_UNSPEC;
119+
if(family == AF_UNSPEC)
120+
return false;
121+
122+
cJSON *item = NULL;
123+
cJSON_ArrayForEach(item, config.dns.rateLimit.exemptIPs.v.json)
124+
{
125+
if(!cJSON_IsString(item) || item->valuestring == NULL)
126+
continue;
127+
128+
const char *raw = item->valuestring;
129+
while(isspace((unsigned char)*raw))
130+
raw++;
131+
132+
size_t len = strlen(raw);
133+
while(len > 0 && isspace((unsigned char)raw[len-1]))
134+
len--;
135+
136+
if(len == 0 || len > ADDRSTRLEN)
137+
continue;
138+
139+
char exemptIP[ADDRSTRLEN + 1] = { 0 };
140+
memcpy(exemptIP, raw, len);
141+
142+
if(family == AF_INET)
143+
{
144+
struct in_addr exempt4;
145+
if(inet_pton(AF_INET, exemptIP, &exempt4) == 1 && memcmp(&client4, &exempt4, sizeof(exempt4)) == 0)
146+
return true;
147+
}
148+
else
149+
{
150+
struct in6_addr exempt6;
151+
if(inet_pton(AF_INET6, exemptIP, &exempt6) == 1 && memcmp(&client6, &exempt6, sizeof(exempt6)) == 0)
152+
return true;
153+
}
154+
}
155+
156+
return false;
157+
}
158+
109159
void FTL_hook(unsigned int flags, const char *name, const union all_addr *addr, char *arg, int id, unsigned short type, const char *file, const int line)
110160
{
111161
// Extract filename from path
@@ -794,9 +844,10 @@ bool _FTL_new_query(const unsigned int flags, const char *name,
794844
// Interface name is only available for regular queries, not for
795845
// automatically generated DNSSEC queries
796846
const char *interface = internal_query ? "-" : next_iface.name;
847+
const bool rate_limit_exempt = client_matches_rate_limit_exempt_ip(clientIP);
797848

798849
// Check rate-limit for this client
799-
if(!internal_query && config.dns.rateLimit.count.v.ui > 0 &&
850+
if(!internal_query && !rate_limit_exempt && config.dns.rateLimit.count.v.ui > 0 &&
800851
(++client->rate_limit > config.dns.rateLimit.count.v.ui || client->flags.rate_limited))
801852
{
802853
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)