OpenSolaris_b135/lib/auditd_plugins/remote/audit_remote.c

Compare this file to the similar file:
Show the results in this format:

/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 *
 * send audit records to remote host
 *
 */

/*
 * auditd_plugin_open(), auditd_plugin() and auditd_plugin_close()
 * implement a replaceable library for use by auditd; they are a
 * project private interface and may change without notice.
 */

#include <arpa/inet.h>
#include <assert.h>
#include <audit_plugin.h>
#include <bsm/audit.h>
#include <bsm/audit_record.h>
#include <bsm/libbsm.h>
#include <errno.h>
#include <fcntl.h>
#include <gssapi/gssapi.h>
#include <libintl.h>
#include <netdb.h>
#include <pthread.h>
#include <rpc/rpcsec_gss.h>
#include <secdb.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <poll.h>

#include "audit_remote.h"

#define	DEFAULT_RETRIES	3	/* default connection retries */
#define	DEFAULT_TIMEOUT	5	/* default connection timeout (in secs) */
#define	NOSUCCESS_DELAY	20	/* unsuccessful delivery to all p_hosts */

#define	FL_SET		B_TRUE	/* set_fdfl(): set the flag */
#define	FL_UNSET	B_FALSE	/* set_fdfl(): unset the flag */

static int	nosuccess_cnt;	/* unsuccessful delivery counter */


static int	retries = DEFAULT_RETRIES;	/* connection retries */
int		timeout = DEFAULT_TIMEOUT;	/* connection timeout */
static int	timeout_p_timeout = -1;		/* p_timeout attr storage */

/* time reset mechanism; x .. timeout_p_timeout */
#define	RST_TIMEOUT(x)		(x != -1 ? x : DEFAULT_TIMEOUT)

/* semi-exponential timeout back off; x .. attempts, y .. timeout */
#define	BOFF_TIMEOUT(x, y)	(x < 3 ? y * 2 * x : y * 8)

/* general plugin lock */
pthread_mutex_t	plugin_mutex = PTHREAD_MUTEX_INITIALIZER;

static struct hostlist_s	*current_host;
static struct hostlist_s	*hosts;

extern struct transq_hdr_s	transq_hdr;
static long			transq_count_max;
extern pthread_mutex_t		transq_lock;

extern pthread_t	recv_tid;

extern boolean_t	notify_pipe_ready;
extern int		notify_pipe[2];

#if DEBUG
FILE		*dfile;		/* debug file */
#endif

/*
 * set_transq_count_max() - sets the transq_count_max value based on kernel
 * audit queue high water mark. This is backup solution for a case, when the
 * plugin audit_control(4) option lacks (intentionally) the qsize option.
 */
static auditd_rc_t
set_transq_count_max()
{
	struct au_qctrl	qctrl;

	if (auditon(A_GETQCTRL, (caddr_t)&qctrl, 0) != -1) {
		transq_count_max = qctrl.aq_hiwater;
		DPRINT((dfile, "Transmission queue max length set to %ld\n",
		    transq_count_max));
		return (AUDITD_SUCCESS);
	}

	DPRINT((dfile, "Setting the transmission queue max length failed\n"));
	return (AUDITD_RETRY);
}

/*
 * get_port_default() - set the default port number; note, that "solaris-audit"
 * used below in the code is the IANA assigned service name for the secure
 * remote solaris audit logging.
 */
static auditd_rc_t
get_port_default(int *port_default)
{

	struct servent  serventry;
	char  		serventry_buf[1024];

	if (getservbyname_r("solaris-audit", "tcp", &serventry,
	    (char *)&serventry_buf, sizeof (serventry_buf)) == NULL) {
		DPRINT((dfile, "unable to get default port number\n"));
#if DEBUG
		if (errno == ERANGE) {
			DPRINT((dfile, "low on buffer\n"));
		}
#endif
		return (AUDITD_INVALID);
	}
	*port_default = ntohs(serventry.s_port);
	DPRINT((dfile, "default port: %d\n", *port_default));

	return (AUDITD_SUCCESS);
}

