diff --git a/.gitignore b/.gitignore index b8f38989b..01066619d 100644 --- a/.gitignore +++ b/.gitignore @@ -61,5 +61,10 @@ /testdata/.done-* /testdata/.skip-* /testdata/.perfstats.txt +/testdata/*.tdir/.tpkg.var.test +/testdata/*.tdir/outfile +/testdata/*.tdir/fwd.log +/testdata/*.tdir/ub.conf +/testdata/*.tdir/unbound.log /doc/html /doc/xml diff --git a/config.h.in b/config.h.in index 735485991..1ae00da96 100644 --- a/config.h.in +++ b/config.h.in @@ -388,6 +388,9 @@ /* If we have atomic_store */ #undef HAVE_LINK_ATOMIC_STORE +/* Define to 1 if you have the header file. */ +#undef HAVE_LINUX_NETFILTER_NF_TABLES_H + /* Define to 1 if you have the header file. */ #undef HAVE_LINUX_NET_TSTAMP_H @@ -1034,7 +1037,7 @@ /* Define to 1 to use ipsecmod support. */ #undef USE_IPSECMOD -/* Define to 1 to use ipset support */ +/* Define to 1 to enable the ip backend in the ipset module */ #undef USE_IPSET /* Define if you enable libevent */ @@ -1054,6 +1057,9 @@ /* Define this to enable client TCP Fast Open. */ #undef USE_MSG_FASTOPEN +/* Define to 1 to enable the nft backend in the ipset module */ +#undef USE_NFTSET + /* Define this to use ngtcp2_crypto_ossl. */ #undef USE_NGTCP2_CRYPTO_OSSL diff --git a/configure.ac b/configure.ac index 27e16f646..75c72c24e 100644 --- a/configure.ac +++ b/configure.ac @@ -2153,57 +2153,88 @@ case "$enable_ipsecmod" in ;; esac -# check for ipset if requested -AC_ARG_ENABLE(ipset, AS_HELP_STRING([--enable-ipset],[enable ipset module])) -case "$enable_ipset" in - yes) - AC_DEFINE([USE_IPSET], [1], [Define to 1 to use ipset support]) - IPSET_SRC="ipset/ipset.c" - AC_SUBST(IPSET_SRC) - IPSET_OBJ="ipset.lo" - AC_SUBST(IPSET_OBJ) +# ipset module — supports ip backend (--enable-ipset) and/or nft backend +# (--enable-nftset). Either or both may be selected; the module is built +# whenever at least one backend is enabled. +AC_ARG_ENABLE(ipset, AS_HELP_STRING([--enable-ipset],[enable ip backend (iptables ipset / BSD PF) in the ipset module])) +AC_ARG_ENABLE(nftset, AS_HELP_STRING([--enable-nftset],[enable nft backend (nftables sets) in the ipset module; Linux only, requires linux/netfilter/nf_tables.h])) - # BSD's pf - AC_CHECK_HEADERS([net/pfvar.h], [], [ - # mnl - AC_ARG_WITH(libmnl, AS_HELP_STRING([--with-libmnl=path],[specify explicit path for libmnl.]), - [ ],[ withval="yes" ]) - found_libmnl="no" - AC_MSG_CHECKING(for libmnl) - if test x_$withval = x_ -o x_$withval = x_yes; then - withval="/usr/local /opt/local /usr/lib /usr/pkg /usr/sfw /usr" - fi - for dir in $withval ; do - if test -f "$dir/include/libmnl/libmnl.h" -o -f "$dir/include/libmnl/libmnl/libmnl.h"; then - found_libmnl="yes" - dnl assume /usr is in default path. - extralibmnl="" - if test -f "$dir/include/libmnl/libmnl/libmnl.h"; then - extralibmnl="/libmnl" - fi - if test "$dir" != "/usr" -o -n "$extralibmnl"; then - CPPFLAGS="$CPPFLAGS -I$dir/include$extralibmnl" - fi - if test "$dir" != "/usr"; then - LDFLAGS="$LDFLAGS -L$dir/lib" - fi - AC_MSG_RESULT(found in $dir) - LIBS="$LIBS -lmnl" - break; +ipset_module_enabled=no + +if test "$enable_ipset" = "yes"; then + AC_DEFINE([USE_IPSET], [1], [Define to 1 to enable the ip backend in the ipset module]) + ipset_module_enabled=yes + + # BSD's pf — if present, the ip backend talks to /dev/pf instead of + # requiring libmnl for the ip backend. libmnl may still be required + # below if --enable-nftset was also requested. + AC_CHECK_HEADERS([net/pfvar.h], [have_pfvar=yes], [have_pfvar=no], [ + #include + #include + ]) +fi + +if test "$enable_nftset" = "yes"; then + AC_CHECK_HEADERS([linux/netfilter/nf_tables.h], [ + AC_DEFINE([USE_NFTSET], [1], [Define to 1 to enable the nft backend in the ipset module]) + ipset_module_enabled=yes + ], [ + AC_MSG_ERROR([Could not find linux/netfilter/nf_tables.h; the nft backend is Linux-only]) + ], [ + #include + #include + #include + ]) +fi + +if test "$ipset_module_enabled" = "yes"; then + IPSET_SRC="ipset/ipset.c" + AC_SUBST(IPSET_SRC) + IPSET_OBJ="ipset.lo" + AC_SUBST(IPSET_OBJ) + + # libmnl is required on Linux for either backend. On BSD only the ip + # backend is supported (talks to PF) so libmnl is not required there. + need_libmnl=no + if test "$enable_nftset" = "yes"; then + need_libmnl=yes + fi + if test "$enable_ipset" = "yes" -a "$have_pfvar" != "yes"; then + need_libmnl=yes + fi + + if test "$need_libmnl" = "yes"; then + AC_ARG_WITH(libmnl, AS_HELP_STRING([--with-libmnl=path],[specify explicit path for libmnl.]), + [ ],[ withval="yes" ]) + found_libmnl="no" + AC_MSG_CHECKING(for libmnl) + if test x_$withval = x_ -o x_$withval = x_yes; then + withval="/usr/local /opt/local /usr/lib /usr/pkg /usr/sfw /usr" + fi + for dir in $withval ; do + if test -f "$dir/include/libmnl/libmnl.h" -o -f "$dir/include/libmnl/libmnl/libmnl.h"; then + found_libmnl="yes" + dnl assume /usr is in default path. + extralibmnl="" + if test -f "$dir/include/libmnl/libmnl/libmnl.h"; then + extralibmnl="/libmnl" fi - done - if test x_$found_libmnl != x_yes; then - AC_MSG_ERROR([Could not find libmnl, libmnl.h]) - fi - ], [ - #include - #include - ]) - ;; - no|*) - # nothing - ;; -esac + if test "$dir" != "/usr" -o -n "$extralibmnl"; then + CPPFLAGS="$CPPFLAGS -I$dir/include$extralibmnl" + fi + if test "$dir" != "/usr"; then + LDFLAGS="$LDFLAGS -L$dir/lib" + fi + AC_MSG_RESULT(found in $dir) + LIBS="$LIBS -lmnl" + break; + fi + done + if test x_$found_libmnl != x_yes; then + AC_MSG_ERROR([Could not find libmnl, libmnl.h]) + fi + fi +fi AC_ARG_ENABLE(explicit-port-randomisation, AS_HELP_STRING([--disable-explicit-port-randomisation],[disable explicit source port randomisation and rely on the kernel to provide random source ports])) case "$enable_explicit_port_randomisation" in no) diff --git a/daemon/unbound.c b/daemon/unbound.c index a787b76fe..b8f1e97ae 100644 --- a/daemon/unbound.c +++ b/daemon/unbound.c @@ -51,6 +51,7 @@ #include "util/config_file.h" #include "util/storage/slabhash.h" #include "services/listen_dnsport.h" +#include "services/modstack.h" #include "services/cache/rrset.h" #include "services/cache/infra.h" #include "util/fptr_wlist.h" @@ -79,6 +80,12 @@ #include #endif +#if (defined(USE_IPSET) || defined(USE_NFTSET)) && defined(__linux__) +# include +# include +# include +#endif + #ifdef UB_ON_WINDOWS # include "winrc/win_svc.h" #endif @@ -486,6 +493,9 @@ perform_setup(struct daemon* daemon, struct config_file* cfg, int debug_mode, #ifdef HAVE_GETPWNAM struct passwd *pwd = NULL; #endif +#if (defined(USE_IPSET) || defined(USE_NFTSET)) && defined(__linux__) + int needs_net_admin = 0; +#endif if(!daemon_privileged(daemon)) fatal_exit("could not do privileged setup"); @@ -639,6 +649,19 @@ perform_setup(struct daemon* daemon, struct config_file* cfg, int debug_mode, endpwent(); # endif +#if (defined(USE_IPSET) || defined(USE_NFTSET)) && defined(__linux__) + /* Preserve CAP_NET_ADMIN across the privilege drop only if the + * ipset/nftset module is actually configured to run. */ + needs_net_admin = + modstack_has_module(cfg->module_conf, "ipset") || + modstack_has_module(cfg->module_conf, "nftset"); + if(needs_net_admin) { + if(prctl(PR_SET_KEEPCAPS, 1) != 0) + log_warn("prctl(PR_SET_KEEPCAPS) failed: %s", + strerror(errno)); + } +#endif + #ifdef HAVE_SETRESGID if(setresgid(cfg_gid,cfg_gid,cfg_gid) != 0) #elif defined(HAVE_SETREGID) && !defined(DARWIN_BROKEN_SETREUID) @@ -655,9 +678,28 @@ perform_setup(struct daemon* daemon, struct config_file* cfg, int debug_mode, #else /* use setuid */ if(setuid(cfg_uid) != 0) #endif /* HAVE_SETRESUID */ - fatal_exit("unable to set user id of %s: %s", + fatal_exit("unable to set user id of %s: %s", cfg->username, strerror(errno)); - verbose(VERB_QUERY, "drop user privileges, run as %s", +#if (defined(USE_IPSET) || defined(USE_NFTSET)) && defined(__linux__) + if(needs_net_admin) { + /* Restore CAP_NET_ADMIN into effective set (permitted was + * preserved by PR_SET_KEEPCAPS); drop all other caps. */ + struct __user_cap_header_struct hdr; + struct __user_cap_data_struct data[2]; + memset(&hdr, 0, sizeof(hdr)); + memset(data, 0, sizeof(data)); + hdr.version = _LINUX_CAPABILITY_VERSION_3; + hdr.pid = 0; + data[0].effective = data[0].permitted = (1u << CAP_NET_ADMIN); + if(syscall(SYS_capset, &hdr, data) != 0) + log_warn("capset(CAP_NET_ADMIN) failed: %s", + strerror(errno)); + else + verbose(VERB_QUERY, + "retained CAP_NET_ADMIN after privilege drop"); + } +#endif + verbose(VERB_QUERY, "drop user privileges, run as %s", cfg->username); } #endif /* HAVE_GETPWNAM */ diff --git a/doc/README.ipset-nftset.md b/doc/README.ipset-nftset.md new file mode 100644 index 000000000..4885e0b34 --- /dev/null +++ b/doc/README.ipset-nftset.md @@ -0,0 +1,205 @@ +## ipset module — populate firewall sets from DNS + +The ipset module lets Unbound automatically populate kernel firewall sets +with the IP addresses it resolves. Whenever a DNS query matches a +configured `local-zone "..." ipset` (or `local-zone "..." nftset`), the +resolved A / AAAA records are inserted into the corresponding set so that +firewall rules acting on that set take effect immediately. + +The module supports two backends, chosen by the configuration section used: + +| Section | Targets | Notes | +|-------------|--------------------------------------|--------------------------------| +| `ipset:` | iptables `ipset` (Linux), PF (BSD) | Legacy ipset API. | +| `nftset:` | nftables sets (Linux only) | Requires `--enable-nftset`. | + +Both sections share the same options; `nftset:` just adds two extra +fields (`family:` and `table:`). Only one section may be used per Unbound +instance. + +--- + +### Use case + +Firewall or routing rules which need to be applied to specific domains, +rather than static IP address lists. +Unbound can populate the firewall ip address set each time a new IP is +resolved for the target domain. + +--- + +### Build + +``` +# ipset backend only (iptables ipset / BSD PF): +./configure --enable-ipset + +# nftset backend only (nftables, Linux only): +./configure --enable-nftset + +# both backends (pick at runtime via section name): +./configure --enable-ipset --enable-nftset + +make && make install +``` + +Either flag may be used on its own, or both together. On Linux either +backend uses `libmnl` send netlink directly to the kernel, so the +build needs `libmnl-dev` (or `libmnl-devel`); the nft backend +additionally needs the kernel headers `linux/netfilter.h` and +`linux/netfilter/nf_tables.h`. On BSD only `--enable-ipset` is supported +and this uses `/dev/pf` directly. + +--- + +### Configuration + +Enable the module in `module-config:`, mark zones with +`local-zone: "..." ipset`, and put the set details under an `ipset:` +or `nftset:` block: + +``` +# unbound.conf — ipset backend +server: + module-config: "ipset validator iterator" + local-zone: "example.com." ipset + local-zone: "blocked.org." ipset + +ipset: + name-v4: "blacklist" + name-v6: "blacklist6" +``` + +For the nft backend, configure `module-config:` with "nftset", use the +`nftset:` block, and add `family:` (defaults to `inet` if omitted) and +`table:` (required): + +``` +# unbound.conf — nftset backend +server: + module-config: "nftset validator iterator" + local-zone: "example.com." nftset + +nftset: + family: "inet" # "inet" (default), "ip", or "ip6" + table: "fw4" + name-v4: "blacklist" + name-v6: "blacklist6" +``` + +#### Per-zone set routing + +`set:` entries route individual zones to specific sets. `name-v4` / +`name-v6` then become optional fallbacks for any zones not covered by a +`set:` line; at least one `set:` line *or* a `name-v4` / `name-v6` pair +must be present. + +``` +server: + local-zone: "netflix.com." nftset + local-zone: "nflxso.net." nftset + local-zone: "example.com." nftset + +nftset: + table: "fw4" + set: "netflix.com." "vpn_v4" "vpn_v6" + set: "nflxso.net." "vpn_v4" "vpn_v6" + set: "example.com." "work_v4" "work_v6" +``` + +Each `set:` line takes three arguments: the zone name, the IPv4 set name, +and the IPv6 set name. The zone name may include or omit the trailing +dot. If a zone has a matching `local-zone: "..." ipset` entry but no +`set:` line, it falls back to the global `name-v4` / `name-v6` (if +configured), or is skipped if neither is present. + +--- + +### Creating the sets + +**ipset (iptables)**: +``` +ipset -N blacklist iphash +ipset -N blacklist6 iphash +``` + +**nft (nftables)**: + +The nft backend always inserts elements as intervals (a start address and +an exclusive end address one greater), so the destination set **must** +declare `flags interval`. Combine it with `flags timeout` if you also +want elements to age off automatically: + +``` +nft add table inet fw4 +nft add set inet fw4 blacklist { type ipv4_addr\; flags interval\; } +nft add set inet fw4 blacklist6 { type ipv6_addr\; flags interval\; } +``` + +Or, in a loaded ruleset file: + +``` +set blacklist { + type ipv4_addr + flags interval, timeout + timeout 5m # fallback TTL when none is supplied by the resolver +} +``` + +A set defined without `flags interval` will reject every insert with +`EOPNOTSUPP` / `EINVAL`; unbound logs the failure once for that +`(table, setname)` and then silently skips further inserts to that +destination for the rest of the process's lifetime. + +--- + +### Firewall rules + +**iptables**: +``` +iptables -A INPUT -m set --match-set blacklist src -j DROP +ip6tables -A INPUT -m set --match-set blacklist6 src -j DROP +``` + +**nftables**: +``` +nft add rule inet fw4 output ip daddr @blacklist drop +nft add rule inet fw4 output ip6 daddr @blacklist6 drop +``` + +--- + +### Set lifecycle — administrator responsibility + +**Unbound does not create or destroy sets.** It only inserts elements. +The sets must already exist when unbound starts, and they must continue +to exist for the lifetime of the unbound process. + +If unbound is unable to add to a set (for example because the set does +not exist), it logs the error **once** for that `(table, setname)` pair +and then suppresses further attempts to that destination for the rest of +the process's lifetime. Fix the configuration and reload to retry. + +On systems where the firewall configuration is managed by a tool (e.g. +OpenWrt's `fw4`, `firewalld`), the sets must be defined in a way that +survives firewall reloads and reboots — typically a drop-in config file +loaded by the firewall tool. Ad-hoc `nft add set` / `ipset -N` commands +at the shell are not sufficient for production use because they are lost +on reload. + +--- + +### Notes + +* On Linux, root privileges (or `CAP_NET_ADMIN`) are required for both + backends. When Unbound drops privileges via the `username:` setting, it + automatically preserves `CAP_NET_ADMIN` (`PR_SET_KEEPCAPS`) if the + module is configured. +* On BSD, Unbound requires read/write access to `/dev/pf`. If dropping + privileges, ensure the Unbound user is in a group with access to the + device. +* `name-v4` / `name-v6` are bare set names. For the nftset backend, the + table that contains them is given separately in `table:`. +* Only one backend is active per Unbound process — the configuration + section used (`ipset:` vs `nftset:`) selects which is active. +* On BSD only the `ipset:` section is supported, this populates PF tables. diff --git a/doc/README.ipset.md b/doc/README.ipset.md deleted file mode 100644 index 4bd993e67..000000000 --- a/doc/README.ipset.md +++ /dev/null @@ -1,65 +0,0 @@ -## Created a module to support the ipset that could add the domain's ip to a list easily. - -### Purposes: -* In my case, I can't access the facebook, twitter, youtube and thousands web site for some reason. VPN is a solution. But the internet too slow whether all traffics pass through the vpn. -So, I set up a transparent proxy to proxy the traffic which has been blocked only. -At the final step, I need to install a dns service which would work with ipset well to launch the system. -I did some research for this. Unfortunately, Unbound, My favorite dns service doesn't support ipset yet. So, I decided to implement it by my self and contribute the patch. It's good for me and the community. -``` -# unbound.conf -server: - ... - local-zone: "facebook.com" ipset - local-zone: "twitter.com" ipset - local-zone: "instagram.com" ipset - more social website - -ipset: - name-v4: "gfwlist" -``` -``` -# iptables -iptables -A PREROUTING -p tcp -m set --match-set gfwlist dst -j REDIRECT --to-ports 10800 -iptables -A OUTPUT -p tcp -m set --match-set gfwlist dst -j REDIRECT --to-ports 10800 -``` - -* This patch could work with iptables rules to batch block the IPs. -``` -# unbound.conf -server: - ... - local-zone: "facebook.com" ipset - local-zone: "twitter.com" ipset - local-zone: "instagram.com" ipset - more social website - -ipset: - name-v4: "blacklist" - name-v6: "blacklist6" -``` -``` -# iptables -iptables -A INPUT -m set --set blacklist src -j DROP -ip6tables -A INPUT -m set --set blacklist6 src -j DROP -``` - -### Notes: -* To enable this module the root privileges is required. -* Please create a set with ipset command first. eg. **ipset -N blacklist iphash** - -### How to use: -``` -./configure --enable-ipset -make && make install -``` - -### Configuration: -``` -# unbound.conf -server: - ... - local-zone: "example.com" ipset - -ipset: - name-v4: "blacklist" -``` diff --git a/doc/example.conf.in b/doc/example.conf.in index 2c6d63409..4151f2637 100644 --- a/doc/example.conf.in +++ b/doc/example.conf.in @@ -1383,16 +1383,30 @@ remote-control: # # redis logical database to use for the replica server, 0 is the default database. # redis-replica-logical-db: 0 -# IPSet -# Add specify domain into set via ipset. -# To enable: -# o use --enable-ipset to configure before compiling; -# o Unbound then needs to run as root user. +# ipset module +# Add resolved A/AAAA addresses into a kernel firewall set. +# The section name selects the backend: +# ipset: — legacy iptables ipset (or PF tables on BSD); --enable-ipset +# nftset: — nftables sets (Linux only); --enable-nftset +# Only one section may be present. Either flag can be used alone or both +# compiled in (runtime choice is the section name, not a 'backend:' field). +# Unbound needs CAP_NET_ADMIN (typically run as root). +# +# iptables ipset example: # ipset: -# # set name for ip v4 addresses # name-v4: "list-v4" -# # set name for ip v6 addresses # name-v6: "list-v6" +# # per-zone set routing (optional) +# # set: "example.com." "v4_example" "v6_example" +# +# nftables example: +# nftset: +# # family: "inet" # "inet" (default), "ip", or "ip6" +# table: "fw4" +# name-v4: "list-v4" +# name-v6: "list-v6" +# # per-zone set routing (optional) +# # set: "example.com." "v4_example" "v6_example" # # Dnstap logging support, if compiled in by using --enable-dnstap to configure. diff --git a/ipset/ipset.c b/ipset/ipset.c index 1f9af4d9c..c546c47d7 100644 --- a/ipset/ipset.c +++ b/ipset/ipset.c @@ -1,9 +1,18 @@ /** * \file - * This file implements the ipset module. It can handle packets by putting - * the A and AAAA addresses that are configured in unbound.conf as type - * ipset (local-zone statements) into a firewall rule IPSet. For firewall + * This file implements the ipset/nftset module. It can handle packets by + * putting the A and AAAA addresses that are configured in unbound.conf as + * type ipset (local-zone statements) into a firewall set. For firewall * blacklist and whitelist usage. + * + * The same module entry points serve two backends, distinguished by the + * section name in unbound.conf: + * ipset: legacy iptables ipset on Linux, or PF tables on BSD. + * nftset: nftables sets on Linux (Linux-only). + * + * Both Linux backends speak netlink directly via libmnl. Sends are + * fire-and-forget; after every send the kernel error queue is drained + * non-blocking and any reported errors are logged. */ #include "config.h" #include "ipset/ipset.h" @@ -17,6 +26,10 @@ #include "sldns/wire2str.h" #include "sldns/parseutil.h" +#if defined(HAVE_NET_PFVAR_H) && defined(USE_NFTSET) +#error "nftset cannot be compiled with PF (BSD) support. nftset is Linux-only." +#endif + #ifdef HAVE_NET_PFVAR_H #include #include @@ -26,12 +39,51 @@ typedef intptr_t filter_dev; #else #include +#include #include +#ifdef USE_IPSET #include +#endif +#ifdef USE_NFTSET +#include +#include +#endif typedef struct mnl_socket * filter_dev; + +/* NETLINK_EXT_ACK and the NLMSGERR attributes were added in Linux 4.12. + * Provide fallbacks so older build environments still compile. */ +#ifndef NETLINK_EXT_ACK +#define NETLINK_EXT_ACK 11 +#endif +#ifndef NLM_F_CAPPED +#define NLM_F_CAPPED 0x100 +#endif +#ifndef NLM_F_ACK_TLVS +#define NLM_F_ACK_TLVS 0x200 +#endif +#ifndef NLMSGERR_ATTR_MSG +#define NLMSGERR_ATTR_MSG 1 #endif -#define BUFF_LEN 256 +/* Fallbacks for nftables name lengths (added in Linux 3.13) */ +#ifndef NFT_TABLE_MAXNAMELEN +#define NFT_TABLE_MAXNAMELEN 256 +#endif +#ifndef NFT_SET_MAXNAMELEN +#define NFT_SET_MAXNAMELEN 256 +#endif +#ifndef IPSET_MAXNAMELEN +#define IPSET_MAXNAMELEN 32 +#endif + +/* A 2KB stack buffer by far large enough to hold the netlink request messages + * that we build, but additionally needs to be large enough to hold the the + * netlink NLMSG_ERROR that may be produced by netlink_drain_errors. + * This includes the entire original message plus an error string. */ +#define NETLINK_BUFF_LEN 2048 +#endif + +#define DNAME_BUFF_LEN (LDNS_MAX_DOMAINLEN*4+16) /** * Return an error @@ -52,7 +104,7 @@ static int error_response(struct module_qstate* qstate, int id, int rcode) { } #ifdef HAVE_NET_PFVAR_H -static void * open_filter() { +static void * open_filter(void) { filter_dev dev; dev = open("/dev/pf", O_RDWR); @@ -64,8 +116,9 @@ static void * open_filter() { return (void *)dev; } #else -static void * open_filter() { +static void * open_filter(void) { filter_dev dev; + int on = 1; dev = mnl_socket_open(NETLINK_NETFILTER); if (!dev) { @@ -78,11 +131,101 @@ static void * open_filter() { log_err("ipset: could not bind netfilter."); return NULL; } + + /* Ask the kernel for a human-readable error string when an add is + * rejected. Best-effort: ignore failure on older kernels. */ + (void)setsockopt(mnl_socket_get_fd(dev), SOL_NETLINK, + NETLINK_EXT_ACK, &on, sizeof(on)); + return (void *)dev; } + +/* Drain any pending kernel replies on the netlink socket non-blocking and + * log any NLMSG_ERROR messages. Called after every send. Best-effort: + * caller / per-message context is not preserved, just the kernel error + * string is enough for diagnosing misconfiguration. */ +static void +netlink_drain_errors(filter_dev dev, char* buf, size_t buflen) +{ + int fd = mnl_socket_get_fd(dev); + ssize_t r; + int n; + struct nlmsghdr *nlh; + + for (;;) { + r = recv(fd, buf, buflen, MSG_DONTWAIT); + if (r < 0) { + if (errno == EINTR) + continue; + break; + } + if (r == 0) + break; + if ((size_t)r == buflen) { + log_warn("ipset: netlink error report possibly truncated"); + } + n = (int)r; + for (nlh = (struct nlmsghdr *)buf; mnl_nlmsg_ok(nlh, n); nlh = mnl_nlmsg_next(nlh, &n)) { + struct nlmsgerr *e; + const char *msg = NULL; + size_t hlen, total, alen; + void *start; + struct nlattr *attr; + + if (nlh->nlmsg_type != NLMSG_ERROR) + continue; + + e = mnl_nlmsg_get_payload(nlh); + if (!e->error) + continue; + + hlen = sizeof(*e); + total = mnl_nlmsg_get_payload_len(nlh); + if (!(nlh->nlmsg_flags & NLM_F_CAPPED)) + hlen += mnl_nlmsg_get_payload_len(&e->msg); + + if ((nlh->nlmsg_flags & NLM_F_ACK_TLVS) && hlen < total) { + start = (char *)e + hlen; + alen = total - hlen; + mnl_attr_for_each_payload(start, alen) { + if (mnl_attr_get_type(attr) == NLMSGERR_ATTR_MSG) { + msg = mnl_attr_get_str(attr); + break; + } + } + } + log_err("ipset: kernel reported error: %s%s%s", + strerror(-e->error), + msg ? ": " : "", + msg ? msg : ""); + } + } +} #endif -#ifdef HAVE_NET_PFVAR_H +#ifndef HAVE_NET_PFVAR_H +static struct nlmsghdr * +netlink_put_hdr(char *buf, uint16_t type, uint16_t family, uint16_t flags, + uint32_t seq, uint16_t res_id) +{ + struct nlmsghdr *nlh; + struct nfgenmsg *nfh; + + nlh = mnl_nlmsg_put_header(buf); + nlh->nlmsg_type = type; + nlh->nlmsg_flags = NLM_F_REQUEST | flags; + nlh->nlmsg_seq = seq; + + nfh = mnl_nlmsg_put_extra_header(nlh, sizeof(struct nfgenmsg)); + nfh->nfgen_family = family; + nfh->version = NFNETLINK_V0; + nfh->res_id = htons(res_id); + + return nlh; +} +#endif + +#if defined(HAVE_NET_PFVAR_H) && defined(USE_IPSET) static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr, int af) { struct pfioc_table io; struct pfr_addr addr; @@ -138,12 +281,11 @@ static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr, } return 0; } -#else +#elif defined(USE_IPSET) static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr, int af) { struct nlmsghdr *nlh; - struct nfgenmsg *nfg; struct nlattr *nested[2]; - char buffer[BUFF_LEN]; + char buffer[NETLINK_BUFF_LEN]; if (strlen(setname) >= IPSET_MAXNAMELEN) { errno = ENAMETOOLONG; @@ -154,14 +296,8 @@ static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr, return -1; } - nlh = mnl_nlmsg_put_header(buffer); - nlh->nlmsg_type = IPSET_CMD_ADD | (NFNL_SUBSYS_IPSET << 8); - nlh->nlmsg_flags = NLM_F_REQUEST|NLM_F_ACK|NLM_F_EXCL; - - nfg = mnl_nlmsg_put_extra_header(nlh, sizeof(struct nfgenmsg)); - nfg->nfgen_family = af; - nfg->version = NFNETLINK_V0; - nfg->res_id = htons(0); + nlh = netlink_put_hdr(buffer, IPSET_CMD_ADD | (NFNL_SUBSYS_IPSET << 8), + af, NLM_F_ACK | NLM_F_EXCL, 0, 0); mnl_attr_put_u8(nlh, IPSET_ATTR_PROTOCOL, IPSET_PROTOCOL); mnl_attr_put(nlh, IPSET_ATTR_SETNAME, strlen(setname) + 1, setname); @@ -175,9 +311,108 @@ static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr, if (mnl_socket_sendto(dev, nlh, nlh->nlmsg_len) < 0) { return -1; } + netlink_drain_errors(dev, buffer, sizeof(buffer)); return 0; } -#endif +#endif /* USE_IPSET */ + +#ifdef USE_NFTSET +static int nft_parse_family(const char *name) { + if (!name || !name[0]) return NFPROTO_INET; + if (strcmp(name, "inet") == 0) return NFPROTO_INET; + if (strcmp(name, "ip") == 0) return NFPROTO_IPV4; + if (strcmp(name, "ip6") == 0) return NFPROTO_IPV6; + return -1; +} + +static int add_to_nftset(filter_dev dev, const char *table, + const char *setname, const void *ipaddr, int af, int nfproto, + uint32_t *seq) +{ + char buffer[NETLINK_BUFF_LEN]; + struct nlmsghdr *nlh; + struct nlattr *nested[3]; + size_t off = 0, addr_size; + uint8_t end_addr[sizeof(struct in6_addr)]; + int i, overflow; + + if (!table || !table[0] || !setname || !setname[0]) { + errno = EINVAL; + return -1; + } + if (strlen(table) >= NFT_TABLE_MAXNAMELEN || + strlen(setname) >= NFT_SET_MAXNAMELEN) { + errno = ENAMETOOLONG; + return -1; + } + if (af == AF_INET) { + addr_size = sizeof(struct in_addr); + } else if (af == AF_INET6) { + addr_size = sizeof(struct in6_addr); + } else { + errno = EAFNOSUPPORT; + return -1; + } + + /* Compute exclusive interval end (ipaddr + 1, network byte order). */ + memcpy(end_addr, ipaddr, addr_size); + overflow = 1; + for (i = (int)addr_size - 1; i >= 0 && overflow; i--) { + overflow += (unsigned char)end_addr[i]; + end_addr[i] = overflow & 0xff; + overflow >>= 8; + } + + nlh = netlink_put_hdr(buffer, NFNL_MSG_BATCH_BEGIN, NFPROTO_UNSPEC, + 0, (*seq)++, NFNL_SUBSYS_NFTABLES); + off += nlh->nlmsg_len; + + nlh = netlink_put_hdr(buffer + off, + (NFNL_SUBSYS_NFTABLES << 8) | NFT_MSG_NEWSETELEM, + nfproto, NLM_F_CREATE | NLM_F_ACK, (*seq)++, 0); + + mnl_attr_put_strz(nlh, NFTA_SET_ELEM_LIST_TABLE, table); + mnl_attr_put_strz(nlh, NFTA_SET_ELEM_LIST_SET, setname); + + nested[0] = mnl_attr_nest_start(nlh, NFTA_SET_ELEM_LIST_ELEMENTS); + + nested[1] = mnl_attr_nest_start(nlh, NLA_F_NESTED | 1); + nested[2] = mnl_attr_nest_start(nlh, NFTA_SET_ELEM_KEY); + mnl_attr_put(nlh, NFTA_DATA_VALUE | NLA_F_NET_BYTEORDER, + addr_size, ipaddr); + mnl_attr_nest_end(nlh, nested[2]); + mnl_attr_nest_end(nlh, nested[1]); + + /* Interval sets need an explicit exclusive end (ipaddr+1, INTERVAL_END + * flag); without it the kernel creates an open-ended interval. + * If the addition overflows (e.g., 255.255.255.255 + 1), the interval + * inherently ends at the boundary of the address space, so no explicit + * INTERVAL_END is needed. */ + if (!overflow) { + nested[1] = mnl_attr_nest_start(nlh, NLA_F_NESTED | 2); + mnl_attr_put_u32(nlh, NFTA_SET_ELEM_FLAGS, + htonl(NFT_SET_ELEM_INTERVAL_END)); + nested[2] = mnl_attr_nest_start(nlh, NFTA_SET_ELEM_KEY); + mnl_attr_put(nlh, NFTA_DATA_VALUE | NLA_F_NET_BYTEORDER, + addr_size, end_addr); + mnl_attr_nest_end(nlh, nested[2]); + mnl_attr_nest_end(nlh, nested[1]); + } + + mnl_attr_nest_end(nlh, nested[0]); + off += nlh->nlmsg_len; + + nlh = netlink_put_hdr(buffer + off, NFNL_MSG_BATCH_END, NFPROTO_UNSPEC, + 0, (*seq)++, NFNL_SUBSYS_NFTABLES); + off += nlh->nlmsg_len; + + if (mnl_socket_sendto(dev, buffer, off) < 0) { + return -1; + } + netlink_drain_errors(dev, buffer, sizeof(buffer)); + return 0; +} +#endif /* USE_NFTSET */ static void ipset_add_rrset_data(struct ipset_env *ie, @@ -205,7 +440,22 @@ ipset_add_rrset_data(struct ipset_env *ie, snprintf(ip, sizeof(ip), "(inet_ntop_error)"); verbose(VERB_QUERY, "ipset: add %s to %s for %s", ip, setname, dname); } - ret = add_to_ipset((filter_dev)ie->dev, setname, rr_data + 2, af); +#ifdef USE_NFTSET + if (ie->use_nft) { + ret = add_to_nftset((filter_dev)ie->dev, + ie->table, setname, rr_data + 2, + af, ie->nfproto, &ie->seq); + } else +#endif + { +#ifdef USE_IPSET + ret = add_to_ipset((filter_dev)ie->dev, + setname, rr_data + 2, af); +#else + (void)setname; + ret = -1; +#endif + } if (ret < 0) { log_err("ipset: could not add %s into %s", dname, setname); break; @@ -219,22 +469,22 @@ ipset_check_zones_for_rrset(struct module_env *env, struct ipset_env *ie, struct ub_packed_rrset_key *rrset, const char *qname, int qlen, const char *setname, int af) { - char dname[LDNS_MAX_DOMAINLEN*4+16]; + char dname[DNAME_BUFF_LEN]; const char *ds, *qs; int dlen, plen; struct config_strlist *p; struct packed_rrset_data *d; - dlen = sldns_wire2str_dname_buf(rrset->rk.dname, rrset->rk.dname_len, dname, sizeof(dname)); + dlen = sldns_wire2str_dname_buf(rrset->rk.dname, rrset->rk.dname_len, dname, DNAME_BUFF_LEN); if (dlen == 0 || dlen >= (int)sizeof(dname)) { log_err("bad domain name"); return -1; } - if (dname[dlen - 1] == '.') { + if (dlen > 0 && dname[dlen - 1] == '.') { dlen--; } - if (qname[qlen - 1] == '.') { + if (qlen > 0 && qname[qlen - 1] == '.') { qlen--; } @@ -242,7 +492,7 @@ ipset_check_zones_for_rrset(struct module_env *env, struct ipset_env *ie, ds = NULL; qs = NULL; plen = strlen(p->str); - if (p->str[plen - 1] == '.') { + if (plen > 0 && p->str[plen - 1] == '.') { plen--; } @@ -254,8 +504,24 @@ ipset_check_zones_for_rrset(struct module_env *env, struct ipset_env *ie, } if ((ds && strncasecmp(p->str, ds, plen) == 0) || (qs && strncasecmp(p->str, qs, plen) == 0)) { - d = (struct packed_rrset_data*)rrset->entry.data; - ipset_add_rrset_data(ie, d, setname, af, dname); + const char *use_setname = setname; /* global fallback */ + struct config_str3list *zp; + int zplen; + /* Check for a per-zone set: entry matching this zone */ + for (zp = env->cfg->ipset_zones; zp; zp = zp->next) { + zplen = strlen(zp->str); + if (zplen > 0 && zp->str[zplen - 1] == '.') zplen--; + if (plen == zplen && + strncasecmp(p->str, zp->str, plen) == 0) { + use_setname = (af == AF_INET) ? + zp->str2 : zp->str3; + break; + } + } + if (use_setname && strlen(use_setname) > 0) { + d = (struct packed_rrset_data*)rrset->entry.data; + ipset_add_rrset_data(ie, d, use_setname, af, dname); + } break; } } @@ -269,7 +535,7 @@ static int ipset_update(struct module_env *env, struct dns_msg *return_msg, const char *setname; struct ub_packed_rrset_key *rrset; int af; - char qname[LDNS_MAX_DOMAINLEN*4+16]; + char qname[DNAME_BUFF_LEN]; int qlen; #ifdef HAVE_NET_PFVAR_H @@ -285,26 +551,34 @@ static int ipset_update(struct module_env *env, struct dns_msg *return_msg, #endif qlen = sldns_wire2str_dname_buf(qinfo.qname, qinfo.qname_len, - qname, sizeof(qname)); + qname, DNAME_BUFF_LEN); if(qlen == 0 || qlen >= (int)sizeof(qname)) { log_err("bad domain name"); return -1; } for(i = 0; i < return_msg->rep->rrset_count; i++) { + int type_matched = 0; setname = NULL; + af = 0; rrset = return_msg->rep->rrsets[i]; if(ntohs(rrset->rk.type) == LDNS_RR_TYPE_A && ie->v4_enabled == 1) { af = AF_INET; setname = ie->name_v4; + type_matched = 1; } else if(ntohs(rrset->rk.type) == LDNS_RR_TYPE_AAAA && ie->v6_enabled == 1) { af = AF_INET6; setname = ie->name_v6; + type_matched = 1; } - if (setname) { + /* Enter zone lookup when this RRset type is enabled, even if no + * global setname is configured — per-zone entries may supply the + * set name, and ipset_check_zones_for_rrset() handles NULL + * setname (no global fallback). */ + if (type_matched) { if(ipset_check_zones_for_rrset(env, ie, rrset, qname, qlen, setname, af) == -1) return -1; @@ -333,6 +607,9 @@ int ipset_startup(struct module_env* env, int id) { } #else ipset_env->dev = NULL; +#endif +#ifdef USE_NFTSET + ipset_env->seq = 1; #endif return 1; } @@ -363,24 +640,89 @@ void ipset_destartup(struct module_env* env, int id) { int ipset_init(struct module_env* env, int id) { struct ipset_env *ipset_env = env->modinfo[id]; +#ifdef USE_NFTSET + ipset_env->use_nft = env->cfg->ipset_use_nft; + if (ipset_env->use_nft) { + ipset_env->family = env->cfg->ipset_family ? + env->cfg->ipset_family : "inet"; + ipset_env->table = env->cfg->ipset_table; + if (!ipset_env->table || !ipset_env->table[0]) { + log_err("nftset: 'table:' is required"); + return 0; + } + ipset_env->nfproto = nft_parse_family(ipset_env->family); + if (ipset_env->nfproto < 0) { + log_err("nftset: invalid family '%s' (expected " + "inet, ip, or ip6)", ipset_env->family); + return 0; + } + } else +#endif + { +#ifdef USE_IPSET + if (env->cfg->ipset_family || env->cfg->ipset_table) { + log_err("ipset: 'family:' and 'table:' are not supported " + "by the ipset backend"); + return 0; + } +#else + log_err("ipset: ip backend not compiled in"); + return 0; +#endif + } + ipset_env->name_v4 = env->cfg->ipset_name_v4; ipset_env->name_v6 = env->cfg->ipset_name_v6; + #ifndef HAVE_NET_PFVAR_H - if (ipset_env->name_v4 && strlen(ipset_env->name_v4) >= IPSET_MAXNAMELEN) { - log_err("ipset: name-v4 exceeds IPSET_MAXNAMELEN (%d)", IPSET_MAXNAMELEN); - return 0; - } - if (ipset_env->name_v6 && strlen(ipset_env->name_v6) >= IPSET_MAXNAMELEN) { - log_err("ipset: name-v6 exceeds IPSET_MAXNAMELEN (%d)", IPSET_MAXNAMELEN); - return 0; + { + int maxlen = IPSET_MAXNAMELEN; + struct config_str3list *zp; +#ifdef USE_NFTSET + if (ipset_env->use_nft) { + maxlen = NFT_SET_MAXNAMELEN; + if (ipset_env->table && strlen(ipset_env->table) >= NFT_TABLE_MAXNAMELEN) { + log_err("nftset: table name exceeds NFT_TABLE_MAXNAMELEN (%d)", NFT_TABLE_MAXNAMELEN); + return 0; + } + } +#endif + if (ipset_env->name_v4 && strlen(ipset_env->name_v4) >= maxlen) { + log_err("ipset: name-v4 exceeds max name length (%d)", maxlen); + return 0; + } + if (ipset_env->name_v6 && strlen(ipset_env->name_v6) >= maxlen) { + log_err("ipset: name-v6 exceeds max name length (%d)", maxlen); + return 0; + } + for (zp = env->cfg->ipset_zones; zp; zp = zp->next) { + if (zp->str2 && strlen(zp->str2) >= maxlen) { + log_err("ipset: per-zone v4 name '%s' exceeds max name length (%d)", zp->str2, maxlen); + return 0; + } + if (zp->str3 && strlen(zp->str3) >= maxlen) { + log_err("ipset: per-zone v6 name '%s' exceeds max name length (%d)", zp->str3, maxlen); + return 0; + } + } } #endif - ipset_env->v4_enabled = !ipset_env->name_v4 || (strlen(ipset_env->name_v4) == 0) ? 0 : 1; - ipset_env->v6_enabled = !ipset_env->name_v6 || (strlen(ipset_env->name_v6) == 0) ? 0 : 1; + /* Enable based on configuration: any global name OR any per-zone entry + * makes the corresponding family active. The per-zone match in + * ipset_check_zones_for_rrset() will choose the actual set name. */ + ipset_env->v4_enabled = ((ipset_env->name_v4 && + strlen(ipset_env->name_v4) > 0) || + env->cfg->ipset_zones) ? 1 : 0; + ipset_env->v6_enabled = ((ipset_env->name_v6 && + strlen(ipset_env->name_v6) > 0) || + env->cfg->ipset_zones) ? 1 : 0; - if ((ipset_env->v4_enabled < 1) && (ipset_env->v6_enabled < 1)) { - log_err("ipset: set name no configuration?"); + /* OK if per-zone set: entries are present even without global name-v4/v6 */ + if ((ipset_env->v4_enabled < 1) && (ipset_env->v6_enabled < 1) && + !env->cfg->ipset_zones) { + log_err("ipset: no set names configured; add 'name-v4:'/'name-v6:' " + "for a global set or 'set: ' for per-zone sets"); return 0; } @@ -469,15 +811,9 @@ void ipset_inform_super(struct module_qstate *ATTR_UNUSED(qstate), } void ipset_clear(struct module_qstate *qstate, int id) { - struct cachedb_qstate *iq; if (!qstate) { return; } - iq = (struct cachedb_qstate *)qstate->minfo[id]; - if (iq) { - /* free contents of iq */ - /* TODO */ - } qstate->minfo[id] = NULL; } @@ -489,8 +825,9 @@ size_t ipset_get_mem(struct module_env *env, int id) { return sizeof(*ie); } +#ifdef USE_IPSET /** - * The ipset function block + * The ipset function block */ static struct module_func_block ipset_block = { "ipset", @@ -501,4 +838,20 @@ static struct module_func_block ipset_block = { struct module_func_block * ipset_get_funcblock(void) { return &ipset_block; } +#endif +#ifdef USE_NFTSET +/** + * The nftset function block — same functions, different name so that + * module_factory matches it against module-config: "nftset ...". + */ +static struct module_func_block nftset_block = { + "nftset", + &ipset_startup, &ipset_destartup, &ipset_init, &ipset_deinit, + &ipset_operate, &ipset_inform_super, &ipset_clear, &ipset_get_mem +}; + +struct module_func_block * nftset_get_funcblock(void) { + return &nftset_block; +} +#endif \ No newline at end of file diff --git a/ipset/ipset.h b/ipset/ipset.h index 195c7db93..729fcd624 100644 --- a/ipset/ipset.h +++ b/ipset/ipset.h @@ -8,26 +8,28 @@ #define IPSET_H /** \file * - * This file implements the ipset module. It can handle packets by putting - * the A and AAAA addresses that are configured in unbound.conf as type - * ipset (local-zone statements) into a firewall rule IPSet. For firewall + * This file implements the ipset/nftset module. It can handle packets by + * putting the A and AAAA addresses that are configured in unbound.conf as + * type ipset (local-zone statements) into a firewall set. For firewall * blacklist and whitelist usage. * - * To use the IPset module, install the libmnl-dev (or libmnl-devel) package - * and configure with --enable-ipset. And compile. Then enable the ipset - * module in unbound.conf with module-config: "ipset validator iterator" - * then create it with ipset -N blacklist iphash and then add - * local-zone: "example.com." ipset - * statements for the zones where you want the addresses of the names - * looked up added to the set. + * Two backends are selected by the section name in unbound.conf: * - * Set the name of the set with - * ipset: - * name-v4: "blacklist" - * name-v6: "blacklist6" - * in unbound.conf. The set can be used in this way: - * iptables -A INPUT -m set --set blacklist src -j DROP - * ip6tables -A INPUT -m set --set blacklist6 src -j DROP + * ipset: legacy iptables ipset on Linux (NFNL_SUBSYS_IPSET) + * name-v4: "blacklist" or PF tables on BSD. + * name-v6: "blacklist6" + * + * nftset: nftables sets on Linux (NFNL_SUBSYS_NFTABLES). + * family: "inet" Adds two extra fields: + * table: "fw4" family: "inet" | "ip" | "ip6" (default "inet") + * name-v4: "blacklist" table: required + * name-v6: "blacklist6" + * + * Addresses go in via netlink (libmnl) on Linux. Sends are fire-and-forget; + * the kernel error queue is drained after every send and any reported + * errors are logged best-effort. Caller / per-message context is not + * preserved — the kernel error string (NETLINK_EXT_ACK) is enough to + * diagnose misconfiguration. */ #include "util/module.h" @@ -44,6 +46,15 @@ struct ipset_env { const char *name_v4; const char *name_v6; + +#ifdef USE_NFTSET + /* nft-only runtime state — unused for ip backend. */ + int use_nft; /* 1 if this env is the nft backend */ + int nfproto; /* NFPROTO_INET / NFPROTO_IPV4 / NFPROTO_IPV6 */ + const char *family; /* original family string from config */ + const char *table; /* nftables table name */ + uint32_t seq; /* monotonic netlink seq counter */ +#endif }; struct ipset_qstate { @@ -69,15 +80,26 @@ void ipset_clear(struct module_qstate* qstate, int id); /** return memory estimate for ipset module */ size_t ipset_get_mem(struct module_env* env, int id); +#ifdef USE_IPSET /** * Get the function block with pointers to the ipset functions * @return the function block for "ipset". */ struct module_func_block* ipset_get_funcblock(void); +#endif + +#ifdef USE_NFTSET +/** + * Get the function block for the nftset module. + * Same functions as ipset, but the block name is "nftset" so that + * modstack_call_init can match it against module-config: "nftset ...". + * @return the function block for "nftset". + */ +struct module_func_block* nftset_get_funcblock(void); +#endif #ifdef __cplusplus } #endif #endif /* IPSET_H */ - diff --git a/services/modstack.c b/services/modstack.c index f5913dc38..b2bce0606 100644 --- a/services/modstack.c +++ b/services/modstack.c @@ -63,7 +63,7 @@ #ifdef CLIENT_SUBNET #include "edns-subnet/subnetmod.h" #endif -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) #include "ipset/ipset.h" #endif @@ -88,6 +88,26 @@ count_modules(const char* s) return num; } +int +modstack_has_module(const char* module_conf, const char* name) +{ + size_t nlen; + if(!module_conf || !name) + return 0; + nlen = strlen(name); + while(*module_conf) { + while(*module_conf && isspace((unsigned char)*module_conf)) + module_conf++; + if(strncmp(module_conf, name, nlen) == 0 && + (module_conf[nlen] == '\0' || + isspace((unsigned char)module_conf[nlen]))) + return 1; + while(*module_conf && !isspace((unsigned char)*module_conf)) + module_conf++; + } + return 0; +} + void modstack_init(struct module_stack* stack) { @@ -170,6 +190,10 @@ module_list_avail(void) #endif #ifdef USE_IPSET "ipset", +#endif +#ifdef USE_NFTSET + /* nftset is an alias — same module, nft backend selected via nftset: section */ + "nftset", #endif "respip", "validator", @@ -204,6 +228,9 @@ module_funcs_avail(void) #endif #ifdef USE_IPSET &ipset_get_funcblock, +#endif +#ifdef USE_NFTSET + &nftset_get_funcblock, #endif &respip_get_funcblock, &val_get_funcblock, diff --git a/services/modstack.h b/services/modstack.h index 03a4c82c4..4a921f0e3 100644 --- a/services/modstack.h +++ b/services/modstack.h @@ -134,6 +134,16 @@ void modstack_call_destartup(struct module_stack* stack, struct module_env* env) */ int modstack_find(struct module_stack* stack, const char* name); +/** + * Test whether a whitespace-separated module-config string contains a given + * module name as a complete token. Unlike strstr(), this does not match + * substrings (e.g. "ipset" does not match "ipsetfoo"). + * @param module_conf: module-config string (may be NULL). + * @param name: module name to look for. + * @return 1 if present as a token, 0 otherwise. + */ +int modstack_has_module(const char* module_conf, const char* name); + /** fetch memory for a module by name, returns 0 if module not there */ size_t mod_get_mem(struct module_env* env, const char* name); diff --git a/smallapp/unbound-checkconf.c b/smallapp/unbound-checkconf.c index a8e19241f..a2f5a9e0e 100644 --- a/smallapp/unbound-checkconf.c +++ b/smallapp/unbound-checkconf.c @@ -983,6 +983,12 @@ morechecks(struct config_file* cfg) && strcmp(cfg->module_conf, "validator ipset respip iterator") != 0 && strcmp(cfg->module_conf, "ipset iterator") != 0 && strcmp(cfg->module_conf, "ipset respip iterator") != 0 +#endif +#ifdef USE_NFTSET + && strcmp(cfg->module_conf, "validator nftset iterator") != 0 + && strcmp(cfg->module_conf, "validator nftset respip iterator") != 0 + && strcmp(cfg->module_conf, "nftset iterator") != 0 + && strcmp(cfg->module_conf, "nftset respip iterator") != 0 #endif ) { fatal_exit("module conf '%s' is not known to work", diff --git a/testdata/04-checkconf.tdir/04-checkconf.test b/testdata/04-checkconf.tdir/04-checkconf.test index 339e346d9..224f9f12d 100644 --- a/testdata/04-checkconf.tdir/04-checkconf.test +++ b/testdata/04-checkconf.tdir/04-checkconf.test @@ -34,6 +34,12 @@ if grep "define USE_DNSCRYPT 1" ../../config.h; then else with_dnscrypt=0 fi +# detect nftset +if grep "define USE_NFTSET 1" ../../config.h; then + with_nftset=1 +else + with_nftset=0 +fi # test check of config files. for f in bad.*; do @@ -47,6 +53,10 @@ for f in bad.*; do echo "skipped; no DNSCRYPT support" continue fi + if echo $f | grep -q "nftset" && test $with_nftset -eq 0; then + echo "skipped; no USE_NFTSET support" + continue + fi $PRE/unbound-checkconf $f if test $? != 1; then @@ -57,6 +67,10 @@ done for f in good.*; do echo echo $PRE/unbound-checkconf $f + if echo $f | grep -q "nftset" && test $with_nftset -eq 0; then + echo "skipped; no USE_NFTSET support" + continue + fi $PRE/unbound-checkconf $f if test $? != 0; then echo "exit code case $f wrong" diff --git a/testdata/04-checkconf.tdir/bad.nftset-dup-family b/testdata/04-checkconf.tdir/bad.nftset-dup-family new file mode 100644 index 000000000..237c71fdd --- /dev/null +++ b/testdata/04-checkconf.tdir/bad.nftset-dup-family @@ -0,0 +1,11 @@ +server: + chroot: "" + username: "" + directory: "." + pidfile: "" + +nftset: + family: "inet" + family: "ip" + table: "fw4" + name-v4: "blocklist" diff --git a/testdata/04-checkconf.tdir/bad.nftset-dup-namev4 b/testdata/04-checkconf.tdir/bad.nftset-dup-namev4 new file mode 100644 index 000000000..8556aab5d --- /dev/null +++ b/testdata/04-checkconf.tdir/bad.nftset-dup-namev4 @@ -0,0 +1,10 @@ +server: + chroot: "" + username: "" + directory: "." + pidfile: "" + +nftset: + table: "fw4" + name-v4: "blocklist" + name-v4: "blocklist2" diff --git a/testdata/04-checkconf.tdir/bad.nftset-dup-namev6 b/testdata/04-checkconf.tdir/bad.nftset-dup-namev6 new file mode 100644 index 000000000..a00bb029f --- /dev/null +++ b/testdata/04-checkconf.tdir/bad.nftset-dup-namev6 @@ -0,0 +1,10 @@ +server: + chroot: "" + username: "" + directory: "." + pidfile: "" + +nftset: + table: "fw4" + name-v6: "blocklist6" + name-v6: "blocklist6b" diff --git a/testdata/04-checkconf.tdir/bad.nftset-dup-section b/testdata/04-checkconf.tdir/bad.nftset-dup-section new file mode 100644 index 000000000..500b8530e --- /dev/null +++ b/testdata/04-checkconf.tdir/bad.nftset-dup-section @@ -0,0 +1,12 @@ +server: + chroot: "" + username: "" + directory: "." + pidfile: "" + +ipset: + name-v4: "blocklist" + +nftset: + table: "fw4" + name-v4: "blocklist" diff --git a/testdata/04-checkconf.tdir/bad.nftset-dup-table b/testdata/04-checkconf.tdir/bad.nftset-dup-table new file mode 100644 index 000000000..c0f859065 --- /dev/null +++ b/testdata/04-checkconf.tdir/bad.nftset-dup-table @@ -0,0 +1,10 @@ +server: + chroot: "" + username: "" + directory: "." + pidfile: "" + +nftset: + table: "fw4" + table: "filter" + name-v4: "blocklist" diff --git a/testdata/04-checkconf.tdir/good.nftset b/testdata/04-checkconf.tdir/good.nftset new file mode 100644 index 000000000..5c3c2664e --- /dev/null +++ b/testdata/04-checkconf.tdir/good.nftset @@ -0,0 +1,13 @@ +server: + chroot: "" + username: "" + directory: "." + pidfile: "" + local-zone: "example.com." nftset + local-zone: "blocked.org." nftset + +nftset: + family: "inet" + table: "fw4" + name-v4: "blocklist" + name-v6: "blocklist6" diff --git a/testdata/04-checkconf.tdir/good.nftset-minimal b/testdata/04-checkconf.tdir/good.nftset-minimal new file mode 100644 index 000000000..71126947d --- /dev/null +++ b/testdata/04-checkconf.tdir/good.nftset-minimal @@ -0,0 +1,9 @@ +server: + chroot: "" + username: "" + directory: "." + pidfile: "" + +nftset: + table: "fw4" + name-v4: "blocklist" diff --git a/testdata/04-checkconf.tdir/good.nftset-no-family b/testdata/04-checkconf.tdir/good.nftset-no-family new file mode 100644 index 000000000..447ed1444 --- /dev/null +++ b/testdata/04-checkconf.tdir/good.nftset-no-family @@ -0,0 +1,10 @@ +server: + chroot: "" + username: "" + directory: "." + pidfile: "" + +nftset: + table: "fw4" + name-v4: "blocklist" + name-v6: "blocklist6" diff --git a/testdata/04-checkconf.tdir/good.nftset-v6only b/testdata/04-checkconf.tdir/good.nftset-v6only new file mode 100644 index 000000000..4671bd0ab --- /dev/null +++ b/testdata/04-checkconf.tdir/good.nftset-v6only @@ -0,0 +1,9 @@ +server: + chroot: "" + username: "" + directory: "." + pidfile: "" + +nftset: + table: "fw4" + name-v6: "blocklist6" diff --git a/testdata/nftset.tdir/nftset.conf b/testdata/nftset.tdir/nftset.conf new file mode 100644 index 000000000..a64eed45d --- /dev/null +++ b/testdata/nftset.tdir/nftset.conf @@ -0,0 +1,30 @@ +server: + verbosity: 3 + num-threads: 1 + module-config: "nftset iterator" + outgoing-range: 16 + interface: 127.0.0.1 + port: @PORT@ + use-syslog: no + directory: "" + pidfile: "unbound.pid" + chroot: "" + username: "" + do-not-query-localhost: no + local-zone: "example.net." nftset + local-zone: "example.com." nftset +stub-zone: + name: "example.net." + stub-addr: "127.0.0.1@@TOPORT@" +stub-zone: + name: "example.com." + stub-addr: "127.0.0.1@@TOPORT@" +stub-zone: + name: "lookslikeexample.net." + stub-addr: "127.0.0.1@@TOPORT@" +nftset: + family: inet + table: nfttest + name-v4: atotallymadeupnamefor4 + name-v6: atotallymadeupnamefor6 + set: "example.com." "custom_v4" "custom_v6" diff --git a/testdata/nftset.tdir/nftset.dsc b/testdata/nftset.tdir/nftset.dsc new file mode 100644 index 000000000..5b1823efc --- /dev/null +++ b/testdata/nftset.tdir/nftset.dsc @@ -0,0 +1,16 @@ +BaseName: nftset +Version: 1.0 +Description: mock test nftset module +CreationDate: Sun Feb 15 2026 +Maintainer: unbound developers +Category: +Component: +CmdDepends: +Depends: +Help: +Pre: nftset.pre +Post: nftset.post +Test: nftset.test +AuxFiles: +Passed: +Failure: diff --git a/testdata/nftset.tdir/nftset.post b/testdata/nftset.tdir/nftset.post new file mode 100644 index 000000000..33921d3ba --- /dev/null +++ b/testdata/nftset.tdir/nftset.post @@ -0,0 +1,13 @@ +# #-- nftset.post --# +# source the master var file when it's there +[ -f ../.tpkg.var.master ] && source ../.tpkg.var.master +# source the test var file when it's there +[ -f .tpkg.var.test ] && source .tpkg.var.test +# +# do your teardown here +. ../common.sh +PRE="../.." +kill_pid $FWD_PID +kill_pid $UNBOUND_PID +cat unbound.log +exit 0 diff --git a/testdata/nftset.tdir/nftset.pre b/testdata/nftset.tdir/nftset.pre new file mode 100644 index 000000000..b092e1d2a --- /dev/null +++ b/testdata/nftset.tdir/nftset.pre @@ -0,0 +1,33 @@ +# #-- nftset.pre--# +# source the master var file when it's there +[ -f ../.tpkg.var.master ] && source ../.tpkg.var.master +# use .tpkg.var.test for in test variable passing +[ -f .tpkg.var.test ] && source .tpkg.var.test + +. ../common.sh + +PRE="../.." +if grep "define USE_NFTSET 1" $PRE/config.h; then echo test enabled; else skip_test "test skipped"; fi + +get_random_port 2 +UNBOUND_PORT=$RND_PORT +FWD_PORT=$(($RND_PORT + 1)) +echo "UNBOUND_PORT=$UNBOUND_PORT" >> .tpkg.var.test +echo "FWD_PORT=$FWD_PORT" >> .tpkg.var.test + +# start forwarder +get_ldns_testns +$LDNS_TESTNS -p $FWD_PORT nftset.testns >fwd.log 2>&1 & +FWD_PID=$! +echo "FWD_PID=$FWD_PID" >> .tpkg.var.test + +# make config file +sed -e 's/@PORT\@/'$UNBOUND_PORT'/' -e 's/@TOPORT\@/'$FWD_PORT'/' < nftset.conf > ub.conf +# start unbound in the background +$PRE/unbound -d -c ub.conf >unbound.log 2>&1 & +UNBOUND_PID=$! +echo "UNBOUND_PID=$UNBOUND_PID" >> .tpkg.var.test + +cat .tpkg.var.test +wait_ldns_testns_up fwd.log +wait_unbound_up unbound.log diff --git a/testdata/nftset.tdir/nftset.test b/testdata/nftset.tdir/nftset.test new file mode 100644 index 000000000..5dc139b97 --- /dev/null +++ b/testdata/nftset.tdir/nftset.test @@ -0,0 +1,222 @@ +# #-- nftset.test --# +# source the master var file when it's there +[ -f ../.tpkg.var.master ] && source ../.tpkg.var.master +# use .tpkg.var.test for in test variable passing +[ -f .tpkg.var.test ] && source .tpkg.var.test + +. ../common.sh +PRE="../.." + +# Make all the queries. They need to succeed by the way. +echo "> dig www.example.net." +dig @127.0.0.1 -p $UNBOUND_PORT www.example.net. | tee outfile +echo "> check answer" +if grep "1.1.1.1" outfile; then + echo "OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi +echo "> check nftset" +if grep "ipset: add 1.1.1.1 to atotallymadeupnamefor4 for www.example.net." unbound.log; then + echo "nftset OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi + +echo "> dig www.example.net. AAAA" +dig @127.0.0.1 -p $UNBOUND_PORT www.example.net. AAAA | tee outfile +echo "> check answer" +if grep "::1" outfile; then + echo "OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi +echo "> check nftset" +if grep "ipset: add ::1 to atotallymadeupnamefor6 for www.example.net." unbound.log; then + echo "nftset OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi + +echo "> dig cname.example.net." +dig @127.0.0.1 -p $UNBOUND_PORT cname.example.net. | tee outfile +echo "> check answer" +if grep "2.2.2.2" outfile; then + echo "OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi +echo "> check nftset" +if grep "ipset: add 2.2.2.2 to atotallymadeupnamefor4 for target.example.net." unbound.log; then + echo "nftset OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi + +echo "> dig cname.example.net. AAAA" +dig @127.0.0.1 -p $UNBOUND_PORT cname.example.net. AAAA | tee outfile +echo "> check answer" +if grep "::2" outfile; then + echo "OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi +echo "> check nftset" +if grep "ipset: add ::2 to atotallymadeupnamefor6 for target.example.net." unbound.log; then + echo "nftset OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi + +echo "> dig outsidecname.example.net." +dig @127.0.0.1 -p $UNBOUND_PORT outsidecname.example.net. | tee outfile +echo "> check answer" +if grep "3.3.3.3" outfile; then + echo "OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi +echo "> check nftset" +if grep "ipset: add 3.3.3.3 to atotallymadeupnamefor4 for target.example.com." unbound.log; then + echo "nftset OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi + +echo "> dig outsidecname.example.net. AAAA" +dig @127.0.0.1 -p $UNBOUND_PORT outsidecname.example.net. AAAA | tee outfile +echo "> check answer" +if grep "::3" outfile; then + echo "OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi +echo "> check nftset" +if grep "ipset: add ::3 to atotallymadeupnamefor6 for target.example.com." unbound.log; then + echo "nftset OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi + +echo "> dig lookslikeexample.net. AAAA" +dig @127.0.0.1 -p $UNBOUND_PORT lookslikeexample.net. AAAA | tee outfile +echo "> check answer" +if grep "::4" outfile; then + echo "OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi +echo "> check nftset (should NOT be added)" +if grep "ipset: add ::4 to atotallymadeupnamefor6 for lookslikeexample.net." unbound.log; then + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +else + echo "nftset OK" +fi + +# Test per-zone set routing +echo "> dig target.example.com. (per-zone)" +dig @127.0.0.1 -p $UNBOUND_PORT target.example.com. | tee outfile +echo "> check answer" +if grep "3.3.3.3" outfile; then + echo "OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi +echo "> check nftset (should use custom_v4)" +if grep "ipset: add 3.3.3.3 to custom_v4 for target.example.com." unbound.log; then + echo "per-zone nftset OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi + +echo "> dig target.example.com. AAAA (per-zone)" +dig @127.0.0.1 -p $UNBOUND_PORT target.example.com. AAAA | tee outfile +echo "> check answer" +if grep "::3" outfile; then + echo "OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi +echo "> check nftset (should use custom_v6)" +if grep "ipset: add ::3 to custom_v6 for target.example.com." unbound.log; then + echo "per-zone nftset OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi + +echo "> cat logfiles" +cat fwd.log +echo "> OK" +exit 0 diff --git a/testdata/nftset.tdir/nftset.testns b/testdata/nftset.tdir/nftset.testns new file mode 100644 index 000000000..f67d77ed6 --- /dev/null +++ b/testdata/nftset.tdir/nftset.testns @@ -0,0 +1,113 @@ +; nameserver test file +$ORIGIN example.net. +$TTL 3600 + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +www IN A +SECTION ANSWER +www IN A 1.1.1.1 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +www IN AAAA +SECTION ANSWER +www IN AAAA ::1 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +cname IN A +SECTION ANSWER +cname IN CNAME target.example.net. +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +cname IN AAAA +SECTION ANSWER +cname IN CNAME target.example.net. +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +outsidecname IN A +SECTION ANSWER +outsidecname IN CNAME target.example.com. +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +outsidecname IN AAAA +SECTION ANSWER +outsidecname IN CNAME target.example.com. +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +target IN A +SECTION ANSWER +target IN A 2.2.2.2 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +target IN AAAA +SECTION ANSWER +target IN AAAA ::2 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +target.example.com. IN A +SECTION ANSWER +target.example.com. IN A 3.3.3.3 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +target.example.com. IN AAAA +SECTION ANSWER +target.example.com. IN AAAA ::3 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +lookslikeexample.net. IN AAAA +SECTION ANSWER +lookslikeexample.net. IN AAAA ::4 +ENTRY_END diff --git a/util/config_file.c b/util/config_file.c index edd12fb2e..01d0f14df 100644 --- a/util/config_file.c +++ b/util/config_file.c @@ -300,7 +300,7 @@ config_create(void) cfg->neg_cache_size = 1 * 1024 * 1024; cfg->local_zones = NULL; cfg->local_zones_nodefault = NULL; -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) cfg->local_zones_ipset = NULL; #endif cfg->local_zones_disable_default = 0; @@ -417,9 +417,12 @@ config_create(void) cfg->redis_replica_logical_db = 0; #endif /* USE_REDIS */ #endif /* USE_CACHEDB */ -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) + cfg->ipset_family = NULL; + cfg->ipset_table = NULL; cfg->ipset_name_v4 = NULL; cfg->ipset_name_v6 = NULL; + cfg->ipset_zones = NULL; #endif cfg->ede = 0; cfg->ede_serve_expired = 0; @@ -1441,7 +1444,9 @@ config_get_option(struct config_file* cfg, const char* opt, else O_DEC(opt, "redis-replica-logical-db", redis_replica_logical_db) #endif /* USE_REDIS */ #endif /* USE_CACHEDB */ -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) + else O_STR(opt, "family", ipset_family) + else O_STR(opt, "table", ipset_table) else O_STR(opt, "name-v4", ipset_name_v4) else O_STR(opt, "name-v6", ipset_name_v6) #endif @@ -1469,6 +1474,9 @@ create_cfg_parser(struct config_file* cfg, char* filename, const char* chroot) cfg_parser->cfg = cfg; cfg_parser->chroot = chroot; cfg_parser->started_toplevel = 0; +#if defined(USE_IPSET) || defined(USE_NFTSET) + cfg_parser->ipset_section_seen = 0; +#endif init_cfg_parse(); } @@ -1705,7 +1713,7 @@ config_delview(struct config_view* p) free(p->name); config_deldblstrlist(p->local_zones); config_delstrlist(p->local_zones_nodefault); -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) config_delstrlist(p->local_zones_ipset); #endif config_delstrlist(p->local_data); @@ -1804,7 +1812,7 @@ config_delete(struct config_file* cfg) free(cfg->val_nsec3_key_iterations); config_deldblstrlist(cfg->local_zones); config_delstrlist(cfg->local_zones_nodefault); -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) config_delstrlist(cfg->local_zones_ipset); #endif config_delstrlist(cfg->local_data); @@ -1863,9 +1871,12 @@ config_delete(struct config_file* cfg) free(cfg->redis_replica_server_password); #endif /* USE_REDIS */ #endif /* USE_CACHEDB */ -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) + free(cfg->ipset_family); + free(cfg->ipset_table); free(cfg->ipset_name_v4); free(cfg->ipset_name_v6); + config_deltrplstrlist(cfg->ipset_zones); #endif free(cfg); } @@ -2707,7 +2718,7 @@ cfg_parse_local_zone(struct config_file* cfg, const char* val) if(strcmp(type, "nodefault")==0) { return cfg_strlist_insert(&cfg->local_zones_nodefault, strdup(name)); -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) } else if(strcmp(type, "ipset")==0) { return cfg_strlist_insert(&cfg->local_zones_ipset, strdup(name)); diff --git a/util/config_file.h b/util/config_file.h index 6dadf0b72..d2203fe96 100644 --- a/util/config_file.h +++ b/util/config_file.h @@ -468,7 +468,7 @@ struct config_file { struct config_str2list* local_zones; /** local zones nodefault list */ struct config_strlist* local_zones_nodefault; -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) /** local zones ipset list */ struct config_strlist* local_zones_ipset; #endif @@ -780,9 +780,13 @@ struct config_file { char* cookie_secret_file; /* ipset module */ -#ifdef USE_IPSET - char* ipset_name_v4; - char* ipset_name_v6; +#if defined(USE_IPSET) || defined(USE_NFTSET) + int ipset_use_nft; /* 1 if nftset: section was used (nft backend), 0 for ipset: (ip backend) */ + char* ipset_family; /* nft backend only: "inet" (default), "ip", or "ip6" */ + char* ipset_table; /* nft backend only: required */ + char* ipset_name_v4; /* global fallback v4 set (optional when ipset_zones covers all zones) */ + char* ipset_name_v6; /* global fallback v6 set (optional when ipset_zones covers all zones) */ + struct config_str3list *ipset_zones; /* per-zone entries: zone name, v4 set name, v6 set name */ #endif /** respond with Extended DNS Errors (RFC8914) */ int ede; @@ -900,7 +904,7 @@ struct config_view { struct config_strlist* local_data; /** local zones nodefault list */ struct config_strlist* local_zones_nodefault; -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) /** local zones ipset list */ struct config_strlist* local_zones_ipset; #endif @@ -1394,6 +1398,10 @@ struct config_parser_state { const char* chroot; /** if we are started in a toplevel, or not, after a force_toplevel */ int started_toplevel; +#if defined(USE_IPSET) || defined(USE_NFTSET) + /** set to 1 once either ipset:/nftset: section is parsed; detects duplicates */ + int ipset_section_seen; +#endif }; /** global config parser object used during config parsing */ diff --git a/util/configlexer.lex b/util/configlexer.lex index d6222cdf9..0b50b2c99 100644 --- a/util/configlexer.lex +++ b/util/configlexer.lex @@ -591,8 +591,12 @@ redis-expire-records{COLON} { YDVAR(1, VAR_CACHEDB_REDISEXPIRERECORDS) } redis-logical-db{COLON} { YDVAR(1, VAR_CACHEDB_REDISLOGICALDB) } redis-replica-logical-db{COLON} { YDVAR(1, VAR_CACHEDB_REDISREPLICALOGICALDB) } ipset{COLON} { YDVAR(0, VAR_IPSET) } +nftset{COLON} { YDVAR(0, VAR_NFTSET) } name-v4{COLON} { YDVAR(1, VAR_IPSET_NAME_V4) } name-v6{COLON} { YDVAR(1, VAR_IPSET_NAME_V6) } +family{COLON} { YDVAR(1, VAR_IPSET_FAMILY) } +table{COLON} { YDVAR(1, VAR_IPSET_TABLE) } +set{COLON} { YDVAR(3, VAR_IPSET_SET) } udp-upstream-without-downstream{COLON} { YDVAR(1, VAR_UDP_UPSTREAM_WITHOUT_DOWNSTREAM) } tcp-connection-limit{COLON} { YDVAR(2, VAR_TCP_CONNECTION_LIMIT) } answer-cookie{COLON} { YDVAR(1, VAR_ANSWER_COOKIE ) } diff --git a/util/configparser.y b/util/configparser.y index 6ea214ea5..3eac19877 100644 --- a/util/configparser.y +++ b/util/configparser.y @@ -200,7 +200,8 @@ extern struct config_parser_state* cfg_parser; %token VAR_WAIT_LIMIT_NETBLOCK VAR_WAIT_LIMIT_COOKIE_NETBLOCK %token VAR_STREAM_WAIT_SIZE VAR_TLS_CIPHERS VAR_TLS_CIPHERSUITES VAR_TLS_USE_SNI %token VAR_TLS_PROTOCOLS -%token VAR_IPSET VAR_IPSET_NAME_V4 VAR_IPSET_NAME_V6 +%token VAR_IPSET VAR_NFTSET VAR_IPSET_NAME_V4 VAR_IPSET_NAME_V6 +%token VAR_IPSET_FAMILY VAR_IPSET_TABLE VAR_IPSET_SET %token VAR_TLS_SESSION_TICKET_KEYS VAR_RPZ VAR_TAGS VAR_RPZ_ACTION_OVERRIDE %token VAR_RPZ_CNAME_OVERRIDE VAR_RPZ_LOG VAR_RPZ_LOG_NAME %token VAR_DYNLIB VAR_DYNLIB_FILE VAR_EDNS_CLIENT_STRING @@ -225,7 +226,7 @@ toplevelvar: serverstart contents_server | stub_clause | forward_clause | pythonstart contents_py | rcstart contents_rc | dtstart contents_dt | view_clause | dnscstart contents_dnsc | cachedbstart contents_cachedb | - ipsetstart contents_ipset | authstart contents_auth | + ipsetstart contents_ipset | nftsetstart contents_ipset | authstart contents_auth | rpzstart contents_rpz | dynlibstart contents_dl | force_toplevel ; @@ -2396,14 +2397,15 @@ server_local_zone: VAR_LOCAL_ZONE STRING_ARG STRING_ARG && strcmp($3, "noview")!=0 && strcmp($3, "inform")!=0 && strcmp($3, "inform_deny")!=0 && strcmp($3, "inform_redirect") != 0 - && strcmp($3, "ipset") != 0) { + && strcmp($3, "ipset") != 0 + && strcmp($3, "nftset") != 0) { yyerror("local-zone type: expected static, deny, " "refuse, redirect, transparent, " "typetransparent, inform, inform_deny, " "inform_redirect, always_transparent, block_a, " "always_refuse, always_nxdomain, " "always_nodata, always_deny, always_null, " - "noview, nodefault or ipset"); + "noview, nodefault, ipset, or nftset"); free($2); free($3); } else if(strcmp($3, "nodefault")==0) { @@ -2411,8 +2413,9 @@ server_local_zone: VAR_LOCAL_ZONE STRING_ARG STRING_ARG local_zones_nodefault, $2)) fatal_exit("out of memory adding local-zone"); free($3); -#ifdef USE_IPSET - } else if(strcmp($3, "ipset")==0) { +#if defined(USE_IPSET) || defined(USE_NFTSET) + } else if(strcmp($3, "ipset")==0 || strcmp($3, "nftset")==0) { + /* nftset is an accepted synonym for ipset */ size_t len = strlen($2); /* Make sure to add the trailing dot. * These are str compared to domain names. */ @@ -3367,14 +3370,15 @@ view_local_zone: VAR_LOCAL_ZONE STRING_ARG STRING_ARG && strcmp($3, "noview")!=0 && strcmp($3, "inform")!=0 && strcmp($3, "inform_deny")!=0 && strcmp($3, "inform_redirect") != 0 - && strcmp($3, "ipset") != 0) { + && strcmp($3, "ipset") != 0 + && strcmp($3, "nftset") != 0) { yyerror("local-zone type: expected static, deny, " "refuse, redirect, transparent, " "typetransparent, inform, inform_deny, " "inform_redirect, always_transparent, " "always_refuse, always_nxdomain, " "always_nodata, always_deny, always_null, " - "noview, nodefault or ipset"); + "noview, nodefault, ipset, or nftset"); free($2); free($3); } else if(strcmp($3, "nodefault")==0) { @@ -3382,8 +3386,9 @@ view_local_zone: VAR_LOCAL_ZONE STRING_ARG STRING_ARG local_zones_nodefault, $2)) fatal_exit("out of memory adding local-zone"); free($3); -#ifdef USE_IPSET - } else if(strcmp($3, "ipset")==0) { +#if defined(USE_IPSET) || defined(USE_NFTSET) + } else if(strcmp($3, "ipset")==0 || strcmp($3, "nftset")==0) { + /* nftset is an accepted synonym for ipset */ size_t len = strlen($2); /* Make sure to add the trailing dot. * These are str compared to domain names. */ @@ -4288,15 +4293,62 @@ ipsetstart: VAR_IPSET { OUTYY(("\nP(ipset:)\n")); cfg_parser->started_toplevel = 1; + #if defined(USE_IPSET) || defined(USE_NFTSET) + if(cfg_parser->ipset_section_seen) + yyerror("only one of ipset: or nftset: may be specified"); + cfg_parser->ipset_section_seen = 1; + /* ipset_use_nft stays 0: ip backend */ + #endif + } + ; +nftsetstart: VAR_NFTSET + { + OUTYY(("\nP(nftset:)\n")); + cfg_parser->started_toplevel = 1; + #if defined(USE_IPSET) || defined(USE_NFTSET) + if(cfg_parser->ipset_section_seen) + yyerror("only one of ipset: or nftset: may be specified"); + cfg_parser->ipset_section_seen = 1; + cfg_parser->cfg->ipset_use_nft = 1; + #endif } ; contents_ipset: contents_ipset content_ipset | ; -content_ipset: ipset_name_v4 | ipset_name_v6 +content_ipset: ipset_family | ipset_table | + ipset_name_v4 | ipset_name_v6 | ipset_set + ; +ipset_family: VAR_IPSET_FAMILY STRING_ARG + { + #if defined(USE_IPSET) || defined(USE_NFTSET) + OUTYY(("P(ipset family:%s)\n", $2)); + if(cfg_parser->cfg->ipset_family) + yyerror("ipset family override, there must be one family"); + free(cfg_parser->cfg->ipset_family); + cfg_parser->cfg->ipset_family = $2; + #else + OUTYY(("P(Compiled without ipset, ignoring)\n")); + free($2); + #endif + } + ; +ipset_table: VAR_IPSET_TABLE STRING_ARG + { + #if defined(USE_IPSET) || defined(USE_NFTSET) + OUTYY(("P(ipset table:%s)\n", $2)); + if(cfg_parser->cfg->ipset_table) + yyerror("ipset table override, there must be one table"); + free(cfg_parser->cfg->ipset_table); + cfg_parser->cfg->ipset_table = $2; + #else + OUTYY(("P(Compiled without ipset, ignoring)\n")); + free($2); + #endif + } ; ipset_name_v4: VAR_IPSET_NAME_V4 STRING_ARG { - #ifdef USE_IPSET + #if defined(USE_IPSET) || defined(USE_NFTSET) OUTYY(("P(name-v4:%s)\n", $2)); if(cfg_parser->cfg->ipset_name_v4) yyerror("ipset name v4 override, there must be one " @@ -4311,7 +4363,7 @@ ipset_name_v4: VAR_IPSET_NAME_V4 STRING_ARG ; ipset_name_v6: VAR_IPSET_NAME_V6 STRING_ARG { - #ifdef USE_IPSET + #if defined(USE_IPSET) || defined(USE_NFTSET) OUTYY(("P(name-v6:%s)\n", $2)); if(cfg_parser->cfg->ipset_name_v6) yyerror("ipset name v6 override, there must be one " @@ -4324,6 +4376,25 @@ ipset_name_v6: VAR_IPSET_NAME_V6 STRING_ARG #endif } ; +ipset_set: VAR_IPSET_SET STRING_ARG STRING_ARG STRING_ARG + { + #if defined(USE_IPSET) || defined(USE_NFTSET) + OUTYY(("P(ipset set:%s %s %s)\n", $2, $3, $4)); + if(!cfg_str3list_insert(&cfg_parser->cfg->ipset_zones, + $2, $3, $4)) { + yyerror("out of memory"); + free($2); + free($3); + free($4); + } + #else + OUTYY(("P(Compiled without ipset, ignoring)\n")); + free($2); + free($3); + free($4); + #endif + } + ; %% /* parse helper routines could be here */ diff --git a/util/fptr_wlist.c b/util/fptr_wlist.c index 18a2fc11b..20cea32ed 100644 --- a/util/fptr_wlist.c +++ b/util/fptr_wlist.c @@ -95,7 +95,7 @@ #ifdef CLIENT_SUBNET #include "edns-subnet/subnetmod.h" #endif -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) #include "ipset/ipset.h" #endif #ifdef USE_DNSTAP @@ -435,7 +435,7 @@ fptr_whitelist_mod_init(int (*fptr)(struct module_env* env, int id)) #ifdef CLIENT_SUBNET else if(fptr == &subnetmod_init) return 1; #endif -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) else if(fptr == &ipset_init) return 1; #endif return 0; @@ -463,7 +463,7 @@ fptr_whitelist_mod_deinit(void (*fptr)(struct module_env* env, int id)) #ifdef CLIENT_SUBNET else if(fptr == &subnetmod_deinit) return 1; #endif -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) else if(fptr == &ipset_deinit) return 1; #endif return 0; @@ -472,7 +472,7 @@ fptr_whitelist_mod_deinit(void (*fptr)(struct module_env* env, int id)) int fptr_whitelist_mod_startup(int (*fptr)(struct module_env* env, int id)) { -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) if(fptr == &ipset_startup) return 1; #else (void)fptr; @@ -483,7 +483,7 @@ fptr_whitelist_mod_startup(int (*fptr)(struct module_env* env, int id)) int fptr_whitelist_mod_destartup(void (*fptr)(struct module_env* env, int id)) { -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) if(fptr == &ipset_destartup) return 1; #else (void)fptr; @@ -514,7 +514,7 @@ fptr_whitelist_mod_operate(void (*fptr)(struct module_qstate* qstate, #ifdef CLIENT_SUBNET else if(fptr == &subnetmod_operate) return 1; #endif -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) else if(fptr == &ipset_operate) return 1; #endif return 0; @@ -543,7 +543,7 @@ fptr_whitelist_mod_inform_super(void (*fptr)( #ifdef CLIENT_SUBNET else if(fptr == &subnetmod_inform_super) return 1; #endif -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) else if(fptr == &ipset_inform_super) return 1; #endif return 0; @@ -572,7 +572,7 @@ fptr_whitelist_mod_clear(void (*fptr)(struct module_qstate* qstate, #ifdef CLIENT_SUBNET else if(fptr == &subnetmod_clear) return 1; #endif -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) else if(fptr == &ipset_clear) return 1; #endif return 0; @@ -600,7 +600,7 @@ fptr_whitelist_mod_get_mem(size_t (*fptr)(struct module_env* env, int id)) #ifdef CLIENT_SUBNET else if(fptr == &subnetmod_get_mem) return 1; #endif -#ifdef USE_IPSET +#if defined(USE_IPSET) || defined(USE_NFTSET) else if(fptr == &ipset_get_mem) return 1; #endif return 0;