diff --git a/daemon/remote.c b/daemon/remote.c index e10dadde7..e20841dfd 100644 --- a/daemon/remote.c +++ b/daemon/remote.c @@ -5153,7 +5153,7 @@ getmem_config_view(struct config_view* p) + getmem_config_strlist(s->local_data) + getmem_config_strlist(s->local_zones_nodefault) #ifdef USE_IPSET - + getmem_config_strlist(s->local_zones_ipset) + + getmem_config_str4list(s->local_zones_ipset) #endif + getmem_config_str2list(s->respip_actions) + getmem_config_str2list(s->respip_data); @@ -5217,7 +5217,7 @@ config_file_getmem(struct config_file* cfg) m += getmem_config_str2list(cfg->local_zones); m += getmem_config_strlist(cfg->local_zones_nodefault); #ifdef USE_IPSET - m += getmem_config_strlist(cfg->local_zones_ipset); + m += getmem_config_str4list(cfg->local_zones_ipset); #endif m += getmem_config_strlist(cfg->local_data); m += getmem_config_str3list(cfg->local_zone_overrides); diff --git a/doc/unbound.conf.5.in b/doc/unbound.conf.5.in index 172eb26c5..167565123 100644 --- a/doc/unbound.conf.5.in +++ b/doc/unbound.conf.5.in @@ -2324,6 +2324,11 @@ queries. Most modules that need to be listed here have to be listed at the beginning of the line. .sp +When the server is built with ipset support and has is run with the \fICAP_NET_ADMIN\fR +capability, the \fBipset\fP module can be utilised. Example configuration for this is +"\fIipset iterator\fR". It can easily be combined with any other module without +any issues. +.sp The \fBsubnetcache\fP module has to be listed just before the iterator. .sp The \fBpython\fP module can be listed in different places, it then processes @@ -2998,6 +3003,50 @@ answered from global local zone contents. .UNINDENT .INDENT 7.0 .TP +.B ipset +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: +.INDENT 7.0 +.INDENT 3.5 +.sp +.nf +.ft C +server: + lcoal\-zone: "example.com." ipset +ipset: + name\-v4: +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +However, per-rule declarations are also supported where delineation of +addresses is required. This is done via the form: +.INDENT 7.0 +.INDENT 3.5 +.sp +.nf +.ft C +local\-zone: "example.net." ipset +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +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. +.UNINDENT +.INDENT 7.0 +.TP .B nodefault Used to turn off default contents for AS112 zones. The other types also turn off default contents for the zone. 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 b1e767b3b..423efd9f6 100644 --- a/util/config_file.c +++ b/util/config_file.c @@ -1631,6 +1631,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) { @@ -1687,7 +1702,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); @@ -1785,7 +1800,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); @@ -1844,8 +1859,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); } @@ -2208,6 +2227,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) @@ -2650,14 +2688,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) { @@ -2675,7 +2726,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++; @@ -2689,8 +2773,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 44ac036b8..13f4d86a6 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; @@ -466,7 +467,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; @@ -774,11 +775,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; @@ -893,7 +892,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 */ @@ -940,6 +939,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 @@ -1134,6 +1148,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 bc258673d..bd69cb1b9 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) } @@ -606,23 +628,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"); @@ -632,18 +662,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"); @@ -725,9 +753,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 82e1d8782..42bdfbf9f 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 @@ -222,9 +226,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 { @@ -2386,22 +2390,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, @@ -2409,6 +2415,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 { @@ -3357,22 +3425,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( @@ -3381,6 +3451,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 { @@ -4239,7 +4371,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")); @@ -4317,3 +4449,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); +}