/*
 * trim_me() - trims the white space characters around the specified string.
 * Inputs - pointer to the beginning of the string (str_ptr); returns - pointer
 * to the trimmed string. Function returns NULL pointer in case of received
 * empty string, NULL pointer or in case the pointed string consists of white
 * space characters only.
 */
static char *
trim_me(char *str_ptr) {

	char	*str_end;

	if (str_ptr == NULL || *str_ptr == '\0') {
		return (NULL);
	}

	while (isspace(*str_ptr)) {
		str_ptr++;
	}
	if (*str_ptr == '\0') {
		return (NULL);
	}

	str_end = str_ptr + strlen(str_ptr);

	while (str_end > str_ptr && isspace(str_end[-1])) {
		str_end--;
	}
	*str_end = '\0';

	return (str_ptr);
}


/*
 * parsehosts() end parses the host string (hosts_str)
 */
static auditd_rc_t
parsehosts(char *hosts_str, char **error)
{
	char 		*hostportmech, *hpm;
	char		*hostname;
	char		*port_str;
	char		*mech_str;
	int		port;
	int		port_default = -1;
	gss_OID		mech_oid;
	char 		*lasts_hpm;
	hostlist_t 	*lasthost = NULL;
	hostlist_t	*newhost;
	struct hostent 	*hostentry;
	int		error_num;
	int		rc;
#if DEBUG
	char 		addr_buf[INET6_ADDRSTRLEN];
	int		num_of_hosts = 0;
#endif

	hosts = lasthost;

	DPRINT((dfile, "parsing %s\n", hosts_str));
	while ((hostportmech = strtok_r(hosts_str, ",", &lasts_hpm)) != NULL) {

		hosts_str = NULL;
		hostname = NULL;
		port_str = NULL;
		port = port_default;
		mech_str = NULL;
		mech_oid = GSS_C_NO_OID;

		DPRINT((dfile, "parsing host:port:mech %s\n", hostportmech));

		if (strncmp(hostportmech, ":", 1 == 0)) { /* ":port:" case */
			*error = strdup(gettext("no hostname specified"));
			return (AUDITD_INVALID);
		}

		/* parse single host:port:mech target */
		while ((hpm = strsep(&hostportmech, ":")) != NULL) {

			if (hostname == NULL) {
				hostname = hpm;
				continue;
			}
			if (port_str == NULL) {
				port_str = hpm;
				continue;
			}
			if (mech_str == NULL) {
				mech_str = hpm;
				continue;
			}

			/* too many colons in the hostportmech string */
			*error = strdup(gettext("invalid host:port:mech "
			    "specification"));
			return (AUDITD_INVALID);
		}

		if (hostname == NULL || *hostname == '\0') {
			*error = strdup(gettext("invalid hostname "
			    "specification"));
			return (AUDITD_INVALID);
		}

		/* trim hostname */
		hostname = trim_me(hostname);
		if (hostname == NULL || *hostname == '\0') {
			*error = strdup(gettext("empty hostname "
			    "specification"));
			return (AUDITD_INVALID);
		}

		DPRINT((dfile, "resolving address for %s\n", hostname));

		hostentry = getipnodebyname(hostname, AF_INET6, 0, &error_num);
		if (!hostentry) {
			hostentry = getipnodebyname(hostname, AF_INET, 0,
			    &error_num);
		}
		if (!hostentry) {
			if (error_num == TRY_AGAIN) {
				*error = strdup(gettext("host not found, "
				    "try later"));
				return (AUDITD_RETRY);
			} else {
				*error = strdup(gettext("host not found"));
				return (AUDITD_INVALID);
			}
		}
		DPRINT((dfile, "hostentry: h_name=%s, addr_len=%d, addr=%s\n",
		    hostentry->h_name, hostentry->h_length,
		    inet_ntop(hostentry->h_addrtype,
		    hostentry->h_addr_list[0], addr_buf,
		    INET6_ADDRSTRLEN)));

		/* trim port */
		port_str = trim_me(port_str);
		if (port_str == NULL || *port_str == '\0') {
			if (port_default == -1 &&
			    (rc = get_port_default(&port_default))
			    != AUDITD_SUCCESS) {
				*error = strdup(gettext(
				    "unable to get default port number"));
				return (rc);
			}
			port = port_default;
			DPRINT((dfile, "port: %d (default)\n", port));
		} else {
			errno = 0;
			port = atoi(port_str);
			if (errno != 0 || port < 1 || port > USHRT_MAX) {
				*error = strdup(gettext("invalid port number"));
				return (AUDITD_INVALID);
			}
			DPRINT((dfile, "port: %d\n", port));
		}

		/* trim mechanism */
		mech_str = trim_me(mech_str);
		if (mech_str != NULL && *mech_str != '\0') {
			if (rpc_gss_mech_to_oid(mech_str, &mech_oid) != TRUE) {
				*error = strdup(gettext("unknown mechanism"));
				return (AUDITD_INVALID);
			}
			DPRINT((dfile, "mechanism: %s\n", mech_str));
#if DEBUG
		} else {
			DPRINT((dfile, "mechanism: null (default)\n"));
#endif
		}

		/* add this host to host list */
		newhost = malloc(sizeof (hostlist_t));
		if (newhost == NULL) {
			*error = strdup(gettext("no memory"));
			return (AUDITD_NO_MEMORY);
		}
		newhost->host = hostentry;
		newhost->port = htons(port);
		newhost->mech = mech_oid;
		newhost->next_host = NULL;
		if (lasthost != NULL) {
			lasthost->next_host = newhost;
			lasthost = lasthost->next_host;
		} else {
			lasthost = newhost;
			hosts = newhost;
		}
#if DEBUG
		num_of_hosts++;
#endif
	}

	current_host = hosts;
	DPRINT((dfile, "Configured %d hosts.\n", num_of_hosts));

	return (AUDITD_SUCCESS);
}


