#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_TIMEOUT 5
#define NOSUCCESS_DELAY 20
#define FL_SET B_TRUE
#define FL_UNSET B_FALSE
static int nosuccess_cnt;
static int retries;
int timeout;
static int timeout_p_timeout;
#define BOFF_TIMEOUT(x, y) (x < 3 ? y * 2 * x : y * 8)
pthread_mutex_t plugin_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct hostlist_s *current_host;
static struct hostlist_s *hosts;
static struct hostlist_s *hosts_prev;
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;
#endif
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);
}
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);
}
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);
}
static void
freehostlist(hostlist_t **hostlist_ptr)
{
hostlist_t *h, *n;
h = *hostlist_ptr;
while (h != NULL) {
n = h->next_host;
freehostent(h->host);
free(h);
h = n;
}
*hostlist_ptr = NULL;
}
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 *hosts_new = 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
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)) {
*error = strdup(gettext("no hostname specified"));
return (AUDITD_INVALID);
}
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;
}
*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);
}
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)));
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));
}
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
}
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_new = newhost;
}
#if DEBUG
num_of_hosts++;
#endif
}
(void) pthread_mutex_lock(&plugin_mutex);
if (hosts_prev == NULL) {
hosts_prev = hosts;
}
hosts = hosts_new;
current_host = hosts;
(void) pthread_mutex_unlock(&plugin_mutex);
DPRINT((dfile, "Configured %d hosts.\n", num_of_hosts));
return (AUDITD_SUCCESS);
}
#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
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:
rc_msg = strdup(gettext("other, not classified error"));
break;
}
return (rc_msg);
}
static boolean_t
set_fdfl(int fd, int fl, boolean_t set_fl)
{
int flags;
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) {
if (flags & fl) {
return (B_TRUE);
}
flags |= fl;
} else {
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);
}
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]));
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_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;
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);
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;
if (hosts_prev != NULL) {
freehostlist(&hosts_prev);
DPRINT((dfile, "stale host list freed\n"));
}
break;
case SEND_RECORD_NEXT:
DPRINT((dfile, "retry the same host: %s (penalty) "
"rsn:%d\n", current_host->host->h_name, err_rsn));
attempts++;
break;
case SEND_RECORD_RETRY:
DPRINT((dfile, "retry the same host: %s (no penalty) "
"rsn:%d\n", current_host->host->h_name, err_rsn));
break;
}
if (send_record_rc == SEND_RECORD_NEXT) {
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", "retry",
ext_error, attempts + 1);
free(rsn_msg);
free(ext_error);
if (attempts < retries) {
timeout = BOFF_TIMEOUT(attempts, timeout);
DPRINT((dfile, "New timeout=%d\n", timeout));
} else {
current_host = current_host->next_host;
if (current_host == NULL) {
current_host = hosts;
}
timeout = timeout_p_timeout;
DPRINT((dfile, "New timeout=%d\n", timeout));
attempts = 0;
}
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",
"retry", ext_error, nosuccess_cnt);
free(ext_error);
(void) sleep(NOSUCCESS_DELAY);
}
}
err_rsn = RSN_UNDEFINED;
}
(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_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
if (kvlist != NULL) {
DPRINT((dfile, "Action: initial open or `audit -s`\n"));
val_str = kva_match(kv, "p_timeout");
if (val_str == NULL) {
*error = strdup(
gettext("p_timeout attribute not found"));
return (AUDITD_RETRY);
}
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;
} else {
timeout_p_timeout = DEFAULT_TIMEOUT;
timeout = timeout_p_timeout;
DPRINT((dfile, "p_timeout set to default value: %d\n",
timeout));
}
val_str = kva_match(kv, "p_retries");
if (val_str == NULL) {
*error = strdup(
gettext("p_retries attribute not found"));
return (AUDITD_RETRY);
}
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) {
*error = strdup(gettext("qsize attribute not found"));
return (AUDITD_RETRY);
}
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;
}
if (transq_count_max == 0 &&
(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);
}
if (!notify_pipe_ready) {
if (create_notify_pipe(notify_pipe, error)) {
notify_pipe_ready = B_TRUE;
} else {
return (AUDITD_RETRY);
}
}
#if DEBUG
} else {
DPRINT((dfile, "Action: `audit -n`\n"));
#endif
}
return (AUDITD_SUCCESS);
}
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);
}
(void) pthread_mutex_lock(&plugin_mutex);
freehostlist(&hosts);
freehostlist(&hosts_prev);
(void) pthread_mutex_unlock(&plugin_mutex);
current_host = NULL;
*error = NULL;
return (AUDITD_SUCCESS);
}