diff options
Diffstat (limited to 'plugins/rzkeychange')
-rw-r--r-- | plugins/rzkeychange/Makefile.am | 23 | ||||
-rw-r--r-- | plugins/rzkeychange/rzkeychange.c | 470 | ||||
-rwxr-xr-x | plugins/rzkeychange/test1.sh | 21 |
3 files changed, 514 insertions, 0 deletions
diff --git a/plugins/rzkeychange/Makefile.am b/plugins/rzkeychange/Makefile.am new file mode 100644 index 0000000..869eba6 --- /dev/null +++ b/plugins/rzkeychange/Makefile.am @@ -0,0 +1,23 @@ +MAINTAINERCLEANFILES = $(srcdir)/Makefile.in +CLEANFILES = *.gcda *.gcno *.gcov + +AM_CFLAGS = -I$(srcdir) \ + -I$(top_srcdir)/src \ + -I$(top_srcdir)/isc \ + $(SECCOMPFLAGS) \ + $(libldns_CFLAGS) + +pkglib_LTLIBRARIES = rzkeychange.la +rzkeychange_la_SOURCES = rzkeychange.c +rzkeychange_la_LDFLAGS = -module -avoid-version $(libldns_LIBS) + +TESTS = test1.sh +EXTRA_DIST = $(TESTS) +CLEANFILES += *.pcap-dist + +if ENABLE_GCOV +gcov-local: + for src in $(rzkeychange_la_SOURCES); do \ + gcov -o .libs -l -r -s "$(srcdir)" "$$src"; \ + done +endif diff --git a/plugins/rzkeychange/rzkeychange.c b/plugins/rzkeychange/rzkeychange.c new file mode 100644 index 0000000..4f14c40 --- /dev/null +++ b/plugins/rzkeychange/rzkeychange.c @@ -0,0 +1,470 @@ +/* + * Author Duane Wessels + */ + +#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <memory.h> +#include <time.h> +#include <stdarg.h> +#include <errno.h> +#include <assert.h> +#include <sys/wait.h> + +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <arpa/nameser.h> + +#include <netinet/in_systm.h> +#include <netinet/ip.h> +#include <netinet/ip6.h> +#include <netinet/ip_icmp.h> + +#include <ldns/ldns.h> + +#include "dnscap_common.h" + +static logerr_t* logerr = 0; +static my_bpftimeval open_ts = { 0, 0 }; +static my_bpftimeval clos_ts = { 0, 0 }; +static char* report_zone = 0; +static char* report_server = 0; +static char* report_node = 0; +static char* keytag_zone = 0; +static unsigned short resolver_port = 0; +static unsigned int resolver_use_tcp = 0; +static ldns_resolver* res; + +static int dry_run = 0; + +output_t rzkeychange_output; +is_responder_t rzkeychange_is_responder = 0; +ia_str_t rzkeychange_ia_str = 0; + +#define MAX_KEY_TAG_SIGNALS 500 +static unsigned int num_key_tag_signals; +struct { + iaddr addr; + uint8_t flags; + const char* signal; +} key_tag_signals[MAX_KEY_TAG_SIGNALS]; + +#define KEYTAG_FLAG_DO 1 +#define KEYTAG_FLAG_CD 2 +#define KEYTAG_FLAG_RD 4 + +struct { + uint64_t dnskey; + uint64_t tc_bit; + uint64_t tcp; + uint64_t icmp_unreach_frag; + uint64_t icmp_timxceed_reass; + uint64_t icmp_timxceed_intrans; + uint64_t total; +} counts; + +#define MAX_NAMESERVERS 10 +static unsigned int num_ns_addrs = 0; +static char* ns_addrs[MAX_NAMESERVERS]; + +void rzkeychange_usage() +{ + fprintf(stderr, + "\nrzkeychange.so options:\n" + "\t-? print these instructions and exit\n" + "\t-D dry run, just print queries\n" + "\t-z <zone> Report counters to DNS zone <zone> (required)\n" + "\t-s <server> Data is from server <server> (required)\n" + "\t-n <node> Data is from site/node <node> (required)\n" + "\t-k <zone> Report RFC 8145 key tag signals to <zone>\n" + "\t-a <addr> Send DNS queries to this addr\n" + "\t-p <port> Send DNS queries to this port\n" + "\t-t Use TCP for DNS queries\n"); +} + +void rzkeychange_extension(int ext, void* arg) +{ + switch (ext) { + case DNSCAP_EXT_IS_RESPONDER: + rzkeychange_is_responder = (is_responder_t)arg; + break; + case DNSCAP_EXT_IA_STR: + rzkeychange_ia_str = (ia_str_t)arg; + break; + } +} + +void rzkeychange_getopt(int* argc, char** argv[]) +{ + int c; + while ((c = getopt(*argc, *argv, "?a:k:n:p:s:tz:D")) != EOF) { + switch (c) { + case 'n': + if (report_node) + free(report_node); + report_node = strdup(optarg); + if (!report_node) { + fprintf(stderr, "strdup() out of memory\n"); + exit(1); + } + break; + case 's': + if (report_server) + free(report_server); + report_server = strdup(optarg); + if (!report_server) { + fprintf(stderr, "strdup() out of memory\n"); + exit(1); + } + break; + case 'z': + if (report_zone) + free(report_zone); + report_zone = strdup(optarg); + if (!report_zone) { + fprintf(stderr, "strdup() out of memory\n"); + exit(1); + } + break; + case 'k': + if (keytag_zone) + free(keytag_zone); + keytag_zone = strdup(optarg); + if (!keytag_zone) { + fprintf(stderr, "strdup() out of memory\n"); + exit(1); + } + break; + case 'a': + if (num_ns_addrs < MAX_NAMESERVERS) { + ns_addrs[num_ns_addrs] = strdup(optarg); + if (!ns_addrs[num_ns_addrs]) { + fprintf(stderr, "strdup() out of memory\n"); + exit(1); + } + num_ns_addrs++; + } else { + fprintf(stderr, "too many nameservers\n"); + exit(1); + } + break; + case 'p': + resolver_port = strtoul(optarg, 0, 10); + break; + case 't': + resolver_use_tcp = 1; + break; + case 'D': + dry_run = 1; + break; + case '?': + rzkeychange_usage(); + if (!optopt || optopt == '?') { + exit(0); + } + // fallthrough + default: + exit(1); + } + } + if (!report_zone || !report_server || !report_node) { + rzkeychange_usage(); + exit(1); + } +} + +ldns_pkt* +dns_query(const char* name, ldns_rr_type type) +{ + fprintf(stderr, "%s\n", name); + if (dry_run) { + return 0; + } + + ldns_rdf* domain = ldns_dname_new_frm_str(name); + if (0 == domain) { + fprintf(stderr, "bad query name: '%s'\n", name); + exit(1); + } + ldns_pkt* pkt = ldns_resolver_query(res, + domain, + type, + LDNS_RR_CLASS_IN, + LDNS_RD); + ldns_rdf_deep_free(domain); + return pkt; +} + +static void +add_resolver_nameserver(const char* s) +{ + ldns_rdf* nsaddr; + fprintf(stderr, "adding nameserver '%s' to resolver config\n", s); + if (strchr(s, ':')) + nsaddr = ldns_rdf_new_frm_str(LDNS_RDF_TYPE_AAAA, s); + else + nsaddr = ldns_rdf_new_frm_str(LDNS_RDF_TYPE_A, s); + if (!nsaddr) { + logerr("rzkeychange.so: invalid IP address '%s'", s); + exit(1); + } + assert(LDNS_STATUS_OK == ldns_resolver_push_nameserver(res, nsaddr)); +} + +int rzkeychange_start(logerr_t* a_logerr) +{ + ldns_pkt* pkt; + struct timeval to; + char qname[256]; + logerr = a_logerr; + if (LDNS_STATUS_OK != ldns_resolver_new_frm_file(&res, NULL)) { + fprintf(stderr, "Failed to initialize ldns resolver\n"); + exit(1); + } + if (num_ns_addrs) { + unsigned int i; + ldns_resolver_set_nameserver_count(res, 0); + for (i = 0; i < num_ns_addrs; i++) + add_resolver_nameserver(ns_addrs[i]); + } + if (0 == ldns_resolver_nameserver_count(res)) + add_resolver_nameserver("127.0.0.1"); + if (resolver_port) + ldns_resolver_set_port(res, resolver_port); + if (resolver_use_tcp) + ldns_resolver_set_usevc(res, 1); + + if (dry_run) { + return 0; + } + + fprintf(stderr, "Testing reachability of zone '%s'\n", report_zone); + pkt = dns_query(report_zone, LDNS_RR_TYPE_TXT); + if (!pkt) { + fprintf(stderr, "Test of zone '%s' failed\n", report_zone); + exit(1); + } + if (0 != ldns_pkt_get_rcode(pkt)) { + fprintf(stderr, "Query to zone '%s' returned rcode %d\n", report_zone, ldns_pkt_get_rcode(pkt)); + exit(1); + } + fprintf(stderr, "Success.\n"); + if (pkt) + ldns_pkt_free(pkt); + /* + * For all subsequent queries we don't actually care about the response + * and don't wait to wait very long for it so the timeout is set really low. + */ + to.tv_sec = 0; + to.tv_usec = 500000; + ldns_resolver_set_timeout(res, to); + snprintf(qname, sizeof(qname), "ts-elapsed-tot-dnskey-tcp-tc-unreachfrag-texcfrag-texcttl.%s.%s.%s", report_node, report_server, report_zone); + pkt = dns_query(qname, LDNS_RR_TYPE_TXT); + if (pkt) + ldns_pkt_free(pkt); + return 0; +} + +void rzkeychange_stop() +{ +} + +int rzkeychange_open(my_bpftimeval ts) +{ + open_ts = clos_ts.tv_sec ? clos_ts : ts; + memset(&counts, 0, sizeof(counts)); + memset(&key_tag_signals, 0, sizeof(key_tag_signals)); + num_key_tag_signals = 0; + return 0; +} + +void rzkeychange_submit_counts(void) +{ + char qname[256]; + ldns_pkt* pkt; + double elapsed = (double)clos_ts.tv_sec - (double)open_ts.tv_sec + 0.000001 * clos_ts.tv_usec - 0.000001 * open_ts.tv_usec; //NOSONAR + int k; + + k = snprintf(qname, sizeof(qname), "%lu-%u-%" PRIu64 "-%" PRIu64 "-%" PRIu64 "-%" PRIu64 "-%" PRIu64 "-%" PRIu64 "-%" PRIu64 ".%s.%s.%s", + (u_long)open_ts.tv_sec, + (unsigned int)(elapsed + 0.5), + counts.total, + counts.dnskey, + counts.tcp, + counts.tc_bit, + counts.icmp_unreach_frag, + counts.icmp_timxceed_reass, + counts.icmp_timxceed_intrans, + report_node, + report_server, + report_zone); + + if (k < sizeof(qname)) { + pkt = dns_query(qname, LDNS_RR_TYPE_TXT); + if (pkt) + ldns_pkt_free(pkt); + } + + if (keytag_zone != 0) { + unsigned int i; + + for (i = 0; i < num_key_tag_signals; i++) { + char* s = strdup(rzkeychange_ia_str(key_tag_signals[i].addr)); + char* t; + + if (0 == s) { + /* + * Apparently out of memory. This function is called in + * a child process which will exit right after this we + * break from the loop and return from this function. + */ + break; + } + + for (t = s; *t; t++) + if (*t == '.' || *t == ':') + *t = '-'; + + k = snprintf(qname, sizeof(qname), "%lu.%s.%hhx.%s.%s.%s.%s", + (u_long)open_ts.tv_sec, + s, + key_tag_signals[i].flags, + key_tag_signals[i].signal, + report_node, + report_server, + keytag_zone); + free(s); + + if (k >= sizeof(qname)) + continue; // qname was truncated in snprintf() + + pkt = dns_query(qname, LDNS_RR_TYPE_TXT); + if (pkt) + ldns_pkt_free(pkt); + } + } +} + +/* + * Fork a separate process so that we don't block the main dnscap. Use + * double-fork to avoid zombies for the main dnscap process. + */ +int rzkeychange_close(my_bpftimeval ts) +{ + pid_t pid; + pid = fork(); + if (pid < 0) { + logerr("rzkeychange.so: fork: %s", strerror(errno)); + return 1; + } else if (pid) { + /* parent */ + waitpid(pid, NULL, 0); + return 0; + } + /* 1st gen child continues */ + pid = fork(); + if (pid < 0) { + logerr("rzkeychange.so: fork: %s", strerror(errno)); + return 1; + } else if (pid) { + /* 1st gen child exits */ + exit(0); + } + /* grandchild (2nd gen) continues */ + clos_ts = ts; + rzkeychange_submit_counts(); + exit(0); +} + +void rzkeychange_keytagsignal(const ldns_pkt* pkt, const ldns_rr* question_rr, iaddr addr) +{ + ldns_rdf* qn; + char* qn_str = 0; + if (LDNS_RR_TYPE_NULL != ldns_rr_get_type(question_rr)) + return; + if (num_key_tag_signals == MAX_KEY_TAG_SIGNALS) + return; + qn = ldns_rr_owner(question_rr); + if (qn == 0) + return; + qn_str = ldns_rdf2str(qn); + if (qn_str == 0) + return; + if (0 != strncasecmp(qn_str, "_ta-", 4)) + goto keytagsignal_done; + qn_str[strlen(qn_str) - 1] = 0; // ldns always adds terminating dot + if (strchr(qn_str, '.')) // dont want non-root keytag signals + goto keytagsignal_done; + key_tag_signals[num_key_tag_signals].addr = addr; + key_tag_signals[num_key_tag_signals].signal = strdup(qn_str); + assert(key_tag_signals[num_key_tag_signals].signal); + if (ldns_pkt_rd(pkt)) + key_tag_signals[num_key_tag_signals].flags |= KEYTAG_FLAG_RD; + if (ldns_pkt_cd(pkt)) + key_tag_signals[num_key_tag_signals].flags |= KEYTAG_FLAG_CD; + if (ldns_pkt_edns_do(pkt)) + key_tag_signals[num_key_tag_signals].flags |= KEYTAG_FLAG_DO; + num_key_tag_signals++; +keytagsignal_done: + if (qn_str) + free(qn_str); +} + +void rzkeychange_output(const char* descr, iaddr from, iaddr to, uint8_t proto, unsigned flags, + unsigned sport, unsigned dport, my_bpftimeval ts, + const u_char* pkt_copy, const unsigned olen, + const u_char* payload, const unsigned payloadlen) +{ + ldns_pkt* pkt = 0; + ldns_rr_list* question_rr_list = 0; + ldns_rr* question_rr = 0; + if (!(flags & DNSCAP_OUTPUT_ISDNS)) { + if (IPPROTO_ICMP == proto && payloadlen >= 4) { + struct icmp* icmp; + if (rzkeychange_is_responder && !rzkeychange_is_responder(to)) + goto done; + icmp = (void*)payload; + if (ICMP_UNREACH == icmp->icmp_type) { + if (ICMP_UNREACH_NEEDFRAG == icmp->icmp_code) + counts.icmp_unreach_frag++; + } else if (ICMP_TIMXCEED == icmp->icmp_type) { + if (ICMP_TIMXCEED_INTRANS == icmp->icmp_code) + counts.icmp_timxceed_intrans++; + else if (ICMP_TIMXCEED_REASS == icmp->icmp_code) + counts.icmp_timxceed_reass++; + } + } + goto done; + } + if (LDNS_STATUS_OK != ldns_wire2pkt(&pkt, payload, payloadlen)) + return; + if (0 == ldns_pkt_qr(pkt)) + goto done; + counts.total++; + if (IPPROTO_UDP == proto) { + if (0 != ldns_pkt_tc(pkt)) + counts.tc_bit++; + } else if (IPPROTO_TCP == proto) { + counts.tcp++; + } + if (LDNS_PACKET_QUERY != ldns_pkt_get_opcode(pkt)) + goto done; + question_rr_list = ldns_pkt_question(pkt); + if (0 == question_rr_list) + goto done; + question_rr = ldns_rr_list_rr(question_rr_list, 0); + if (0 == question_rr) + goto done; + if (LDNS_RR_CLASS_IN == ldns_rr_get_class(question_rr)) + if (LDNS_RR_TYPE_DNSKEY == ldns_rr_get_type(question_rr)) + counts.dnskey++; + if (keytag_zone != 0) + rzkeychange_keytagsignal(pkt, question_rr, to); // 'to' here because plugin should be processing responses +done: + ldns_pkt_free(pkt); +} diff --git a/plugins/rzkeychange/test1.sh b/plugins/rzkeychange/test1.sh new file mode 100755 index 0000000..de066e3 --- /dev/null +++ b/plugins/rzkeychange/test1.sh @@ -0,0 +1,21 @@ +#!/bin/sh -xe + +plugin=`find . -name 'rzkeychange.so' | head -n 1` +if [ -z "$plugin" ]; then + echo "Unable to find the rzkeychange plugin" + exit 1 +fi + +ln -fs "$srcdir/../../src/test/dns.pcap" dns.pcap-dist + +../../src/dnscap -r dns.pcap-dist -g -P "$plugin" -? +! ../../src/dnscap -r dns.pcap-dist -g -P "$plugin" -X +! ../../src/dnscap -r dns.pcap-dist -g -P "$plugin" -n text -n text +! ../../src/dnscap -r dns.pcap-dist -g -P "$plugin" -s text -s text +! ../../src/dnscap -r dns.pcap-dist -g -P "$plugin" -z text -z text +! ../../src/dnscap -r dns.pcap-dist -g -P "$plugin" -k text -k text +! ../../src/dnscap -r dns.pcap-dist -g -P "$plugin" -a 1 -a 2 -a 3 -a 4 -a 5 -a 6 -a 7 -a 8 -a 9 -a 10 -a 11 + +# LDNS resolver needs /etc/resolv.conf +test -f /etc/resolv.conf || exit 0 +../../src/dnscap -r dns.pcap-dist -g -P "$plugin" -D -t -p 5353 -a 127.0.0.1 -n n -s s -z example.com -k k |