Skip to content

Commit 6149956

Browse files
feat: add rate limit exempt IPs
Signed-off-by: Simpliq OpenClaw <openclaw@local> Signed-off-by: simpliq-marvin <[email protected]>
1 parent 71b6fc6 commit 6149956

6 files changed

Lines changed: 145 additions & 1 deletion

File tree

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,36 @@ const char *readFTLlegacy(struct config *conf)
405405
conf->dns.rateLimit.interval.v.ui = interval;
406406
}
407407

408+
// RATE_LIMIT_EXEMPT_IPS
409+
buffer = parseFTLconf(fp, "RATE_LIMIT_EXEMPT_IPS");
410+
if(buffer != NULL)
411+
{
412+
char *copy = strdup(buffer);
413+
char *tmp = copy;
414+
char *token = NULL;
415+
416+
if(copy != NULL)
417+
{
418+
cJSON_Delete(conf->dns.rateLimit.exemptIPs.v.json);
419+
conf->dns.rateLimit.exemptIPs.v.json = cJSON_CreateArray();
420+
421+
while((token = strsep(&tmp, ",;\n")) != NULL)
422+
{
423+
while(isspace((unsigned char)*token))
424+
token++;
425+
426+
char *end = token + strlen(token);
427+
while(end > token && isspace((unsigned char)end[-1]))
428+
*--end = '\0';
429+
430+
if(*token != '\0')
431+
cJSON_AddItemToArray(conf->dns.rateLimit.exemptIPs.v.json, cJSON_CreateString(token));
432+
}
433+
434+
free(copy);
435+
}
436+
}
437+
408438
// LOCAL_IPV4
409439
// Use a specific IP address instead of automatically detecting the
410440
// IPv4 interface address a query arrived on for A hostname queries

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 > 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[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)

0 commit comments

Comments
 (0)