blob: 7a7ec4241ea0eb7d04de1179111fac2adaf03063 [file] [log] [blame]
/*
* 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] <desc> <calloutinfo>\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;<query_type>:<name>") */
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);
}