/*
 * Frees host list
 */
static void
freehostlist()
{
	hostlist_t *h, *n;

	(void) pthread_mutex_lock(&plugin_mutex);
	h = hosts;
	while (h) {
		n = h->next_host;
		freehostent(h->host);
		free(h);
		h = n;
	}
	current_host = NULL;
	hosts = NULL;
	(void) pthread_mutex_unlock(&plugin_mutex);
}

#if DEBUG
static char *
auditd_message(auditd_rc_t msg_code) {
	char 	*rc_msg;

	switch (msg_code) {
	case AUDITD_SUCCESS:
		rc_msg = strdup("ok");
		break;
	case AUDITD_RETRY:
		rc_msg = strdup("retry after a delay");
		break;
	case AUDITD_NO_MEMORY:
		rc_msg = strdup("can't allocate memory");
		break;
	case AUDITD_INVALID:
		rc_msg = strdup("bad input");
		break;
	case AUDITD_COMM_FAIL:
		rc_msg = strdup("communications failure");
		break;
	case AUDITD_FATAL:
		rc_msg = strdup("other error");
		break;
	case AUDITD_FAIL:
		rc_msg = strdup("other non-fatal error");
		break;
	}
	return (rc_msg);
}
#endif

/*
 * rsn_to_msg() - translation of the reason of closure identifier to the more
 * human readable/understandable form.
 */
static char *
rsn_to_msg(close_rsn_t reason)
{
	char 	*rc_msg;

	switch (reason) {
	case RSN_UNDEFINED:
		rc_msg = strdup(gettext("not defined reason of failure"));
		break;
	case RSN_INIT_POLL:
		rc_msg = strdup(gettext("poll() initialization failed"));
		break;
	case RSN_TOK_RECV_FAILED:
		rc_msg = strdup(gettext("token receiving failed"));
		break;
	case RSN_TOK_TOO_BIG:
		rc_msg = strdup(gettext("unacceptable token size"));
		break;
	case RSN_TOK_UNVERIFIABLE:
		rc_msg = strdup(gettext("received unverifiable token"));
		break;
	case RSN_SOCKET_CLOSE:
		rc_msg = strdup(gettext("closed socket"));
		break;
	case RSN_SOCKET_CREATE:
		rc_msg = strdup(gettext("socket creation failed"));
		break;
	case RSN_CONNECTION_CREATE:
		rc_msg = strdup(gettext("connection creation failed"));
		break;
	case RSN_PROTOCOL_NEGOTIATE:
		rc_msg = strdup(gettext("protocol negotiation failed"));
		break;
	case RSN_GSS_CTX_ESTABLISH:
		rc_msg = strdup(gettext("context establishing failed"));
		break;
	case RSN_GSS_CTX_EXP:
		rc_msg = strdup(gettext("context expired"));
		break;
	case RSN_UNKNOWN_AF:
		rc_msg = strdup(gettext("unknown address family"));
		break;
	case RSN_MEMORY_ALLOCATE:
		rc_msg = strdup(gettext("memory allocation failed"));
		break;
	default:	/* RSN_OTHER_ERR */
		rc_msg = strdup(gettext("other, not classified error"));
		break;
	}
	return (rc_msg);
}

