diff --git a/CHANGELOG b/CHANGELOG index d2ae0dd..50b17aa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,9 @@ version 2.66 Thanks to Robert M. Albrecht for the bug report and chasing the problem. + Add --ipset option. Thanks to Jason A. Donenfeld for the + patch. + version 2.65 Fix regression which broke forwarding of queries sent via diff --git a/Makefile b/Makefile index c9cdb90..7240fbf 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ version = -DVERSION='\"`$(top)/bld/get-version $(top)`\"' objs = cache.o rfc1035.o util.o option.o forward.o network.o \ dnsmasq.o dhcp.o lease.o rfc2131.o netlink.o dbus.o bpf.o \ helper.o tftp.o log.o conntrack.o dhcp6.o rfc3315.o \ - dhcp-common.o outpacket.o radv.o slaac.o auth.o + dhcp-common.o outpacket.o radv.o slaac.o auth.o ipset.o hdrs = dnsmasq.h config.h dhcp-protocol.h dhcp6-protocol.h \ dns-protocol.h radv-protocol.h diff --git a/dnsmasq.conf.example b/dnsmasq.conf.example index 5215c2a..9a0dbd7 100644 --- a/dnsmasq.conf.example +++ b/dnsmasq.conf.example @@ -69,6 +69,10 @@ # --address (and --server) work with IPv6 addresses too. #address=/www.thekelleys.org.uk/fe80::20d:60ff:fe36:f83 +# Add the IPs of all queries to yahoo.com, google.com, and their +# subdomains to the vpn and search ipsets: +#ipset=/yahoo.com/google.com/vpn,search + # You can control how dnsmasq talks to a server: this forces # queries to 10.1.2.3 to be routed via eth1 # server=10.1.2.3@eth1 diff --git a/man/dnsmasq.8 b/man/dnsmasq.8 index aff2c4d..bbed144 100644 --- a/man/dnsmasq.8 +++ b/man/dnsmasq.8 @@ -412,6 +412,12 @@ additional facility that /#/ matches any domain. Thus answered from /etc/hosts or DHCP and not sent to an upstream nameserver by a more specific --server directive. .TP +.B --ipset=//[domain/][,] +Places the resolved IP addresses of queries for the specified domains +in the specified netfilter ip sets. Domains and subdomains are matched +in the same way as --address. These ip sets must already exist. See +ipset(8) for more details. +.TP .B \-m, --mx-host=[[,],] Return an MX record named pointing to the given hostname (if given), or diff --git a/src/config.h b/src/config.h index cff278d..9c8e785 100644 --- a/src/config.h +++ b/src/config.h @@ -97,6 +97,10 @@ HAVE_CONNTRACK a build-dependency on libnetfilter_conntrack, but the resulting binary will still run happily on a kernel without conntrack support. +HAVE_IPSET + define this to include the ability to selectively add resolved ip addresses + to given ipsets. + HAVE_AUTH define this to include the facility to act as an authoritative DNS server for one or more zones. @@ -136,7 +140,7 @@ RESOLVFILE /* #define HAVE_DBUS */ /* #define HAVE_IDN */ /* #define HAVE_CONNTRACK */ - +/* #define HAVE_IPSET */ /* Default locations for important system files. */ @@ -323,6 +327,10 @@ HAVE_SOCKADDR_SA_LEN #undef HAVE_AUTH #endif +#ifndef HAVE_LINUX_NETWORK +#undef HAVE_IPSET +#endif + /* Define a string indicating which options are in use. DNSMASQP_COMPILE_OPTS is only defined in dnsmasq.c */ @@ -381,6 +389,10 @@ static char *compile_opts = "no-" #endif "conntrack " +#ifndef HAVE_IPSET +"no-" +#endif +"ipset " #ifndef HAVE_AUTH "no-" #endif diff --git a/src/dnsmasq.c b/src/dnsmasq.c index e36835d..43b8cb1 100644 --- a/src/dnsmasq.c +++ b/src/dnsmasq.c @@ -213,6 +213,11 @@ int main (int argc, char **argv) #endif +#ifdef HAVE_IPSET + if (daemon->ipsets) + ipset_init(); +#endif + #ifdef HAVE_LINUX_NETWORK netlink_init(); diff --git a/src/dnsmasq.h b/src/dnsmasq.h index 21a309c..a24cf41 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -430,6 +430,12 @@ struct server { struct server *next; }; +struct ipsets { + char **sets; + char *domain; + struct ipsets *next; +}; + struct irec { union mysockaddr addr; struct in_addr netmask; /* only valid for IPv4 */ @@ -779,6 +785,7 @@ extern struct daemon { struct iname *if_names, *if_addrs, *if_except, *dhcp_except, *auth_peers; struct bogus_addr *bogus_addr; struct server *servers; + struct ipsets *ipsets; int log_fac; /* log facility */ char *log_file; /* optional log file */ int max_logs; /* queue limit */ @@ -903,7 +910,8 @@ size_t setup_reply(struct dns_header *header, size_t qlen, struct all_addr *addrp, unsigned int flags, unsigned long local_ttl); int extract_addresses(struct dns_header *header, size_t qlen, char *namebuff, - time_t now, int is_sign, int checkrebind, int checking_disabled); + time_t now, char **ipsets, int is_sign, int checkrebind, + int checking_disabled); size_t answer_request(struct dns_header *header, char *limit, size_t qlen, struct in_addr local_addr, struct in_addr local_netmask, time_t now); int check_for_bogus_wildcard(struct dns_header *header, size_t qlen, char *name, @@ -1117,6 +1125,12 @@ void emit_dbus_signal(int action, struct dhcp_lease *lease, char *hostname); # endif #endif +/* ipset.c */ +#ifdef HAVE_IPSET +void ipset_init(void); +int add_to_ipset(const char *setname, const struct all_addr *ipaddr, int flags, int remove); +#endif + /* helper.c */ #if defined(HAVE_SCRIPT) int create_helper(int event_fd, int err_fd, uid_t uid, gid_t gid, long max_fd); diff --git a/src/forward.c b/src/forward.c index fb0b4c4..9a1e15a 100644 --- a/src/forward.c +++ b/src/forward.c @@ -439,9 +439,28 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server *server, size_t n, int check_rebind, int checking_disabled) { unsigned char *pheader, *sizep; + char **sets = 0; int munged = 0, is_sign; size_t plen; +#ifdef HAVE_IPSET + /* Similar algorithm to search_servers. */ + struct ipsets *ipset_pos; + unsigned int namelen = strlen(daemon->namebuff); + unsigned int matchlen = 0; + for (ipset_pos = daemon->ipsets; ipset_pos; ipset_pos = ipset_pos->next) + { + unsigned int domainlen = strlen(ipset_pos->domain); + char *matchstart = daemon->namebuff + namelen - domainlen; + if (namelen >= domainlen && hostname_isequal(matchstart, ipset_pos->domain) && + (domainlen == 0 || namelen == domainlen || *(matchstart - 1) == '.' ) && + domainlen >= matchlen) { + matchlen = domainlen; + sets = ipset_pos->sets; + } + } +#endif + /* If upstream is advertising a larger UDP packet size than we allow, trim it so that we don't get overlarge requests for the client. We can't do this for signed packets. */ @@ -494,7 +513,7 @@ static size_t process_reply(struct dns_header *header, time_t now, SET_RCODE(header, NOERROR); } - if (extract_addresses(header, n, daemon->namebuff, now, is_sign, check_rebind, checking_disabled)) + if (extract_addresses(header, n, daemon->namebuff, now, sets, is_sign, check_rebind, checking_disabled)) { my_syslog(LOG_WARNING, _("possible DNS-rebind attack detected: %s"), daemon->namebuff); munged = 1; diff --git a/src/ipset.c b/src/ipset.c new file mode 100644 index 0000000..c16cc85 --- /dev/null +++ b/src/ipset.c @@ -0,0 +1,205 @@ +/* ipset.c is Copyright (c) 2013 Jason A. Donenfeld . All Rights Reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 dated June, 1991, or + (at your option) version 3 dated 29 June, 2007. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "dnsmasq.h" + +#ifdef HAVE_IPSET + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef NFNL_SUBSYS_IPSET +#define NFNL_SUBSYS_IPSET 6 +#define IPSET_ATTR_DATA 7 +#define IPSET_ATTR_IP 1 +#define IPSET_ATTR_IPADDR_IPV4 1 +#define IPSET_ATTR_IPADDR_IPV6 2 +#define IPSET_ATTR_PROTOCOL 1 +#define IPSET_ATTR_SETNAME 2 +#define IPSET_CMD_ADD 9 +#define IPSET_CMD_DEL 10 +#define IPSET_MAXNAMELEN 32 +#define IPSET_PROTOCOL 6 +#else +#include +#endif + +/* data structure size in here is fixed */ +#define BUFF_SZ 256 + +#define NL_ALIGN(len) (((len)+3) & ~(3)) +static const struct sockaddr_nl snl = { .nl_family = AF_NETLINK }; +static int ipset_sock, old_kernel; +static char *buffer; + +static inline void add_attr(struct nlmsghdr *nlh, uint16_t type, size_t len, const void *data) +{ + struct nlattr *attr = (void *)nlh + NL_ALIGN(nlh->nlmsg_len); + uint16_t payload_len = NL_ALIGN(sizeof(struct nlattr)) + len; + attr->nla_type = type; + attr->nla_len = payload_len; + memcpy((void *)attr + NL_ALIGN(sizeof(struct nlattr)), data, len); + nlh->nlmsg_len += NL_ALIGN(payload_len); +} + +void ipset_init(void) +{ + struct utsname utsname; + int version; + char *split; + + if (uname(&utsname) < 0) + die(_("failed to find kernel version: %s"), NULL, EC_MISC); + + split = strtok(utsname.release, "."); + version = (split ? atoi(split) : 0); + split = strtok(NULL, "."); + version = version * 256 + (split ? atoi(split) : 0); + split = strtok(NULL, "."); + version = version * 256 + (split ? atoi(split) : 0); + old_kernel = (version < KERNEL_VERSION(2,6,32)); + + if (old_kernel && (ipset_sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) != -1) + return; + + if (!old_kernel && + (buffer = safe_malloc(BUFF_SZ)) && + (ipset_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER)) != -1 && + (bind(ipset_sock, (struct sockaddr *)&snl, sizeof(snl)) != -1)) + return; + + die (_("failed to create IPset control socket: %s"), NULL, EC_MISC); +} + +static int new_add_to_ipset(const char *setname, const struct all_addr *ipaddr, int af, int remove) +{ + struct nlmsghdr *nlh; + struct nfgenmsg *nfg; + struct nlattr *nested[2]; + uint8_t proto; + int addrsz = INADDRSZ; + +#ifdef HAVE_IPV6 + if (af == AF_INET6) + addrsz = IN6ADDRSZ; +#endif + + if (strlen(setname) >= IPSET_MAXNAMELEN) + { + errno = ENAMETOOLONG; + return -1; + } + + memset(buffer, 0, sizeof(buffer)); + + nlh = (struct nlmsghdr *)buffer; + nlh->nlmsg_len = NL_ALIGN(sizeof(struct nlmsghdr)); + nlh->nlmsg_type = (remove ? IPSET_CMD_DEL : IPSET_CMD_ADD) | (NFNL_SUBSYS_IPSET << 8); + nlh->nlmsg_flags = NLM_F_REQUEST; + + nfg = (struct nfgenmsg *)(buffer + nlh->nlmsg_len); + nlh->nlmsg_len += NL_ALIGN(sizeof(struct nfgenmsg)); + nfg->nfgen_family = af; + nfg->version = NFNETLINK_V0; + nfg->res_id = htons(0); + + proto = IPSET_PROTOCOL; + add_attr(nlh, IPSET_ATTR_PROTOCOL, sizeof(proto), &proto); + add_attr(nlh, IPSET_ATTR_SETNAME, strlen(setname) + 1, setname); + nested[0] = (struct nlattr *)(buffer + NL_ALIGN(nlh->nlmsg_len)); + nlh->nlmsg_len += NL_ALIGN(sizeof(struct nlattr)); + nested[0]->nla_type = NLA_F_NESTED | IPSET_ATTR_DATA; + nested[1] = (struct nlattr *)(buffer + NL_ALIGN(nlh->nlmsg_len)); + nlh->nlmsg_len += NL_ALIGN(sizeof(struct nlattr)); + nested[1]->nla_type = NLA_F_NESTED | IPSET_ATTR_IP; + add_attr(nlh, + (af == AF_INET ? IPSET_ATTR_IPADDR_IPV4 : IPSET_ATTR_IPADDR_IPV6) | NLA_F_NET_BYTEORDER, + addrsz, &ipaddr->addr); + nested[1]->nla_len = (void *)buffer + NL_ALIGN(nlh->nlmsg_len) - (void *)nested[1]; + nested[0]->nla_len = (void *)buffer + NL_ALIGN(nlh->nlmsg_len) - (void *)nested[0]; + + if (sendto(ipset_sock, buffer, nlh->nlmsg_len, 0, (struct sockaddr *)&snl, sizeof(snl)) < 0) + return -1; + + return 0; +} + + +static int old_add_to_ipset(const char *setname, const struct all_addr *ipaddr, int remove) +{ + socklen_t size; + struct ip_set_req_adt_get { + unsigned op; + unsigned version; + union { + char name[IPSET_MAXNAMELEN]; + uint16_t index; + } set; + char typename[IPSET_MAXNAMELEN]; + } req_adt_get; + struct ip_set_req_adt { + unsigned op; + uint16_t index; + uint32_t ip; + } req_adt; + + if (strlen(setname) >= sizeof(req_adt_get.set.name)) + { + errno = ENAMETOOLONG; + return -1; + } + + req_adt_get.op = 0x10; + req_adt_get.version = 3; + strcpy(req_adt_get.set.name, setname); + size = sizeof(req_adt_get); + if (getsockopt(ipset_sock, SOL_IP, 83, &req_adt_get, &size) < 0) + return -1; + req_adt.op = remove ? 0x102 : 0x101; + req_adt.index = req_adt_get.set.index; + req_adt.ip = ntohl(ipaddr->addr.addr4.s_addr); + if (setsockopt(ipset_sock, SOL_IP, 83, &req_adt, sizeof(req_adt)) < 0) + return -1; + + return 0; +} + + + +int add_to_ipset(const char *setname, const struct all_addr *ipaddr, int flags, int remove) +{ + int af = AF_INET; + +#ifdef HAVE_IPV6 + if (flags & F_IPV6) + { + af = AF_INET6; + /* old method only supports IPv4 */ + if (old_kernel) + return -1; + } +#endif + + return old_kernel ? old_add_to_ipset(setname, ipaddr, remove) : new_add_to_ipset(setname, ipaddr, af, remove); +} + +#endif diff --git a/src/option.c b/src/option.c index 3fc3e03..9315694 100644 --- a/src/option.c +++ b/src/option.c @@ -127,6 +127,7 @@ struct myoption { #define LOPT_AUTHSOA 316 #define LOPT_AUTHSFS 317 #define LOPT_AUTHPEER 318 +#define LOPT_IPSET 319 #ifdef HAVE_GETOPT_LONG static const struct option opts[] = @@ -259,6 +260,7 @@ static const struct myoption opts[] = { "auth-soa", 1, 0, LOPT_AUTHSOA }, { "auth-sec-servers", 1, 0, LOPT_AUTHSFS }, { "auth-peer", 1, 0, LOPT_AUTHPEER }, + { "ipset", 1, 0, LOPT_IPSET }, { NULL, 0, 0, 0 } }; @@ -397,6 +399,7 @@ static struct { { LOPT_AUTHSOA, ARG_ONE, "[,...]", gettext_noop("Set authoritive zone information"), NULL }, { LOPT_AUTHSFS, ARG_DUP, "[,...]", gettext_noop("Secondary authoritative nameservers for forward domains"), NULL }, { LOPT_AUTHPEER, ARG_DUP, "[,...]", gettext_noop("Peers which are allowed to do zone transfer"), NULL }, + { LOPT_IPSET, ARG_DUP, "//[,...]", gettext_noop("Specify ipsets to which matching domains should be added"), NULL }, { 0, 0, NULL, NULL, NULL } }; @@ -2021,6 +2024,74 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma daemon->servers = newlist; break; } + + case LOPT_IPSET: /* --ipset */ +#ifndef HAVE_IPSET + ret_err(_("recompile with HAVE_IPSET defined to enable ipset directives")); + break; +#else + { + struct ipsets ipsets_head; + struct ipsets *ipsets = &ipsets_head; + int size; + char *end; + char **sets, **sets_pos; + memset(ipsets, 0, sizeof(struct ipsets)); + unhide_metas(arg); + if (arg && *arg == '/') + { + arg++; + while ((end = split_chr(arg, '/'))) + { + char *domain = NULL; + /* elide leading dots - they are implied in the search algorithm */ + while (*arg == '.') + arg++; + /* # matches everything and becomes a zero length domain string */ + if (strcmp(arg, "#") == 0 || !*arg) + domain = ""; + else if (strlen(arg) != 0 && !(domain = canonicalise_opt(arg))) + option = '?'; + ipsets->next = opt_malloc(sizeof(struct ipsets)); + ipsets = ipsets->next; + memset(ipsets, 0, sizeof(struct ipsets)); + ipsets->domain = domain; + arg = end; + } + } + else + { + ipsets->next = opt_malloc(sizeof(struct ipsets)); + ipsets = ipsets->next; + memset(ipsets, 0, sizeof(struct ipsets)); + ipsets->domain = ""; + } + if (!arg || !*arg) + { + option = '?'; + break; + } + size = 2; + for (end = arg; *end; ++end) + if (*end == ',') + ++size; + + sets = sets_pos = opt_malloc(sizeof(char *) * size); + + do { + end = split(arg); + *sets_pos++ = opt_string_alloc(arg); + arg = end; + } while (end); + *sets_pos = 0; + for (ipsets = &ipsets_head; ipsets->next; ipsets = ipsets->next) + ipsets->next->sets = sets; + ipsets->next = daemon->ipsets; + daemon->ipsets = ipsets_head.next; + + break; + } +#endif case 'c': /* --cache-size */ { diff --git a/src/rfc1035.c b/src/rfc1035.c index 721cd61..8d55ffd 100644 --- a/src/rfc1035.c +++ b/src/rfc1035.c @@ -777,13 +777,18 @@ static int find_soa(struct dns_header *header, size_t qlen, char *name) expired and cleaned out that way. Return 1 if we reject an address because it look like part of dns-rebinding attack. */ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t now, - int is_sign, int check_rebind, int checking_disabled) + char **ipsets, int is_sign, int check_rebind, int checking_disabled) { unsigned char *p, *p1, *endrr, *namep; int i, j, qtype, qclass, aqtype, aqclass, ardlen, res, searched_soa = 0; unsigned long ttl = 0; struct all_addr addr; - +#ifdef HAVE_IPSET + char **ipsets_cur; +#else + (void)ipsets; /* unused */ +#endif + cache_start_insert(); /* find_soa is needed for dns_doctor and logging side-effects, so don't call it lazily if there are any. */ @@ -966,6 +971,15 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t (flags & F_IPV4) && private_net(addr.addr.addr4, !option_bool(OPT_LOCAL_REBIND))) return 1; + +#ifdef HAVE_IPSET + if (ipsets && (flags & (F_IPV4 | F_IPV6))) + { + ipsets_cur = ipsets; + while (*ipsets_cur) + add_to_ipset(*ipsets_cur++, &addr, flags, 0); + } +#endif newc = cache_insert(name, &addr, now, attl, flags | F_FORWARD); if (newc && cpp)