/* * DNS Resolver Module User-space Helper for AFSDB records * * Copyright (C) Wang Lei (wang840925@gmail.com) 2010 * Authors: Wang Lei (wang840925@gmail.com) * David Howells (dhowells@redhat.com) * * This is a userspace tool for querying AFSDB RR records in the DNS on behalf * of the kernel, and converting the VL server addresses to IPv4 format so that * they can be used by the kAFS filesystem. * * Compile with: * * cc -o key.dns_resolver key.dns_resolver.c -lresolv -lkeyutils * * As some function like res_init() should use the static library, which is a * bug of libresolv, that is the reason for cifs.upcall to reimplement. * * To use this program, you must tell /sbin/request-key how to invoke it. You * need to have the keyutils package installed and something like the following * lines added to your /etc/request-key.conf file: * * #OP TYPE DESCRIPTION CALLOUT INFO PROGRAM ARG1 ARG2 ARG3 ... * ====== ============ =========== ============ ========================== * create dns_resolver afsdb:* * /sbin/key.dns_resolver %k * * * 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; either version 2 of the License, or * (at your option) any later version. * * 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "key.dns.h" static const char *DNS_PARSE_VERSION = "1.0"; static const char prog[] = "key.dns_resolver"; static const char key_type[] = "dns_resolver"; static const char a_query_type[] = "a"; static const char aaaa_query_type[] = "aaaa"; static const char afsdb_query_type[] = "afsdb"; static const char *config_file = "/etc/keyutils/key.dns_resolver.conf"; static bool config_specified = false; key_serial_t key; static int verbose; int debug_mode; unsigned mask = INET_ALL; unsigned int key_expiry = 5; /* * segmental payload */ struct iovec payload[N_PAYLOAD]; int payload_index; /* * Print an error to stderr or the syslog, negate the key being created and * exit */ void error(const char *fmt, ...) { va_list va; va_start(va, fmt); if (isatty(2)) { vfprintf(stderr, fmt, va); fputc('\n', stderr); } else { vsyslog(LOG_ERR, fmt, va); } va_end(va); /* * on error, negatively instantiate the key ourselves so that we can * make sure the kernel doesn't hang it off of a searchable keyring * and interfere with the next attempt to instantiate the key. */ if (!debug_mode) keyctl_negate(key, 1, KEY_REQKEY_DEFL_DEFAULT); exit(1); } #define error(FMT, ...) error("Error: " FMT, ##__VA_ARGS__); /* * Just print an error to stderr or the syslog */ void _error(const char *fmt, ...) { va_list va; va_start(va, fmt); if (isatty(2)) { vfprintf(stderr, fmt, va); fputc('\n', stderr); } else { vsyslog(LOG_ERR, fmt, va); } va_end(va); } /* * Print a warning to stderr or the syslog */ void warning(const char *fmt, ...) { va_list va; va_start(va, fmt); if (isatty(2)) { vfprintf(stderr, fmt, va); fputc('\n', stderr); } else { vsyslog(LOG_WARNING, fmt, va); } va_end(va); } /* * Print status information */ void info(const char *fmt, ...) { va_list va; if (verbose < 1) return; va_start(va, fmt); if (isatty(1)) { fputs("I: ", stdout); vfprintf(stdout, fmt, va); fputc('\n', stdout); } else { vsyslog(LOG_INFO, fmt, va); } va_end(va); } /* * Print a nameserver error and exit */ static const int ns_errno_map[] = { [0] = ECONNREFUSED, [HOST_NOT_FOUND] = ENODATA, [TRY_AGAIN] = EAGAIN, [NO_RECOVERY] = ECONNREFUSED, [NO_DATA] = ENODATA, }; void _nsError(int err, const char *domain) { if (isatty(2)) fprintf(stderr, "NS:%s: %s.\n", domain, hstrerror(err)); else syslog(LOG_INFO, "%s: %s", domain, hstrerror(err)); if (err >= sizeof(ns_errno_map) / sizeof(ns_errno_map[0])) err = ECONNREFUSED; else err = ns_errno_map[err]; info("Reject the key with error %d", err); } void nsError(int err, const char *domain) { unsigned timeout; int ret; _nsError(err, domain); switch (err) { case TRY_AGAIN: timeout = 1; break; case 0: case NO_RECOVERY: timeout = 10; break; default: timeout = 1 * 60; break; } if (!debug_mode) { ret = keyctl_reject(key, timeout, err, KEY_REQKEY_DEFL_DEFAULT); if (ret == -1) error("%s: keyctl_reject: %m", __func__); } exit(0); } /* * Print debugging information */ void debug(const char *fmt, ...) { va_list va; if (verbose < 2) return; va_start(va, fmt); if (isatty(1)) { fputs("D: ", stdout); vfprintf(stdout, fmt, va); fputc('\n', stdout); } else { vsyslog(LOG_DEBUG, fmt, va); } va_end(va); } /* * Append an address to the payload segment list */ void append_address_to_payload(const char *addr) { size_t sz = strlen(addr); char *copy; int loop; debug("append '%s'", addr); if (payload_index + 2 > N_PAYLOAD - 1) return; /* discard duplicates */ for (loop = 0; loop < payload_index; loop++) if (payload[loop].iov_len == sz && memcmp(payload[loop].iov_base, addr, sz) == 0) return; copy = malloc(sz); if (!copy) error("%s: malloc: %m", __func__); memcpy(copy, addr, sz); if (payload_index != 0) { payload[payload_index ].iov_base = ","; payload[payload_index++].iov_len = 1; } payload[payload_index ].iov_base = copy; payload[payload_index++].iov_len = sz; } /* * Dump the payload when debugging */ void dump_payload(void) { size_t plen, n; char *buf, *p; int loop; if (debug_mode) verbose = 1; if (verbose < 1) return; plen = 0; for (loop = 0; loop < payload_index; loop++) { n = payload[loop].iov_len; debug("seg[%d]: %zu", loop, n); plen += n; } if (plen == 0) { info("The key instantiation data is empty"); return; } debug("total: %zu", plen); buf = malloc(plen + 1); if (!buf) return; p = buf; for (loop = 0; loop < payload_index; loop++) { n = payload[loop].iov_len; memcpy(p, payload[loop].iov_base, n); p += n; } info("The key instantiation data is '%s'", buf); info("The expiry time is %us", key_expiry); free(buf); } /* * Perform address resolution on a hostname and add the resulting address as a * string to the list of payload segments. */ int dns_resolver(const char *server_name, const char *port) { struct addrinfo hints, *addr, *ai; char buf[INET6_ADDRSTRLEN + 8 + 1]; int ret, len; void *sa; debug("Resolve '%s' with %x", server_name, mask); memset(&hints, 0, sizeof(hints)); switch (mask & INET_ALL) { case INET_IP4_ONLY: hints.ai_family = AF_INET; debug("IPv4"); break; case INET_IP6_ONLY: hints.ai_family = AF_INET6; debug("IPv6"); break; default: break; } /* resolve name to ip */ ret = getaddrinfo(server_name, NULL, &hints, &addr); if (ret) { info("unable to resolve hostname: %s [%s]", server_name, gai_strerror(ret)); return -1; } for (ai = addr; ai; ai = ai->ai_next) { debug("RR: %x,%x,%x,%x,%x,%s", ai->ai_flags, ai->ai_family, ai->ai_socktype, ai->ai_protocol, ai->ai_addrlen, ai->ai_canonname); /* convert address to string */ switch (ai->ai_family) { case AF_INET: if (!(mask & INET_IP4_ONLY)) continue; sa = &(((struct sockaddr_in *)ai->ai_addr)->sin_addr); len = INET_ADDRSTRLEN; break; case AF_INET6: if (!(mask & INET_IP6_ONLY)) continue; sa = &(((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr); len = INET6_ADDRSTRLEN; break; default: debug("Address of unknown family %u", addr->ai_family); continue; } if (!inet_ntop(ai->ai_family, sa, buf, len)) error("%s: inet_ntop: %m", __func__); if (port) strcat(buf, port); append_address_to_payload(buf); if (mask & ONE_ADDR_ONLY) break; } freeaddrinfo(addr); return 0; } /* * Look up a A and/or AAAA records to get host addresses * * The callout_info is parsed for request options. For instance, "ipv4" to * request only IPv4 addresses, "ipv6" to request only IPv6 addresses and * "list" to get multiple addresses. */ static __attribute__((noreturn)) int dns_query_a_or_aaaa(const char *hostname, char *options) { int ret; debug("Get A/AAAA RR for hostname:'%s', options:'%s'", hostname, options); if (!options[0]) { /* legacy mode */ mask = INET_IP4_ONLY | ONE_ADDR_ONLY; } else { char *k, *val; mask = INET_ALL | ONE_ADDR_ONLY; do { k = options; options = strchr(options, ' '); if (!options) options = k + strlen(k); else *options++ = '\0'; if (!*k) continue; if (strchr(k, ',')) error("Option name '%s' contains a comma", k); val = strchr(k, '='); if (val) *val++ = '\0'; debug("Opt %s", k); if (strcmp(k, "ipv4") == 0) { mask &= ~INET_ALL; mask |= INET_IP4_ONLY; } else if (strcmp(k, "ipv6") == 0) { mask &= ~INET_ALL; mask |= INET_IP6_ONLY; } else if (strcmp(k, "list") == 0) { mask &= ~ONE_ADDR_ONLY; } } while (*options); } /* Turn the hostname into IP addresses */ ret = dns_resolver(hostname, NULL); if (ret) nsError(NO_DATA, hostname); /* handle a lack of results */ if (payload_index == 0) nsError(NO_DATA, hostname); /* must include a NUL char at the end of the payload */ payload[payload_index].iov_base = ""; payload[payload_index++].iov_len = 1; dump_payload(); /* load the key with data key */ if (!debug_mode) { ret = keyctl_set_timeout(key, key_expiry); if (ret == -1) error("%s: keyctl_set_timeout: %m", __func__); ret = keyctl_instantiate_iov(key, payload, payload_index, 0); if (ret == -1) error("%s: keyctl_instantiate: %m", __func__); } exit(0); } /* * Read the config file. */ static void read_config(void) { FILE *f; char buf[4096], *b, *p, *k, *v; unsigned int line = 0, u; int n; info("READ CONFIG %s", config_file); f = fopen(config_file, "r"); if (!f) { if (errno == ENOENT && !config_specified) { debug("%s: %m", config_file); return; } error("%s: %m", config_file); } while (fgets(buf, sizeof(buf) - 1, f)) { line++; /* Trim off leading and trailing spaces and discard whole-line * comments. */ b = buf; while (isspace(*b)) b++; if (!*b || *b == '#') continue; p = strchr(b, '\n'); if (!p) error("%s:%u: line missing newline or too long", config_file, line); while (p > buf && isspace(p[-1])) p--; *p = 0; /* Split into key[=value] pairs and trim spaces. */ k = b; v = NULL; b = strchr(b, '='); if (b) { char quote = 0; bool esc = false; if (b == k) error("%s:%u: Unspecified key", config_file, line); /* NUL-terminate the key. */ for (p = b - 1; isspace(*p); p--) ; p[1] = 0; /* Strip leading spaces */ b++; while (isspace(*b)) b++; if (!*b) goto missing_value; if (*b == '"' || *b == '\'') { quote = *b; b++; } v = p = b; while (*b) { if (esc) { switch (*b) { case ' ': case '\t': case '"': case '\'': case '\\': break; default: goto invalid_escape_char; } esc = false; *p++ = *b++; continue; } if (*b == '\\') { esc = true; b++; continue; } if (*b == quote) { b++; if (*b) goto post_quote_data; quote = 0; break; } if (!quote && *b == '#') break; /* Terminal comment */ *p++ = *b++; } if (esc) error("%s:%u: Incomplete escape", config_file, line); if (quote) error("%s:%u: Unclosed quotes", config_file, line); *p = 0; } if (strcmp(k, "default_ttl") == 0) { if (!v) goto missing_value; if (sscanf(v, "%u%n", &u, &n) != 1) goto bad_value; if (v[n]) goto extra_data; if (u < 1 || u > INT_MAX) goto out_of_range; key_expiry = u; } else { warning("%s:%u: Unknown option '%s'", config_file, line, k); } } if (ferror(f) || fclose(f) == EOF) error("%s: %m", config_file); return; missing_value: error("%s:%u: %s: Missing value", config_file, line, k); invalid_escape_char: error("%s:%u: %s: Invalid char in escape", config_file, line, k); post_quote_data: error("%s:%u: %s: Data after closing quote", config_file, line, k); bad_value: error("%s:%u: %s: Bad value", config_file, line, k); extra_data: error("%s:%u: %s: Extra data supplied", config_file, line, k); out_of_range: error("%s:%u: %s: Value out of range", config_file, line, k); } /* * Dump the configuration after parsing the config file. */ static __attribute__((noreturn)) void config_dumper(void) { printf("default_ttl = %u\n", key_expiry); exit(0); } /* * Print usage details, */ static __attribute__((noreturn)) void usage(void) { if (isatty(2)) { fprintf(stderr, "Usage: %s [-vv] [-c config] key_serial\n", prog); fprintf(stderr, "Usage: %s -D [-vv] [-c config] \n", prog); } else { info("Usage: %s [-vv] [-c config] key_serial", prog); } exit(2); } static const struct option long_options[] = { { "config", 0, NULL, 'c' }, { "debug", 0, NULL, 'D' }, { "dump-config", 0, NULL, 2 }, { "verbose", 0, NULL, 'v' }, { "version", 0, NULL, 'V' }, { NULL, 0, NULL, 0 } }; /* * */ int main(int argc, char *argv[]) { int ktlen, qtlen, ret; char *keyend, *p; char *callout_info = NULL; char *buf = NULL, *name; bool dump_config = false; openlog(prog, 0, LOG_DAEMON); while ((ret = getopt_long(argc, argv, "c:vDV", long_options, NULL)) != -1) { switch (ret) { case 'c': config_file = optarg; config_specified = true; continue; case 2: dump_config = true; continue; case 'D': debug_mode = 1; continue; case 'V': printf("version: %s from %s (%s)\n", DNS_PARSE_VERSION, keyutils_version_string, keyutils_build_string); exit(0); case 'v': verbose++; continue; default: if (!isatty(2)) syslog(LOG_ERR, "unknown option: %c", ret); usage(); } } argc -= optind; argv += optind; read_config(); if (dump_config) config_dumper(); if (!debug_mode) { if (argc != 1) usage(); /* get the key ID */ if (!**argv) error("Invalid blank key ID"); key = strtol(*argv, &p, 10); if (*p) error("Invalid key ID format"); /* get the key description (of the form "x;x;x;x;:") */ ret = keyctl_describe_alloc(key, &buf); if (ret == -1) error("keyctl_describe_alloc failed: %m"); /* get the callout_info (which can supply options) */ ret = keyctl_read_alloc(KEY_SPEC_REQKEY_AUTH_KEY, (void **)&callout_info); if (ret == -1) error("Invalid key callout_info read: %m"); } else { if (argc != 2) usage(); ret = asprintf(&buf, "%s;-1;-1;0;%s", key_type, argv[0]); if (ret < 0) error("Error %m"); callout_info = argv[1]; } ret = 1; info("Key description: '%s'", buf); info("Callout info: '%s'", callout_info); p = strchr(buf, ';'); if (!p) error("Badly formatted key description '%s'", buf); ktlen = p - buf; /* make sure it's the type we are expecting */ if (ktlen != sizeof(key_type) - 1 || memcmp(buf, key_type, ktlen) != 0) error("Key type is not supported: '%*.*s'", ktlen, ktlen, buf); keyend = buf + ktlen + 1; /* the actual key description follows the last semicolon */ keyend = rindex(keyend, ';'); if (!keyend) error("Invalid key description: %s", buf); keyend++; name = index(keyend, ':'); if (!name) dns_query_a_or_aaaa(keyend, callout_info); qtlen = name - keyend; name++; info("Query type: '%*.*s'", qtlen, qtlen, keyend); if ((qtlen == sizeof(a_query_type) - 1 && memcmp(keyend, a_query_type, sizeof(a_query_type) - 1) == 0) || (qtlen == sizeof(aaaa_query_type) - 1 && memcmp(keyend, aaaa_query_type, sizeof(aaaa_query_type) - 1) == 0) ) { info("Do DNS query of A/AAAA type for:'%s' mask:'%s'", name, callout_info); dns_query_a_or_aaaa(name, callout_info); } if (qtlen == sizeof(afsdb_query_type) - 1 && memcmp(keyend, afsdb_query_type, sizeof(afsdb_query_type) - 1) == 0 ) { info("Do AFS VL server query for:'%s' mask:'%s'", name, callout_info); afs_look_up_VL_servers(name, callout_info); } error("Query type: \"%*.*s\" is not supported", qtlen, qtlen, keyend); }