aboutsummaryrefslogtreecommitdiffstats
path: root/ipset-dns.c
diff options
context:
space:
mode:
Diffstat (limited to 'ipset-dns.c')
-rw-r--r--ipset-dns.c447
1 files changed, 447 insertions, 0 deletions
diff --git a/ipset-dns.c b/ipset-dns.c
new file mode 100644
index 0000000..80c7cc1
--- /dev/null
+++ b/ipset-dns.c
@@ -0,0 +1,447 @@
+/* ipset-dns: lightweight DNS IPSet forwarding server
+ * by Jason A. Donenfeld (zx2c4) <Jason@zx2c4.com>
+ *
+ * This is a lightweight DNS forwarding server that adds all resolved IPs
+ * to a given netfilter ipset. It is designed to be used in conjunction with
+ * dnsmasq's upstream server directive.
+ *
+ * Copyright (C) 2013 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ * DNS parsing code loosely based on uClibc's resolv.c:
+ * Copyright (C) 1998 Kenneth Albanowski <kjahds@kjahds.com>, The Silver Hammer Group, Ltd.
+ * Copyright (C) 1985, 1993 The Regents of the University of California. All Rights Reserved.
+ * This file is licensed under the GPLv2. Please see COPYING for more information.
+ *
+ *
+ * Usage Example:
+ *
+ * In dnsmasq.conf:
+ * server=/c.youtube.com/127.0.0.1#1919
+ * Make an ipset:
+ * # ipset -N youtube iphash
+ * Start the ipset-dns server:
+ * # ipset-dns youtube 1919 8.8.8.8
+ * Query a hostname:
+ * # host r4---bru02t12.c.youtube.com
+ * r4---bru02t12.c.youtube.com is an alias for r4.bru02t12.c.youtube.com.
+ * r4.bru02t12.c.youtube.com has address 74.125.216.51
+ * Observe that it was added to the ipset:
+ * # ipset -L youtube
+ * Name: youtube
+ * Type: iphash
+ * References: 1
+ * Header: hashsize: 1024 probes: 8 resize: 50
+ * Members:
+ * 74.125.216.51
+ */
+
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <arpa/nameser.h>
+#include <arpa/inet.h>
+#ifndef OLD_IPSET
+#include <libmnl/libmnl.h>
+#include <linux/netfilter/nfnetlink.h>
+#include <linux/netfilter/ipset/ip_set.h>
+#endif
+
+struct resolv_header {
+ int id;
+ int qr, opcode, aa, tc, rd, ra, rcode;
+ int qdcount;
+ int ancount;
+ int nscount;
+ int arcount;
+};
+
+struct resolv_answer {
+ char dotted[256];
+ int atype;
+ int aclass;
+ int ttl;
+ int rdlength;
+ const unsigned char *rdata;
+ int rdoffset;
+};
+
+static void decode_header(unsigned char *data, struct resolv_header *h)
+{
+ h->id = (data[0] << 8) | data[1];
+ h->qr = (data[2] & 0x80) ? 1 : 0;
+ h->opcode = (data[2] >> 3) & 0x0f;
+ h->aa = (data[2] & 0x04) ? 1 : 0;
+ h->tc = (data[2] & 0x02) ? 1 : 0;
+ h->rd = (data[2] & 0x01) ? 1 : 0;
+ h->ra = (data[3] & 0x80) ? 1 : 0;
+ h->rcode = data[3] & 0x0f;
+ h->qdcount = (data[4] << 8) | data[5];
+ h->ancount = (data[6] << 8) | data[7];
+ h->nscount = (data[8] << 8) | data[9];
+ h->arcount = (data[10] << 8) | data[11];
+}
+static int length_question(const unsigned char *data, int maxlen)
+{
+ const unsigned char *start;
+ unsigned int b;
+
+ if (!data)
+ return -1;
+
+ start = data;
+ for (;;) {
+ if (maxlen <= 0)
+ return -1;
+ b = *data++;
+ if (b == 0)
+ break;
+ if ((b & 0xc0) == 0xc0) {
+ /* It's a "compressed" name. */
+ ++data; /* skip lsb of redirected offset */
+ maxlen -= 2;
+ break;
+ }
+ data += b;
+ maxlen -= (b + 1); /* account for data++ above */
+ }
+ /* Up to here we were skipping encoded name */
+
+ /* Account for QTYPE and QCLASS fields */
+ if (maxlen < 4)
+ return -1;
+ return data - start + 2 + 2;
+}
+static int decode_dotted(const unsigned char *packet, int offset, int packet_len, char *dest, int dest_len)
+{
+ unsigned int b, total = 0, used = 0;
+ int measure = 1;
+
+ if (!packet)
+ return -1;
+
+ for (;;) {
+ if (offset >= packet_len)
+ return -1;
+ b = packet[offset++];
+ if (b == 0)
+ break;
+
+ if (measure)
+ ++total;
+
+ if ((b & 0xc0) == 0xc0) {
+ if (offset >= packet_len)
+ return -1;
+ if (measure)
+ ++total;
+ /* compressed item, redirect */
+ offset = ((b & 0x3f) << 8) | packet[offset];
+ measure = 0;
+ continue;
+ }
+
+ if (used + b + 1 >= dest_len || offset + b >= packet_len)
+ return -1;
+ memcpy(dest + used, packet + offset, b);
+ offset += b;
+ used += b;
+
+ if (measure)
+ total += b;
+
+ if (packet[offset] != 0)
+ dest[used++] = '.';
+ else
+ dest[used++] = '\0';
+ }
+
+ if (measure)
+ ++total;
+
+ return total;
+}
+static int decode_answer(const unsigned char *message, int offset, int len, struct resolv_answer *a)
+{
+ int i;
+
+ i = decode_dotted(message, offset, len, a->dotted, sizeof(a->dotted));
+ if (i < 0)
+ return i;
+
+ message += offset + i;
+ len -= i + RRFIXEDSZ + offset;
+ if (len < 0)
+ return len;
+
+ a->atype = (message[0] << 8) | message[1];
+ message += 2;
+ a->aclass = (message[0] << 8) | message[1];
+ message += 2;
+ a->ttl = (message[0] << 24) | (message[1] << 16) | (message[2] << 8) | (message[3] << 0);
+ message += 4;
+ a->rdlength = (message[0] << 8) | message[1];
+ message += 2;
+ a->rdata = message;
+ a->rdoffset = offset + i + RRFIXEDSZ;
+
+ if (len < a->rdlength)
+ return -1;
+ return i + RRFIXEDSZ + a->rdlength;
+}
+
+static int add_to_ipset(const char *setname, const void *ipaddr, int af)
+{
+#ifndef OLD_IPSET
+ struct nlmsghdr *nlh;
+ struct nfgenmsg *nfg;
+ struct mnl_socket *mnl;
+ struct nlattr *nested[2];
+ char buffer[256];
+ int rc;
+
+ rc = 0;
+
+ if (strlen(setname) >= IPSET_MAXNAMELEN) {
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+ if (af != AF_INET && af != AF_INET6) {
+ errno = EAFNOSUPPORT;
+ return -1;
+ }
+
+ nlh = mnl_nlmsg_put_header(buffer);
+ nlh->nlmsg_type = IPSET_CMD_ADD | (NFNL_SUBSYS_IPSET << 8);
+ nlh->nlmsg_flags = NLM_F_REQUEST;
+
+ nfg = mnl_nlmsg_put_extra_header(nlh, sizeof(struct nfgenmsg));
+ nfg->nfgen_family = af;
+ nfg->version = NFNETLINK_V0;
+ nfg->res_id = htons(0);
+
+ mnl_attr_put_u8(nlh, IPSET_ATTR_PROTOCOL, IPSET_PROTOCOL);
+ mnl_attr_put(nlh, IPSET_ATTR_SETNAME, strlen(setname) + 1, setname);
+ nested[0] = mnl_attr_nest_start(nlh, IPSET_ATTR_DATA);
+ nested[1] = mnl_attr_nest_start(nlh, IPSET_ATTR_IP);
+ 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]);
+ mnl_attr_nest_end(nlh, nested[0]);
+
+ mnl = mnl_socket_open(NETLINK_NETFILTER);
+ if (mnl <= 0)
+ return -1;
+ if (mnl_socket_bind(mnl, 0, MNL_SOCKET_AUTOPID) < 0) {
+ rc = -1;
+ goto close;
+ }
+ if (mnl_socket_sendto(mnl, nlh, nlh->nlmsg_len) < 0) {
+ rc = -1;
+ goto close;
+ }
+close:
+ mnl_socket_close(mnl);
+ return rc;
+#else
+ int sock, rc;
+ socklen_t size;
+ struct ip_set_req_adt_get {
+ unsigned op;
+ unsigned version;
+ union {
+ char name[32];
+ uint16_t index;
+ } set;
+ char typename[32];
+ } req_adt_get;
+ struct ip_set_req_adt {
+ unsigned op;
+ uint16_t index;
+ uint32_t ip;
+ } req_adt;
+
+ rc = 0;
+
+ if (strlen(setname) >= sizeof(req_adt_get.set.name)) {
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+ if (af != AF_INET) {
+ errno = EAFNOSUPPORT;
+ return -1;
+ }
+
+ sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
+ if (sock < 0)
+ 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(sock, SOL_IP, 83, &req_adt_get, &size) < 0) {
+ rc = -1;
+ goto close;
+ }
+ req_adt.op = 0x101;
+ req_adt.index = req_adt_get.set.index;
+ req_adt.ip = ntohl(*(uint32_t *)ipaddr);
+ if (setsockopt(sock, SOL_IP, 83, &req_adt, sizeof(req_adt)) < 0) {
+ rc = -1;
+ goto close;
+ }
+close:
+ close(sock);
+ return rc;
+#endif
+}
+
+int main(int argc, char *argv[])
+{
+ struct sockaddr_in client_addr, listen_addr, upstream_addr;
+ struct resolv_header question_header, answer_header;
+ struct resolv_answer answer;
+ struct timeval tv;
+ char msg[512];
+ char ip[INET6_ADDRSTRLEN];
+ char *ipset;
+ int listen_sock, upstream_sock;
+ int pos, i, size, af;
+ socklen_t len;
+ size_t received;
+ pid_t child;
+
+ if (argc != 4) {
+ fprintf(stderr, "Usage: %s ipset port upstream\n", argv[0]);
+ return 1;
+ }
+
+ ipset = argv[1];
+
+ listen_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+ if (listen_sock < 0) {
+ perror("socket");
+ return 1;
+ }
+
+ memset(&listen_addr, 0, sizeof(listen_addr));
+ listen_addr.sin_family = AF_INET;
+ listen_addr.sin_port = htons(atoi(argv[2]));
+ listen_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ i = 1;
+ setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));
+ if (bind(listen_sock, (struct sockaddr *)&listen_addr, sizeof(listen_addr)) < 0) {
+ perror("bind");
+ return 1;
+ }
+
+ memset(&upstream_addr, 0, sizeof(upstream_addr));
+ upstream_addr.sin_family = AF_INET;
+ upstream_addr.sin_port = htons(53);
+ inet_aton(argv[3], &upstream_addr.sin_addr);
+
+ /* TODO: Put all of the below code in several forks all listening on the same sock. */
+
+ if (!getenv("NO_DAEMONIZE")) {
+ if (daemon(0, 0) < 0) {
+ perror("daemon");
+ return 1;
+ }
+ }
+
+ upstream_sock = -1;
+
+ for (;;) {
+ if (upstream_sock >= 0)
+ close(upstream_sock);
+
+ len = sizeof(client_addr);
+ received = recvfrom(listen_sock, msg, sizeof(msg), 0, (struct sockaddr *)&client_addr, &len);
+ if (received < 0) {
+ perror("recvfrom");
+ continue;
+ }
+ if (received < HFIXEDSZ) {
+ fprintf(stderr, "Did not receive full DNS header from client.\n");
+ continue;
+ }
+
+ decode_header(msg, &question_header);
+
+ upstream_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+ if (upstream_sock < 0) {
+ perror("socket");
+ continue;
+ }
+ tv.tv_sec = 1;
+ tv.tv_usec = 0;
+ setsockopt(upstream_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+ if (sendto(upstream_sock, msg, received, 0, (struct sockaddr *)&upstream_addr, sizeof(upstream_addr)) < 0) {
+ perror("sendto");
+ continue;
+ }
+ received = recv(upstream_sock, msg, sizeof(msg), 0);
+ if (received < 0) {
+ perror("recv");
+ continue;
+ }
+ if (received < HFIXEDSZ) {
+ fprintf(stderr, "Did not receive full DNS header from upstream.\n");
+ continue;
+ }
+ close(upstream_sock);
+ upstream_sock = -1;
+
+ decode_header(msg, &answer_header);
+ if (answer_header.id != question_header.id || !answer_header.qr) {
+ fprintf(stderr, "Unsolicited response from upstream.\n");
+ continue;
+ }
+ if (answer_header.rcode || answer_header.ancount <= 0)
+ goto send_back;
+
+ pos = HFIXEDSZ;
+ for (i = 0; i < answer_header.qdcount; ++i) {
+ if (pos >= received || pos < 0)
+ goto send_back;
+ size = length_question(msg + pos, received - pos);
+ if (size < 0)
+ goto send_back;
+ pos += size;
+ }
+ for (i = 0; i < answer_header.ancount; ++i) {
+ if (pos >= received || pos < 0)
+ goto send_back;
+ size = decode_answer(msg, pos, received, &answer);
+ if (size < 0) {
+ if (i && answer_header.tc)
+ break;
+ goto send_back;
+ }
+ pos += size;
+
+ if (!(answer.atype == T_A && answer.rdlength == sizeof(struct in_addr)) &&
+ !(answer.atype == T_AAAA && answer.rdlength == sizeof(struct in6_addr)))
+ continue;
+
+ af = answer.atype == T_A ? AF_INET : AF_INET6;
+
+ if (!inet_ntop(af, answer.rdata, ip, sizeof(ip))) {
+ perror("inet_ntop");
+ continue;
+ }
+
+ printf("%s: %s\n", answer.dotted, ip);
+ if (add_to_ipset(ipset, answer.rdata, af) < 0)
+ perror("add_to_ipset");
+ }
+
+ send_back:
+ if (sendto(listen_sock, msg, received, 0, (struct sockaddr *)&client_addr, sizeof(client_addr)) < 0)
+ perror("sendto");
+ }
+ return 0;
+}