Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Unbound: Support per-rule (local-zone) ipset targets #1162

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9661b03
Refactored ipset to support per-rule configuration
EngineersBox Oct 22, 2024
eb349ad
Refactored ipset to support per-rule configuration
EngineersBox Oct 22, 2024
45f9bbe
Merged in upstream changes
EngineersBox Oct 22, 2024
5008820
Merged in upstream changes
EngineersBox Oct 22, 2024
ba17288
Added checkconf warning for ipset with TTL when compiled with BSD pf
EngineersBox Oct 22, 2024
c12ee39
Supported backwards compat with global ipsets
Oct 25, 2024
93ff2a5
Supported backwards compat with global ipsets
Oct 25, 2024
a1e3eb7
Supported backwards compat with global ipsets
Oct 25, 2024
6209e2d
Supported backwards compat with global ipsets
Oct 25, 2024
fd6740b
Removed unused lexer token return
Oct 25, 2024
124c612
Removed TODO comment
Oct 25, 2024
70c252e
Fixed memory leak and missing lexer tokens
EngineersBox Oct 25, 2024
d35a4d7
Fixed memory leak and missing lexer tokens
EngineersBox Oct 25, 2024
c1879ae
Fixed BSD warning flag not updated to true
EngineersBox Oct 25, 2024
89dedee
Added tests
EngineersBox Oct 28, 2024
11c7cab
Added tests
EngineersBox Oct 28, 2024
3e037da
Updated docs
EngineersBox Oct 28, 2024
d415337
Updated docs
EngineersBox Oct 28, 2024
1b39c41
Fixed timeouts
Nov 4, 2024
9b209ae
Merge branch 'master' of https://github.com/EngineersBox/unbound
Nov 4, 2024
839b7ab
Ensured creation with no TTL and no replacement enacted
Nov 4, 2024
1a94857
Ensured creation with no TTL and no replacement enacted
Nov 4, 2024
1c6c67c
Ensured creation with no TTL and no replacement enacted
Nov 4, 2024
deb6ec8
Added conversion from host byte order to net byte order
Nov 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion doc/unbound.conf.5.in
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,11 @@ The python module can be listed in different places, it then processes the
output of the module it is just before. The dynlib module can be listed pretty
much anywhere, it is only a very thin wrapper that allows dynamic libraries to
run in its place.
.IP
When the server is built with ipset support and has is run with the \fICAP_NET_ADMIN\fR
capability, the ipset module can be utilised. Example configuration for this is
"\fIipset iterator\fR". I can easily be combined with any other module without
any issues.
.TP
.B trust\-anchor\-file: \fI<filename>
File with trusted keys for validation. Both DS and DNSKEY entries can appear
Expand Down Expand Up @@ -1507,7 +1512,7 @@ Configure a local zone. The type determines the answer to give if
there is no match from local\-data. The types are deny, refuse, static,
transparent, redirect, nodefault, typetransparent, inform, inform_deny,
inform_redirect, always_transparent, block_a, always_refuse, always_nxdomain,
always_null, noview, and are explained below. After that the default settings
always_null, noview, ipset, and are explained below. After that the default settings
are listed. Use local\-data: to enter data into the local zone. Answers for
local zones are authoritative DNS answers. By default the zones are class IN.
.IP
Expand Down Expand Up @@ -1620,6 +1625,30 @@ also turn off default contents for the zone. The 'nodefault' option
has no other effect than turning off default contents for the
given zone. Use \fInodefault\fR if you use exactly that zone, if you want to
use a subzone, use \fItransparent\fR.
.TP 10
\h'5'\fInodefault\fR
Used to specify an ipset to insert resolved addresses into. If the deprecated
global \fIipset\fR block is used, then it can be referenced using the form
.nf
server:
lcoal\-zone: "example.com." ipset
ipset:
name\-v4: <ipset name>
.fi
However, per-rule declarations are also supported where delineation of
addresses is required. This is done via the form:
.nf
local\-zone: "example.net." ipset <protocol> <ipset name> <tll | no\-ttl>
.fi
Here you can specify the \fIprotocol\fR as \fIipv4\fR or \fIipv6\fR, the
name of the ipset and finally whether to use the DNS record TTL as an
auto-expiry on the inserted ipset entry. Note that on a BSD distribution,
where the server is compiled with the packet filter framework, there is
no support for TTLs to be set on individual table entries. The only support
that pf provides is invoking manual expiry of table entries past a delta of
\fIn\fR seconds via the \fIpfctl -t <table> -T expire <seconds>\fR flag.
See \fIpftcl\fR(8) Thus no support is added there for TTLs and a suitable warning
is raised from the parser when the config is checked.
.P
The default zones are localhost, reverse 127.0.0.1 and ::1, the home.arpa,
the onion, test, invalid and the AS112 zones. The AS112 zones are reverse
Expand Down
144 changes: 116 additions & 28 deletions ipset/ipset.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
#include "sldns/sbuffer.h"
#include "sldns/wire2str.h"
#include "sldns/parseutil.h"
#include <string.h>
#include <errno.h>
#include <time.h>

