diff options
author | Simon Kelley <simon@thekelleys.org.uk> | 2021-09-27 21:31:20 +0100 |
---|---|---|
committer | Simon Kelley <simon@thekelleys.org.uk> | 2021-09-27 21:49:28 +0100 |
commit | 47aefca5e405b4b6627ef952fdc42e61b1baa770 (patch) | |
tree | 853a36100c922de403e543fa779bb1ce58c7ab2e | |
parent | 981fb037102306a4ca683f14c8469db4d5e27233 (diff) | |
download | dnsmasq-47aefca5e405b4b6627ef952fdc42e61b1baa770.tar.gz |
Add --nftset option, like --ipset but for the newer nftables.v2.87test2
Thanks to Chen Zhenge for the original patch, which I've
reworked. Any bugs down to SRK.
-rw-r--r-- | CHANGELOG | 6 | ||||
-rw-r--r-- | Makefile | 7 | ||||
-rw-r--r-- | dnsmasq.conf.example | 10 | ||||
-rw-r--r-- | man/dnsmasq.8 | 9 | ||||
-rw-r--r-- | src/cache.c | 2 | ||||
-rw-r--r-- | src/config.h | 10 | ||||
-rw-r--r-- | src/dnsmasq.c | 10 | ||||
-rw-r--r-- | src/dnsmasq.h | 12 | ||||
-rw-r--r-- | src/forward.c | 49 | ||||
-rw-r--r-- | src/nftset.c | 94 | ||||
-rw-r--r-- | src/option.c | 36 | ||||
-rw-r--r-- | src/rfc1035.c | 26 |
12 files changed, 226 insertions, 45 deletions
@@ -5,6 +5,9 @@ version 2.87 Replace --address=/#/..... functionality which got missed in the 2.86 domain search rewrite. + Add --nftset option, like --ipset but for the newer nftables. + Thanks to Chen Zhenge for the patch. + version 2.86 Handle DHCPREBIND requests in the DHCPv6 server code. @@ -100,6 +103,9 @@ version 2.86 of filename). Thanks to Ed Wildgoose for the initial patch and motivation for this. + Allow adding IP address to nftables set in addition to + ipset. + version 2.85 Fix problem with DNS retries in 2.83/2.84. @@ -70,6 +70,7 @@ nettle_libs = `echo $(COPTS) | $(top)/bld/pkg-wrapper HAVE_DNSSEC $(PKG_CO HAVE_NETTLEHASH $(PKG_CONFIG) --libs nettle` gmp_libs = `echo $(COPTS) | $(top)/bld/pkg-wrapper HAVE_DNSSEC NO_GMP --copy -lgmp` sunos_libs = `if uname | grep SunOS >/dev/null 2>&1; then echo -lsocket -lnsl -lposix4; fi` +nft_libs = `echo $(COPTS) | $(top)/bld/pkg-wrapper HAVE_NFTSET $(PKG_CONFIG) --libs libnftables` version = -DVERSION='\"`$(top)/bld/get-version $(top)`\"' sum?=$(shell $(CC) -DDNSMASQ_COMPILE_OPTS $(COPTS) -E $(top)/$(SRC)/dnsmasq.h | ( md5sum 2>/dev/null || md5 ) | cut -f 1 -d ' ') @@ -82,7 +83,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 hash-questions.o domain-match.o + metrics.o hash-questions.o domain-match.o nftset.o hdrs = dnsmasq.h config.h dhcp-protocol.h dhcp6-protocol.h \ dns-protocol.h radv-protocol.h ip6addr.h metrics.h @@ -91,7 +92,7 @@ all : $(BUILDDIR) @cd $(BUILDDIR) && $(MAKE) \ top="$(top)" \ build_cflags="$(version) $(dbus_cflags) $(idn2_cflags) $(idn_cflags) $(ct_cflags) $(lua_cflags) $(nettle_cflags)" \ - build_libs="$(dbus_libs) $(idn2_libs) $(idn_libs) $(ct_libs) $(lua_libs) $(sunos_libs) $(nettle_libs) $(gmp_libs) $(ubus_libs)" \ + build_libs="$(dbus_libs) $(idn2_libs) $(idn_libs) $(ct_libs) $(lua_libs) $(sunos_libs) $(nettle_libs) $(gmp_libs) $(ubus_libs) $(nft_libs)" \ -f $(top)/Makefile dnsmasq mostly_clean : @@ -116,7 +117,7 @@ all-i18n : $(BUILDDIR) top="$(top)" \ i18n=-DLOCALEDIR=\'\"$(LOCALEDIR)\"\' \ build_cflags="$(version) $(dbus_cflags) $(idn2_cflags) $(idn_cflags) $(ct_cflags) $(lua_cflags) $(nettle_cflags)" \ - build_libs="$(dbus_libs) $(idn2_libs) $(idn_libs) $(ct_libs) $(lua_libs) $(sunos_libs) $(nettle_libs) $(gmp_libs) $(ubus_libs)" \ + build_libs="$(dbus_libs) $(idn2_libs) $(idn_libs) $(ct_libs) $(lua_libs) $(sunos_libs) $(nettle_libs) $(gmp_libs) $(ubus_libs) $(nft_libs)" \ -f $(top)/Makefile dnsmasq for f in `cd $(PO); echo *.po`; do \ cd $(top) && cd $(BUILDDIR) && $(MAKE) top="$(top)" -f $(top)/Makefile $${f%.po}.mo; \ diff --git a/dnsmasq.conf.example b/dnsmasq.conf.example index bf19424..2047630 100644 --- a/dnsmasq.conf.example +++ b/dnsmasq.conf.example @@ -85,6 +85,16 @@ # subdomains to the vpn and search ipsets: #ipset=/yahoo.com/google.com/vpn,search +# Add the IPs of all queries to yahoo.com, google.com, and their +# subdomains to netfilters sets, which is equivalent to +# 'nft add element ip test vpn { ... }; nft add element ip test search { ... }' +#nftset=/yahoo.com/google.com/ip#test#vpn,ip#test#search + +# Use netfilters sets for both IPv4 and IPv6: +# This adds all addresses in *.yahoo.com to vpn4 and vpn6 for IPv4 and IPv6 addresses. +#nftset=/yahoo.com/4#ip#test#vpn4 +#nftset=/yahoo.com/6#ip#test#vpn6 + # You can control how dnsmasq talks to a server: this forces # queries to 10.1.2.3 to be routed via eth1 # server=10.1.2.3@eth1 diff --git a/man/dnsmasq.8 b/man/dnsmasq.8 index a71610c..1d4993c 100644 --- a/man/dnsmasq.8 +++ b/man/dnsmasq.8 @@ -550,6 +550,15 @@ These IP sets must already exist. See .BR ipset (8) for more details. .TP +.B --nftset=/<domain>[/<domain>...]/[(6|4)#[<family>#]<table>#<set>[,[(6|4)#[<family>#]<table>#<set>]...] +Similar to the \fB--ipset\fP option, but accepts one or more nftables +sets to add IP addresses into. +These sets must already exist. See +.BR nft (8) +for more details. The family, table and set are passed directly to the nft. If the spec starts with 4# or 6# then +only A or AAAA records respectively are added to the set. Since an nftset can hold only IPv4 or IPv6 addresses, this +avoids errors being logged for addresses of the wrong type. +.TP .B --connmark-allowlist-enable[=<mask>] Enables filtering of incoming DNS queries with associated Linux connection track marks according to individual allowlists configured via a series of \fB--connmark-allowlist\fP diff --git a/src/cache.c b/src/cache.c index 568cbec..e1d17c4 100644 --- a/src/cache.c +++ b/src/cache.c @@ -2060,7 +2060,7 @@ void log_query(unsigned int flags, char *name, union all_addr *addr, char *arg, } else if (flags & F_IPSET) { - source = "ipset add"; + source = type ? "ipset add" : "nftset add"; dest = name; name = arg; verb = daemon->addrbuff; diff --git a/src/config.h b/src/config.h index 30e23d8..2bb6683 100644 --- a/src/config.h +++ b/src/config.h @@ -115,6 +115,10 @@ HAVE_IPSET define this to include the ability to selectively add resolved ip addresses to given ipsets. +HAVE_NFTSET + define this to include the ability to selectively add resolved ip addresses + to given nftables sets. + HAVE_AUTH define this to include the facility to act as an authoritative DNS server for one or more zones. @@ -192,7 +196,7 @@ RESOLVFILE /* #define HAVE_CONNTRACK */ /* #define HAVE_CRYPTOHASH */ /* #define HAVE_DNSSEC */ - +/* #define HAVE_NFTSET */ /* Default locations for important system files. */ @@ -420,6 +424,10 @@ static char *compile_opts = "no-" #endif "ipset " +#ifndef HAVE_NFTSET +"no-" +#endif +"nftset " #ifndef HAVE_AUTH "no-" #endif diff --git a/src/dnsmasq.c b/src/dnsmasq.c index 3e1bfe8..c7fa024 100644 --- a/src/dnsmasq.c +++ b/src/dnsmasq.c @@ -346,6 +346,16 @@ int main (int argc, char **argv) } #endif +#ifdef HAVE_NFTSET + if (daemon->nftsets) + { + nftset_init(); +# ifdef HAVE_LINUX_NETWORK + need_cap_net_admin = 1; +# endif + } +#endif + #if defined(HAVE_LINUX_NETWORK) netlink_warn = netlink_init(); #elif defined(HAVE_BSD_NETWORK) diff --git a/src/dnsmasq.h b/src/dnsmasq.h index 1a66e9b..c8a918a 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -1114,7 +1114,7 @@ extern struct daemon { struct rebind_domain *no_rebind; int server_has_wildcard; int serverarraysz, serverarrayhwm; - struct ipsets *ipsets; + struct ipsets *ipsets, *nftsets; u32 allowlist_mask; struct allowlist *allowlists; int log_fac; /* log facility */ @@ -1303,8 +1303,8 @@ unsigned int extract_request(struct dns_header *header, size_t qlen, char *name, unsigned short *typep); void setup_reply(struct dns_header *header, unsigned int flags, int ede); int extract_addresses(struct dns_header *header, size_t qlen, char *name, - time_t now, char **ipsets, int is_sign, int check_rebind, - int no_cache_dnssec, int secure, int *doctored); + time_t now, struct ipsets *ipsets, struct ipsets *nftsets, int is_sign, + int check_rebind, int no_cache_dnssec, int secure, int *doctored); #if defined(HAVE_CONNTRACK) && defined(HAVE_UBUS) void report_addresses(struct dns_header *header, size_t len, u32 mark); #endif @@ -1588,6 +1588,12 @@ void ipset_init(void); int add_to_ipset(const char *setname, const union all_addr *ipaddr, int flags, int remove); #endif +/* nftset.c */ +#ifdef HAVE_NFTSET +void nftset_init(void); +int add_to_nftset(const char *setpath, const union all_addr *ipaddr, int flags, int remove); +#endif + /* pattern.c */ #ifdef HAVE_CONNTRACK int is_valid_dns_name(const char *value); diff --git a/src/forward.c b/src/forward.c index e74e396..b921168 100644 --- a/src/forward.c +++ b/src/forward.c @@ -556,12 +556,33 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, return 0; } +static struct ipsets *domain_find_sets(struct ipsets *setlist, const char *domain) { + /* Similar algorithm to search_servers. */ + struct ipsets *ipset_pos, *ret = NULL; + unsigned int namelen = strlen(domain); + unsigned int matchlen = 0; + for (ipset_pos = setlist; ipset_pos; ipset_pos = ipset_pos->next) + { + unsigned int domainlen = strlen(ipset_pos->domain); + const char *matchstart = domain + namelen - domainlen; + if (namelen >= domainlen && hostname_isequal(matchstart, ipset_pos->domain) && + (domainlen == 0 || namelen == domainlen || *(matchstart - 1) == '.' ) && + domainlen >= matchlen) + { + matchlen = domainlen; + ret = ipset_pos; + } + } + + return ret; +} + static size_t process_reply(struct dns_header *header, time_t now, struct server *server, size_t n, int check_rebind, int no_cache, int cache_secure, int bogusanswer, int ad_reqd, int do_bit, int added_pheader, int check_subnet, union mysockaddr *query_source, unsigned char *limit, int ede) { unsigned char *pheader, *sizep; - char **sets = 0; + struct ipsets *ipsets = NULL, *nftsets = NULL; int munged = 0, is_sign; unsigned int rcode = RCODE(header); size_t plen; @@ -572,24 +593,12 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server #ifdef HAVE_IPSET if (daemon->ipsets && extract_request(header, n, daemon->namebuff, NULL)) - { - /* Similar algorithm to search_servers. */ - struct ipsets *ipset_pos; - unsigned int namelen = strlen(daemon->namebuff); - unsigned int matchlen = 0; - for (ipset_pos = daemon->ipsets; ipset_pos; ipset_pos = ipset_pos->next) - { - unsigned int domainlen = strlen(ipset_pos->domain); - char *matchstart = daemon->namebuff + namelen - domainlen; - if (namelen >= domainlen && hostname_isequal(matchstart, ipset_pos->domain) && - (domainlen == 0 || namelen == domainlen || *(matchstart - 1) == '.' ) && - domainlen >= matchlen) - { - matchlen = domainlen; - sets = ipset_pos->sets; - } - } - } + ipsets = domain_find_sets(daemon->ipsets, daemon->namebuff); +#endif + +#ifdef HAVE_NFTSET + if (daemon->nftsets && extract_request(header, n, daemon->namebuff, NULL)) + nftsets = domain_find_sets(daemon->nftsets, daemon->namebuff); #endif if ((pheader = find_pseudoheader(header, n, &plen, &sizep, &is_sign, NULL))) @@ -698,7 +707,7 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server } } - if (extract_addresses(header, n, daemon->namebuff, now, sets, is_sign, check_rebind, no_cache, cache_secure, &doctored)) + if (extract_addresses(header, n, daemon->namebuff, now, ipsets, nftsets, is_sign, check_rebind, no_cache, cache_secure, &doctored)) { my_syslog(LOG_WARNING, _("possible DNS-rebind attack detected: %s"), daemon->namebuff); munged = 1; diff --git a/src/nftset.c b/src/nftset.c new file mode 100644 index 0000000..5373a1c --- /dev/null +++ b/src/nftset.c @@ -0,0 +1,94 @@ +/* dnsmasq is Copyright (c) 2000-2021 Simon Kelley + + 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" + +#if defined (HAVE_NFTSET) && defined (HAVE_LINUX_NETWORK) + +#include <nftables/libnftables.h> + +#include <string.h> +#include <arpa/inet.h> + +static struct nft_ctx *ctx = NULL; +static const char *cmd_add = "add element %s { %s }"; +static const char *cmd_del = "delete element %s { %s }"; + +void nftset_init() +{ + ctx = nft_ctx_new(NFT_CTX_DEFAULT); + if (ctx == NULL) + die(_("failed to create nftset context"), NULL, EC_MISC); + + /* disable libnftables output */ + nft_ctx_buffer_error(ctx); +} + +int add_to_nftset(const char *setname, const union all_addr *ipaddr, int flags, int remove) +{ + const char *cmd = remove ? cmd_del : cmd_add; + int ret, af = (flags & F_IPV4) ? AF_INET : AF_INET6; + size_t new_sz; + char *new, *err, *nl; + static char *cmd_buf = NULL; + static size_t cmd_buf_sz = 0; + + inet_ntop(af, ipaddr, daemon->addrbuff, ADDRSTRLEN); + + if (setname[1] == ' ' && (setname[0] == '4' || setname[0] == '6')) + { + if (setname[0] == '4' && !(flags & F_IPV4)) + return -1; + + if (setname[0] == '6' && !(flags & F_IPV6)) + return -1; + + setname += 2; + } + + if (cmd_buf_sz == 0) + new_sz = 150; /* initial allocation */ + else + new_sz = snprintf(cmd_buf, cmd_buf_sz, cmd, setname, daemon->addrbuff); + + if (new_sz > cmd_buf_sz) + { + if (!(new = whine_malloc(new_sz + 10))) + return 0; + + if (cmd_buf) + free(cmd_buf); + cmd_buf = new; + cmd_buf_sz = new_sz + 10; + snprintf(cmd_buf, cmd_buf_sz, cmd, setname, daemon->addrbuff); + } + + ret = nft_run_cmd_from_buffer(ctx, cmd_buf); + err = (char *)nft_ctx_get_error_buffer(ctx); + + if (ret != 0) + { + /* Log only first line of error return. */ + if ((nl = strchr(err, '\n'))) + *nl = 0; + my_syslog(LOG_ERR, "nftset %s %s", setname, err); + } + + return ret; +} + +#endif diff --git a/src/option.c b/src/option.c index 5402170..4e533be 100644 --- a/src/option.c +++ b/src/option.c @@ -174,6 +174,7 @@ struct myoption { #define LOPT_CMARK_ALST_EN 365 #define LOPT_CMARK_ALST 366 #define LOPT_QUIET_TFTP 367 +#define LOPT_NFTSET 368 #ifdef HAVE_GETOPT_LONG static const struct option opts[] = @@ -327,6 +328,7 @@ static const struct myoption opts[] = { "auth-sec-servers", 1, 0, LOPT_AUTHSFS }, { "auth-peer", 1, 0, LOPT_AUTHPEER }, { "ipset", 1, 0, LOPT_IPSET }, + { "nftset", 1, 0, LOPT_NFTSET }, { "connmark-allowlist-enable", 2, 0, LOPT_CMARK_ALST_EN }, { "connmark-allowlist", 1, 0, LOPT_CMARK_ALST }, { "synth-domain", 1, 0, LOPT_SYNTH }, @@ -514,6 +516,7 @@ static struct { { LOPT_AUTHSFS, ARG_DUP, "<NS>[,<NS>...]", gettext_noop("Secondary authoritative nameservers for forward domains"), NULL }, { LOPT_AUTHPEER, ARG_DUP, "<ipaddr>[,<ipaddr>...]", gettext_noop("Peers which are allowed to do zone transfer"), NULL }, { LOPT_IPSET, ARG_DUP, "/<domain>[/<domain>...]/<ipset>...", gettext_noop("Specify ipsets to which matching domains should be added"), NULL }, + { LOPT_NFTSET, ARG_DUP, "/<domain>[/<domain>...]/<nftset>...", gettext_noop("Specify nftables sets to which matching domains should be added"), NULL }, { LOPT_CMARK_ALST_EN, ARG_ONE, "[=<mask>]", gettext_noop("Enable filtering of DNS queries with connection-track marks."), NULL }, { LOPT_CMARK_ALST, ARG_DUP, "<connmark>[/<mask>][,<pattern>[/<pattern>...]]", gettext_noop("Set allowed DNS patterns for a connection-track mark."), NULL }, { LOPT_SYNTH, ARG_DUP, "<domain>,<range>,[<prefix>]", gettext_noop("Specify a domain and address range for synthesised names"), NULL }, @@ -2850,13 +2853,27 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma } case LOPT_IPSET: /* --ipset */ + case LOPT_NFTSET: /* --nftset */ #ifndef HAVE_IPSET - ret_err(_("recompile with HAVE_IPSET defined to enable ipset directives")); - break; -#else + if (option == LOPT_IPSET) + { + ret_err(_("recompile with HAVE_IPSET defined to enable ipset directives")); + break; + } +#endif +#ifndef HAVE_NFTSET + if (option == LOPT_NFTSET) + { + ret_err(_("recompile with HAVE_NFTSET defined to enable nftset directives")); + break; + } +#endif + { struct ipsets ipsets_head; struct ipsets *ipsets = &ipsets_head; + struct ipsets **daemon_sets = + (option == LOPT_IPSET) ? &daemon->ipsets : &daemon->nftsets; int size; char *end; char **sets, **sets_pos; @@ -2901,19 +2918,24 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma sets = sets_pos = opt_malloc(sizeof(char *) * size); do { + char *p; end = split(arg); - *sets_pos++ = opt_string_alloc(arg); + *sets_pos = opt_string_alloc(arg); + /* Use '#' to delimit table and set */ + if (option == LOPT_NFTSET) + while ((p = strchr(*sets_pos, '#'))) + *p = ' '; + sets_pos++; arg = end; } while (end); *sets_pos = 0; for (ipsets = &ipsets_head; ipsets->next; ipsets = ipsets->next) ipsets->next->sets = sets; - ipsets->next = daemon->ipsets; - daemon->ipsets = ipsets_head.next; + ipsets->next = *daemon_sets; + *daemon_sets = ipsets_head.next; break; } -#endif case LOPT_CMARK_ALST_EN: /* --connmark-allowlist-enable */ #ifndef HAVE_CONNTRACK diff --git a/src/rfc1035.c b/src/rfc1035.c index 3a7f3a5..124a4f2 100644 --- a/src/rfc1035.c +++ b/src/rfc1035.c @@ -540,8 +540,8 @@ static int print_txt(struct dns_header *header, const size_t qlen, char *name, expired and cleaned out that way. Return 1 if we reject an address because it look like part of dns-rebinding attack. */ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t now, - char **ipsets, int is_sign, int check_rebind, int no_cache_dnssec, - int secure, int *doctored) + struct ipsets *ipsets, struct ipsets *nftsets, int is_sign, int check_rebind, + int no_cache_dnssec, int secure, int *doctored) { unsigned char *p, *p1, *endrr, *namep; int j, qtype, qclass, aqtype, aqclass, ardlen, res, searched_soa = 0; @@ -552,6 +552,11 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t #else (void)ipsets; /* unused */ #endif +#ifdef HAVE_NFTSET + char **nftsets_cur; +#else + (void)nftsets; /* unused */ +#endif int found = 0, cname_count = CNAME_CHAIN; struct crec *cpp = NULL; int flags = RCODE(header) == NXDOMAIN ? F_NXDOMAIN : 0; @@ -843,14 +848,15 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t #ifdef HAVE_IPSET if (ipsets && (flags & (F_IPV4 | F_IPV6))) - { - ipsets_cur = ipsets; - while (*ipsets_cur) - { - log_query((flags & (F_IPV4 | F_IPV6)) | F_IPSET, name, &addr, *ipsets_cur, 0); - add_to_ipset(*ipsets_cur++, &addr, flags, 0); - } - } + for (ipsets_cur = ipsets->sets; *ipsets_cur; ipsets_cur++) + if (add_to_ipset(*ipsets_cur, &addr, flags, 0) == 0) + log_query((flags & (F_IPV4 | F_IPV6)) | F_IPSET, ipsets->domain, &addr, *ipsets_cur, 1); +#endif +#ifdef HAVE_NFTSET + if (nftsets && (flags & (F_IPV4 | F_IPV6))) + for (nftsets_cur = nftsets->sets; *nftsets_cur; nftsets_cur++) + if (add_to_nftset(*nftsets_cur, &addr, flags, 0) == 0) + log_query((flags & (F_IPV4 | F_IPV6)) | F_IPSET, nftsets->domain, &addr, *nftsets_cur, 0); #endif } |