/*
 * set_fdfl() - based on set_fl (FL_SET/FL_UNSET) un/sets the fl flag associated
 * with fd file descriptor.
 */
static boolean_t
set_fdfl(int fd, int fl, boolean_t set_fl)
{
	int	flags;

	/* power of two test - only single bit flags are allowed */
	if (!fl || (fl & (fl-1))) {
		DPRINT((dfile, "incorrect flag - %d isn't power of two\n", fl));
		return (B_FALSE);
	}

	if ((flags = fcntl(fd, F_GETFL, 0)) < 0) {
		DPRINT((dfile, "cannot get file descriptor flags\n"));
		return (B_FALSE);
	}

	if (set_fl) {	/* set the fl flag */
		if (flags & fl) {
			return (B_TRUE);
		}

		flags |= fl;

	} else {	/* unset the fl flag */
		if (~flags & fl) {
			return (B_TRUE);
		}

		flags &= ~fl;
	}

	if (fcntl(fd, F_SETFL, flags) == -1) {
		DPRINT((dfile, "cannot %s file descriptor flags\n",
		    (set_fl ? "set" : "unset")));
		return (B_FALSE);
	}

	DPRINT((dfile, "fd: %d - flag: 0%o was %s\n", fd, fl,
	    (set_fl ? "set" : "unset")));
	return (B_TRUE);
}


/*
 * create_notify_pipe() - creates the notification pipe. Function returns
 * B_TRUE/B_FALSE on success/failure.
 */
static boolean_t
create_notify_pipe(int *notify_pipe, char **error)
{

	if (pipe(notify_pipe) < 0) {
		DPRINT((dfile, "Cannot create notify pipe: %s\n",
		    strerror(errno)));
		*error = strdup(gettext("failed to create notification pipe"));
		return (B_FALSE);
	} else {
		DPRINT((dfile, "Pipe created in:%d out:%d\n", notify_pipe[0],
		    notify_pipe[1]));
		/* make (only) the pipe "in" end nonblocking */
		if (!set_fdfl(notify_pipe[0], O_NONBLOCK, FL_UNSET) ||
		    !set_fdfl(notify_pipe[1], O_NONBLOCK, FL_SET)) {
			DPRINT((dfile, "Cannot prepare blocking scheme on top "
			    "of the notification pipe: %s\n", strerror(errno)));
			(void) close(notify_pipe[0]);
			(void) close(notify_pipe[1]);

			*error = strdup(gettext("failed to prepare blocking "
			    "scheme on top of the notification pipe"));
			return (B_FALSE);
		}
	}

	return (B_TRUE);
}


/*
 * auditd_plugin() sends a record via a tcp connection.
 *
 * Operation:
 *   - 1 tcp connection opened at a time, referenced by current_host->sockfd
 *   - tries to (open and) send a record to the current_host where its address
 *     is taken from the first hostent h_addr_list entry
 *   - if connection times out, tries second host
 *   - if all hosts where tried tries again for retries number of times
 *   - if everything fails, it bails out with AUDITD_RETRY
 *
 *   Note, that space on stack allocated for any error message returned along
 *   with AUDITD_RETRY is subsequently freed by auditd.
 *
 */
