From b935e83ceecfa1c85ca42aee57d41c59dde9fd15 Mon Sep 17 00:00:00 2001 From: Jonathan Duncan Date: Sun, 10 May 2026 22:23:07 +0100 Subject: [PATCH] Add support for nftables sets mirroring existing ipset support Adds an nftset module alongside the existing ipset module. A new nftset: configuration section selects the nftables backend and is configured with family:, table:, name-v4:, name-v6:. Additionally both nftset and ipset have been given support for per-zone sets (configured with set: "zone" "name-v4" "name-v6"). Previously only a single global set name was supported. The nftset and ipset sections are mutually exclusive within a single config. Both backends share the majority of their code in ipset.c and use netlink via libmnl on Linux. The ipset path is unchanged on BSD. New support for checking netlink errors and best-effort error logging voa NETLINK_EXT_ACK string where available has been added for both nft and the original ipset support (which previously lacked reporting). Addditionally CAP_NET_ADMIN is now preserved across the privilege drop on Linux when the ipset or nftset module is configured, so the netlink socket can be opened after dropping root. --- .gitignore | 5 + config.h.in | 8 +- configure.ac | 129 +++-- daemon/unbound.c | 46 +- doc/README.ipset-nftset.md | 205 ++++++++ doc/README.ipset.md | 65 --- doc/example.conf.in | 28 +- ipset/ipset.c | 449 ++++++++++++++++-- ipset/ipset.h | 58 ++- services/modstack.c | 29 +- services/modstack.h | 10 + smallapp/unbound-checkconf.c | 6 + testdata/04-checkconf.tdir/04-checkconf.test | 14 + .../04-checkconf.tdir/bad.nftset-dup-family | 11 + .../04-checkconf.tdir/bad.nftset-dup-namev4 | 10 + .../04-checkconf.tdir/bad.nftset-dup-namev6 | 10 + .../04-checkconf.tdir/bad.nftset-dup-section | 12 + .../04-checkconf.tdir/bad.nftset-dup-table | 10 + testdata/04-checkconf.tdir/good.nftset | 13 + .../04-checkconf.tdir/good.nftset-minimal | 9 + .../04-checkconf.tdir/good.nftset-no-family | 10 + testdata/04-checkconf.tdir/good.nftset-v6only | 9 + testdata/nftset.tdir/nftset.conf | 30 ++ testdata/nftset.tdir/nftset.dsc | 16 + testdata/nftset.tdir/nftset.post | 13 + testdata/nftset.tdir/nftset.pre | 33 ++ testdata/nftset.tdir/nftset.test | 222 +++++++++ testdata/nftset.tdir/nftset.testns | 113 +++++ util/config_file.c | 25 +- util/config_file.h | 18 +- util/configlexer.lex | 4 + util/configparser.y | 97 +++- util/fptr_wlist.c | 18 +- 33 files changed, 1510 insertions(+), 225 deletions(-) create mode 100644 doc/README.ipset-nftset.md delete mode 100644 doc/README.ipset.md create mode 100644 testdata/04-checkconf.tdir/bad.nftset-dup-family create mode 100644 testdata/04-checkconf.tdir/bad.nftset-dup-namev4 create mode 100644 testdata/04-checkconf.tdir/bad.nftset-dup-namev6 create mode 100644 testdata/04-checkconf.tdir/bad.nftset-dup-section create mode 100644 testdata/04-checkconf.tdir/bad.nftset-dup-table create mode 100644 testdata/04-checkconf.tdir/good.nftset create mode 100644 testdata/04-checkconf.tdir/good.nftset-minimal create mode 100644 testdata/04-checkconf.tdir/good.nftset-no-family create mode 100644 testdata/04-checkconf.tdir/good.nftset-v6only create mode 100644 testdata/nftset.tdir/nftset.conf create mode 100644 testdata/nftset.tdir/nftset.dsc create mode 100644 testdata/nftset.tdir/nftset.post create mode 100644 testdata/nftset.tdir/nftset.pre create mode 100644 testdata/nftset.tdir/nftset.test create mode 100644 testdata/nftset.tdir/nftset.testns 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;