getdns/src/tools/getdns_server_mon.c

1990 lines
73 KiB
C

/*
* Copyright (c) 2018, NLNet Labs, Sinodun
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the names of the copyright holders nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL Verisign, Inc. BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include <ctype.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef USE_GNUTLS
#include <gnutls/x509.h>
#else
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/bio.h>
#endif
#include <getdns/getdns.h>
#include <getdns/getdns_extra.h>
#define APP_NAME "getdns_server_mon"
#define RTT_CRITICAL_MS 500
#define RTT_WARNING_MS 250
#define CERT_EXPIRY_CRITICAL_DAYS 7
#define CERT_EXPIRY_WARNING_DAYS 14
#define DEFAULT_LOOKUP_NAME "getdnsapi.net"
#define DEFAULT_LOOKUP_TYPE GETDNS_RRTYPE_AAAA
#define EDNS0_PADDING_CODE 12
static const char TLS13_CIPHER_SUITE[] =
"TLS13-AES-256-GCM-SHA384:"
"TLS13-CHACHA20-POLY1305-SHA256:"
"TLS13-AES-128-GCM-SHA256:"
"TLS13-AES-128-CCM-8-SHA256:"
"TLS13-AES-128-CCM-SHA256";
#define EXAMPLE_PIN "pin-sha256=\"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=\""
/* Plugin exit values */
typedef enum {
EXIT_OK = 0,
EXIT_WARNING,
EXIT_CRITICAL,
EXIT_UNKNOWN,
EXIT_USAGE /* Special case - internal only. */
} exit_value;
/* Plugin verbosity values */
typedef enum {
VERBOSITY_MINIMAL = 0,
VERBOSITY_ADDITIONAL,
VERBOSITY_CONFIG,
VERBOSITY_DEBUG
} plugin_verbosity;
#define MAX_BASE_OUTPUT_LEN 256
#define MAX_PERF_OUTPUT_LEN 256
static struct test_info_s
{
getdns_context *context;
/* Output */
bool monitoring;
FILE *errout;
plugin_verbosity verbosity;
bool debug_output;
char base_output[MAX_BASE_OUTPUT_LEN + 1];
char perf_output[MAX_PERF_OUTPUT_LEN + 1];
/* Test config info */
bool fail_on_dns_errors;
} test_info;
static void snprintcat(char *buf, size_t buflen, const char *format, ...)
{
va_list ap;
int l = strlen(buf);
buf += l;
buflen -= l;
va_start(ap, format);
vsnprintf(buf, buflen, format, ap);
va_end(ap);
buf[buflen] = '\0';
}
static int get_rrtype(const char *t)
{
char buf[128] = "GETDNS_RRTYPE_";
uint32_t rrtype;
long int l;
size_t i;
char *endptr;
if (strlen(t) > sizeof(buf) - 15)
return -1;
for (i = 14; *t && i < sizeof(buf) - 1; i++, t++)
buf[i] = *t == '-' ? '_' : toupper((unsigned char)*t);
buf[i] = '\0';
if (!getdns_str2int(buf, &rrtype))
return (int)rrtype;
if (strncasecmp(buf + 14, "TYPE", 4) == 0) {
l = strtol(buf + 18, &endptr, 10);
if (!*endptr && l >= 0 && l < 65536)
return l;
}
return -1;
}
static const char *getdns_intval_text(int val, const char *name, const char *prefix)
{
getdns_dict *d = getdns_dict_create();
char buf[128];
static char res[20];
if (getdns_dict_set_int(d, name, val) != GETDNS_RETURN_GOOD)
goto err;
getdns_pretty_snprint_dict(buf, sizeof(buf), d);
const char *p = strstr(buf, prefix);
if (!p)
goto err;
p += strlen(prefix);
char *q = res;
while (*p && *p != '\n')
*q++ = *p++;
getdns_dict_destroy(d);
return res;
err:
getdns_dict_destroy(d);
snprintf(res, sizeof(res), "%d", val);
return res;
}
static const char *rrtype_text(int rrtype)
{
return getdns_intval_text(rrtype, "type", "GETDNS_RRTYPE_");
}
static const char *rcode_text(int rcode)
{
return getdns_intval_text(rcode, "rcode", "GETDNS_RCODE_");
}
#if !defined(USE_GNUTLS) && (OPENSSL_VERSION_NUMBER < 0x10002000 || defined(LIBRESSL_VERSION_NUMBER))
/*
* Convert date to Julian day.
* See https://en.wikipedia.org/wiki/Julian_day
*/
static long julian_day(const struct tm *tm)
{
long dd, mm, yyyy;
dd = tm->tm_mday;
mm = tm->tm_mon + 1;
yyyy = tm->tm_year + 1900;
return (1461 * (yyyy + 4800 + (mm - 14) / 12)) / 4 +
(367 * (mm - 2 - 12 * ((mm - 14) / 12))) / 12 -
(3 * ((yyyy + 4900 + (mm - 14) / 12) / 100)) / 4 +
dd - 32075;
}
static long secs_in_day(const struct tm *tm)
{
return ((tm->tm_hour * 60) + tm->tm_min) * 60 + tm->tm_sec;
}
#endif
/*
* Thanks to:
* https://zakird.com/2013/10/13/certificate-parsing-with-openssl
*/
static bool extract_cert_expiry(const unsigned char *data, size_t len, time_t *t)
{
#ifdef USE_GNUTLS
gnutls_x509_crt_t cert;
gnutls_datum_t datum;
bool res = false;
datum.data = (unsigned char*) data;
datum.size = len;
if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS)
return false;
if (gnutls_x509_crt_import(cert, &datum, GNUTLS_X509_FMT_DER) == GNUTLS_E_SUCCESS) {
time_t expiry = gnutls_x509_crt_get_expiration_time(cert);
if (expiry != GNUTLS_X509_NO_WELL_DEFINED_EXPIRATION) {
res = true;
*t = expiry;
}
}
gnutls_x509_crt_deinit(cert);
return res;
#else
X509 *cert = d2i_X509(NULL, &data, len);
if (!cert)
return false;
int day_diff, sec_diff;
const long SECS_IN_DAY = 60 * 60 * 24;
#if defined(X509_get_notAfter) || defined(HAVE_X509_GET_NOTAFTER)
const ASN1_TIME *not_after = X509_get_notAfter(cert);
#elif defined(X509_get0_notAfter) || defined(HAVE_X509_GET0_NOTAFTER)
const ASN1_TIME *not_after = X509_get0_notAfter(cert);
#endif
*t = time(NULL);
#if OPENSSL_VERSION_NUMBER < 0x10002000 || defined(LIBRESSL_VERSION_NUMBER)
/*
* OpenSSL before 1.0.2 does not support ASN1_TIME_diff().
* So work around by using ASN1_TIME_print() to print to a buffer
* and parsing that. This does not do any kind of sane format,
* but 'Mar 15 11:58:50 2018 GMT'. Note the month name is not
* locale-dependent but always English, so strptime() to parse
* isn't going to work. It also *appears* to always end 'GMT'.
* Ideally one could then convert this UTC time to a time_t, but
* there's no way to do that in standard C/POSIX. So follow the
* lead of OpenSSL, convert to Julian days and use the difference.
*/
char buf[40];
BIO *b = BIO_new(BIO_s_mem());
if (ASN1_TIME_print(b, not_after) <= 0) {
BIO_free(b);
X509_free(cert);
return false;
}
if (BIO_gets(b, buf, sizeof(buf)) <= 0) {
BIO_free(b);
X509_free(cert);
return false;
}
BIO_free(b);
X509_free(cert);
struct tm tm;
char month[4];
char tz[4];
memset(&tm, 0, sizeof(tm));
if (sscanf(buf,
"%3s %d %d:%d:%d %d %3s",
month,
&tm.tm_mday,
&tm.tm_hour,
&tm.tm_min,
&tm.tm_sec,
&tm.tm_year,
tz) != 7)
return false;
tm.tm_year -= 1900;
if (strcmp(tz, "GMT") != 0)
return false;
const char *mon[] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
while(tm.tm_mon < 12 && strcmp(mon[tm.tm_mon], month) != 0)
++tm.tm_mon;
if (tm.tm_mon > 11)
return false;
struct tm tm_now;
gmtime_r(t, &tm_now);
day_diff = julian_day(&tm) - julian_day(&tm_now);
sec_diff = secs_in_day(&tm) - secs_in_day(&tm_now);
if (sec_diff < 0) {
sec_diff += SECS_IN_DAY;
--day_diff;
}
#else
/*
* Use ASN1_TIME_diff to get a time delta between now and expiry.
* This is much easier than trying to parse the time.
*/
ASN1_TIME_diff(&day_diff, &sec_diff, NULL, not_after);
X509_free(cert);
#endif
*t += day_diff * SECS_IN_DAY + sec_diff;
#endif /* USE_GNUTLS */
return true;
}
static void exit_tidy()
{
if (test_info.context)
getdns_context_destroy(test_info.context);
}
static void usage()
{
fputs(
"Usage: " APP_NAME " [-M] [-E] [(-u|-t|-T)] [-S] [-K <spki-pin>]\n"
" [-v [-v [-v]]] [-V] @upstream testname [<test args>]\n"
" -M|--monitoring Make output suitable for monitoring tools\n"
" -E|--fail-on-dns-errors Fail on DNS error (NXDOMAIN, SERVFAIL)\n"
" -u|--udp Use UDP transport\n"
" -t|--tcp Use TCP transport\n"
" -T|--tls Use TLS transport\n"
" -S|--strict-usage-profile Use strict profile (require authentication)\n"
" -K|--spki-pin <spki-pin> SPKI pin for TLS connections (can repeat)\n"
" -v|--verbose Increase output verbosity\n"
" -D|--debug Enable debugging output\n"
" -V|--version Report GetDNS version\n"
"\n"
"spki-pin: Should look like '" EXAMPLE_PIN "'\n"
"\n"
"upstream: @<ip>[%<scope_id][@<port>][#<tls_port>][~tls name>][^<tsig spec>]\n"
" <ip>@<port> may be given as <IPv4>:<port> or\n"
" '['<IPv6>[%<scope_id>]']':<port>\n"
"\n"
"tsig spec: [<algorithm>:]<name>:<secret in Base64>\n"
"\n"
"Tests:\n"
" lookup [<name> [<type>]] Check lookup on server\n"
" keepalive [<name> [<type>]] Check server support for EDNS0 keepalive in\n"
" TCP or TLS connections\n"
" Timeout of 0 is off.\n"
" OOOR Check whether server delivers responses out of\n"
" query order on a TCP or TLS connection\n"
" qname-min Check whether server supports QNAME minimisation\n"
" rtt [warn-ms,crit-ms] [<name> [<type>]]\n"
" Check if server round trip time exceeds\n"
" thresholds (default 250,500)\n"
"\n"
" dnssec-validate Check whether server does DNSSEC validation\n"
"\n"
" tls-auth [<name> [<type>]] Check authentication of TLS server\n"
" If both a SPKI pin and authentication name are\n"
" provided, both must authenticate for this test\n"
" to pass.\n"
" tls-cert-valid [warn-days,crit-days] [<name> [type]]\n"
" Check server certificate validity, report\n"
" warning or critical if days to expiry at\n"
" or below thresholds (default 14,7).\n"
" tls-padding <blocksize> [<name> [<type>]]\n"
" Check server support for EDNS0 padding in TLS\n"
" Special blocksize values are 0 = off,\n"
" 1 = sensible default.\n"
" tls-1.3 Check whether server supports TLS 1.3\n"
"\n"
"Enabling monitoring mode ensures output messages and exit statuses conform\n"
"to the requirements of monitoring plugins (www.monitoring-plugins.org).\n",
test_info.errout);
exit(EXIT_UNKNOWN);
}
static void version()
{
fprintf(test_info.errout,
APP_NAME ": getdns version %s, API version '%s'.\n",
getdns_get_version(),
getdns_get_api_version());
exit(EXIT_UNKNOWN);
}
/**
** Functions used by tests.
**/
static void get_thresholds(char ***av,
int *critical,
int *warning)
{
if (**av) {
char *comma = strchr(**av, ',');
if (!comma)
return;
char *end;
long w,c;
w = strtol(**av, &end, 10);
/*
* If the number doesn't end at a comma, this isn't a
* properly formatted thresholds arg. Pass over it.
*/
if (end != comma)
return;
/*
* Similarly, if the number doesn't end at the end of the
* argument, this isn't a properly formatted arg.
*/
c = strtol(comma + 1, &end, 10);
if (*end != '\0')
return;
/* Got two numbers, so consume the argument. */
*critical = (int) c;
*warning = (int) w;
++*av;
return;
}
return;
}
static exit_value get_name_type_args(struct test_info_s *test_info,
char ***av,
const char **lookup_name,
uint32_t *lookup_type)
{
if (**av) {
if (strlen(**av) > 0) {
*lookup_name = **av;
} else {
strcpy(test_info->base_output, "Empty name not valid");
return EXIT_UNKNOWN;
}
++*av;
if (**av) {
int rrtype = get_rrtype(**av);
if (rrtype >= 0) {
*lookup_type = (uint32_t) rrtype;
++*av;
}
}
}
return EXIT_OK;
}
static exit_value search(struct test_info_s *test_info,
const char *name,
uint16_t type,
getdns_dict *extensions,
getdns_dict **response,
getdns_return_t *getdns_return)
{
getdns_return_t ret;
getdns_dict *search_extensions =
(extensions) ? extensions : getdns_dict_create();
/* We always turn on the return_call_reporting extension. */
if ((ret = getdns_dict_set_int(search_extensions, "return_call_reporting", GETDNS_EXTENSION_TRUE)) != GETDNS_RETURN_GOOD) {
if (getdns_return)
*getdns_return = ret;
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot set return call reporting: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
if (!extensions)
getdns_dict_destroy(search_extensions);
return EXIT_UNKNOWN;
}
if (test_info->verbosity >= VERBOSITY_ADDITIONAL &&
!test_info->monitoring) {
printf("DNS Lookup name:\t%s\n", name);
printf("DNS Lookup RR type:\t%s\n", rrtype_text(type));
}
if (test_info->debug_output) {
printf("Context: %s\n",
getdns_pretty_print_dict(getdns_context_get_api_information(test_info->context)));
}
ret = getdns_general_sync(test_info->context,
name,
type,
search_extensions,
response);
if (!extensions)
getdns_dict_destroy(search_extensions);
if (ret != GETDNS_RETURN_GOOD) {
if (getdns_return) {
*getdns_return = ret;
} else {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Error resolving '%s': %s (%d)",
name,
getdns_get_errorstr_by_id(ret),
ret);
}
return EXIT_CRITICAL;
}
if (test_info->debug_output) {
printf("Response: %s\n",
getdns_pretty_print_dict(*response));
}
if (getdns_return)
*getdns_return = ret;
return EXIT_OK;
}
static exit_value get_result(struct test_info_s *test_info,
const getdns_dict *response,
uint32_t *error_id,
uint32_t *rcode)
{
getdns_return_t ret;
if ((ret = getdns_dict_get_int(response, "status", error_id)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get result status: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if (*error_id != GETDNS_RESPSTATUS_GOOD && *error_id != GETDNS_RESPSTATUS_NO_NAME) {
*rcode = 0;
return EXIT_OK;
}
if ((ret = getdns_dict_get_int(response, "/replies_tree/0/header/rcode", rcode)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get DNS return code: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
return EXIT_OK;
}
static exit_value check_result(struct test_info_s *test_info,
const getdns_dict *response)
{
exit_value xit;
uint32_t error_id, rcode;
if ((xit = get_result(test_info, response, &error_id, &rcode)) != EXIT_OK)
return xit;
switch(error_id) {
case GETDNS_RESPSTATUS_ALL_TIMEOUT:
strcpy(test_info->base_output, "Search timed out");
return EXIT_CRITICAL;
case GETDNS_RESPSTATUS_NO_SECURE_ANSWERS:
strcpy(test_info->base_output, "No secure answers");
return EXIT_CRITICAL;
case GETDNS_RESPSTATUS_ALL_BOGUS_ANSWERS:
strcpy(test_info->base_output, "All answers are bogus");
return EXIT_CRITICAL;
default:
break;
}
if (test_info->verbosity >= VERBOSITY_ADDITIONAL){
if (test_info->monitoring) {
snprintcat(test_info->perf_output,
MAX_PERF_OUTPUT_LEN,
"getdns=%d;",
error_id);
} else {
printf("getdns result:\t\t%s (%d)\n",
getdns_get_errorstr_by_id(error_id),
error_id);
}
}
if (test_info->fail_on_dns_errors && rcode > 0) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"DNS error %s (%d)",
rcode_text(rcode),
rcode);
return EXIT_CRITICAL;
}
return EXIT_OK;
}
static exit_value get_report_info(struct test_info_s *test_info,
const getdns_dict *response,
uint32_t *rtt,
getdns_bindata **auth_status,
time_t *cert_expire_time)
{
getdns_return_t ret;
getdns_list *l;
uint32_t rtt_val;
getdns_bindata *auth_status_val = NULL;
time_t cert_expire_time_val = 0;
if ((ret = getdns_dict_get_list(response, "call_reporting", &l)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get call report: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
getdns_dict *d;
if ((ret = getdns_list_get_dict(l, 0, &d)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get call report first item: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if (test_info->verbosity >= VERBOSITY_ADDITIONAL) {
uint32_t transport;
const char *transport_text = "???";
if ((ret = getdns_dict_get_int(d, "transport", &transport)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get transport: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
switch(transport) {
case GETDNS_TRANSPORT_UDP:
transport_text = "UDP";
break;
case GETDNS_TRANSPORT_TCP:
transport_text = "TCP";
break;
case GETDNS_TRANSPORT_TLS:
transport_text = "TLS";
break;
}
if (test_info->monitoring) {
snprintcat(test_info->perf_output,
MAX_PERF_OUTPUT_LEN,
"transport=%s;",
transport_text);
} else {
printf("Transport:\t\t%s\n", transport_text);
}
}
if ((ret = getdns_dict_get_int(d, "run_time/ms", &rtt_val)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get RTT: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if (rtt)
*rtt = rtt_val;
if (test_info->verbosity >= VERBOSITY_ADDITIONAL) {
if (test_info->monitoring) {
snprintcat(test_info->perf_output,
MAX_PERF_OUTPUT_LEN,
"rtt=%dms;",
rtt_val);
} else {
printf("RTT:\t\t\t%dms\n", rtt_val);
}
}
if (getdns_dict_get_bindata(d, "tls_auth_status", &auth_status_val) == GETDNS_RETURN_GOOD) {
const char *auth_status_text = (char *) auth_status_val->data;
/* Just in case - not sure this is necessary */
auth_status_val->data[auth_status_val->size] = '\0';
if (test_info->verbosity >= VERBOSITY_ADDITIONAL) {
if (test_info->monitoring) {
snprintcat(test_info->perf_output,
MAX_PERF_OUTPUT_LEN,
"auth=%s;",
auth_status_text);
} else {
printf("Authentication:\t\t%s\n", auth_status_text);
}
}
}
if (auth_status)
*auth_status = auth_status_val;
getdns_bindata *cert;
if (getdns_dict_get_bindata(d, "tls_peer_cert", &cert) == GETDNS_RETURN_GOOD) {
if (!extract_cert_expiry(cert->data, cert->size, &cert_expire_time_val)) {
strcpy(test_info->base_output, "Cannot parse PKIX certificate");
return EXIT_UNKNOWN;
}
if (test_info->verbosity >= VERBOSITY_ADDITIONAL) {
struct tm *tm = gmtime(&cert_expire_time_val);
char buf[25];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm);
if (test_info->monitoring) {
snprintcat(test_info->perf_output,
MAX_PERF_OUTPUT_LEN,
"expire=%s;",
buf);
} else {
printf("Certificate expires:\t%s UTC\n", buf);
}
}
}
if (cert_expire_time)
*cert_expire_time = cert_expire_time_val;
getdns_bindata *tls_version;
if (getdns_dict_get_bindata(d, "tls_version", &tls_version) == GETDNS_RETURN_GOOD) {
const char *version_text = (char *) tls_version->data;
if (test_info->verbosity >= VERBOSITY_ADDITIONAL) {
if (test_info->monitoring) {
snprintcat(test_info->perf_output,
MAX_PERF_OUTPUT_LEN,
"tls=%s;",
version_text);
} else {
printf("TLS version:\t\t%s\n", version_text);
}
}
/*
* This is always in the context, so show only if we got
* TLS info in the call reporting.
*/
uint32_t tls_auth;
getdns_dict *context_dict = getdns_context_get_api_information(test_info->context);
if (getdns_dict_get_int(context_dict, "/all_context/tls_authentication", &tls_auth) == GETDNS_RETURN_GOOD) {
const char *auth_text = "???";
switch(tls_auth) {
case GETDNS_AUTHENTICATION_NONE:
auth_text = "Opportunistic";
break;
case GETDNS_AUTHENTICATION_REQUIRED:
auth_text = "Strict";
break;
}
if (test_info->verbosity >= VERBOSITY_ADDITIONAL) {
if (test_info->monitoring) {
snprintcat(test_info->perf_output,
MAX_PERF_OUTPUT_LEN,
"%s;",
auth_text);
} else {
printf("TLS authentication:\t%s\n", auth_text);
}
}
}
}
return EXIT_OK;
}
static exit_value get_answers(struct test_info_s *test_info,
const getdns_dict *response,
const char *section,
getdns_list **answers,
size_t *no_answers)
{
getdns_return_t ret;
char buf[40];
snprintf(buf, sizeof(buf), "/replies_tree/0/%s", section);
if ((ret = getdns_dict_get_list(response, buf, answers)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get section '%s': %s (%d)",
section,
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if ((ret = getdns_list_get_length(*answers, no_answers)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get number of items in '%s': %s (%d)",
section,
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if (*no_answers <= 0) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Zero entries in '%s'",
section);
return EXIT_WARNING;
}
return EXIT_OK;
}
static exit_value check_answer_type(struct test_info_s *test_info,
const getdns_dict *response,
uint32_t rrtype)
{
getdns_list *answers;
size_t no_answers;
exit_value xit;
if ((xit = get_answers(test_info, response, "answer", &answers, &no_answers)) != EXIT_OK)
return xit;
for (size_t i = 0; i < no_answers; ++i) {
getdns_dict *answer;
getdns_return_t ret;
if ((ret = getdns_list_get_dict(answers, i, &answer)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get answer number %zu: %s (%d)",
i,
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
uint32_t rtype;
if ((ret = getdns_dict_get_int(answer, "type", &rtype)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get answer type: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if (rtype == rrtype)
return EXIT_OK;
}
strcpy(test_info->base_output, "Answer does not contain expected type");
return EXIT_UNKNOWN;
}
static exit_value search_check(struct test_info_s *test_info,
const char *lookup_name,
uint16_t lookup_type,
getdns_dict **response,
uint32_t *rtt,
getdns_bindata **auth_status,
time_t *cert_expire_time)
{
exit_value xit;
getdns_dict *resp;
if ((xit = search(test_info, lookup_name, lookup_type, NULL, &resp, NULL)) != EXIT_OK)
return xit;
if ((xit = check_result(test_info, resp)) != EXIT_OK)
return xit;
if ((xit = get_report_info(test_info, resp, rtt, auth_status, cert_expire_time)) != EXIT_OK)
return xit;
if ((xit = check_answer_type(test_info, resp, lookup_type)) != EXIT_OK)
return xit;
if (response)
*response = resp;
return xit;
}
static exit_value parse_search_check(struct test_info_s *test_info,
char **av,
const char *usage,
getdns_dict **response,
uint32_t *rtt,
getdns_bindata **auth_status,
time_t *cert_expire_time)
{
const char *lookup_name = DEFAULT_LOOKUP_NAME;
uint32_t lookup_type = DEFAULT_LOOKUP_TYPE;
exit_value xit;
if ((xit = get_name_type_args(test_info, &av, &lookup_name, &lookup_type)) != EXIT_OK)
return xit;
if (*av) {
strcpy(test_info->base_output, usage);
return EXIT_USAGE;
}
return search_check(test_info,
lookup_name, lookup_type,
response,
rtt, auth_status, cert_expire_time);
}
/**
** Test routines.
**/
static exit_value test_lookup(struct test_info_s *test_info,
char ** av)
{
exit_value xit;
if ((xit = parse_search_check(test_info,
av,
"lookup takes arguments [<name> [<type>]]",
NULL,
NULL,
NULL,
NULL)) != EXIT_OK)
return xit;
strcpy(test_info->base_output, "Lookup succeeded");
return EXIT_OK;
}
static exit_value test_rtt(struct test_info_s *test_info,
char ** av)
{
exit_value xit;
int critical_ms = RTT_CRITICAL_MS;
int warning_ms = RTT_WARNING_MS;
uint32_t rtt_val;
get_thresholds(&av, &critical_ms, &warning_ms);
if ((xit = parse_search_check(test_info,
av,
"rtt takes arguments [warn-ms,crit-ms] [<name> [<type>]]",
NULL,
&rtt_val,
NULL,
NULL)) != EXIT_OK)
return xit;
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"RTT lookup succeeded in %dms",
rtt_val);
if ((int) rtt_val > critical_ms)
return EXIT_CRITICAL;
else if ((int) rtt_val > warning_ms)
return EXIT_WARNING;
return EXIT_OK;
}
static exit_value test_authenticate(struct test_info_s *test_info,
char ** av)
{
exit_value xit;
getdns_bindata *auth_status;
if ((xit = parse_search_check(test_info,
av,
"auth takes arguments [<name> [<type>]]",
NULL,
NULL,
&auth_status,
NULL)) != EXIT_OK)
return xit;
if (!auth_status || strcmp((char *) auth_status->data, "Success") != 0) {
strcpy(test_info->base_output, "Authentication failed");
return EXIT_CRITICAL;
} else {
strcpy(test_info->base_output, "Authentication succeeded");
return EXIT_OK;
}
}
static exit_value test_certificate_valid(struct test_info_s *test_info,
char **av)
{
exit_value xit;
int warning_days = CERT_EXPIRY_WARNING_DAYS;
int critical_days = CERT_EXPIRY_CRITICAL_DAYS;
time_t expire_time;
get_thresholds(&av, &critical_days, &warning_days);
if ((xit = parse_search_check(test_info,
av,
"cert-valid takes arguments [warn-days,crit-days] [<name> [<type>]]",
NULL,
NULL,
NULL,
&expire_time)) != EXIT_OK)
return xit;
if (expire_time == 0) {
strcpy(test_info->base_output, "No PKIX certificate");
return EXIT_CRITICAL;
}
time_t now = time(NULL);
int days_to_expiry = (expire_time - now) / 86400;
if (days_to_expiry < 0) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Certificate expired %d day%s ago",
-days_to_expiry,
(days_to_expiry < -1) ? "s" : "");
return EXIT_CRITICAL;
}
if (days_to_expiry == 0) {
strcpy(test_info->base_output, "Certificate expires today");
} else {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Certificate will expire in %d day%s",
days_to_expiry,
(days_to_expiry > 1) ? "s" : "");
}
if (days_to_expiry <= critical_days) {
return EXIT_CRITICAL;
}
if (days_to_expiry <= warning_days) {
return EXIT_WARNING;
}
return EXIT_OK;
}
static exit_value test_qname_minimisation(struct test_info_s *test_info,
char ** av)
{
if (*av) {
strcpy(test_info->base_output, "qname-min takes no arguments");
return EXIT_USAGE;
}
getdns_dict *response;
exit_value xit;
if ((xit = search_check(test_info,
"qnamemintest.internet.nl",
GETDNS_RRTYPE_TXT,
&response,
NULL,
NULL,
NULL)) != EXIT_OK)
return xit;
getdns_list *answers;
size_t no_answers;
if ((xit = get_answers(test_info, response, "answer", &answers, &no_answers)) != EXIT_OK)
return xit;
for (size_t i = 0; i < no_answers; ++i) {
getdns_dict *answer;
getdns_return_t ret;
if ((ret = getdns_list_get_dict(answers, i, &answer)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get answer number %zu: %s (%d)",
i,
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
uint32_t rtype;
if ((ret = getdns_dict_get_int(answer, "type", &rtype)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get answer type: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if (rtype != GETDNS_RRTYPE_TXT)
continue;
getdns_bindata *rtxt;
if ((ret = getdns_dict_get_bindata(answer, "/rdata/txt_strings/0", &rtxt)) != GETDNS_RETURN_GOOD) {
strcpy(test_info->base_output, "No answer text");
return EXIT_UNKNOWN;
}
if (rtxt->size > 0 ) {
switch(rtxt->data[0]) {
case 'H':
strcpy(test_info->base_output, "QNAME minimisation ON");
return EXIT_OK;
case 'N':
strcpy(test_info->base_output, "QNAME minimisation OFF");
return EXIT_CRITICAL;
default:
/* Unrecognised message. */
break;
}
}
}
strcpy(test_info->base_output, "No valid QNAME minimisation data");
return EXIT_UNKNOWN;
}
static exit_value test_padding(struct test_info_s *test_info,
char ** av)
{
getdns_dict *response;
exit_value xit;
long blocksize;
char *endptr;
const char USAGE[] = "padding takes arguments <blocksize> [<name> [<type>]]";
if (!*av || (blocksize = strtol(*av, &endptr, 10), *endptr != '\0' || blocksize < 0)) {
strcpy(test_info->base_output, USAGE);
return EXIT_USAGE;
}
++av;
getdns_return_t ret;
if ((ret = getdns_context_set_tls_query_padding_blocksize(test_info->context, (uint16_t) blocksize)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot set padding blocksize: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if ((xit = parse_search_check(test_info,
av,
USAGE,
&response,
NULL,
NULL,
NULL)) != EXIT_OK)
return xit;
getdns_list *answers;
size_t no_answers;
if ((xit = get_answers(test_info, response, "additional", &answers, &no_answers)) != EXIT_OK)
return xit;
for (size_t i = 0; i < no_answers; ++i) {
getdns_dict *answer;
getdns_return_t ret;
if ((ret = getdns_list_get_dict(answers, i, &answer)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get answer number %zu: %s (%d)",
i,
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
uint32_t rtype;
if ((ret = getdns_dict_get_int(answer, "type", &rtype)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get answer type: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if (rtype != GETDNS_RRTYPE_OPT)
continue;
getdns_list *options;
size_t no_options;
if ((ret = getdns_dict_get_list(answer, "/rdata/options", &options)) != GETDNS_RETURN_GOOD) {
goto no_padding;
}
if ((ret = getdns_list_get_length(options, &no_options)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get number of options: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
for (size_t j = 0; j < no_options; ++j) {
getdns_dict *option;
uint32_t code;
if ((ret = getdns_list_get_dict(options, j, &option)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get option number %zu: %s (%d)",
j,
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if ((ret = getdns_dict_get_int(option, "option_code", &code)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get option code: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if (code != EDNS0_PADDING_CODE)
continue;
/* Yes, we have padding! */
getdns_bindata *data;
if ((ret = getdns_dict_get_bindata(option, "option_data", &data)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get option code: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Padding found, length %zu",
data->size);
return EXIT_OK;
}
}
no_padding:
strcpy(test_info->base_output, "No padding found");
return EXIT_CRITICAL;
}
static exit_value test_keepalive(struct test_info_s *test_info,
char ** av)
{
getdns_dict *response;
exit_value xit;
const uint64_t MAX_TIMEOUT_VALUE = 0xffff * 10000;
getdns_return_t ret;
if ((ret = getdns_context_set_idle_timeout(test_info->context, MAX_TIMEOUT_VALUE)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot set keepalive timeout: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if ((xit = parse_search_check(test_info,
av,
"keepalive takes arguments [<name> [<type>]]",
&response,
NULL,
NULL,
NULL)) != EXIT_OK)
return xit;
getdns_list *l;
if ((ret = getdns_dict_get_list(response, "call_reporting", &l)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get call report: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
getdns_dict *d;
if ((ret = getdns_list_get_dict(l, 0, &d)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get call report first item: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
/* Search is forced to be TCP or TLS, so server keepalive flag must exist. */
uint32_t server_keepalive_received;
if ((ret = getdns_dict_get_int(d, "server_keepalive_received", &server_keepalive_received)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get server keepalive flag: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if (server_keepalive_received) {
uint32_t t;
bool overflow = false;
if (!((ret = getdns_dict_get_int(d, "idle timeout in ms", &t)) == GETDNS_RETURN_GOOD ||
(overflow = true, ret = getdns_dict_get_int(d, "idle timeout in ms (overflow)", &t)) == GETDNS_RETURN_GOOD)) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get idle timeout: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
if (overflow) {
strcpy(test_info->base_output, "Server sent keepalive, idle timeout now (overflow)");
} else {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Server sent keepalive, idle timeout now %ums",
t);
}
return EXIT_OK;
} else {
strcpy(test_info->base_output, "Server did not send keepalive");
return EXIT_CRITICAL;
}
}
static exit_value test_dnssec_validate(struct test_info_s *test_info,
char ** av)
{
if (*av) {
strcpy(test_info->base_output, "dnssec-validate takes no arguments");
return EXIT_USAGE;
}
getdns_dict *response;
exit_value xit;
if ((xit = search(test_info,
"dnssec-failed.org",
GETDNS_RRTYPE_A,
NULL,
&response,
NULL)) != EXIT_OK)
return xit;
uint32_t error_id, rcode;
if ((xit = get_result(test_info, response, &error_id, &rcode)) != EXIT_OK)
return xit;
if (error_id == GETDNS_RESPSTATUS_ALL_TIMEOUT) {
strcpy(test_info->base_output, "Search timed out");
return EXIT_CRITICAL;
}
if (rcode != GETDNS_RCODE_SERVFAIL) {
strcpy(test_info->base_output, "Server does NOT validate DNSSEC");
return EXIT_CRITICAL;
}
/*
* Rerun the query, but this time set the CD bit. The lookup should
* succeed.
*/
getdns_return_t ret;
getdns_dict *response2;
getdns_dict *extensions = getdns_dict_create();
if ((ret = getdns_dict_set_int(extensions, "/header/cd", 1)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot set CD bit: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
getdns_dict_destroy(extensions);
return EXIT_UNKNOWN;
}
if ((xit = search(test_info,
"dnssec-failed.org",
GETDNS_RRTYPE_A,
extensions,
&response2,
NULL)) != EXIT_OK)
return xit;
getdns_dict_destroy(extensions);
/*
* Only now get report info from the first search, so that any
* verbose output appears after the context/response dumps.
*/
if ((xit = get_report_info(test_info, response, NULL, NULL, NULL)) != EXIT_OK)
return xit;
if ((xit = get_result(test_info, response2, &error_id, &rcode)) != EXIT_OK)
return xit;
if (error_id == GETDNS_RESPSTATUS_ALL_TIMEOUT) {
strcpy(test_info->base_output, "Search timed out");
return EXIT_CRITICAL;
}
if (error_id != GETDNS_RESPSTATUS_GOOD || rcode != GETDNS_RCODE_NOERROR) {
strcpy(test_info->base_output, "Server error - cannot determine DNSSEC status");
return EXIT_UNKNOWN;
}
strcpy(test_info->base_output, "Server validates DNSSEC");
return EXIT_OK;
}
static exit_value test_tls13(struct test_info_s *test_info,
char ** av)
{
exit_value xit;
getdns_return_t ret;
if (*av) {
strcpy(test_info->base_output, "tls-1.3 takes no arguments");
return EXIT_USAGE;
}
/*
* Set cipher list to TLS 1.3-only ciphers. If we are using
* an OpenSSL version that doesn't support TLS 1.3 this will cause
* a Bad Context error on the lookup.
*/
if ((ret = getdns_context_set_tls_cipher_list(test_info->context, TLS13_CIPHER_SUITE)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot set TLS 1.3 cipher list: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
getdns_dict *response;
if ((xit = search(test_info,
DEFAULT_LOOKUP_NAME,
DEFAULT_LOOKUP_TYPE,
NULL,
&response,
&ret)) != EXIT_OK) {
if (xit == EXIT_CRITICAL) {
if (ret == GETDNS_RETURN_BAD_CONTEXT) {
strcpy(test_info->base_output, "Your version of OpenSSL does not support TLS 1.3 ciphers. You need at least OpenSSL 1.1.1.");
return EXIT_UNKNOWN;
} else {
strcpy(test_info->base_output, "Cannot establish TLS 1.3 connection.");
return EXIT_CRITICAL;
}
} else {
return xit;
}
}
if ((xit = get_report_info(test_info, response, NULL, NULL, NULL)) != EXIT_OK)
return xit;
getdns_list *l;
if ((ret = getdns_dict_get_list(response, "call_reporting", &l)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get call report: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
getdns_dict *d;
if ((ret = getdns_list_get_dict(l, 0, &d)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get call report first item: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
/* Search is forced TLS, so tls_version flag must exist. */
getdns_bindata *version;
if ((ret = getdns_dict_get_bindata(d, "tls_version", &version)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Cannot get TLS version: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
const char TLS_1_3_SIG[] = "TLSv1.3";
if (strncmp((const char *)version->data, TLS_1_3_SIG, sizeof(TLS_1_3_SIG) - 1) != 0) {
strcpy(test_info->base_output, "Server does not support TLS 1.3");
return EXIT_CRITICAL;
}
if ((xit = check_result(test_info, response)) != EXIT_OK)
return xit;
strcpy(test_info->base_output, "Server supports TLS 1.3");
return EXIT_OK;
}
struct async_query
{
const char *name;
unsigned done_order;
bool error;
};
static void out_of_order_callback(getdns_context *context,
getdns_callback_type_t callback_type,
getdns_dict *response,
void *userarg,
getdns_transaction_t transaction_id)
{
(void) context;
(void) callback_type;
(void) response;
(void) transaction_id;
struct async_query *query = (struct async_query *) userarg;
static unsigned callback_no;
query->done_order = ++callback_no;
query->error = (callback_type != GETDNS_CALLBACK_COMPLETE);
if (test_info.verbosity >= VERBOSITY_ADDITIONAL) {
if (test_info.monitoring) {
snprintcat(test_info.perf_output,
MAX_PERF_OUTPUT_LEN,
"->%s;",
query->name);
} else {
printf("Result:\t\t\t%s", query->name);
if (query->error)
printf(" (callback %d)\n", callback_type);
else
putchar('\n');
}
}
}
static exit_value test_out_of_order(struct test_info_s *test_info,
char ** av)
{
if (*av) {
strcpy(test_info->base_output, "OOOR takes no arguments");
return EXIT_USAGE;
}
/*
* A set of asynchronous queries to send. One exists.
*
* Replies from delay.getdns.api come back with a 1s TTL. It turns out
* that delay.getdns.api will ignore all leftmost labels bar the first,
* so to further mitigate any cache effects insert a random string
* into the name.
*/
const char GOOD_NAME[] = "getdnsapi.net";
char delay_name[50];
srand(time(NULL));
snprintf(delay_name, sizeof(delay_name) - 1, "400.n%04d.delay.getdnsapi.net", rand() % 10000);
struct async_query async_queries[] = {
{ delay_name, 0, false },
{ GOOD_NAME, 0, false }
};
unsigned NQUERIES = sizeof(async_queries) / sizeof(async_queries[0]);
/* Pre-load the cache with result of the good query. */
getdns_dict *response;
exit_value xit;
if ((xit = search_check(test_info,
GOOD_NAME,
GETDNS_RRTYPE_AAAA,
&response,
NULL,
NULL,
NULL)) != EXIT_OK)
return xit;
/* Now launch async searches for all the above. */
for (unsigned i = 0; i < NQUERIES; ++i) {
getdns_return_t ret;
if (test_info->verbosity >= VERBOSITY_ADDITIONAL) {
if (test_info->monitoring) {
snprintcat(test_info->perf_output,
MAX_PERF_OUTPUT_LEN,
"%s->;",
async_queries[i].name);
} else {
printf("Search:\t\t\t%s\n", async_queries[i].name);
}
}
if ((ret = getdns_general(test_info->context,
async_queries[i].name,
GETDNS_RRTYPE_AAAA,
NULL,
&async_queries[i],
NULL,
out_of_order_callback)) != GETDNS_RETURN_GOOD) {
snprintf(test_info->base_output,
MAX_BASE_OUTPUT_LEN,
"Async search failed: %s (%d)",
getdns_get_errorstr_by_id(ret),
ret);
return EXIT_UNKNOWN;
}
}
/* And wait for them to complete. */
getdns_context_run(test_info->context);
for (unsigned i = 0; i < NQUERIES; ++i) {
if (async_queries[i].error) {
strcpy(test_info->base_output, "Query did not complete");
return EXIT_UNKNOWN;
}
}
if (async_queries[NQUERIES - 1].done_order == NQUERIES) {
strcpy(test_info->base_output, "Responses are in query order");
return EXIT_CRITICAL;
} else {
strcpy(test_info->base_output, "Responses are not in query order");
return EXIT_OK;
}
}
static struct test_funcs_s
{
const char *name;
bool implies_tls;
bool implies_tcp;
exit_value (*func)(struct test_info_s *test_info, char **av);
} TESTS[] =
{
{ "lookup", false, false, test_lookup },
{ "rtt", false, false, test_rtt },
{ "qname-min", false, false, test_qname_minimisation },
{ "tls-auth", true, false, test_authenticate },
{ "tls-cert-valid", true, false, test_certificate_valid },
{ "tls-padding", true, false, test_padding },
{ "keepalive", false, true, test_keepalive },
{ "dnssec-validate", false, false, test_dnssec_validate },
{ "tls-1.3", true, false, test_tls13 },
{ "OOOR", false, true, test_out_of_order },
{ NULL, false, false, NULL }
};
int main(int ac, char *av[])
{
getdns_return_t ret;
getdns_list *pinset = NULL;
size_t pinset_size = 0;
bool strict_usage_profile = false;
bool use_udp = false;
bool use_tcp = false;
bool use_tls = false;
(void) ac;
test_info.errout = stderr;
atexit(exit_tidy);
if ((ret = getdns_context_create(&test_info.context, 1)) != GETDNS_RETURN_GOOD) {
fprintf(test_info.errout,
"Create context failed: %s (%d)\n",
getdns_get_errorstr_by_id(ret),
ret);
exit(EXIT_UNKNOWN);
}
for (++av; *av && *av[0] == '-'; ++av) {
if (strcmp(*av, "-M") == 0 ||
strcmp(*av, "--monitoring") == 0) {
test_info.monitoring = true;
test_info.errout = stdout;
} else if (strcmp(*av, "-D") == 0 ||
strcmp(*av, "--debug") == 0) {
test_info.debug_output = true;
} else if (strcmp(*av, "-E") == 0 ||
strcmp(*av, "--fail-on-dns-errors") == 0) {
test_info.fail_on_dns_errors = true;
} else if (strcmp(*av, "-u") == 0 ||
strcmp(*av, "--udp") == 0 ) {
use_udp = true;
} else if (strcmp(*av, "-t") == 0 ||
strcmp(*av, "--tcp") == 0 ) {
use_tcp = true;
} else if (strcmp(*av, "-T") == 0 ||
strcmp(*av, "--tls") == 0 ) {
use_tls = true;
} else if (strcmp(*av, "-S") == 0 ||
strcmp(*av, "--strict-usage-profile") == 0 ) {
strict_usage_profile = true;
use_tls = true;
} else if (strcmp(*av, "-K") == 0 ||
strcmp(*av, "--spki-pin") == 0 ) {
++av;
if (!*av) {
fputs("pin string of the form " EXAMPLE_PIN "expected after -K|--pin\n", test_info.errout);
exit(EXIT_UNKNOWN);
}
getdns_dict *pin;
pin = getdns_pubkey_pin_create_from_string(test_info.context, *av);
if (!pin) {
fprintf(test_info.errout,
"Could not convert '%s' into a public key pin.\n"
"Good pins look like: " EXAMPLE_PIN "\n"
"Please see RFC 7469 for details about the format.\n", *av);
exit(EXIT_UNKNOWN);
}
if (!pinset)
pinset = getdns_list_create_with_context(test_info.context);
ret = getdns_list_set_dict(pinset,
pinset_size++,
pin);
getdns_dict_destroy(pin);
if (ret != GETDNS_RETURN_GOOD) {
fprintf(test_info.errout,
"Could not add pin '%s' to pin set.\n",
*av);
getdns_list_destroy(pinset);
exit(EXIT_UNKNOWN);
}
use_tls = true;
} else if (strcmp(*av, "-v") == 0 ||
strcmp(*av, "--verbose") == 0) {
++test_info.verbosity;
} else if (strcmp(*av, "-V") == 0 ||
strcmp(*av, "--version") == 0) {
version();
} else {
usage();
}
}
if (*av == NULL || *av[0] != '@')
usage();
/* Non-monitoring output is chatty by default. */
if (!test_info.monitoring)
++test_info.verbosity;
const char *upstream = *av++;
getdns_dict *resolver;
getdns_bindata *address;
if ((ret = getdns_str2dict(&upstream[1], &resolver)) != GETDNS_RETURN_GOOD) {
fprintf(test_info.errout,
"Could not convert \"%s\" to an IP dict: %s (%d)\n",
&upstream[1],
getdns_get_errorstr_by_id(ret),
ret);
exit(EXIT_UNKNOWN);
}
/* If the resolver info include TLS auth name, use TLS. */
getdns_bindata *tls_auth_name;
if (getdns_dict_get_bindata(resolver, "tls_auth_name", &tls_auth_name) == GETDNS_RETURN_GOOD)
use_tls = true;
if ((ret = getdns_dict_get_bindata(resolver, "address_data", &address)) != GETDNS_RETURN_GOOD) {
fprintf(test_info.errout,
"\"%s\" did not translate to an IP dict: %s (%d)\n",
&upstream[1],
getdns_get_errorstr_by_id(ret),
ret);
getdns_dict_destroy(resolver);
exit(EXIT_UNKNOWN);
}
/* Set parameters on the resolver. */
if (pinset) {
ret = getdns_dict_set_list(resolver,
"tls_pubkey_pinset",
pinset);
getdns_list_destroy(pinset);
if (ret != GETDNS_RETURN_GOOD) {
fprintf(test_info.errout,
"Cannot set keys for \"%s\": %s (%d)\n",
&upstream[1],
getdns_get_errorstr_by_id(ret),
ret);
exit(EXIT_UNKNOWN);
}
}
/* Set getdns context to use the indicated resolver. */
getdns_list *l = getdns_list_create();
ret = getdns_list_set_dict(l, 0, resolver);
getdns_dict_destroy(resolver);
if (ret != GETDNS_RETURN_GOOD) {
fprintf(test_info.errout,
"Unable to add upstream '%s' to list: %s (%d)\n",
upstream,
getdns_get_errorstr_by_id(ret),
ret);
getdns_list_destroy(l);
exit(EXIT_UNKNOWN);
}
ret = getdns_context_set_upstream_recursive_servers(test_info.context, l);
getdns_list_destroy(l);
if (ret != GETDNS_RETURN_GOOD) {
fprintf(test_info.errout,
"Unable to set upstream resolver to '%s': %s (%d)\n",
upstream,
getdns_get_errorstr_by_id(ret),
ret);
exit(EXIT_UNKNOWN);
}
/* Set context to stub mode. */
if ((ret = getdns_context_set_resolution_type(test_info.context, GETDNS_RESOLUTION_STUB)) != GETDNS_RETURN_GOOD) {
fprintf(test_info.errout,
"Unable to set stub mode: %s (%d)\n",
getdns_get_errorstr_by_id(ret),
ret);
exit(EXIT_UNKNOWN);
}
/* Set other context parameters. */
if (strict_usage_profile) {
ret = getdns_context_set_tls_authentication(test_info.context, GETDNS_AUTHENTICATION_REQUIRED);
if (ret != GETDNS_RETURN_GOOD) {
fprintf(test_info.errout,
"Unable to set strict profile: %s (%d)\n",
getdns_get_errorstr_by_id(ret),
ret);
exit(EXIT_UNKNOWN);
}
}
/* Choose and run test */
const char *testname = *av;
if (!testname)
usage();
++av;
const struct test_funcs_s *f;
for (f = TESTS; f->name != NULL; ++f) {
if (strcmp(testname, f->name) == 0)
break;
}
if (f->name == NULL) {
fprintf(test_info.errout, "Unknown test %s\n", testname);
exit(EXIT_UNKNOWN);
}
if (f->implies_tcp) {
if (use_udp) {
fputs("Test requires TCP or TLS\n", test_info.errout);
exit(EXIT_UNKNOWN);
}
if (!use_tls)
use_tcp = true;
}
if (f->implies_tls) {
if (use_udp | use_tcp) {
fputs("Test requires TLS, or TLS authentication specified\n", test_info.errout);
exit(EXIT_UNKNOWN);
}
use_tls = true;
}
if ((use_tls + use_udp + use_tcp) > 1) {
fputs("Specify one only of -u, -t, -T\n", test_info.errout);
exit(EXIT_UNKNOWN);
}
if (use_tls || use_udp || use_tcp) {
getdns_transport_list_t udp[] = { GETDNS_TRANSPORT_UDP };
getdns_transport_list_t tcp[] = { GETDNS_TRANSPORT_TCP };
getdns_transport_list_t tls[] = { GETDNS_TRANSPORT_TLS };
getdns_transport_list_t *transport =
(use_tls) ? tls : (use_tcp) ? tcp : udp;
if ((ret = getdns_context_set_dns_transport_list(test_info.context, 1, transport)) != GETDNS_RETURN_GOOD) {
fprintf(test_info.errout,
"Unable to set %s transport: %s (%d)\n",
(use_tls) ? "TLS" : (use_tcp) ? "TCP" : "UDP",
getdns_get_errorstr_by_id(ret),
ret);
exit(EXIT_UNKNOWN);
}
}
exit_value xit = f->func(&test_info, av);
const char *xit_text = "(\?\?\?)";
FILE *out = stdout;
switch(xit) {
case EXIT_OK:
xit_text = "OK";
break;
case EXIT_WARNING:
xit_text = "WARNING";
break;
case EXIT_CRITICAL:
xit_text = "CRITICAL";
break;
case EXIT_UNKNOWN:
case EXIT_USAGE:
xit = EXIT_UNKNOWN;
xit_text = "UNKNOWN";
out = test_info.errout;
break;
}
if (test_info.monitoring)
fprintf(out, "DNS SERVER %s - ", xit_text);
fputs(test_info.base_output, out);
if (test_info.verbosity >= VERBOSITY_ADDITIONAL &&
test_info.monitoring &&
strlen(test_info.perf_output) > 0) {
fprintf(out, "|%s", test_info.perf_output);
}
fputc('\n', out);
exit(xit);
}