/* * 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 (c) 2009, 2010, Oracle and/or its affiliates. All rights reserved. * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "audit_remote.h" #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; /* connection retries */ int timeout; /* connection timeout */ static int timeout_p_timeout; /* p_timeout attr storage */ /* 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; 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; /* 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 * the default qsize zero value is (intentionally) set in the audit_remote(7) * plugin configuration. */ 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); } /* * Frees host list - should be called while keeping auditd_mutex. */ 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; } /* * 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 *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)) { /* ":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_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 /* * 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; 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) { /* 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", "retry", 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 = 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", "retry", 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(8). * * 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) { *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); } /* 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(8) 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); } (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); }