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.
This commit is contained in:
Jonathan Duncan 2026-05-10 22:23:07 +01:00
parent 3eab974ca2
commit b935e83cee
33 changed files with 1510 additions and 225 deletions

5
.gitignore vendored
View file

@ -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

View file

@ -388,6 +388,9 @@
/* If we have atomic_store */
#undef HAVE_LINK_ATOMIC_STORE
/* Define to 1 if you have the <linux/netfilter/nf_tables.h> header file. */
#undef HAVE_LINUX_NETFILTER_NF_TABLES_H
/* Define to 1 if you have the <linux/net_tstamp.h> 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

View file

@ -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 <netinet/in.h>
#include <net/if.h>
])
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 <netinet/in.h>
#include <net/if.h>
#include <linux/netfilter.h>
])
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 <netinet/in.h>
#include <net/if.h>
])
;;
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)

View file

@ -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 <login_cap.h>
#endif
#if (defined(USE_IPSET) || defined(USE_NFTSET)) && defined(__linux__)
# include <sys/prctl.h>
# include <sys/syscall.h>
# include <linux/capability.h>
#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 */

205
doc/README.ipset-nftset.md Normal file
View file

@ -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.

View file

@ -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"
```

View file

@ -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.

View file

@ -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 <fcntl.h>
#include <sys/ioctl.h>
@ -26,12 +39,51 @@
typedef intptr_t filter_dev;
#else
#include <libmnl/libmnl.h>
#include <linux/netlink.h>
#include <linux/netfilter/nfnetlink.h>
#ifdef USE_IPSET
#include <linux/netfilter/ipset/ip_set.h>
#endif
#ifdef USE_NFTSET
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#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: <zone> <v4> <v6>' 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

View file

@ -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 */

View file

@ -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,

View file

@ -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);

View file

@ -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",

View file

@ -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"

View file

@ -0,0 +1,11 @@
server:
chroot: ""
username: ""
directory: "."
pidfile: ""
nftset:
family: "inet"
family: "ip"
table: "fw4"
name-v4: "blocklist"

View file

@ -0,0 +1,10 @@
server:
chroot: ""
username: ""
directory: "."
pidfile: ""
nftset:
table: "fw4"
name-v4: "blocklist"
name-v4: "blocklist2"

View file

@ -0,0 +1,10 @@
server:
chroot: ""
username: ""
directory: "."
pidfile: ""
nftset:
table: "fw4"
name-v6: "blocklist6"
name-v6: "blocklist6b"

View file

@ -0,0 +1,12 @@
server:
chroot: ""
username: ""
directory: "."
pidfile: ""
ipset:
name-v4: "blocklist"
nftset:
table: "fw4"
name-v4: "blocklist"

View file

@ -0,0 +1,10 @@
server:
chroot: ""
username: ""
directory: "."
pidfile: ""
nftset:
table: "fw4"
table: "filter"
name-v4: "blocklist"

13
testdata/04-checkconf.tdir/good.nftset vendored Normal file
View file

@ -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"

View file

@ -0,0 +1,9 @@
server:
chroot: ""
username: ""
directory: "."
pidfile: ""
nftset:
table: "fw4"
name-v4: "blocklist"

View file

@ -0,0 +1,10 @@
server:
chroot: ""
username: ""
directory: "."
pidfile: ""
nftset:
table: "fw4"
name-v4: "blocklist"
name-v6: "blocklist6"

View file

@ -0,0 +1,9 @@
server:
chroot: ""
username: ""
directory: "."
pidfile: ""
nftset:
table: "fw4"
name-v6: "blocklist6"

30
testdata/nftset.tdir/nftset.conf vendored Normal file
View file

@ -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"

16
testdata/nftset.tdir/nftset.dsc vendored Normal file
View file

@ -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:

13
testdata/nftset.tdir/nftset.post vendored Normal file
View file

@ -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

33
testdata/nftset.tdir/nftset.pre vendored Normal file
View file

@ -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

222
testdata/nftset.tdir/nftset.test vendored Normal file
View file

@ -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

113
testdata/nftset.tdir/nftset.testns vendored Normal file
View file

@ -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

View file

@ -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));

View file

@ -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 */

View file

@ -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 ) }

View file

@ -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 */

View file

@ -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;