#ifdef HAVE_NET_PFVAR_H
#include <fcntl.h>
Expand Down Expand Up @@ -83,7 +86,8 @@ static void * open_filter() {
#endif

#ifdef HAVE_NET_PFVAR_H
static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr, int af) {
static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr,
int af, time_t _ttl) {
struct pfioc_table io;
struct pfr_addr addr;
const char *p;
Expand Down Expand Up @@ -139,10 +143,15 @@ static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr,
return 0;
}
#else
static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr, int af) {
static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr,
int af, time_t ttl) {
int result;
int seq;
unsigned int port_id;
struct nlmsghdr *nlh;
struct nfgenmsg *nfg;
struct nlattr *nested[2];
struct nlattr *nested[3];
char* recv_buffer;
static char buffer[BUFF_LEN];

if (strlen(setname) >= IPSET_MAXNAMELEN) {
Expand All @@ -157,6 +166,7 @@ static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr,
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;
nlh->nlmsg_seq = seq = time(NULL);

nfg = mnl_nlmsg_put_extra_header(nlh, sizeof(struct nfgenmsg));
nfg->nfgen_family = af;
Expand All @@ -170,42 +180,88 @@ static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr,
mnl_attr_put(nlh, (af == AF_INET ? IPSET_ATTR_IPADDR_IPV4 : IPSET_ATTR_IPADDR_IPV6)
| NLA_F_NET_BYTEORDER, (af == AF_INET ? sizeof(struct in_addr) : sizeof(struct in6_addr)), ipaddr);
mnl_attr_nest_end(nlh, nested[1]);
if (ttl >= 0) {
nested[2] = mnl_attr_nest_start(nlh, IPSET_ATTR_TIMEOUT);
mnl_attr_put(nlh, NLA_F_NET_BYTEORDER, sizeof(time_t), &ttl);
EngineersBox marked this conversation as resolved.
Show resolved Hide resolved
mnl_attr_nest_end(nlh, nested[2]);
}
mnl_attr_nest_end(nlh, nested[0]);

if (mnl_socket_sendto(dev, nlh, nlh->nlmsg_len) < 0) {
if ((result = mnl_socket_sendto(dev, nlh, nlh->nlmsg_len)) < 0) {
log_err("ipset: failed to send netlink packet: %s", strerror(errno));
return -1;
}
port_id = mnl_socket_get_portid(dev);
recv_buffer = (char*) calloc(MNL_SOCKET_BUFFER_SIZE, sizeof(char));
if (!recv_buffer) {
log_err("ipset: failed to allocate receive buffer");
return -1;
}
do {
result = mnl_socket_recvfrom(dev, recv_buffer, MNL_SOCKET_BUFFER_SIZE);
if (result < 0) {
log_err("ipset: failed to ACK netlink request: %s", strerror(errno));
free(recv_buffer);
return -1;
}
result = mnl_cb_run(recv_buffer, result, seq, port_id, NULL, NULL);
if (result < 0) {
log_err("ipset: netlink response had error: %s", strerror(errno));
free(recv_buffer);
return -1;
} else if (result == 0) {
break;
}
} while (result > 0);
free(recv_buffer);
return 0;
}
#endif

static void
ipset_add_rrset_data(struct ipset_env *ie,
struct packed_rrset_data *d, const char* setname, int af,
const char* dname)
const char* dname, bool set_ttl)
{
int ret;
size_t j, rr_len, rd_len;
time_t rr_ttl;
uint8_t *rr_data;

/* to d->count, not d->rrsig_count, because we do not want to add the RRSIGs, only the addresses */
for (j = 0; j < d->count; j++) {
rr_len = d->rr_len[j];
rr_data = d->rr_data[j];
rr_ttl = d->rr_ttl[j];

rd_len = sldns_read_uint16(rr_data);
if(af == AF_INET && rd_len != INET_SIZE)
continue;
if(af == AF_INET6 && rd_len != INET6_SIZE)
continue;
if (!set_ttl) {
rr_ttl = -1;
}
if (rr_len - 2 >= rd_len) {
if(verbosity >= VERB_QUERY) {
char ip[128];
if(inet_ntop(af, rr_data+2, ip, (socklen_t)sizeof(ip)) == 0)
snprintf(ip, sizeof(ip), "(inet_ntop_error)");
verbose(VERB_QUERY, "ipset: add %s to %s for %s", ip, setname, dname);
if (set_ttl) {
verbose(
VERB_QUERY,
"ipset: add %s to %s for %s with ttl %lds",
ip, setname, dname, rr_ttl
);
} else {
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);
ret = add_to_ipset((filter_dev)ie->dev, setname, rr_data + 2, af, rr_ttl);
if (ret < 0) {
log_err("ipset: could not add %s into %s", dname, setname);

Expand All @@ -224,13 +280,13 @@ ipset_add_rrset_data(struct ipset_env *ie,
static int
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)
int af)
{
static char dname[BUFF_LEN];
const char *ds, *qs;
int dlen, plen;

struct config_strlist *p;
struct config_str4list *p;
struct packed_rrset_data *d;

dlen = sldns_wire2str_dname_buf(rrset->rk.dname, rrset->rk.dname_len, dname, BUFF_LEN);
Expand All @@ -252,20 +308,29 @@ ipset_check_zones_for_rrset(struct module_env *env, struct ipset_env *ie,
if (p->str[plen - 1] == '.') {
plen--;
}

int set_af;
if (strncasecmp(p->str2, "ipv4", 4) == 0) {
set_af = AF_INET;
} else if (strncasecmp(p->str2, "ipv6", 4) == 0) {
set_af = AF_INET6;
} else {
continue;
}
if (dlen == plen || (dlen > plen && dname[dlen - plen - 1] == '.' )) {
ds = dname + (dlen - plen);
}
if (qlen == plen || (qlen > plen && qname[qlen - plen - 1] == '.' )) {
qs = qname + (qlen - plen);
}
if ((ds && strncasecmp(p->str, ds, plen) == 0)
|| (qs && strncasecmp(p->str, qs, plen) == 0)) {
if (((ds && strncasecmp(p->str, ds, plen) == 0)
|| (qs && strncasecmp(p->str, qs, plen) == 0))
&& set_af == af) {
d = (struct packed_rrset_data*)rrset->entry.data;
ipset_add_rrset_data(ie, d, setname, af, dname);
bool set_ttl = strncasecmp(p->str4, "ttl", 3) == 0;
ipset_add_rrset_data(ie, d, p->str3, af, dname, set_ttl);
break;
}
}
}
return 0;
}

Expand Down Expand Up @@ -299,23 +364,16 @@ static int ipset_update(struct module_env *env, struct dns_msg *return_msg,
}

for(i = 0; i < return_msg->rep->rrset_count; i++) {
setname = NULL;
rrset = return_msg->rep->rrsets[i];
if(ntohs(rrset->rk.type) == LDNS_RR_TYPE_A &&
ie->v4_enabled == 1) {
if(ntohs(rrset->rk.type) == LDNS_RR_TYPE_A) {
af = AF_INET;
setname = ie->name_v4;
} else if(ntohs(rrset->rk.type) == LDNS_RR_TYPE_AAAA &&
ie->v6_enabled == 1) {
} else if(ntohs(rrset->rk.type) == LDNS_RR_TYPE_AAAA) {
af = AF_INET6;
setname = ie->name_v6;
}

if (setname) {
if(ipset_check_zones_for_rrset(env, ie, rrset, qname,
qlen, setname, af) == -1)
return -1;
}
if(ipset_check_zones_for_rrset(env, ie, rrset, qname,
qlen, af) == -1)
return -1;
}

return 0;
Expand Down Expand Up @@ -367,6 +425,35 @@ void ipset_destartup(struct module_env* env, int id) {
env->modinfo[id] = NULL;
}

int convert_global_ipset(struct module_env* env, struct ipset_env* ipset_env) {
struct config_str4list *p;
for (p = env->cfg->local_zones_ipset; p; p = p->next) {
if (strncmp(p->str3, "@global@", 8) != 0) {
continue;
}
if (ipset_env->v4_enabled) {
p->str2 = strdup("ipv4");
p->str3 = strdup(ipset_env->name_v4);
} else if (ipset_env->v6_enabled) {
p->str2 = strdup("ipv6");
p->str3 = strdup(ipset_env->name_v6);
continue;
}
if (ipset_env->v4_enabled && ipset_env->v6_enabled) {
if (!cfg_str4list_insert(
&env->cfg->local_zones_ipset,
strdup(p->str),
strdup("ipv6"),
strdup(ipset_env->name_v6),
strdup("no-ttl")
)) {
log_err("ipset: out of memory adding rule mapping for global declaration");
return 0;
}
}
}
}

int ipset_init(struct module_env* env, int id) {
struct ipset_env *ipset_env = env->modinfo[id];

Expand All @@ -377,9 +464,10 @@ int ipset_init(struct module_env* env, int id) {
ipset_env->v6_enabled = !ipset_env->name_v6 || (strlen(ipset_env->name_v6) == 0) ? 0 : 1;

if ((ipset_env->v4_enabled < 1) && (ipset_env->v6_enabled < 1)) {
log_err("ipset: set name no configuration?");
return 0;
return 1;
}

convert_global_ipset(env, ipset_env);

return 1;
}
Expand Down
21 changes: 10 additions & 11 deletions ipset/ipset.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
*
* Author: Kevin Chou
* Email: [email protected]
*
* Updated with per-zone support and TTLs.
* Author: Jack Kilrain (EngineersBox)
*/
#ifndef IPSET_H
#define IPSET_H
Expand All @@ -16,18 +19,14 @@
* 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
* then create it with "ipset create <set name> hash:ip" and then add
* local-zone: "example.com." ipset <protocol> <set name> <ttl/no-ttl>
* statements for the zones where you want the addresses of the names
* looked up added to the set.
*
* 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
* looked up added to specified set. Declaring the protocol as either
* "ipv4" or "ipv6" determines which address family to use from the RRSet
* when populating the ipset entry. Specifying "ttl" at the end will mark the
* ipset entry with a timeout (aka expiry) matching the RRSet TTL, specifying
* "no-ttl" will prevent setting the TTL on the set entry.
*/

#include "util/module.h"
Expand Down
18 changes: 18 additions & 0 deletions testdata/ipset_inline.tdir/ipset_inline.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
server:
verbosity: 3
num-threads: 1
module-config: "ipset 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." ipset ipv4 anothermadeupnamefor4 ttl
local-zone: "example.net." ipset ipv6 anothermadeupnamefor6 no-ttl
stub-zone:
name: "example.net."
stub-addr: "127.0.0.1@@TOPORT@"
16 changes: 16 additions & 0 deletions testdata/ipset_inline.tdir/ipset_inline.dsc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
BaseName: ipset_inline
Version: 1.0
Description: mock test ipset module with inline declarations
CreationDate: Mon Oct 28 15:22:32 AEST 2024
Maintainer: Jack Kilrain
Category:
Component:
CmdDepends:
Depends:
Help:
Pre: ipset_inline.pre
Post: ipset_inline.post
Test: ipset_inline.test
AuxFiles:
Passed:
Failure:
Loading