diff options
Diffstat (limited to '')
-rw-r--r-- | src/resperf.c | 816 |
1 files changed, 816 insertions, 0 deletions
diff --git a/src/resperf.c b/src/resperf.c new file mode 100644 index 0000000..97c998d --- /dev/null +++ b/src/resperf.c @@ -0,0 +1,816 @@ +/* + * Copyright 2019-2021 OARC, Inc. + * Copyright 2017-2018 Akamai Technologies + * Copyright 2006-2016 Nominum, Inc. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*** + *** DNS Resolution Performance Testing Tool + ***/ + +#include "config.h" + +#include "datafile.h" +#include "dns.h" +#include "log.h" +#include "net.h" +#include "opt.h" +#include "util.h" +#include "os.h" +#include "list.h" +#include "result.h" +#include "buffer.h" + +#include <errno.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/time.h> +#include <openssl/ssl.h> +#include <openssl/conf.h> +#include <openssl/err.h> +#include <signal.h> + +/* + * Global stuff + */ + +#define DEFAULT_SERVER_NAME "127.0.0.1" +#define DEFAULT_SERVER_PORT 53 +#define DEFAULT_SERVER_TLS_PORT 853 +#define DEFAULT_SERVER_PORTS "udp/tcp 53 or dot/tls 853" +#define DEFAULT_LOCAL_PORT 0 +#define DEFAULT_SOCKET_BUFFER 32 +#define DEFAULT_TIMEOUT 45 +#define DEFAULT_MAX_OUTSTANDING (64 * 1024) + +#define MAX_INPUT_DATA (64 * 1024) + +#define TIMEOUT_CHECK_TIME 5000000 + +#define DNS_RCODE_NOERROR 0 +#define DNS_RCODE_NXDOMAIN 3 + +struct query_info; + +typedef perf_list(struct query_info) query_list; + +typedef struct query_info { + uint64_t sent_timestamp; + /* + * This link links the query into the list of outstanding + * queries or the list of available query IDs. + */ + perf_link(struct query_info); + /* + * The list this query is on. + */ + query_list* list; +} query_info; + +static query_list outstanding_list; +static query_list instanding_list; + +static query_info* queries; + +static perf_sockaddr_t server_addr; +static perf_sockaddr_t local_addr; +static unsigned int nsocks; +static struct perf_net_socket* socks; +static enum perf_net_mode mode; + +static int dummypipe[2]; + +static uint64_t query_timeout; +static bool edns; +static bool dnssec; + +static perf_datafile_t* input; + +/* The target traffic level at the end of the ramp-up */ +double max_qps = 100000.0; + +/* The time period over which we ramp up traffic */ +#define DEFAULT_RAMP_TIME 60 +static uint64_t ramp_time; + +/* How long to send constant traffic after the initial ramp-up */ +#define DEFAULT_SUSTAIN_TIME 0 +static uint64_t sustain_time; + +/* How long to wait for responses after sending traffic */ +static uint64_t wait_time = 40 * MILLION; + +/* Total duration of the traffic-sending part of the test */ +static uint64_t traffic_time; + +/* Total duration of the test */ +static uint64_t end_time; + +/* Interval between plot data points, in microseconds */ +#define DEFAULT_BUCKET_INTERVAL 0.5 +static uint64_t bucket_interval; + +/* The number of plot data points */ +static int n_buckets; + +/* The plot data file */ +static const char* plotfile = "resperf.gnuplot"; + +/* The largest acceptable query loss when reporting max throughput */ +static double max_loss_percent = 100.0; + +/* The maximum number of outstanding queries */ +static unsigned int max_outstanding; + +static uint64_t num_queries_sent; +static uint64_t num_queries_outstanding; +static uint64_t num_responses_received; +static uint64_t num_queries_timed_out; +static uint64_t rcodecounts[16]; + +static uint64_t time_now; +static uint64_t time_of_program_start; +static uint64_t time_of_end_of_run; + +/* + * The last plot data point containing actual data; this can + * be less than than (n_buckets - 1) if the traffic sending + * phase is cut short + */ +static int last_bucket_used; + +/* + * The statistics for queries sent during one bucket_interval + * of the traffic sending phase. + */ +typedef struct { + int queries; + int responses; + int failures; + double latency_sum; +} ramp_bucket; + +/* Pointer to array of n_buckets ramp_bucket structures */ +static ramp_bucket* buckets; + +enum phase { + /* + * The ramp-up phase: we are steadily increasing traffic. + */ + PHASE_RAMP, + /* + * The sustain phase: we are sending traffic at a constant + * rate. + */ + PHASE_SUSTAIN, + /* + * The wait phase: we have stopped sending queries and are + * just waiting for any remaining responses. + */ + PHASE_WAIT +}; +static enum phase phase = PHASE_RAMP; + +/* The time when the sustain/wait phase began */ +static uint64_t sustain_phase_began, wait_phase_began; + +static perf_tsigkey_t* tsigkey; + +static bool verbose; + +const char* progname = "resperf"; + +static char* +stringify(double value, int precision) +{ + static char buf[20]; + + snprintf(buf, sizeof(buf), "%.*f", precision, value); + return buf; +} + +static void +setup(int argc, char** argv) +{ + const char* family = NULL; + const char* server_name = DEFAULT_SERVER_NAME; + in_port_t server_port = 0; + const char* local_name = NULL; + in_port_t local_port = DEFAULT_LOCAL_PORT; + const char* filename = NULL; + const char* tsigkey_str = NULL; + int sock_family; + unsigned int bufsize; + unsigned int i; + const char* _mode = 0; + + sock_family = AF_UNSPEC; + server_port = 0; + local_port = DEFAULT_LOCAL_PORT; + bufsize = DEFAULT_SOCKET_BUFFER; + query_timeout = DEFAULT_TIMEOUT * MILLION; + ramp_time = DEFAULT_RAMP_TIME * MILLION; + sustain_time = DEFAULT_SUSTAIN_TIME * MILLION; + bucket_interval = DEFAULT_BUCKET_INTERVAL * MILLION; + max_outstanding = DEFAULT_MAX_OUTSTANDING; + nsocks = 1; + mode = sock_udp; + verbose = false; + + perf_opt_add('f', perf_opt_string, "family", + "address family of DNS transport, inet or inet6", "any", + &family); + perf_opt_add('M', perf_opt_string, "mode", "set transport mode: udp, tcp or dot/tls", "udp", &_mode); + perf_opt_add('s', perf_opt_string, "server_addr", + "the server to query", DEFAULT_SERVER_NAME, &server_name); + perf_opt_add('p', perf_opt_port, "port", + "the port on which to query the server", + DEFAULT_SERVER_PORTS, &server_port); + perf_opt_add('a', perf_opt_string, "local_addr", + "the local address from which to send queries", NULL, + &local_name); + perf_opt_add('x', perf_opt_port, "local_port", + "the local port from which to send queries", + stringify(DEFAULT_LOCAL_PORT, 0), &local_port); + perf_opt_add('d', perf_opt_string, "datafile", + "the input data file", "stdin", &filename); + perf_opt_add('t', perf_opt_timeval, "timeout", + "the timeout for query completion in seconds", + stringify(DEFAULT_TIMEOUT, 0), &query_timeout); + perf_opt_add('b', perf_opt_uint, "buffer_size", + "socket send/receive buffer size in kilobytes", NULL, + &bufsize); + perf_opt_add('e', perf_opt_boolean, NULL, + "enable EDNS 0", NULL, &edns); + perf_opt_add('D', perf_opt_boolean, NULL, + "set the DNSSEC OK bit (implies EDNS)", NULL, &dnssec); + perf_opt_add('y', perf_opt_string, "[alg:]name:secret", + "the TSIG algorithm, name and secret", NULL, &tsigkey_str); + perf_opt_add('i', perf_opt_timeval, "plot_interval", + "the time interval between plot data points, in seconds", + stringify(DEFAULT_BUCKET_INTERVAL, 1), &bucket_interval); + perf_opt_add('m', perf_opt_double, "max_qps", + "the maximum number of queries per second", + stringify(max_qps, 0), &max_qps); + perf_opt_add('P', perf_opt_string, "plotfile", + "the name of the plot data file", plotfile, &plotfile); + perf_opt_add('r', perf_opt_timeval, "ramp_time", + "the ramp-up time in seconds", + stringify(DEFAULT_RAMP_TIME, 0), &ramp_time); + perf_opt_add('c', perf_opt_timeval, "constant_traffic_time", + "how long to send constant traffic, in seconds", + stringify(DEFAULT_SUSTAIN_TIME, 0), &sustain_time); + perf_opt_add('L', perf_opt_double, "max_query_loss", + "the maximum acceptable query loss, in percent", + stringify(max_loss_percent, 0), &max_loss_percent); + perf_opt_add('C', perf_opt_uint, "clients", + "the number of clients to act as", NULL, &nsocks); + perf_opt_add('q', perf_opt_uint, "num_outstanding", + "the maximum number of queries outstanding", + stringify(DEFAULT_MAX_OUTSTANDING, 0), &max_outstanding); + perf_opt_add('v', perf_opt_boolean, NULL, + "verbose: report additional information to stdout", + NULL, &verbose); + bool log_stdout = false; + perf_opt_add('W', perf_opt_boolean, NULL, "log warnings and errors to stdout instead of stderr", NULL, &log_stdout); + + perf_opt_parse(argc, argv); + + if (log_stdout) { + perf_log_tostdout(); + } + + if (_mode != 0) + mode = perf_net_parsemode(_mode); + + if (!server_port) { + server_port = mode == sock_tls ? DEFAULT_SERVER_TLS_PORT : DEFAULT_SERVER_PORT; + } + + if (max_outstanding > nsocks * DEFAULT_MAX_OUTSTANDING) + perf_log_fatal("number of outstanding packets (%u) must not " + "be more than 64K per client", + max_outstanding); + + if (ramp_time + sustain_time == 0) + perf_log_fatal("rampup_time and constant_traffic_time must not " + "both be 0"); + + perf_list_init(outstanding_list); + perf_list_init(instanding_list); + if (!(queries = calloc(max_outstanding, sizeof(query_info)))) { + perf_log_fatal("out of memory"); + } + for (i = 0; i < max_outstanding; i++) { + perf_link_init(&queries[i]); + perf_list_append(instanding_list, &queries[i]); + queries[i].list = &instanding_list; + } + + if (family != NULL) + sock_family = perf_net_parsefamily(family); + perf_net_parseserver(sock_family, server_name, server_port, &server_addr); + perf_net_parselocal(server_addr.sa.sa.sa_family, local_name, + local_port, &local_addr); + + input = perf_datafile_open(filename); + + if (dnssec) + edns = true; + + if (tsigkey_str != NULL) + tsigkey = perf_tsig_parsekey(tsigkey_str); + + if (!(socks = calloc(nsocks, sizeof(*socks)))) { + perf_log_fatal("out of memory"); + } + for (i = 0; i < nsocks; i++) + socks[i] = perf_net_opensocket(mode, &server_addr, &local_addr, i, bufsize); +} + +static void +cleanup(void) +{ + unsigned int i; + + perf_datafile_close(&input); + for (i = 0; i < nsocks; i++) + (void)perf_net_close(&socks[i]); + close(dummypipe[0]); + close(dummypipe[1]); +} + +/* Find the ramp_bucket for queries sent at time "when" */ + +static ramp_bucket* +find_bucket(uint64_t when) +{ + uint64_t sent_at = when - time_of_program_start; + int i = (int)((n_buckets * sent_at) / traffic_time); + /* + * Guard against array bounds violations due to roundoff + * errors or scheduling jitter + */ + if (i < 0) + i = 0; + if (i > n_buckets - 1) + i = n_buckets - 1; + return &buckets[i]; +} + +/* + * print_statistics: + * Print out statistics based on the results of the test + */ +static void +print_statistics(void) +{ + int i; + double max_throughput; + double loss_at_max_throughput; + bool first_rcode; + uint64_t run_time = time_of_end_of_run - time_of_program_start; + + printf("\nStatistics:\n\n"); + + printf(" Queries sent: %" PRIu64 "\n", + num_queries_sent); + printf(" Queries completed: %" PRIu64 "\n", + num_responses_received); + printf(" Queries lost: %" PRIu64 "\n", + num_queries_sent - num_responses_received); + printf(" Response codes: "); + first_rcode = true; + for (i = 0; i < 16; i++) { + if (rcodecounts[i] == 0) + continue; + if (first_rcode) + first_rcode = false; + else + printf(", "); + printf("%s %" PRIu64 " (%.2lf%%)", + perf_dns_rcode_strings[i], rcodecounts[i], + (rcodecounts[i] * 100.0) / num_responses_received); + } + printf("\n"); + printf(" Run time (s): %u.%06u\n", + (unsigned int)(run_time / MILLION), + (unsigned int)(run_time % MILLION)); + + /* Find the maximum throughput, subject to the -L option */ + max_throughput = 0.0; + loss_at_max_throughput = 0.0; + for (i = 0; i <= last_bucket_used; i++) { + ramp_bucket* b = &buckets[i]; + double responses_per_sec = b->responses / (bucket_interval / (double)MILLION); + double loss = b->queries ? (b->queries - b->responses) / (double)b->queries : 0.0; + double loss_percent = loss * 100.0; + if (loss_percent > max_loss_percent) + break; + if (responses_per_sec > max_throughput) { + max_throughput = responses_per_sec; + loss_at_max_throughput = loss_percent; + } + } + printf(" Maximum throughput: %.6lf qps\n", max_throughput); + printf(" Lost at that point: %.2f%%\n", loss_at_max_throughput); +} + +static ramp_bucket* +init_buckets(int n) +{ + ramp_bucket* p; + int i; + + if (!(p = calloc(n, sizeof(*p)))) { + perf_log_fatal("out of memory"); + return 0; // fix clang scan-build + } + for (i = 0; i < n; i++) { + p[i].queries = p[i].responses = p[i].failures = 0; + p[i].latency_sum = 0.0; + } + return p; +} + +/* + * Send a query based on a line of input. + * Return PERF_R_NOMORE if we ran out of query IDs. + */ +static perf_result_t +do_one_line(perf_buffer_t* lines, perf_buffer_t* msg) +{ + query_info* q; + unsigned int qid; + unsigned int sock; + perf_region_t used; + unsigned char* base; + unsigned int length; + perf_result_t result; + + q = perf_list_head(instanding_list); + if (!q) + return (PERF_R_NOMORE); + qid = (q - queries) / nsocks; + sock = (q - queries) % nsocks; + + if (socks[sock].sending) { + if (perf_net_sockready(&socks[sock], dummypipe[0], TIMEOUT_CHECK_TIME) == -1) { + if (errno == EINPROGRESS) { + if (verbose) { + perf_log_warning("network congested, packet sending in progress"); + } + } else { + if (verbose) { + char __s[256]; + perf_log_warning("failed to send packet: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + } + return (PERF_R_FAILURE); + } + + perf_list_unlink(instanding_list, q); + perf_list_prepend(outstanding_list, q); + q->list = &outstanding_list; + + num_queries_sent++; + num_queries_outstanding++; + + q = perf_list_head(instanding_list); + if (!q) + return (PERF_R_NOMORE); + qid = (q - queries) / nsocks; + sock = (q - queries) % nsocks; + } + + switch (perf_net_sockready(&socks[sock], dummypipe[0], TIMEOUT_CHECK_TIME)) { + case 0: + if (verbose) { + perf_log_warning("failed to send packet: socket %d not ready", sock); + } + return (PERF_R_FAILURE); + case -1: + perf_log_warning("failed to send packet: socket %d readiness check timed out", sock); + return (PERF_R_FAILURE); + default: + break; + } + + perf_buffer_clear(lines); + result = perf_datafile_next(input, lines, false); + if (result != PERF_R_SUCCESS) + perf_log_fatal("ran out of query data"); + perf_buffer_usedregion(lines, &used); + + perf_buffer_clear(msg); + result = perf_dns_buildrequest(&used, qid, + edns, dnssec, false, + tsigkey, 0, + msg); + if (result != PERF_R_SUCCESS) + return (result); + + q->sent_timestamp = time_now; + + base = perf_buffer_base(msg); + length = perf_buffer_usedlength(msg); + if (perf_net_sendto(&socks[sock], base, length, 0, + &server_addr.sa.sa, server_addr.length) + < 1) { + if (errno == EINPROGRESS) { + if (verbose) { + perf_log_warning("network congested, packet sending in progress"); + } + } else { + if (verbose) { + char __s[256]; + perf_log_warning("failed to send packet: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + } + return (PERF_R_FAILURE); + } + + perf_list_unlink(instanding_list, q); + perf_list_prepend(outstanding_list, q); + q->list = &outstanding_list; + + num_queries_sent++; + num_queries_outstanding++; + + return PERF_R_SUCCESS; +} + +static void +enter_sustain_phase(void) +{ + phase = PHASE_SUSTAIN; + if (sustain_time != 0.0) + printf("[Status] Ramp-up done, sending constant traffic\n"); + sustain_phase_began = time_now; +} + +static void +enter_wait_phase(void) +{ + phase = PHASE_WAIT; + printf("[Status] Waiting for more responses\n"); + wait_phase_began = time_now; +} + +/* + * try_process_response: + * + * Receive from the given socket & process an individual response packet. + * Remove it from the list of open queries (status[]) and decrement the + * number of outstanding queries if it matches an open query. + */ +static void +try_process_response(unsigned int sockindex) +{ + unsigned char packet_buffer[MAX_EDNS_PACKET]; + uint16_t* packet_header; + uint16_t qid, rcode; + query_info* q; + double latency; + ramp_bucket* b; + int n; + + packet_header = (uint16_t*)packet_buffer; + n = perf_net_recv(&socks[sockindex], packet_buffer, sizeof(packet_buffer), 0); + if (n < 0) { + if (errno == EAGAIN || errno == EINTR) { + return; + } else { + char __s[256]; + perf_log_fatal("failed to receive packet: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + } else if (!n) { + // Treat connection closed like try again until reconnection features are in + return; + } else if (n < 4) { + perf_log_warning("received short response"); + return; + } + + qid = ntohs(packet_header[0]); + rcode = ntohs(packet_header[1]) & 0xF; + + q = &queries[qid * nsocks + sockindex]; + if (q->list != &outstanding_list) { + perf_log_warning("received a response with an unexpected id: %u", qid); + return; + } + + perf_list_unlink(outstanding_list, q); + perf_list_append(instanding_list, q); + q->list = &instanding_list; + + num_queries_outstanding--; + + latency = (time_now - q->sent_timestamp) / (double)MILLION; + b = find_bucket(q->sent_timestamp); + b->responses++; + if (!(rcode == DNS_RCODE_NOERROR || rcode == DNS_RCODE_NXDOMAIN)) + b->failures++; + b->latency_sum += latency; + num_responses_received++; + rcodecounts[rcode]++; +} + +static void +retire_old_queries(void) +{ + query_info* q; + + while (true) { + q = perf_list_tail(outstanding_list); + if (q == NULL || (time_now - q->sent_timestamp) < query_timeout) + break; + perf_list_unlink(outstanding_list, q); + perf_list_append(instanding_list, q); + q->list = &instanding_list; + + num_queries_outstanding--; + num_queries_timed_out++; + } +} + +static inline int +num_scheduled(uint64_t time_since_start) +{ + if (phase == PHASE_RAMP) { + return 0.5 * max_qps * (double)time_since_start * time_since_start / (ramp_time * MILLION); + } else { /* PHASE_SUSTAIN */ + return 0.5 * max_qps * (ramp_time / (double)MILLION) + max_qps * (time_since_start - ramp_time) / (double)MILLION; + } +} + +static void +handle_sigpipe(int sig) +{ + (void)sig; + switch (mode) { + case sock_tcp: + case sock_tls: + // if connection is closed it will generate a signal + perf_log_fatal("SIGPIPE received, connection(s) likely closed, can't continue"); + break; + default: + break; + } +} + +int main(int argc, char** argv) +{ + int i; + FILE* plotf; + perf_buffer_t lines, msg; + char input_data[MAX_INPUT_DATA]; + unsigned char outpacket_buffer[MAX_EDNS_PACKET]; + unsigned int max_packet_size; + unsigned int current_sock; + perf_result_t result; + + printf("DNS Resolution Performance Testing Tool\n" + "Version " PACKAGE_VERSION "\n\n"); + + (void)SSL_library_init(); +#if OPENSSL_VERSION_NUMBER < 0x10100000L + SSL_load_error_strings(); + OPENSSL_config(0); +#endif + + setup(argc, argv); + + if (pipe(dummypipe) < 0) + perf_log_fatal("creating pipe"); + + perf_os_handlesignal(SIGPIPE, handle_sigpipe); + + perf_buffer_init(&lines, input_data, sizeof(input_data)); + + max_packet_size = edns ? MAX_EDNS_PACKET : MAX_UDP_PACKET; + perf_buffer_init(&msg, outpacket_buffer, max_packet_size); + + traffic_time = ramp_time + sustain_time; + end_time = traffic_time + wait_time; + + n_buckets = (traffic_time + bucket_interval - 1) / bucket_interval; + buckets = init_buckets(n_buckets); + + time_now = perf_get_time(); + time_of_program_start = time_now; + + printf("[Status] Command line: %s", progname); + for (i = 1; i < argc; i++) { + printf(" %s", argv[i]); + } + printf("\n"); + + printf("[Status] Sending\n"); + + current_sock = 0; + for (;;) { + int should_send; + uint64_t time_since_start = time_now - time_of_program_start; + switch (phase) { + case PHASE_RAMP: + if (time_since_start >= ramp_time) + enter_sustain_phase(); + break; + case PHASE_SUSTAIN: + if (time_since_start >= traffic_time) + enter_wait_phase(); + break; + case PHASE_WAIT: + if (time_since_start >= end_time || perf_list_empty(outstanding_list)) + goto end_loop; + break; + } + if (phase != PHASE_WAIT) { + should_send = num_scheduled(time_since_start) - num_queries_sent; + if (should_send >= 1000) { + printf("[Status] Fell behind by %d queries, " + "ending test at %.0f qps\n", + should_send, (max_qps * time_since_start) / ramp_time); + enter_wait_phase(); + } + if (should_send > 0) { + result = do_one_line(&lines, &msg); + if (result == PERF_R_SUCCESS) + find_bucket(time_now)->queries++; + if (result == PERF_R_NOMORE) { + printf("[Status] Reached %u outstanding queries\n", + max_outstanding); + enter_wait_phase(); + } + } + } + try_process_response(current_sock++); + current_sock = current_sock % nsocks; + retire_old_queries(); + time_now = perf_get_time(); + } +end_loop: + time_now = perf_get_time(); + time_of_end_of_run = time_now; + + printf("[Status] Testing complete\n"); + + plotf = fopen(plotfile, "w"); + if (!plotf) { + char __s[256]; + perf_log_fatal("could not open %s: %s", plotfile, perf_strerror_r(errno, __s, sizeof(__s))); + } + + /* Print column headers */ + fprintf(plotf, "# time target_qps actual_qps " + "responses_per_sec failures_per_sec avg_latency\n"); + + /* Don't print unused buckets */ + last_bucket_used = find_bucket(wait_phase_began) - buckets; + + /* Don't print a partial bucket at the end */ + if (last_bucket_used > 0) + --last_bucket_used; + + for (i = 0; i <= last_bucket_used; i++) { + double t = (i + 0.5) * traffic_time / (n_buckets * (double)MILLION); + double ramp_dtime = ramp_time / (double)MILLION; + double target_qps = t <= ramp_dtime ? (t / ramp_dtime) * max_qps : max_qps; + double latency = buckets[i].responses ? buckets[i].latency_sum / buckets[i].responses : 0; + double interval = bucket_interval / (double)MILLION; + fprintf(plotf, "%7.3f %8.2f %8.2f %8.2f %8.2f %8.6f\n", + t, + target_qps, + buckets[i].queries / interval, + buckets[i].responses / interval, + buckets[i].failures / interval, + latency); + } + + fclose(plotf); + print_statistics(); + cleanup(); +#if OPENSSL_VERSION_NUMBER < 0x10100000L + ERR_free_strings(); +#endif + + return 0; +} |