Skip to content
Open
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
17 changes: 16 additions & 1 deletion src/config/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,22 @@ void initConfig(struct config *conf)
conf->dns.cache.rrtype.f = FLAG_RESTART_FTL;
conf->dns.cache.rrtype.d.s = (char*)"ANY";
conf->dns.cache.rrtype.c = validate_str_no_newline;


conf->dns.cache.excludeAnswerCIDRs.k = "dns.cache.excludeAnswerCIDRs";
conf->dns.cache.excludeAnswerCIDRs.h = "Array of CIDR ranges. If any A or AAAA record in an upstream DNS answer matches one of these ranges, FTL will return the answer to the client but will not commit this answer to its DNS cache.\n\nThis is useful when Pi-hole is upstreamed to a fake-IP resolver where cached fake-IP answers may outlive the resolver's own mapping state.";
conf->dns.cache.excludeAnswerCIDRs.a = cJSON_CreateStringReference("Array of IPv4 or IPv6 CIDR ranges");
conf->dns.cache.excludeAnswerCIDRs.t = CONF_JSON_STRING_ARRAY;
conf->dns.cache.excludeAnswerCIDRs.d.json = cJSON_CreateArray();
cJSON_AddItemToArray(conf->dns.cache.excludeAnswerCIDRs.d.json, cJSON_CreateStringReference("198.18.0.0/15"));
cJSON_AddItemToArray(conf->dns.cache.excludeAnswerCIDRs.d.json, cJSON_CreateStringReference("10.0.0.0/8"));
cJSON_AddItemToArray(conf->dns.cache.excludeAnswerCIDRs.d.json, cJSON_CreateStringReference("172.16.0.0/12"));
cJSON_AddItemToArray(conf->dns.cache.excludeAnswerCIDRs.d.json, cJSON_CreateStringReference("192.168.0.0/16"));
cJSON_AddItemToArray(conf->dns.cache.excludeAnswerCIDRs.d.json, cJSON_CreateStringReference("169.254.0.0/16"));
cJSON_AddItemToArray(conf->dns.cache.excludeAnswerCIDRs.d.json, cJSON_CreateStringReference("fc00::/7"));
cJSON_AddItemToArray(conf->dns.cache.excludeAnswerCIDRs.d.json, cJSON_CreateStringReference("fe80::/10"));
conf->dns.cache.excludeAnswerCIDRs.f = FLAG_RESTART_FTL;
conf->dns.cache.excludeAnswerCIDRs.c = validate_stub; // CIDRs are parsed when answers are evaluated

