diff --git a/doc/unbound.conf.5.in b/doc/unbound.conf.5.in index da494087c..70af9dadc 100644 --- a/doc/unbound.conf.5.in +++ b/doc/unbound.conf.5.in @@ -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 File with trusted keys for validation. Both DS and DNSKEY entries can appear @@ -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 @@ -1620,6 +1625,32 @@ 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: +.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 +.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. When declaring a local-zone with +TTL support, the associated ipset must be created with \fIIPSET_EXT_TIMEOUT\fR, +or with the \fIipset\fR CLI, using the \fItimeout \fR option. +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 -T expire \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 diff --git a/ipset/ipset.c b/ipset/ipset.c index 1ad2c09f4..ac741602d 100644 --- a/ipset/ipset.c +++ b/ipset/ipset.c @@ -16,6 +16,9 @@ #include "sldns/sbuffer.h" #include "sldns/wire2str.h" #include "sldns/parseutil.h" +#include +#include +#include #ifdef HAVE_NET_PFVAR_H #include @@ -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; @@ -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]; + char* recv_buffer; static char buffer[BUFF_LEN]; if (strlen(setname) >= IPSET_MAXNAMELEN) { @@ -154,9 +163,18 @@ static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr, return -1; } + const bool set_ttl = ttl >= 0; 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_flags = NLM_F_REQUEST | NLM_F_ACK; + if (set_ttl) { + // Replace if we a TTL to extend the entry time + nlh->nlmsg_flags |= NLM_F_REPLACE | NLM_F_CREATE; + } else { + // Don't replace if we have no TTL since entry doesn't expire + nlh->nlmsg_flags |= NLM_F_EXCL; + } + nlh->nlmsg_seq = seq = time(NULL); nfg = mnl_nlmsg_put_extra_header(nlh, sizeof(struct nfgenmsg)); nfg->nfgen_family = af; @@ -170,11 +188,56 @@ 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 (set_ttl) { + // Netlink packets are packed based on a pointer and data size + // to memcpy into an appropriately sized buffer within the packet + // data section. Thus we need to ensure that the TTL is in a u32 + // sized variable, otherwise we would end up copying the upper + // 32 bits of a 64 bit integer. + const uint32_t entry_ttl = (uint32_t) ttl > UINT32_MAX ? UINT32_MAX : ttl; + mnl_attr_put_u32( + nlh, + IPSET_ATTR_TIMEOUT | NLA_F_NET_BYTEORDER, + // Expecting net byte order, we should convert from host order + // into net byte order + htonl(entry_ttl) + ); + } 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 (!set_ttl && errno == IPSET_ERR_EXIST) { + // If we have no TTL, then we don't replace entries. + // This error indicates we already have an entry, so we + // can ignore it and move on. + break; + } + 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 @@ -182,30 +245,47 @@ static int add_to_ipset(filter_dev dev, const char *setname, const void *ipaddr, 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); @@ -224,13 +304,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); @@ -252,20 +332,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; } @@ -299,23 +388,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; @@ -367,6 +449,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]; @@ -377,9 +488,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; } diff --git a/ipset/ipset.h b/ipset/ipset.h index 195c7db93..77daccceb 100644 --- a/ipset/ipset.h +++ b/ipset/ipset.h @@ -3,6 +3,9 @@ * * Author: Kevin Chou * Email: k9982874@gmail.com + * + * Updated with per-zone support and TTLs. + * Author: Jack Kilrain (EngineersBox) */ #ifndef IPSET_H #define IPSET_H @@ -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 hash:ip" 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. - * - * 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" diff --git a/testdata/ipset_inline.tdir/ipset_inline.conf b/testdata/ipset_inline.tdir/ipset_inline.conf new file mode 100644 index 000000000..4e893e626 --- /dev/null +++ b/testdata/ipset_inline.tdir/ipset_inline.conf @@ -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@" diff --git a/testdata/ipset_inline.tdir/ipset_inline.dsc b/testdata/ipset_inline.tdir/ipset_inline.dsc new file mode 100644 index 000000000..85ed78e9e --- /dev/null +++ b/testdata/ipset_inline.tdir/ipset_inline.dsc @@ -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: diff --git a/testdata/ipset_inline.tdir/ipset_inline.post b/testdata/ipset_inline.tdir/ipset_inline.post new file mode 100644 index 000000000..0b0f914ee --- /dev/null +++ b/testdata/ipset_inline.tdir/ipset_inline.post @@ -0,0 +1,13 @@ +# #-- ipset_inline.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/ipset_inline.tdir/ipset_inline.pre b/testdata/ipset_inline.tdir/ipset_inline.pre new file mode 100644 index 000000000..e62039ed5 --- /dev/null +++ b/testdata/ipset_inline.tdir/ipset_inline.pre @@ -0,0 +1,38 @@ +# #-- ipset_inline.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_IPSET 1" $PRE/config.h; then echo test enabled; else skip_test "test skipped"; fi +if grep "define HAVE_NET_PFVAR_H 1" $PRE/config.h; then + if test ! -f /dev/pf; then + skip_test "no /dev/pf" + fi +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 ipset_inline.testns >fwd.log 2>&1 & +FWD_PID=$!1 +echo "FWD_PID=$FWD_PID" >> .tpkg.var.test + +# make config file +sed -e 's/@PORT\@/'$UNBOUND_PORT'/' -e 's/@TOPORT\@/'$FWD_PORT'/' < ipset_inline.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/ipset_inline.tdir/ipset_inline.test b/testdata/ipset_inline.tdir/ipset_inline.test new file mode 100644 index 000000000..6b9d9ab3c --- /dev/null +++ b/testdata/ipset_inline.tdir/ipset_inline.test @@ -0,0 +1,66 @@ +# #-- ipset_inline.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 + +. ...netmon.sh +PRE="../.." + +# Global ipset declaration + +# Make all the queries. They need to succeed by the way. +echo "> dig cname.example.net. A" +dig @127.0.0.1 -p $UNBOUND_PORT cname.example.net. A | 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 ipset" +if grep "ipset: add 1.1.1.1 to anothermadeupnamefor4 for target.example.net. with ttl 3600" unbound.log; then + echo "ipset 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 "::1" outfile; then + echo "OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi +echo "> check ipset" +if grep "ipset: add ::1 to anothermadeupnamefor6 for target.example.net." unbound.log; then + echo "ipset OK" +else + echo "> cat logfiles" + cat fwd.log + cat unbound.log + echo "Not OK" + exit 1 +fi + +# Finalisation + +echo "> cat logfiles" +cat tap.log +cat tap.errlog +cat fwd.log +echo "> OK" +exit 0 diff --git a/testdata/ipset_inline.tdir/ipset_inline.testns b/testdata/ipset_inline.tdir/ipset_inline.testns new file mode 100644 index 000000000..683317ddf --- /dev/null +++ b/testdata/ipset_inline.tdir/ipset_inline.testns @@ -0,0 +1,43 @@ +; nameserver test file +$ORIGIN example.net. +$TTL 3600 + +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 +target IN A +SECTION ANSWER +target IN A 1.1.1.1 +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 ::1 +ENTRY_END diff --git a/util/config_file.c b/util/config_file.c index aca0039d4..b5cc4fa27 100644 --- a/util/config_file.c +++ b/util/config_file.c @@ -1557,6 +1557,21 @@ config_deltrplstrlist(struct config_str3list* p) } } +void +config_delstr4list(struct config_str4list* p) +{ + struct config_str4list *np; + while(p) { + np = p->next; + free(p->str); + free(p->str2); + free(p->str3); + free(p->str4); + free(p); + p = np; + } +} + void config_delauth(struct config_auth* p) { @@ -1613,7 +1628,7 @@ config_delview(struct config_view* p) config_deldblstrlist(p->local_zones); config_delstrlist(p->local_zones_nodefault); #ifdef USE_IPSET - config_delstrlist(p->local_zones_ipset); + config_delstr4list(p->local_zones_ipset); #endif config_delstrlist(p->local_data); free(p); @@ -1711,7 +1726,7 @@ config_delete(struct config_file* cfg) config_deldblstrlist(cfg->local_zones); config_delstrlist(cfg->local_zones_nodefault); #ifdef USE_IPSET - config_delstrlist(cfg->local_zones_ipset); + config_delstr4list(cfg->local_zones_ipset); #endif config_delstrlist(cfg->local_data); config_deltrplstrlist(cfg->local_zone_overrides); @@ -1766,8 +1781,12 @@ config_delete(struct config_file* cfg) #endif /* USE_REDIS */ #endif /* USE_CACHEDB */ #ifdef USE_IPSET - free(cfg->ipset_name_v4); - free(cfg->ipset_name_v6); + if (cfg->ipset_name_v4 != NULL) { + free(cfg->ipset_name_v4); + } + if (cfg->ipset_name_v6 != NULL) { + free(cfg->ipset_name_v6); + } #endif free(cfg); } @@ -2130,6 +2149,25 @@ cfg_str3list_insert(struct config_str3list** head, char* item, char* i2, return 1; } +int +cfg_str4list_insert(struct config_str4list** head, char* item, char* i2, + char* i3, char* i4) +{ + struct config_str4list *s; + if(!item || !i2 || !i3 || !i4 || !head) + return 0; + s = (struct config_str4list*)calloc(1, sizeof(struct config_str4list)); + if(!s) + return 0; + s->str = item; + s->str2 = i2; + s->str3 = i3; + s->str4 = i4; + s->next = *head; + *head = s; + return 1; +} + int cfg_strbytelist_insert(struct config_strbytelist** head, char* item, uint8_t* i2, size_t i2len) @@ -2574,14 +2612,27 @@ static char* last_space_pos(const char* str) return (sp>tab)?sp:tab; } +static int get_next_token(const char* str, char** start, char** end) { + while(*start && **start && isspace((unsigned char)*str)) + start++; + if(!*start || !**start) { + return 1; + } + *end = next_space_pos(*start); + if (!*end || !**end) { + return 2; + } + return 0; +} + int cfg_parse_local_zone(struct config_file* cfg, const char* val) { - const char *type, *name_end, *name; + char *type, *type_end, *name_end, *name; char buf[256]; /* parse it as: [zone_name] [between stuff] [zone_type] */ - name = val; + name = (char*) val; while(*name && isspace((unsigned char)*name)) name++; if(!*name) { @@ -2599,7 +2650,40 @@ cfg_parse_local_zone(struct config_file* cfg, const char* val) } (void)strlcpy(buf, name, sizeof(buf)); buf[name_end-name] = '\0'; - +#ifdef USE_IPSET + type = name_end; + int result = get_next_token(name_end, &type, &type_end); + if (result == 1) { + log_err("syntax error: expected zone type: %s", val); + } else if (result == 0 && strcmp(type, "ipset") == 0) { + char *protocol, *protocol_end, *ip_table, *ip_table_end, + *ipset_name, *ipset_name_end, *ttl; + protocol = type_end; + if (get_next_token(type_end, &protocol, &protocol_end)) { + /* We don't have the 5 argument variant, so defer to 2 arg variant */ + goto parse_global_ipset; + } + ipset_name = protocol_end; + if (get_next_token(protocol_end, &ipset_name, &ipset_name_end)) { + log_err("syntax error: expected ipset zone set name: %s", val); + return 0; + } + ttl = last_space_pos(ipset_name_end); + while(ttl && *ttl && isspace((unsigned char)*ttl)) + ttl++; + if(!ttl || !*ttl) { + log_err("syntax error: expected ipset zone ttl: %s", val); + return 0; + } + return cfg_str4list_insert(&cfg->local_zones_ipset, + strdup(name), strdup(protocol), + strdup(ipset_name), strdup(ttl)); + } +parse_global_ipset:; + /* There was no next-space after this token, so it must be final + * and as such we don't have enough tokens*/ +#endif + type = last_space_pos(name_end); while(type && *type && isspace((unsigned char)*type)) type++; @@ -2613,8 +2697,8 @@ cfg_parse_local_zone(struct config_file* cfg, const char* val) strdup(name)); #ifdef USE_IPSET } else if(strcmp(type, "ipset")==0) { - return cfg_strlist_insert(&cfg->local_zones_ipset, - strdup(name)); + return cfg_str4list_insert(&cfg->local_zones_ipset, + strdup(name), "@global@", "@global@", "no-ttl"); #endif } else { return cfg_str2list_insert(&cfg->local_zones, strdup(buf), diff --git a/util/config_file.h b/util/config_file.h index 2969f8433..ecae593c2 100644 --- a/util/config_file.h +++ b/util/config_file.h @@ -48,6 +48,7 @@ struct config_view; struct config_strlist; struct config_str2list; struct config_str3list; +struct config_str4list; struct config_strbytelist; struct module_qstate; struct sock_list; @@ -465,7 +466,7 @@ struct config_file { struct config_strlist* local_zones_nodefault; #ifdef USE_IPSET /** local zones ipset list */ - struct config_strlist* local_zones_ipset; + struct config_str4list* local_zones_ipset; #endif /** do not add any default local zone */ int local_zones_disable_default; @@ -765,11 +766,9 @@ struct config_file { size_t cookie_secret_len; /** path to cookie secret store */ char* cookie_secret_file; - - /* ipset module */ #ifdef USE_IPSET - char* ipset_name_v4; - char* ipset_name_v6; + char* ipset_name_v4; + char* ipset_name_v6; #endif /** respond with Extended DNS Errors (RFC8914) */ int ede; @@ -880,7 +879,7 @@ struct config_view { struct config_strlist* local_zones_nodefault; #ifdef USE_IPSET /** local zones ipset list */ - struct config_strlist* local_zones_ipset; + struct config_str4list* local_zones_ipset; #endif /** Fallback to global local_zones when there is no match in the view * view specific tree. 1 for yes, 0 for no */ @@ -927,6 +926,21 @@ struct config_str3list { char* str3; }; +struct config_str4list { + /** next item in list */ + struct config_str4list* next; + /** first string */ + char* str; + /** second string */ + char* str2; + /** third string */ + char* str3; + /** fourth string */ + char* str4; + /** fifth string */ + char* str5; +}; + /** * List of string, bytestring for config options @@ -1106,6 +1120,18 @@ int cfg_str2list_insert(struct config_str2list** head, char* item, char* i2); int cfg_str3list_insert(struct config_str3list** head, char* item, char* i2, char* i3); +/** + * Insert string into str4list. + * @param head: pointer to str4list head variable. + * @param item: new item. malloced by caller. If NULL the insertion fails. + * @param i2: 2nd string, malloced by caller. If NULL the insertion fails. + * @param i3: 3rd string, malloced by caller. If NULL the insertion fails. + * @param i4: 4th string, malloced by caller. If NULL the insertion fails. + * @return: true on success. + */ +int cfg_str4list_insert(struct config_str4list** head, char* item, char* i2, + char* i3, char* i4); + /** * Insert string into strbytelist. * @param head: pointer to strbytelist head variable. diff --git a/util/configlexer.lex b/util/configlexer.lex index 4c0416f73..ecc501875 100644 --- a/util/configlexer.lex +++ b/util/configlexer.lex @@ -32,14 +32,19 @@ void ub_c_error(const char *message); /** avoid warning in about fwrite return value */ #define ECHO ub_c_error_msg("syntax error at text: %s", yytext) -/** A parser variable, this is a statement in the config file which is - * of the form variable: value1 value2 ... nargs is the number of values. */ -#define YDVAR(nargs, var) \ - num_args=(nargs); \ - LEXOUT(("v(%s%d) ", yytext, num_args)); \ +/* A parser variable of variable argument count in the range [min, max] in + * the config of the form: value1 value 2 ... */ +#define YDVARMM(nargs_min, nargs_max, var) \ + num_args=(nargs_min); \ + num_args_max=(nargs_max); \ + LEXOUT(("v(%s%d-%d) ", yytext, num_args, num_args_max)); \ if(num_args > 0) { BEGIN(val); } \ return (var); +/** A parser variable, this is a statement in the config file which is + * of the form variable: value1 value2 ... nargs is the number of values. */ +#define YDVAR(nargs, var) YDVARMM(nargs, nargs, var) + struct inc_state { char* filename; int line; @@ -51,6 +56,7 @@ static struct inc_state* config_include_stack = NULL; static int inc_depth = 0; static int inc_prev = 0; static int num_args = 0; +static int num_args_max = 0; static int inc_toplevel = 0; void init_cfg_parse(void) @@ -184,6 +190,22 @@ static void config_end_include(void) } #endif +#define ENSURE_VARARG_CONSISTENCY \ + if (num_args == 0 && num_args_max > 0) { \ + num_args = num_args_max; \ + } \ + num_args_max--; \ + if(--num_args == 0) { \ + if (num_args_max > 0) { \ + LEXOUT(("ARGC(0,%d) ",num_args_max)); \ + BEGIN(val); \ + } else { \ + BEGIN(INITIAL); \ + } \ + } else { \ + BEGIN(val); \ + } + %} %option noinput %option nounput @@ -440,7 +462,7 @@ log-tag-queryreply{COLON} { YDVAR(1, VAR_LOG_TAG_QUERYREPLY) } log-local-actions{COLON} { YDVAR(1, VAR_LOG_LOCAL_ACTIONS) } log-servfail{COLON} { YDVAR(1, VAR_LOG_SERVFAIL) } log-destaddr{COLON} { YDVAR(1, VAR_LOG_DESTADDR) } -local-zone{COLON} { YDVAR(2, VAR_LOCAL_ZONE) } +local-zone{COLON} { YDVARMM(2, 5, VAR_LOCAL_ZONE) } local-data{COLON} { YDVAR(1, VAR_LOCAL_DATA) } local-data-ptr{COLON} { YDVAR(1, VAR_LOCAL_DATA_PTR) } unblock-lan-zones{COLON} { YDVAR(1, VAR_UNBLOCK_LAN_ZONES) } @@ -597,23 +619,31 @@ proxy-protocol-port{COLON} { YDVAR(1, VAR_PROXY_PROTOCOL_PORT) } iter-scrub-ns{COLON} { YDVAR(1, VAR_ITER_SCRUB_NS) } iter-scrub-cname{COLON} { YDVAR(1, VAR_ITER_SCRUB_CNAME) } max-global-quota{COLON} { YDVAR(1, VAR_MAX_GLOBAL_QUOTA) } -{NEWLINE} { LEXOUT(("NL\n")); cfg_parser->line++; } +{NEWLINE} { + LEXOUT(("NL(%d,%d)\n", num_args, num_args_max)); + if (num_args == 0 && num_args_max > 0) { + /* Early match a set of tokens between the min and max */ + num_args = 0; + num_args_max = 0; + BEGIN(INITIAL); + } else { + cfg_parser->line++; + } +} /* Quoted strings. Strip leading and ending quotes */ \" { BEGIN(quotedstring); LEXOUT(("QS ")); } <> { - yyerror("EOF inside quoted string"); - if(--num_args == 0) { BEGIN(INITIAL); } - else { BEGIN(val); } + yyerror("EOF inside quoted string"); + ENSURE_VARARG_CONSISTENCY } {DQANY}* { LEXOUT(("STR(%s) ", yytext)); yymore(); } {NEWLINE} { yyerror("newline inside quoted string, no end \""); cfg_parser->line++; BEGIN(INITIAL); } \" { - LEXOUT(("QE ")); - if(--num_args == 0) { BEGIN(INITIAL); } - else { BEGIN(val); } - yytext[yyleng - 1] = '\0'; + LEXOUT(("QE ")); + ENSURE_VARARG_CONSISTENCY + yytext[yyleng - 1] = '\0'; yylval.str = strdup(yytext); if(!yylval.str) yyerror("out of memory"); @@ -623,18 +653,16 @@ max-global-quota{COLON} { YDVAR(1, VAR_MAX_GLOBAL_QUOTA) } /* Single Quoted strings. Strip leading and ending quotes */ \' { BEGIN(singlequotedstr); LEXOUT(("SQS ")); } <> { - yyerror("EOF inside quoted string"); - if(--num_args == 0) { BEGIN(INITIAL); } - else { BEGIN(val); } + yyerror("EOF inside quoted string"); + ENSURE_VARARG_CONSISTENCY } {SQANY}* { LEXOUT(("STR(%s) ", yytext)); yymore(); } {NEWLINE} { yyerror("newline inside quoted string, no end '"); cfg_parser->line++; BEGIN(INITIAL); } \' { - LEXOUT(("SQE ")); - if(--num_args == 0) { BEGIN(INITIAL); } - else { BEGIN(val); } - yytext[yyleng - 1] = '\0'; + LEXOUT(("SQE ")); + ENSURE_VARARG_CONSISTENCY + yytext[yyleng - 1] = '\0'; yylval.str = strdup(yytext); if(!yylval.str) yyerror("out of memory"); @@ -716,9 +744,12 @@ max-global-quota{COLON} { YDVAR(1, VAR_MAX_GLOBAL_QUOTA) } return (VAR_FORCE_TOPLEVEL); } -{UNQUOTEDLETTER}* { LEXOUT(("unquotedstr(%s) ", yytext)); - if(--num_args == 0) { BEGIN(INITIAL); } - yylval.str = strdup(yytext); return STRING_ARG; } +{UNQUOTEDLETTER}* { + LEXOUT(("unquotedstr(%s) ", yytext)); + ENSURE_VARARG_CONSISTENCY + yylval.str = strdup(yytext); + return STRING_ARG; +} {UNQUOTEDLETTER_NOCOLON}* { ub_c_error_msg("unknown keyword '%s'", yytext); diff --git a/util/configparser.y b/util/configparser.y index c10a5f475..efcd86e68 100644 --- a/util/configparser.y +++ b/util/configparser.y @@ -43,6 +43,7 @@ #include #include #include +#include #include "util/configyyrename.h" #include "util/config_file.h" @@ -52,12 +53,15 @@ int ub_c_lex(void); void ub_c_error(const char *message); +static void yywarn(const char *str); static void validate_respip_action(const char* action); static void validate_acl_action(const char* action); /* these need to be global, otherwise they cannot be used inside yacc */ extern struct config_parser_state* cfg_parser; +static bool ttl_pf_has_warned = false; + #if 0 #define OUTYY(s) printf s /* used ONLY when debugging */ #else @@ -216,9 +220,9 @@ 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 | - rpzstart contents_rpz | dynlibstart contents_dl | - force_toplevel + ipsetstart contents_ipset |authstart contents_auth | + rpzstart contents_rpz | dynlibstart contents_dl | + force_toplevel ; force_toplevel: VAR_FORCE_TOPLEVEL { @@ -2377,22 +2381,24 @@ 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) { - size_t len = strlen($2); - /* Make sure to add the trailing dot. - * These are str compared to domain names. */ - if($2[len-1] != '.') { - if(!($2 = realloc($2, len+2))) { - fatal_exit("out of memory adding local-zone"); - } - $2[len] = '.'; - $2[len+1] = 0; - } - if(!cfg_strlist_insert(&cfg_parser->cfg-> - local_zones_ipset, $2)) - fatal_exit("out of memory adding local-zone"); - free($3); +#ifdef USE_IPSET + } else if (strcmp($3, "ipset") == 0) { + /* Transform existing 2 param variant into 5 param with global lookup */ + size_t len = strlen($2); + /* Make sure to add the trailing dot. + * These are str compared to domain names. */ + if ($2[len-1] != '.') { + if (!($2 = realloc($2, len+2))) { + fatal_exit("out of memory adding local-zone"); + } + $2[len] = '.'; + $2[len+1] = 0; + } + if(!cfg_str4list_insert(&cfg_parser->cfg-> + local_zones_ipset, $2, strdup("@global@"), strdup("@global@"), strdup("no-ttl"))) { + fatal_exit("out of memory adding local-zone"); + } + free($3); #endif } else { if(!cfg_str2list_insert(&cfg_parser->cfg->local_zones, @@ -2400,6 +2406,68 @@ server_local_zone: VAR_LOCAL_ZONE STRING_ARG STRING_ARG fatal_exit("out of memory adding local-zone"); } } + | VAR_LOCAL_ZONE STRING_ARG STRING_ARG STRING_ARG STRING_ARG STRING_ARG + { + OUTYY(("P(server_local_zone: %s %s %s %s %s)\n", $2, $3, $4, $5, $6)); + if (strcmp($3, "ipset") != 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"); + free($2); + free($3); + free($4); + free($5); + free($6); +#ifdef USE_IPSET + } else if (strncmp($3, "ipset", 5) == 0) { + /* Format: ipset */ + if (strncmp($6, "ttl", 3) != 0 + && strncmp($6, "no-ttl", 6) != 0) { + yyerror("local-zone with ipset expected ttl/no-ttl"); + free($2); + free($3); + free($4); + free($5); + free($6); + } else { +#ifdef HAVE_NET_PFVAR_H + if (!ttl_pf_has_warned && strncmp($6, "ttl", 3) == 0) { + yywarn( + "local-zone ipset: per-address TTL not supported in" + "BSD packet filter tables, ignoring" + ); + ttl_pf_has_warned = true; + } +#endif + size_t len = strlen($2); + /* Make sure to add the trailing dot. + * These are str compared to domain names. */ + if ($2[len-1] != '.') { + if (!($2 = realloc($2, len+2))) { + fatal_exit("out of memory adding local-zone"); + } + $2[len] = '.'; + $2[len+1] = 0; + } + if(!cfg_str4list_insert(&cfg_parser->cfg-> + local_zones_ipset, $2, $4, $5, $6)) + fatal_exit("out of memory adding local-zone"); + free($3); + } +#endif + } else { + yyerror("local-zone: too many parameters"); + free($2); + free($3); + free($4); + free($5); + free($6); + } + } ; server_local_data: VAR_LOCAL_DATA STRING_ARG { @@ -3339,22 +3407,24 @@ 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) { - size_t len = strlen($2); - /* Make sure to add the trailing dot. - * These are str compared to domain names. */ - if($2[len-1] != '.') { - if(!($2 = realloc($2, len+2))) { - fatal_exit("out of memory adding local-zone"); - } - $2[len] = '.'; - $2[len+1] = 0; - } - if(!cfg_strlist_insert(&cfg_parser->cfg->views-> - local_zones_ipset, $2)) - fatal_exit("out of memory adding local-zone"); - free($3); +#ifdef USE_IPSET + } else if (strcmp($3, "ipset") == 0) { + /* Transform existing 2 param variant into 5 param with global lookup */ + size_t len = strlen($2); + /* Make sure to add the trailing dot. + * These are str compared to domain names. */ + if ($2[len-1] != '.') { + if (!($2 = realloc($2, len+2))) { + fatal_exit("out of memory adding local-zone"); + } + $2[len] = '.'; + $2[len+1] = 0; + } + if(!cfg_str4list_insert(&cfg_parser->cfg->views-> + local_zones_ipset, $2, strdup("@global@"), strdup("@global@"), strdup("no-ttl"))) { + fatal_exit("out of memory adding local-zone"); + } + free($3); #endif } else { if(!cfg_str2list_insert( @@ -3363,6 +3433,68 @@ view_local_zone: VAR_LOCAL_ZONE STRING_ARG STRING_ARG fatal_exit("out of memory adding local-zone"); } } + | VAR_LOCAL_ZONE STRING_ARG STRING_ARG STRING_ARG STRING_ARG STRING_ARG + { + OUTYY(("P(server_local_zone: %s %s %s %s %s)\n", $2, $3, $4, $5, $6)); + if (strcmp($3, "ipset") != 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"); + free($2); + free($3); + free($4); + free($5); + free($6); +#ifdef USE_IPSET + } else if (strcmp($3, "ipset") == 0) { + /* Format: ipset */ + if (strncmp($6, "ttl", 3) != 0 + || strncmp($6, "no-ttl", 6) != 0) { + yyerror("local-zone with ipset expected ttl/no-ttl"); + free($2); + free($3); + free($4); + free($5); + free($6); + } else { +#ifdef HAVE_NET_PFVAR_H + if (!ttl_pf_has_warned && strncmp($6, "ttl", 3) == 0) { + yywarn( + "local-zone ipset: per-address TTL not supported in" + "BSD packet filter tables, ignoring" + ); + ttl_pf_has_warned = true; + } +#endif + size_t len = strlen($2); + /* Make sure to add the trailing dot. + * These are str compared to domain names. */ + if ($2[len-1] != '.') { + if (!($2 = realloc($2, len+2))) { + fatal_exit("out of memory adding local-zone"); + } + $2[len] = '.'; + $2[len+1] = 0; + } + if(!cfg_str4list_insert(&cfg_parser->cfg->views-> + local_zones_ipset, $2, $4, $5, $6)) + fatal_exit("out of memory adding local-zone"); + free($3); + } +#endif + } else { + yyerror("local-zone: too many parameters"); + free($2); + free($3); + free($4); + free($5); + free($6); + } + } ; view_response_ip: VAR_RESPONSE_IP STRING_ARG STRING_ARG { @@ -4110,7 +4242,7 @@ server_max_global_quota: VAR_MAX_GLOBAL_QUOTA STRING_ARG else cfg_parser->cfg->max_global_quota = atoi($2); free($2); } - ; + ; ipsetstart: VAR_IPSET { OUTYY(("\nP(ipset:)\n")); @@ -4188,3 +4320,7 @@ validate_acl_action(const char* action) "allow_snoop or allow_cookie as access control action"); } } + +static void yywarn(const char *str) { + fprintf(stderr, "warning: %s\n", str); +}