auditd_rc_t
auditd_plugin(const char *input, size_t in_len, uint64_t sequence, char **error)
{
	int 		rc = AUDITD_FAIL;
	int 		send_record_rc = SEND_RECORD_FAIL;
	hostlist_t 	*start_host;
	int 		attempts = 0;
	char		*ext_error;	/* extended error string */
	close_rsn_t	err_rsn = RSN_UNDEFINED;
	char		*rsn_msg;

#if DEBUG
	char		*rc_msg;
	static uint64_t	last_sequence = 0;

	if ((last_sequence > 0) && (sequence != last_sequence + 1)) {
		DPRINT((dfile, "audit_remote: buffer sequence=%llu "
		    "but prev=%llu\n", sequence, last_sequence));
	}
	last_sequence = sequence;

	DPRINT((dfile, "audit_remote: input seq=%llu, len=%d\n",
	    sequence, in_len));
#endif

	(void) pthread_mutex_lock(&transq_lock);

	if (transq_hdr.count == transq_count_max) {
		DPRINT((dfile, "Transmission queue is full (%ld)\n",
		    transq_hdr.count));
		(void) pthread_mutex_unlock(&transq_lock);
		*error = strdup(gettext("retransmission queue is full"));
		return (AUDITD_RETRY);
	}
	(void) pthread_mutex_unlock(&transq_lock);


	(void) pthread_mutex_lock(&plugin_mutex);

	/* cycle over the hosts and possibly deliver the record */
	start_host = current_host;
	while (rc != AUDITD_SUCCESS) {
		DPRINT((dfile, "Trying to send record to %s [attempt:%d/%d]\n",
		    current_host->host->h_name, attempts + 1, retries));

		send_record_rc = send_record(current_host, input, in_len,
		    sequence, &err_rsn);
		DPRINT((dfile, "send_record() returned %d - ", send_record_rc));

		switch (send_record_rc) {
		case SEND_RECORD_SUCCESS:
			DPRINT((dfile, "success\n"));
			nosuccess_cnt = 0;
			rc = AUDITD_SUCCESS;
			break;
		case SEND_RECORD_NEXT:
			DPRINT((dfile, "retry the same host: %s (penalty)\n",
			    current_host->host->h_name));
			attempts++;
			break;
		case SEND_RECORD_RETRY:
			DPRINT((dfile, "retry the same host: %s (no penalty)\n",
			    current_host->host->h_name));
			break;
		}


		if (send_record_rc == SEND_RECORD_NEXT) {

			/* warn about unsuccessful auditd record delivery */
			rsn_msg = rsn_to_msg(err_rsn);
			(void) asprintf(&ext_error,
			    "retry %d connection %s:%d %s", attempts + 1,
			    current_host->host->h_name,
			    ntohs(current_host->port), rsn_msg);
			if (ext_error == NULL) {
				free(rsn_msg);
				*error = strdup(gettext("no memory"));
				rc = AUDITD_NO_MEMORY;
				break;
			}
			__audit_dowarn2("plugin", "audit_remote.so",
			    gettext("auditd record delivery failed"),
			    ext_error, attempts + 1);
			free(rsn_msg);
			free(ext_error);


			if (attempts < retries) {
				/* semi-exponential timeout back off */
				timeout = BOFF_TIMEOUT(attempts, timeout);
				DPRINT((dfile, "New timeout=%d\n", timeout));
			} else {
				/* get next host */
				current_host = current_host->next_host;
				if (current_host == NULL) {
					current_host = hosts;
				}
				timeout = RST_TIMEOUT(timeout_p_timeout);
				DPRINT((dfile, "New timeout=%d\n", timeout));
				attempts = 0;
			}


			/* one cycle finished */
			if (current_host == start_host && attempts == 0) {
				nosuccess_cnt++;
				(void) asprintf(&ext_error, "all hosts defined "
				    "as p_hosts were tried to deliver "
				    "the audit record to with no success "
				    "- sleeping for %d seconds",
				    NOSUCCESS_DELAY);
				if (ext_error == NULL) {
					*error = strdup(gettext("no memory"));
					rc = AUDITD_NO_MEMORY;
					break;
				}
				__audit_dowarn2("plugin", "audit_remote.so",
				    "unsuccessful attempt to deliver audit "
				    "record",
				    ext_error, nosuccess_cnt);
				free(ext_error);
				(void) sleep(NOSUCCESS_DELAY);
			}

		} /* if (send_record_rc == SEND_RECORD_NEXT) */

		err_rsn = RSN_UNDEFINED;

	} /* while (rc != AUDITD_SUCCESS) */

	(void) pthread_mutex_unlock(&plugin_mutex);

#if DEBUG
	rc_msg = auditd_message(rc);
	DPRINT((dfile, "audit_remote: returning: %s\n", rc_msg));
	free(rc_msg);
#endif

	return (rc);
}

