Ad- and malware-filtering lists now run to several hundred thousand
names. Loaded as --address lines they cost a lot of memory, and each
query for a name not on the list walks the domain-matching code.
--blocklist=<file> reads such a list as it is published, one name per
line, and keeps only a sorted array of 64-bit hashes of the names, eight
bytes each. A query is answered NXDOMAIN with EDE "Blocked" when a
binary search finds the name or one of its parents.
The search stops with one label left, so a listed TLD blocks only
itself. With 64-bit hashes a lookup mistakes an unlisted name for a
listed one about once in 2^64 / n tries.
The hash is SipHash-1-3 under a key chosen at random at start-up, so
nobody can craft a list entry that looks harmless but has the hash of a
name it means to block. It is taken a label at a time from the right,
each label hashed with its parent's hash, so checking every parent of a
name hashes each label once and the cost grows linearly with the name.
The check is made on the UDP and TCP paths after the cache, hosts files,
--address and --local, and is skipped for a name with a --server of its
own, so configuration still wins. Blocked names are logged as "blocklist
<name> is NXDOMAIN".
The file is read again on SIGHUP, like --addn-hosts. If it cannot be
read then, the error is logged and the old list is kept.
---
Makefile | 2 +-
man/dnsmasq.8 | 22 ++++-
src/blocklist.c | 237 +++++++++++++++++++++++++++++++++++++++++++++
src/config.h | 12 ++-
src/dnsmasq.c | 16 +++
src/dnsmasq.h | 14 ++-
src/domain-match.c | 7 +-
src/forward.c | 30 +++++-
src/option.c | 11 +++
9 files changed, 343 insertions(+), 8 deletions(-)
create mode 100644 src/blocklist.c
diff --git a/Makefile b/Makefile
index 70309e7..d7e3f48 100644
--- a/Makefile
+++ b/Makefile
@@ -81,7 +81,7 @@ objs = cache.o rfc1035.o util.o option.o forward.o network.o \
dhcp-common.o outpacket.o radv.o slaac.o auth.o ipset.o pattern.o \
domain.o dnssec.o blockdata.o tables.o loop.o inotify.o \
poll.o rrfilter.o edns0.o arp.o crypto.o dump.o ubus.o \
- metrics.o domain-match.o nftset.o
+ metrics.o domain-match.o nftset.o blocklist.o
hdrs = dnsmasq.h config.h dhcp-protocol.h dhcp6-protocol.h \
dns-protocol.h radv-protocol.h ip6addr.h metrics.h
diff --git a/man/dnsmasq.8 b/man/dnsmasq.8
index 8923325..d7a2c98 100644
--- a/man/dnsmasq.8
+++ b/man/dnsmasq.8
@@ -613,6 +613,26 @@ Previous versions, configured with (eg)
--address=/example.com/1.2.3.4 and then
A would return a NoData answer. From 2.86, the query is sent upstream. To
restore the pre-2.86 behaviour,
use the configuration --address=/example.com/1.2.3.4 --local=/example.com/
.TP
+.B --blocklist=<file>
+Answer NXDOMAIN for the names listed in <file>, one per line, with blank
+lines and anything after a # ignored. This is the format of the domain
+lists published for ad- and malware-filtering, which can be used as they
+are, and it holds them in much less memory than the same names as
+\fB--address\fP lines.
+
+A name matches if it is listed or if any of its parent domains is, so one
+entry covers a domain and its subdomains. The search stops with one label
+left: an entry for a bare TLD blocks only a query for the TLD itself.
+Names answered from a hosts file, by \fB--address\fP or \fB--local\fP,
+or sent to a
+\fB--server\fP given for that domain with an address, are not blocked.
+A \fB--server\fP using # does not exempt a name, since it hands the
+query back to the default servers.
+
+The file is read again on SIGHUP; if it cannot be read then, the old list
+is kept. Available only if dnsmasq
+is compiled with HAVE_BLOCKLIST defined.
+.TP
.B --ipset=/<domain>[/<domain>...]/<ipset>[,<ipset>...]
Places the resolved IP addresses of queries for one or more domains in
the specified Netfilter IP set. If multiple setnames are given, then the
@@ -2402,7 +2422,7 @@ clears its cache and then re-loads
and
.I /etc/ethers
and any file given by \fB--dhcp-hostsfile\fP, \fB--dhcp-hostsdir\fP,
\fB--dhcp-optsfile\fP,
-\fB--dhcp-optsdir\fP, \fB--addn-hosts\fP or \fB--hostsdir\fP.
+\fB--dhcp-optsdir\fP, \fB--addn-hosts\fP, \fB--hostsdir\fP or
\fB--blocklist\fP.
The DHCP lease change script is called for all
existing DHCP leases. If
.B
diff --git a/src/blocklist.c b/src/blocklist.c
new file mode 100644
index 0000000..0d4b98c
--- /dev/null
+++ b/src/blocklist.c
@@ -0,0 +1,237 @@
+/* Copyright (c) 2026 Roman Valls Guimera <[email protected]>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; version 2 dated June, 1991, or
+ (at your option) version 3 dated 29 June, 2007.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "dnsmasq.h"
+
+#ifdef HAVE_BLOCKLIST
+
+/* Names are hashed with SipHash-1-3 (Aumasson and Bernstein,
+ https://www.aumasson.jp/siphash/) under a random key chosen at
+ start-up. Without the key nobody can work out a name whose hash
+ matches a listed one, and so get a list to block a name it does not
+ show.
+
+ A name is hashed a label at a time from the right: the hash of a
+ label is the hash of its parent's hash followed by the label. The
+ search through a name's parents then hashes each label only once. */
+static u64 key0, key1;
+
+#define ROTL(x, b) (((x) << (b)) | ((x) >> (64 - (b))))
+
+#define SIPROUND \
+ do { \
+ v0 += v1; v1 = ROTL(v1, 13); v1 ^= v0; v0 = ROTL(v0, 32); \
+ v2 += v3; v3 = ROTL(v3, 16); v3 ^= v2; \
+ v0 += v3; v3 = ROTL(v3, 21); v3 ^= v0; \
+ v2 += v1; v1 = ROTL(v1, 17); v1 ^= v2; v2 = ROTL(v2, 32); \
+ } while (0)
+
+/* Add an eight byte word of the message. */
+#define SIPWORD(m) \
+ do { \
+ v3 ^= (m); SIPROUND; v0 ^= (m); \
+ } while (0)
+
+/* SipHash-1-3 of the parent's hash, as eight little-endian bytes,
+ followed by the label. The four constants are SipHash's own: the
+ ASCII of "somepseudorandomlygeneratedbytes". DNS folds the case of
+ ASCII letters only, so this does not use tolower(), which depends on
+ the locale. */
+static u64 hash_label(u64 parent, const char *label, size_t len)
+{
+ u64 v0 = key0 ^ 0x736f6d6570736575ULL;
+ u64 v1 = key1 ^ 0x646f72616e646f6dULL;
+ u64 v2 = key0 ^ 0x6c7967656e657261ULL;
+ u64 v3 = key1 ^ 0x7465646279746573ULL;
+ u64 m = 0;
+ size_t i;
+
+ SIPWORD(parent);
+
+ for (i = 0; i < len; i++)
+ {
+ unsigned char c = label[i];
+
+ if (c >= 'A' && c <= 'Z')
+ c += 'a' - 'A';
+
+ m |= (u64)c << (8 * (i % 8));
+
+ if (i % 8 == 7)
+ {
+ SIPWORD(m);
+ m = 0;
+ }
+ }
+
+ /* The last word carries the length of the message in its top byte. */
+ m |= (u64)(len + 8) << 56;
+ SIPWORD(m);
+
+ /* The "3" of SipHash-1-3: one round per word above, three to finish,
+ so that every bit of the last word reaches every bit of the result
+ before it is used to pick a list entry or a Bloom bit. */
+ v2 ^= 0xff;
+ SIPROUND;
+ SIPROUND;
+ SIPROUND;
+
+ return v0 ^ v1 ^ v2 ^ v3;
+}
+
+static u64 hash_name(const char *name, size_t len)
+{
+ const char *p = name + len;
+ u64 h = 0;
+
+ while (p > name)
+ {
+ const char *label = p;
+
+ while (label > name && label[-1] != '.')
+ label--;
+
+ h = hash_label(h, label, p - label);
+ p = label > name ? label - 1 : name;
+ }
+
+ return h;
+}
+
+static int cmp_hash(const void *a, const void *b)
+{
+ u64 x = *(const u64 *)a, y = *(const u64 *)b;
+
+ return (x > y) - (x < y);
+}
+
+/* Read the --blocklist file: one name per line, ignoring blank lines
+ and anything after a '#', which is the format of the domain lists
+ published for ad-blocking. Only a sorted array of 64 bit hashes is
+ kept, eight bytes a name. Called at start-up, where an error is
+ fatal, and on SIGHUP, where it keeps the old list. */
+void blocklist_init(int reload)
+{
+ char line[MAXDNAME + 2];
+ size_t lines = 0, n = 0;
+ u64 *list = NULL;
+ FILE *f;
+ int err;
+
+ if (!reload)
+ {
+ key0 = rand64();
+ key1 = rand64();
+ }
+
+ if (!(f = fopen(daemon->blocklist_file, "r")))
+ goto fail;
+
+ /* Size the array with a first pass, which splits long lines exactly
+ as the second will. The second stops at that size in case the
+ file grew in between. */
+ while (fgets(line, sizeof(line), f))
+ lines++;
+
+ if (!(list = whine_malloc((lines + 1) * sizeof(u64))))
+ goto fail;
+
+ rewind(f);
+
+ while (n < lines && fgets(line, sizeof(line), f))
+ {
+ char *p = line, *end;
+
+ /* No name is this long, so skip the whole line rather than read
+ what follows as another name. */
+ if (!strchr(line, '\n') && !feof(f))
+ {
+ int c;
+
+ while ((c = getc(f)) != EOF && c != '\n');
+ continue;
+ }
+
+ while (isspace((unsigned char)*p))
+ p++;
+
+ for (end = p; *end && *end != '#' && !isspace((unsigned char)*end);
end++);
+
+ if (end > p && end[-1] == '.')
+ end--;
+
+ if (end > p)
+ list[n++] = hash_name(p, end - p);
+ }
+
+ if (ferror(f))
+ goto fail;
+
+ fclose(f);
+
+ qsort(list, n, sizeof(u64), cmp_hash);
+ free(daemon->blocklist);
+ daemon->blocklist = list;
+ daemon->blocklist_count = n;
+
+ if (reload)
+ my_syslog(LOG_INFO, _("blocklist %s, %u names"), daemon->blocklist_file,
daemon->blocklist_count);
+
+ return;
+
+ fail:
+ err = errno;
+ if (f)
+ fclose(f);
+ free(list);
+ errno = err;
+
+ if (!reload)
+ die(_("cannot read blocklist %s: %s"), daemon->blocklist_file, EC_FILE);
+
+ my_syslog(LOG_ERR, _("cannot read blocklist %s: %s, keeping the old list"),
+ daemon->blocklist_file, strerror(errno));
+}
+
+/* Is the name, or any of its parent domains, listed? Parents with one
+ label are not checked, so a listed TLD blocks only a query for
+ itself. */
+int blocklist_match(char *name)
+{
+ char *p = name + strlen(name);
+ u64 h = 0;
+ int labels = 0;
+
+ while (p > name)
+ {
+ char *label = p;
+
+ while (label > name && label[-1] != '.')
+ label--;
+
+ h = hash_label(h, label, p - label);
+
+ if ((++labels > 1 || label == name) &&
+ bsearch(&h, daemon->blocklist, daemon->blocklist_count, sizeof(u64),
cmp_hash))
+ return 1;
+
+ p = label > name ? label - 1 : name;
+ }
+
+ return 0;
+}
+
+#endif
diff --git a/src/config.h b/src/config.h
index a010c8c..c8a2a38 100644
--- a/src/config.h
+++ b/src/config.h
@@ -139,6 +139,11 @@ HAVE_DNSSEC
HAVE_DUMPFILE
include code to dump packets to a libpcap-format file for debugging.
+HAVE_BLOCKLIST
+ include code to block the names listed in a file given with
+ --blocklist, using much less memory than the same names as
+ --address lines.
+
HAVE_LOOP
include functionality to probe for and remove DNS forwarding loops.
@@ -205,6 +210,7 @@ RESOLVFILE
/* #define HAVE_CONNTRACK */
/* #define HAVE_DNSSEC */
/* #define HAVE_NFTSET */
+/* #define HAVE_BLOCKLIST */
/* Default locations for important system files. */
@@ -483,6 +489,10 @@ static char *compile_opts =
#ifndef HAVE_DUMPFILE
"no-"
#endif
-"dumpfile";
+"dumpfile "
+#ifndef HAVE_BLOCKLIST
+"no-"
+#endif
+"blocklist";
#endif /* defined(DNSMASQ_COMPILE_OPTS) */
diff --git a/src/dnsmasq.c b/src/dnsmasq.c
index 10d25c1..95817d8 100644
--- a/src/dnsmasq.c
+++ b/src/dnsmasq.c
@@ -417,6 +417,11 @@ int main (int argc, char **argv)
cache_init();
blockdata_init();
+#ifdef HAVE_BLOCKLIST
+ if (daemon->blocklist_file)
+ blocklist_init(0);
+#endif
+
/* Scale random socket pool by ftabsize, but
limit it based on available fds. */
daemon->numrrand = daemon->ftabsize/2;
@@ -883,6 +888,12 @@ int main (int argc, char **argv)
my_syslog(LOG_INFO, _("compile time options: %s"), compile_opts);
+#ifdef HAVE_BLOCKLIST
+ if (daemon->blocklist_file)
+ my_syslog(LOG_INFO, _("blocklist %s, %u names"),
+ daemon->blocklist_file, daemon->blocklist_count);
+#endif
+
if (chown_warn != 0)
{
#if defined(HAVE_LINUX_NETWORK)
@@ -1573,6 +1584,11 @@ static void async_event(int pipe, time_t now)
{
case EVENT_RELOAD:
daemon->soa_sn++; /* Bump zone serial, as it may have changed. */
+
+#ifdef HAVE_BLOCKLIST
+ if (daemon->blocklist_file)
+ blocklist_init(1);
+#endif
/* fall through */
diff --git a/src/dnsmasq.h b/src/dnsmasq.h
index ecff18d..02357fd 100644
--- a/src/dnsmasq.h
+++ b/src/dnsmasq.h
@@ -1269,6 +1269,12 @@ extern struct daemon {
char *timestamp_file;
#endif
+#ifdef HAVE_BLOCKLIST
+ char *blocklist_file;
+ u64 *blocklist;
+ unsigned int blocklist_count;
+#endif
+
/* globally used stuff for DNS */
char *packet; /* packet buffer */
int packet_buff_sz; /* size of above */
@@ -1961,13 +1967,19 @@ void dump_packet_icmp(int mask, void *packet, size_t
len, union mysockaddr *src,
union mysockaddr *dst);
#endif
+/* blocklist.c */
+#ifdef HAVE_BLOCKLIST
+void blocklist_init(int reload);
+int blocklist_match(char *name);
+#endif
+
/* domain-match.c */
void build_server_array(void);
int lookup_domain(char *qdomain, int flags, int *lowout, int *highout);
int filter_servers(int seed, int flags, int *lowout, int *highout);
int is_local_answer(time_t now, int first, char *name);
size_t make_local_answer(int flags, int gotname, size_t size, struct
dns_header *header,
- char *name, size_t limit, int first, int last, int
ede);
+ char *name, size_t limit, int first, int last, int
ede, char *source);
int server_samegroup(struct server *a, struct server *b);
#ifdef HAVE_DNSSEC
int dnssec_server(struct server *server, char *keyname, int is_ds, int
*firstp, int *lastp);
diff --git a/src/domain-match.c b/src/domain-match.c
index d3ab6b3..0bed6a4 100644
--- a/src/domain-match.c
+++ b/src/domain-match.c
@@ -408,7 +408,7 @@ int is_local_answer(time_t now, int first, char *name)
return rc;
}
-size_t make_local_answer(int flags, int gotname, size_t size, struct
dns_header *header, char *name, size_t limit, int first, int last, int ede)
+size_t make_local_answer(int flags, int gotname, size_t size, struct
dns_header *header, char *name, size_t limit, int first, int last, int ede,
char *source)
{
int trunc = 0, anscount = 0;
unsigned char *p;
@@ -420,8 +420,11 @@ size_t make_local_answer(int flags, int gotname, size_t
size, struct dns_header
gotname &= ~(F_QUERY | F_DS);
+ /* With a source, log it in place of "config"; F_HOSTS makes
+ log_query() take it from its argument. */
if (flags & (F_NXDOMAIN | F_NOERR))
- log_query(flags | gotname | F_NEG | F_CONFIG | F_FORWARD, name, NULL,
NULL, 0);
+ log_query(flags | gotname | F_NEG | F_FORWARD | (source ? F_HOSTS :
F_CONFIG),
+ name, NULL, source, 0);
if (flags & F_RCODE)
{
diff --git a/src/forward.c b/src/forward.c
index f736c30..cfb4685 100644
--- a/src/forward.c
+++ b/src/forward.c
@@ -176,6 +176,7 @@ static void forward_query(int udpfd, union mysockaddr
*udpaddr,
int first, last, start = 0;
int forwarded = 0;
int ede = EDE_UNSET;
+ char *source = NULL;
unsigned short rrtype, rrclass;
unsigned short id = ntohs(header->id); /* Retrieve the id from the new query
before we overwrite it. */
unsigned int casediff = 0;
@@ -355,6 +356,18 @@ static void forward_query(int udpfd, union mysockaddr
*udpaddr,
flags = 0;
}
+#ifdef HAVE_BLOCKLIST
+ /* A name with a --server of its own is not blocked. */
+ if (!flags && daemon->blocklist_file &&
+ (ede == EDE_NOT_READY || daemon->serverarray[first]->domain_len == 0)
&&
+ blocklist_match(daemon->namebuff))
+ {
+ flags = F_NXDOMAIN;
+ ede = EDE_BLOCKED;
+ source = "blocklist";
+ }
+#endif
+
/* don't forward A or AAAA queries for simple names, except the empty
name */
if (!flags &&
option_bool(OPT_NODOTS_LOCAL) &&
@@ -601,7 +614,7 @@ static void forward_query(int udpfd, union mysockaddr
*udpaddr,
if (casediff != 0)
extract_name(header, plen, NULL, (char *)(bitvector ? bitvector :
&casediff), EXTR_NAME_FLIP, bitvector ? casediff : 1);
- if (!(plen = make_local_answer(flags, gotname, plen, header,
daemon->namebuff, replylimit, first, last, ede)))
+ if (!(plen = make_local_answer(flags, gotname, plen, header,
daemon->namebuff, replylimit, first, last, ede, source)))
return;
if (fwd_flags & FREC_HAS_PHEADER)
@@ -2407,6 +2420,7 @@ void tcp_request(int confd, time_t now, struct iovec
*bigbuff,
unsigned int mark = 0;
int have_mark = 0;
int first, last, filtered, do_stale = 0;
+ char *source = NULL;
struct iovec out_iov[2];
bigbuff->iov_base = NULL;
@@ -2617,6 +2631,18 @@ void tcp_request(int confd, time_t now, struct iovec
*bigbuff,
flags = is_local_answer(now, first, daemon->namebuff);
else
ede = EDE_NOT_READY;
+
+#ifdef HAVE_BLOCKLIST
+ /* A name with a --server of its own is not blocked. */
+ if (!flags && daemon->blocklist_file &&
+ (ede == EDE_NOT_READY || daemon->serverarray[first]->domain_len
== 0) &&
+ blocklist_match(daemon->namebuff))
+ {
+ flags = F_NXDOMAIN;
+ ede = EDE_BLOCKED;
+ source = "blocklist";
+ }
+#endif
if (!flags && ede == EDE_UNSET)
{
@@ -2749,7 +2775,7 @@ void tcp_request(int confd, time_t now, struct iovec
*bigbuff,
if (m == 0)
{
if (!(m = make_local_answer(flags, gotname, size, out_header,
daemon->namebuff,
- 65536, first, last, ede)))
+ 65536, first, last, ede, source)))
break;
}
else if (ede == EDE_UNSET)
diff --git a/src/option.c b/src/option.c
index 44bd522..8028222 100644
--- a/src/option.c
+++ b/src/option.c
@@ -201,6 +201,7 @@ struct myoption {
#define LOPT_LEASEQUERY 389
#define LOPT_SPLIT_RELAY 390
#define LOPT_LOG_MALLOC 391
+#define LOPT_BLOCKLIST 392
#ifdef HAVE_GETOPT_LONG
static const struct option opts[] =
@@ -403,6 +404,7 @@ static const struct myoption opts[] =
{ "max-tcp-connections", 1, 0, LOPT_MAX_PROCS },
{ "leasequery", 2, 0, LOPT_LEASEQUERY },
{ "log-malloc", 0, 0, LOPT_LOG_MALLOC },
+ { "blocklist", 1, 0, LOPT_BLOCKLIST },
{ NULL, 0, 0, 0 }
};
@@ -612,6 +614,7 @@ static struct {
{ LOPT_CACHE_RR, ARG_DUP, "<RR-type>", gettext_noop("Cache this DNS resource
record type."), NULL },
{ LOPT_MAX_PROCS, ARG_ONE, "<integer>", gettext_noop("Maximum number of
concurrent tcp connections."), NULL },
{ LOPT_LOG_MALLOC, OPT_LOG_MALLOC, NULL, gettext_noop("Log memory allocation
for debugging."), NULL },
+ { LOPT_BLOCKLIST, ARG_ONE, "<path>", gettext_noop("Block the names listed in
this file."), NULL },
{ 0, 0, NULL, NULL, NULL }
};
@@ -2410,6 +2413,14 @@ static int one_opt(int option, char *arg, char *errstr,
char *gen_err, int comma
case LOPT_DUMPMASK: /* --dumpmask */
daemon->dump_mask = strtol(arg, NULL, 0);
break;
+
+ case LOPT_BLOCKLIST: /* --blocklist */
+#ifdef HAVE_BLOCKLIST
+ daemon->blocklist_file = opt_string_alloc(arg);
+#else
+ ret_err(_("recompile with HAVE_BLOCKLIST defined to enable
--blocklist"));
+#endif
+ break;
#ifdef HAVE_DHCP
case 'l': /* --dhcp-leasefile */
--
2.53.0
_______________________________________________
Dnsmasq-discuss mailing list
[email protected]
https://lists.thekelleys.org.uk/cgi-bin/mailman/listinfo/dnsmasq-discuss