Skip to content

Commit

Permalink
Add --nftset option, like --ipset but for the newer nftables.
Browse files Browse the repository at this point in the history
Thanks to Chen Zhenge for the original patch, which I've
reworked. Any bugs down to SRK.
  • Loading branch information
simonkelley committed Sep 27, 2021
1 parent 981fb03 commit 47aefca
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 45 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 ' ')
Expand All @@ -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
Expand All @@ -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 :
Expand All @@ -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; \
Expand Down
10 changes: 10 additions & 0 deletions dnsmasq.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions man/dnsmasq.8
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/cache.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 9 additions & 1 deletion src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -192,7 +196,7 @@ RESOLVFILE
/* #define HAVE_CONNTRACK */
/* #define HAVE_CRYPTOHASH */
/* #define HAVE_DNSSEC */

/* #define HAVE_NFTSET */

/* Default locations for important system files. */

Expand Down Expand Up @@ -420,6 +424,10 @@ static char *compile_opts =
"no-"
#endif
"ipset "
#ifndef HAVE_NFTSET
"no-"
#endif
"nftset "
#ifndef HAVE_AUTH
"no-"
#endif
Expand Down
10 changes: 10 additions & 0 deletions src/dnsmasq.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 9 additions & 3 deletions src/dnsmasq.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
49 changes: 29 additions & 20 deletions src/forward.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)))
Expand Down Expand Up @@ -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;
Expand Down
94 changes: 94 additions & 0 deletions src/nftset.c
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 47aefca

Please sign in to comment.