// sub-struct dns.blocking
conf->dns.blocking.active.k = "dns.blocking.active";
conf->dns.blocking.active.h = "Should FTL block queries?";
Expand Down
1 change: 1 addition & 0 deletions src/config/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ struct config {
struct conf_item optimizer;
struct conf_item upstreamBlockedTTL;
struct conf_item rrtype;
struct conf_item excludeAnswerCIDRs;
} cache;
struct {
struct conf_item active;
Expand Down
2 changes: 2 additions & 0 deletions src/dnsmasq/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ set(sources
blockdata.c
bpf.c
cache.c
cache_exclude.c
cache_exclude.h
config.h
conntrack.c
crypto.c
Expand Down
112 changes: 112 additions & 0 deletions src/dnsmasq/cache_exclude.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#include "cache_exclude.h"

#include <arpa/inet.h>
#include <ctype.h>
#include <stddef.h>
#include <string.h>

static void mask_address(uint8_t *addr, size_t len, unsigned int prefix)
{
const unsigned int full_bytes = prefix / 8;
const unsigned int remaining_bits = prefix % 8;

if(full_bytes < len && remaining_bits > 0)
addr[full_bytes] &= (uint8_t)(0xffu << (8u - remaining_bits));

for(size_t i = full_bytes + (remaining_bits > 0 ? 1u : 0u); i < len; i++)
addr[i] = 0;
}

bool cache_exclude_cidr_parse(const char *input, struct cache_exclude_cidr *cidr)
{
char addrbuf[INET6_ADDRSTRLEN];
const char *slash = NULL;
unsigned long prefix = 0;
size_t addrlen = 0;
int family = AF_UNSPEC;

if(input == NULL || cidr == NULL)
return false;

slash = strchr(input, '/');
if(slash == NULL || slash == input || slash[1] == '\0')
return false;

addrlen = (size_t)(slash - input);
if(addrlen >= sizeof(addrbuf))
return false;

for(const char *p = slash + 1; *p != '\0'; p++)
{
if(!isdigit((unsigned char)*p))
return false;
prefix = prefix * 10u + (unsigned long)(*p - '0');
if(prefix > 128u)
return false;
}

memcpy(addrbuf, input, addrlen);
addrbuf[addrlen] = '\0';
memset(cidr, 0, sizeof(*cidr));

if(inet_pton(AF_INET, addrbuf, cidr->addr) == 1)
family = AF_INET;
else if(inet_pton(AF_INET6, addrbuf, cidr->addr) == 1)
family = AF_INET6;
else
return false;

if((family == AF_INET && prefix > 32u) ||
(family == AF_INET6 && prefix > 128u))
return false;

cidr->family = family;
cidr->prefix = (unsigned int)prefix;
mask_address(cidr->addr, family == AF_INET ? 4u : 16u, cidr->prefix);
return true;
}

bool cache_exclude_cidr_matches(const struct cache_exclude_cidr *cidr, int family, const void *addr)
{
uint8_t candidate[16];
size_t len = 0;
const unsigned int full_bytes = cidr != NULL ? cidr->prefix / 8 : 0;
const unsigned int remaining_bits = cidr != NULL ? cidr->prefix % 8 : 0;

if(cidr == NULL || addr == NULL || cidr->family != family)
return false;

len = family == AF_INET ? 4u : 16u;
memcpy(candidate, addr, len);
mask_address(candidate, len, cidr->prefix);

if(full_bytes > 0 && memcmp(candidate, cidr->addr, full_bytes) != 0)
return false;

if(remaining_bits > 0 && candidate[full_bytes] != cidr->addr[full_bytes])
return false;

return true;
}

bool cache_exclude_address_matches_json(cJSON *cidrs, int family, const void *addr)
{
cJSON *item = NULL;

if(!cJSON_IsArray(cidrs))
return false;

cJSON_ArrayForEach(item, cidrs)
{
struct cache_exclude_cidr cidr;

if(!cJSON_IsString(item) || item->valuestring == NULL)
continue;

if(cache_exclude_cidr_parse(item->valuestring, &cidr) &&
cache_exclude_cidr_matches(&cidr, family, addr))
return true;
}

return false;
}
20 changes: 20 additions & 0 deletions src/dnsmasq/cache_exclude.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#ifndef CACHE_EXCLUDE_H
#define CACHE_EXCLUDE_H

#include <netinet/in.h>
#include <stdbool.h>
#include <stdint.h>

#include "webserver/cJSON/cJSON.h"

struct cache_exclude_cidr {
int family;
uint8_t addr[16];
unsigned int prefix;
};

bool cache_exclude_cidr_parse(const char *input, struct cache_exclude_cidr *cidr);
bool cache_exclude_cidr_matches(const struct cache_exclude_cidr *cidr, int family, const void *addr);
bool cache_exclude_address_matches_json(cJSON *cidrs, int family, const void *addr);

#endif
9 changes: 9 additions & 0 deletions src/dnsmasq/rfc1035.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

#include "dnsmasq.h"
#include "dnsmasq_interface.h"
#include "cache_exclude.h"
#include "config/config.h"

/* EXTR_NAME_EXTRACT -> extract name
EXTR_NAME_COMPARE -> compare name, case insensitive
Expand Down Expand Up @@ -1014,6 +1016,13 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t
if (!CHECK_LEN(header, p1, qlen, addrlen))
return 2; /* bad packet */
memcpy(&addr, p1, addrlen);
if (((flags & F_IPV4) &&
cache_exclude_address_matches_json(config.dns.cache.excludeAnswerCIDRs.v.json,
AF_INET, &addr.addr4)) ||
((flags & F_IPV6) &&
cache_exclude_address_matches_json(config.dns.cache.excludeAnswerCIDRs.v.json,
AF_INET6, &addr.addr6)))
no_cache_dnssec = 1;

/* check for returned address in private space */
if (check_rebind)
Expand Down
52 changes: 52 additions & 0 deletions test/cache_exclude_cidr_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#include <arpa/inet.h>
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>

#include "../src/dnsmasq/cache_exclude.h"

static void test_ipv4_cidr_match(void)
{
struct cache_exclude_cidr cidr;
struct in_addr inside;
struct in_addr outside;

assert(cache_exclude_cidr_parse("198.18.0.0/15", &cidr));
assert(inet_pton(AF_INET, "198.18.4.112", &inside) == 1);
assert(inet_pton(AF_INET, "8.8.8.8", &outside) == 1);

assert(cache_exclude_cidr_matches(&cidr, AF_INET, &inside));
assert(!cache_exclude_cidr_matches(&cidr, AF_INET, &outside));
}

static void test_ipv6_cidr_match(void)
{
struct cache_exclude_cidr cidr;
struct in6_addr inside;
struct in6_addr outside;

assert(cache_exclude_cidr_parse("fd00::/8", &cidr));
assert(inet_pton(AF_INET6, "fd00::1234", &inside) == 1);
assert(inet_pton(AF_INET6, "2001:4860:4860::8888", &outside) == 1);

assert(cache_exclude_cidr_matches(&cidr, AF_INET6, &inside));
assert(!cache_exclude_cidr_matches(&cidr, AF_INET6, &outside));
}

static void test_invalid_cidrs_are_rejected(void)
{
struct cache_exclude_cidr cidr;

assert(!cache_exclude_cidr_parse("198.18.0.0/33", &cidr));
assert(!cache_exclude_cidr_parse("fd00::/129", &cidr));
assert(!cache_exclude_cidr_parse("not-a-cidr", &cidr));
}

int main(void)
{
test_ipv4_cidr_match();
test_ipv6_cidr_match();
test_invalid_cidrs_are_rejected();
puts("cache_exclude_cidr_test: ok");
return 0;
}
Loading