/*
 * auditd_plugin_open() may be called multiple times; on initial open or
 * `audit -s`, then kvlist != NULL; on `audit -n`, then kvlist == NULL.
 * For more information see audit(1M).
 *
 * Note, that space on stack allocated for any error message returned along
 * with AUDITD_RETRY is subsequently freed by auditd.
 *
 */
auditd_rc_t
auditd_plugin_open(const kva_t *kvlist, char **ret_list, char **error)
{
	kva_t	*kv;
	char	*val_str;
	int	val;
	long	val_l;
	int	rc = 0;

	*error = NULL;
	*ret_list = NULL;
	kv = (kva_t *)kvlist;

#if DEBUG
	dfile = __auditd_debug_file_open();
#endif

	/* initial open or audit -s */
	if (kvlist != NULL) {
		DPRINT((dfile, "Action: initial open or `audit -s`\n"));
		val_str = kva_match(kv, "p_timeout");
		if (val_str != NULL) {
			DPRINT((dfile, "val_str=%s\n", val_str));
			errno = 0;
			val = atoi(val_str);
			if (errno == 0 && val >= 1) {
				timeout_p_timeout = val;
				timeout = val;
			}
		}

		val_str = kva_match(kv, "p_retries");
		if (val_str != NULL) {
			DPRINT((dfile, "val_str=%s\n", val_str));
			errno = 0;
			val = atoi(val_str);
			if (errno == 0 && val >= 0) {
				retries = val;
			}
		}

		val_str = kva_match(kv, "qsize");
		if (val_str != NULL) {
			DPRINT((dfile, "qsize=%s\n", val_str));
			errno = 0;
			val_l = atol(val_str);
			if (errno == 0 && val_l > 0) {
				transq_count_max = val_l;
			}

		} else {
			DPRINT((dfile, "qsize not in kvlist\n"));
			if ((rc = set_transq_count_max()) != AUDITD_SUCCESS) {
				*error = strdup(gettext("cannot get kernel "
				    "auditd queue high water mark\n"));
				return (rc);
			}
		}
		DPRINT((dfile, "timeout=%d, retries=%d, transq_count_max=%ld\n",
		    timeout, retries, transq_count_max));

		val_str = kva_match(kv, "p_hosts");
		if (val_str == NULL) {
			*error = strdup(gettext("no hosts configured"));
			return (AUDITD_RETRY);
		}
		if ((rc = parsehosts(val_str, error)) != AUDITD_SUCCESS) {
			return (rc);
		}

		/* create the notification pipe towards the receiving thread */
		if (!notify_pipe_ready) {
			if (create_notify_pipe(notify_pipe, error)) {
				notify_pipe_ready = B_TRUE;
			} else {
				return (AUDITD_RETRY);
			}
		}

#if DEBUG
	} else { /* audit -n */
		DPRINT((dfile, "Action: `audit -n`\n"));
#endif
	}

	return (AUDITD_SUCCESS);
}

/*
 * auditd_plugin_close() performs shutdown operations. The return values are
 * used by auditd to output warnings via the audit_warn(1M) script and the
 * string returned via "error_text", is passed to audit_warn.
 *
 * Note, that space on stack allocated for any error message returned along
 * with AUDITD_RETRY is subsequently freed by auditd.
 *
 */
auditd_rc_t
auditd_plugin_close(char **error)
{
	reset_transport(DO_EXIT, DO_SYNC);
	if (pthread_join(recv_tid, NULL) != 0) {
		*error = strdup(gettext("unable to close receiving thread"));
		return (AUDITD_RETRY);
	}

	freehostlist();
	*error = NULL;
	return (AUDITD_SUCCESS);
}