diff --git a/NEWS b/NEWS index 9663d1028e..5c7d7a7903 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,4 @@ -$Id: NEWS,v 1.1486 2013-01-24 22:32:35 castaglia Exp $ +$Id: NEWS,v 1.1487 2013-01-25 00:04:13 castaglia Exp $ ----------------------------------------------------------------------------- More details on the bugs listed below can be found by using the bug number @@ -39,6 +39,7 @@ CVS limits and tallies. - Bug 3882 - DisplayLogin with an absolute path does not work properly within an section. +- Added new mod_log_forensic contrib module. 1.3.5rc1 - Released 04-Jan-2013 -------------------------------- diff --git a/README.modules b/README.modules index cf3f527698..6bd4c0a62a 100644 --- a/README.modules +++ b/README.modules @@ -151,6 +151,11 @@ Feature modules: See doc/contrib/mod_load.html Contributed by TJ Saunders + mod_log_forensic + Only write log information when criteria are met. + See doc/contrib/mod_log_forensic.html + Contributed by TJ Saunders + mod_qos Tweak the network TCP QoS bits for better routing See doc/contrib/mod_qos.html diff --git a/RELEASE_NOTES b/RELEASE_NOTES index de7062d191..5f70b1ab45 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -11,6 +11,10 @@ ChangeLog files. + New Modules + mod_log_forensic + The mod_log_forensic module ... + See doc/contrib/mod_log_forensic.html for more information. + + New Configuration Directives @@ -246,4 +250,4 @@ ChangeLog files. and cmdtable.class renamed to cmdtable.cmd_class. See Bug#3079 for details. -Last Updated: $Date: 2013-01-18 20:29:46 $ +Last Updated: $Date: 2013-01-25 00:04:13 $ diff --git a/contrib/mod_log_forensic.c b/contrib/mod_log_forensic.c new file mode 100644 index 0000000000..ed89e2d1f6 --- /dev/null +++ b/contrib/mod_log_forensic.c @@ -0,0 +1,955 @@ +/* + * mod_log_forensic - a buffering log module for aiding in server behavior + * forensic analysis + * + * Copyright (c) 2011 TJ Saunders + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + * + * As a special exemption, TJ Saunders and other respective copyright holders + * give permission to link this program with OpenSSL, and distribute the + * resulting executable, without including the source code for OpenSSL in the + * source distribution. + */ + +#include "conf.h" +#include "privs.h" + +#include + +#define MOD_LOG_FORENSIC_VERSION "mod_log_forensic/0.2" + +/* Make sure the version of proftpd is as necessary. */ +#if PROFTPD_VERSION_NUMBER < 0x0001030403 +# error "ProFTPD 1.3.4rc3 or later required" +#endif + +module log_forensic_module; + +static pool *forensic_pool = NULL; +static int forensic_engine = FALSE; +static int forensic_logfd = -1; + +/* Criteria for flushing out the "forensic" logs. */ +#define FORENSIC_CRIT_FAILED_LOGIN 0x00001 +#define FORENSIC_CRIT_MODULE_CONFIG 0x00002 +#define FORENSIC_CRIT_UNTIMELY_DEATH 0x00004 + +#define FORENSIC_CRIT_DEFAULT \ + (FORENSIC_CRIT_FAILED_LOGIN|FORENSIC_CRIT_UNTIMELY_DEATH) + +static unsigned long forensic_criteria = FORENSIC_CRIT_DEFAULT; + +/* Use a ring buffer for the cached/buffered log messages; the index pointing + * to where to stash the next message then moves around the ring. + * + * Overwritten messages will be allocated of a module-specific pool. To + * prevent this pool from growing unboundedly, we need to clear/destroy it + * periodically. But doing this without having to re-copy all of the + * buffered log lines could be expensive. + * + * Instead, what if we use subpools, for every 1/10th of the ring. When + * the last message for a subpool is purged/overwritten, that subpool can + * be destroyed without effecting any existing message in the ring. + */ + +#define FORENSIC_DEFAULT_NMSGS 1024 + +/* Regardless of the configured ForensicLogBufferSize, this defines the + * number of messages per sub-pool. + * + * Why 256 messages per sub-pool? + * + * 80 chars (avg) per message * 512 messages = 20 KB + * + * This means that a given sub-pool will hold roughly 20 KB. Which means + * that 20 KB + ring max size is the largest memory that mod_log_forensic + * should hold, before releasing a sub-pool back to the Pool API. + */ + +#define FORENSIC_DEFAULT_MSGS_PER_POOL 256 +static unsigned int forensic_msgs_per_pool = FORENSIC_DEFAULT_MSGS_PER_POOL; + +struct forensic_msg { + pool *fm_pool; + unsigned int fm_pool_msgno; + + unsigned int fm_log_type; + int fm_log_level; + const char *fm_msg; + size_t fm_msglen; +}; + +static struct forensic_msg **forensic_msgs = NULL; +static unsigned int forensic_nmsgs = FORENSIC_DEFAULT_NMSGS; +static unsigned int forensic_msg_idx = 0; + +static pool *forensic_subpool = NULL; +static unsigned int forensic_subpool_msgno = 1; + +#define FORENSIC_MAX_LEVELS 50 +static const char *forensic_log_levels[] = { + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", + "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", + "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", + "40", "41", "42", "43", "44", "45", "46", "47", "48", "49" +}; + +static void forensic_add_msg(unsigned int log_type, int log_level, + const char *log_msg, size_t log_msglen) { + struct forensic_msg *fm; + + /* Get the message that's currently in the ring where we want add our new + * one. + */ + fm = forensic_msgs[forensic_msg_idx]; + if (fm) { + /* If this message is the last one of it subpool, destroy that pool. */ + if (fm->fm_pool_msgno == forensic_msgs_per_pool) { + destroy_pool(fm->fm_pool); + } + + forensic_msgs[forensic_msg_idx] = NULL; + } + + /* Add this message into the ring. */ + + fm = pcalloc(forensic_subpool, sizeof(struct forensic_msg)); + fm->fm_pool = forensic_subpool; + fm->fm_pool_msgno = forensic_subpool_msgno; + fm->fm_log_type = log_type; + fm->fm_log_level = log_level; + fm->fm_msg = pstrndup(fm->fm_pool, log_msg, log_msglen); + fm->fm_msglen = log_msglen; + + forensic_msgs[forensic_msg_idx] = fm; + + forensic_msg_idx += 1; + if (forensic_msg_idx == forensic_nmsgs) { + /* Wrap around */ + forensic_msg_idx = 0; + } + + if (forensic_subpool_msgno == forensic_msgs_per_pool) { + /* Time to create a new subpool */ + forensic_subpool = pr_pool_create_sz(forensic_pool, 256); + forensic_subpool_msgno = 1; + + } else { + forensic_subpool_msgno++; + } +} + +static const char *forensic_get_begin_marker(unsigned int criterion, + size_t *markerlen) { + const char *marker; + + switch (criterion) { + case FORENSIC_CRIT_FAILED_LOGIN: + marker = "-----BEGIN FAILED LOGIN FORENSICS-----\n"; + break; + + case FORENSIC_CRIT_MODULE_CONFIG: + marker = "-----BEGIN MODULE CONFIG FORENSICS-----\n"; + break; + + case FORENSIC_CRIT_UNTIMELY_DEATH: + marker = "-----BEGIN UNTIMELY DEATH FORENSICS-----\n"; + break; + } + + *markerlen = strlen(marker); + return marker; +} + +static const char *forensic_get_end_marker(unsigned int criterion, + size_t *markerlen) { + const char *marker; + + switch (criterion) { + case FORENSIC_CRIT_FAILED_LOGIN: + marker = "-----END FAILED LOGIN FORENSICS-----\n"; + break; + + case FORENSIC_CRIT_MODULE_CONFIG: + marker = "-----END MODULE CONFIG FORENSICS-----\n"; + break; + + case FORENSIC_CRIT_UNTIMELY_DEATH: + marker = "-----END UNTIMELY DEATH FORENSICS-----\n"; + break; + } + + *markerlen = strlen(marker); + return marker; +} + +/* Rather than deal with some homegrown itoa() sort of thing for converting + * the log level number to an easily printable string, I'm using the level + * as an index in a precomputed array of strings. + */ +static const char *forensic_get_level_str(int log_level) { + int i; + + i = log_level; + if (i < 0) { + i = 0; + } + + if (i >= FORENSIC_MAX_LEVELS) { + return "N"; + } + + return forensic_log_levels[i]; +} + +static void forensic_write_metadata(void) { + const char *client_ip, *server_ip; + int server_port; + char server_port_str[32], + raw_bytes_in_str[64], raw_bytes_out_str[64], + total_bytes_in_str[64], total_bytes_out_str[64], + total_files_in_str[64], total_files_out_str[64]; + + /* 32 vectors is currently more than necessary, but it's better to have + * too many than too little. + */ + struct iovec iov[32]; + int niov = 0, res; + + /* Write session metadata in key/value message headers: + * + * Client-Address: + * Server-Address: + * User: + * Raw-Bytes-In: + * Raw-Bytes-Out: + * Total-Bytes-In: + * Total-Bytes-Out: + * Total-Files-In: + * Total-Files-Out: + */ + + client_ip = pr_netaddr_get_ipstr(session.c->remote_addr); + server_ip = pr_netaddr_get_ipstr(session.c->local_addr); + server_port = pr_netaddr_get_port(session.c->local_addr); + + /* Client address */ + iov[niov].iov_base = "Client-Address: "; + iov[niov].iov_len = 16; + niov++; + + iov[niov].iov_base = (void *) client_ip; + iov[niov].iov_len = strlen(client_ip); + niov++; + + iov[niov].iov_base = "\n"; + iov[niov].iov_len = 1; + niov++; + + /* Server address */ + iov[niov].iov_base = "Server-Address: "; + iov[niov].iov_len = 16; + niov++; + + iov[niov].iov_base = (void *) server_ip; + iov[niov].iov_len = strlen(server_ip); + niov++; + + memset(server_port_str, '\0', sizeof(server_port_str)); + res = snprintf(server_port_str, sizeof(server_port_str)-1, ":%d\n", + server_port); + iov[niov].iov_base = server_port_str; + iov[niov].iov_len = res; + niov++; + + /* User */ + if (session.user) { + iov[niov].iov_base = "User: "; + iov[niov].iov_len = 6; + niov++; + + iov[niov].iov_base = session.user; + iov[niov].iov_len = strlen(session.user); + niov++; + + iov[niov].iov_base = "\n"; + iov[niov].iov_len = 1; + niov++; + } + + /* Raw bytes in */ + iov[niov].iov_base = "Raw-Bytes-In: "; + iov[niov].iov_len = 14; + niov++; + + if (session.total_raw_in == 0) { + iov[niov].iov_base = "0\n"; + iov[niov].iov_len = 2; + niov++; + + } else { + memset(raw_bytes_in_str, '\0', sizeof(raw_bytes_in_str)); + res = snprintf(raw_bytes_in_str, sizeof(raw_bytes_in_str)-1, + "%" PR_LU "\n", (pr_off_t) session.total_raw_in); + iov[niov].iov_base = raw_bytes_in_str; + iov[niov].iov_len = res; + niov++; + } + + /* Raw bytes out */ + iov[niov].iov_base = "Raw-Bytes-Out: "; + iov[niov].iov_len = 15; + niov++; + + if (session.total_raw_out == 0) { + iov[niov].iov_base = "0\n"; + iov[niov].iov_len = 2; + niov++; + + } else { + memset(raw_bytes_out_str, '\0', sizeof(raw_bytes_out_str)); + res = snprintf(raw_bytes_out_str, sizeof(raw_bytes_out_str)-1, + "%" PR_LU "\n", (pr_off_t) session.total_raw_out); + iov[niov].iov_base = raw_bytes_out_str; + iov[niov].iov_len = res; + niov++; + } + + /* Total bytes in */ + iov[niov].iov_base = "Total-Bytes-In: "; + iov[niov].iov_len = 16; + niov++; + + if (session.total_bytes_in == 0) { + iov[niov].iov_base = "0\n"; + iov[niov].iov_len = 2; + niov++; + + } else { + memset(total_bytes_in_str, '\0', sizeof(total_bytes_in_str)); + res = snprintf(total_bytes_in_str, sizeof(total_bytes_in_str)-1, + "%" PR_LU "\n", (pr_off_t) session.total_bytes_in); + iov[niov].iov_base = total_bytes_in_str; + iov[niov].iov_len = res; + niov++; + } + + /* Total bytes out */ + iov[niov].iov_base = "Total-Bytes-Out: "; + iov[niov].iov_len = 17; + niov++; + + if (session.total_bytes_out == 0) { + iov[niov].iov_base = "0\n"; + iov[niov].iov_len = 2; + niov++; + + } else { + memset(total_bytes_out_str, '\0', sizeof(total_bytes_out_str)); + res = snprintf(total_bytes_out_str, sizeof(total_bytes_out_str)-1, + "%" PR_LU "\n", (pr_off_t) session.total_bytes_out); + iov[niov].iov_base = total_bytes_out_str; + iov[niov].iov_len = res; + niov++; + } + + /* Total files in */ + iov[niov].iov_base = "Total-Files-In: "; + iov[niov].iov_len = 16; + niov++; + + if (session.total_files_in == 0) { + iov[niov].iov_base = "0\n"; + iov[niov].iov_len = 2; + niov++; + + } else { + memset(total_files_in_str, '\0', sizeof(total_files_in_str)); + res = snprintf(total_files_in_str, sizeof(total_files_in_str)-1, + "%u\n", session.total_files_in); + iov[niov].iov_base = total_files_in_str; + iov[niov].iov_len = res; + niov++; + } + + /* Total files out */ + iov[niov].iov_base = "Total-Files-Out: "; + iov[niov].iov_len = 17; + niov++; + + if (session.total_files_out == 0) { + iov[niov].iov_base = "0\n"; + iov[niov].iov_len = 2; + niov++; + + } else { + memset(total_files_out_str, '\0', sizeof(total_files_out_str)); + res = snprintf(total_files_out_str, sizeof(total_files_out_str)-1, + "%u\n", session.total_files_out); + iov[niov].iov_base = total_files_out_str; + iov[niov].iov_len = res; + niov++; + } + + iov[niov].iov_base = "\n"; + iov[niov].iov_len = 1; + niov++; + + res = writev(forensic_logfd, iov, niov); +} + +static void forensic_write_msgs(unsigned int criterion) { + register unsigned int i; + unsigned int start_idx, end_idx; + int res; + const char *crit_marker; + size_t crit_markerlen; + + /* XXX An interesting optimization would be to rework this code so that + * we used writev(2) to write out the buffer as quickly as possible, + * taking IOV_MAX into account. + */ + + crit_marker = forensic_get_begin_marker(criterion, &crit_markerlen); + res = write(forensic_logfd, crit_marker, crit_markerlen); + + forensic_write_metadata(); + + /* The head of the log messages (i.e. the oldest message) is always where we + * want to place the newest message. + */ + start_idx = forensic_msg_idx; + end_idx = forensic_msg_idx - 1; + if (forensic_msg_idx == 0) { + end_idx = forensic_nmsgs - 1; + } + + i = start_idx; + while (i != end_idx) { + struct forensic_msg *fm; + + pr_signals_handle(); + + fm = forensic_msgs[i]; + if (fm != NULL) { + const char *level; + size_t level_len; + + level = forensic_get_level_str(fm->fm_log_level); + level_len = strlen(level); + + switch (fm->fm_log_type) { + case PR_LOG_TYPE_UNSPEC: + res = write(forensic_logfd, "[Unspec:", 8); + res = write(forensic_logfd, level, level_len); + res = write(forensic_logfd, "] ", 2); + break; + + case PR_LOG_TYPE_XFERLOG: + res = write(forensic_logfd, "[TransferLog:", 13); + res = write(forensic_logfd, level, level_len); + res = write(forensic_logfd, "] ", 2); + break; + + case PR_LOG_TYPE_SYSLOG: { + char pid_str[32]; + + res = write(forensic_logfd, "[syslog:", 8); + res = write(forensic_logfd, level, level_len); + + /* syslogd normally adds the PID; we thus need to add the PID in + * here as well, to aid in the correlation of these log lines + * with other tools/diagnostics. + */ + res = write(forensic_logfd, ", PID ", 6); + + memset(pid_str, '\0', sizeof(pid_str)); + res = snprintf(pid_str, sizeof(pid_str)-1, "%lu", + (unsigned long) (session.pid ? session.pid : getpid())); + res = write(forensic_logfd, pid_str, res); + + res = write(forensic_logfd, "] ", 2); + break; + } + + case PR_LOG_TYPE_SYSTEMLOG: + res = write(forensic_logfd, "[SystemLog:", 11); + res = write(forensic_logfd, level, level_len); + res = write(forensic_logfd, "] ", 2); + break; + + case PR_LOG_TYPE_EXTLOG: + res = write(forensic_logfd, "[ExtendedLog:", 13); + res = write(forensic_logfd, level, level_len); + res = write(forensic_logfd, "] ", 2); + break; + + case PR_LOG_TYPE_TRACELOG: + res = write(forensic_logfd, "[TraceLog:", 10); + res = write(forensic_logfd, level, level_len); + res = write(forensic_logfd, "] ", 2); + break; + } + + res = write(forensic_logfd, fm->fm_msg, fm->fm_msglen); + while (res < 0) { + if (errno == EINTR) { + pr_signals_handle(); + res = write(forensic_logfd, fm->fm_msg, fm->fm_msglen); + continue; + } + } + + /* syslog-type messages don't have a newline appended to them, since + * syslogd handles that. So we then need to add our own newline here. + */ + if (fm->fm_log_type == PR_LOG_TYPE_SYSLOG) { + res = write(forensic_logfd, "\n", 1); + } + + if (fm->fm_pool_msgno == forensic_msgs_per_pool) { + destroy_pool(fm->fm_pool); + } + + forensic_msgs[i] = NULL; + } + + i++; + if (i == forensic_nmsgs) { + /* Wrap around */ + i = 0; + } + } + + crit_marker = forensic_get_end_marker(criterion, &crit_markerlen); + res = write(forensic_logfd, crit_marker, crit_markerlen); +} + +/* Configuration handlers + */ + +/* usage: ForensicLogBufferSize count */ +MODRET set_forensiclogbuffersize(cmd_rec *cmd) { + config_rec *c; + unsigned int count; + char *ptr = NULL; + + CHECK_ARGS(cmd, 1); + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); + + count = strtoul(cmd->argv[1], &ptr, 10); + if (ptr && *ptr) { + CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "badly formatted number: ", + cmd->argv[1], NULL)); + } + + if (count == 0) { + CONF_ERROR(cmd, "size must be greater than zero"); + } + + c = add_config_param(cmd->argv[0], 1, NULL); + c->argv[0] = palloc(c->pool, sizeof(unsigned int)); + *((unsigned int *) c->argv[0]) = count; + + return PR_HANDLED(cmd); +} + +/* usage: ForensicLogCapture type1 ... typeN */ +MODRET set_forensiclogcapture(cmd_rec *cmd) { + config_rec *c; + int unspec_listen = FALSE; + int xferlog_listen = FALSE; + int syslog_listen = FALSE; + int systemlog_listen = FALSE; + int extlog_listen = FALSE; + int tracelog_listen = FALSE; + register unsigned int i; + + if (cmd->argc-1 < 1) { + CONF_ERROR(cmd, "wrong number of parameters"); + } + + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); + + for (i = 1; i < cmd->argc; i++) { + if (strncasecmp(cmd->argv[i], "Unspec", 7) == 0 || + strncasecmp(cmd->argv[i], "Unknown", 8) == 0) { + unspec_listen = TRUE; + + } else if (strncasecmp(cmd->argv[i], "TransferLog", 12) == 0) { + xferlog_listen = TRUE; + + } else if (strncasecmp(cmd->argv[i], "Syslog", 7) == 0) { + syslog_listen = TRUE; + + } else if (strncasecmp(cmd->argv[i], "SystemLog", 10) == 0) { + systemlog_listen = TRUE; + + } else if (strncasecmp(cmd->argv[i], "ExtendedLog", 12) == 0) { + extlog_listen = TRUE; + + } else if (strncasecmp(cmd->argv[i], "TraceLog", 9) == 0) { + tracelog_listen = TRUE; + + } else { + CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown log type: ", + cmd->argv[i], NULL)); + } + } + + c = add_config_param(cmd->argv[0], 6, NULL, NULL, NULL, NULL, NULL, NULL); + c->argv[0] = palloc(c->pool, sizeof(int)); + *((int *) c->argv[0]) = unspec_listen; + c->argv[1] = palloc(c->pool, sizeof(int)); + *((int *) c->argv[1]) = xferlog_listen; + c->argv[2] = palloc(c->pool, sizeof(int)); + *((int *) c->argv[2]) = syslog_listen; + c->argv[3] = palloc(c->pool, sizeof(int)); + *((int *) c->argv[3]) = systemlog_listen; + c->argv[4] = palloc(c->pool, sizeof(int)); + *((int *) c->argv[4]) = extlog_listen; + c->argv[5] = palloc(c->pool, sizeof(int)); + *((int *) c->argv[5]) = tracelog_listen; + + return PR_HANDLED(cmd); +} + +/* usage: ForensicLogCriteria ... */ +MODRET set_forensiclogcriteria(cmd_rec *cmd) { + config_rec *c; + unsigned long criteria = 0UL; + register unsigned int i; + + if (cmd->argc-1 < 1) { + CONF_ERROR(cmd, "wrong number of parameters"); + } + + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); + + /* Possible criteria: + * + * FailedLogin + * ModuleConfig + * UntimelyDeath + */ + + for (i = 1; i < cmd->argc; i++) { + if (strncasecmp(cmd->argv[i], "FailedLogin", 12) == 0) { + criteria |= FORENSIC_CRIT_FAILED_LOGIN; + + } else if (strncasecmp(cmd->argv[i], "ModuleConfig", 13) == 0) { + criteria |= FORENSIC_CRIT_MODULE_CONFIG; + + } else if (strncasecmp(cmd->argv[i], "UntimelyDeath", 14) == 0) { + criteria |= FORENSIC_CRIT_UNTIMELY_DEATH; + + } else { + CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown criterion: ", + cmd->argv[i], NULL)); + } + } + + c = add_config_param(cmd->argv[0], 1, NULL); + c->argv[0] = palloc(c->pool, sizeof(unsigned long)); + *((unsigned long *) c->argv[0]) = criteria; + + return PR_HANDLED(cmd); +} + +/* usage: ForensicLogEngine on|off */ +MODRET set_forensiclogengine(cmd_rec *cmd) { + config_rec *c; + int bool; + + CHECK_ARGS(cmd, 1); + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); + + bool = get_boolean(cmd, 1); + if (bool == -1) { + CONF_ERROR(cmd, "expected Boolean parameter"); + } + + c = add_config_param(cmd->argv[0], 1, NULL); + c->argv[0] = palloc(c->pool, sizeof(int)); + *((int *) c->argv[0]) = bool; + + return PR_HANDLED(cmd); +} + +/* usage: ForensicLogFile path */ +MODRET set_forensiclogfile(cmd_rec *cmd) { + CHECK_ARGS(cmd, 1); + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); + + if (pr_fs_valid_path(cmd->argv[1]) < 0) { + CONF_ERROR(cmd, "must be an absolute path"); + } + + (void) add_config_param_str(cmd->argv[0], 1, cmd->argv[1]); + return PR_HANDLED(cmd); +} + +/* Command handlers + */ + +MODRET forensic_pass_err(cmd_rec *cmd) { + if (!forensic_engine) { + return PR_DECLINED(cmd); + } + + if (forensic_criteria & FORENSIC_CRIT_FAILED_LOGIN) { + forensic_write_msgs(FORENSIC_CRIT_FAILED_LOGIN); + } + + return PR_DECLINED(cmd); +} + +/* Event Listeners + */ + +static void forensic_exit_ev(const void *event_data, void *user_data) { + + switch (session.disconnect_reason) { + case PR_SESS_DISCONNECT_SIGNAL: + if (forensic_criteria & FORENSIC_CRIT_UNTIMELY_DEATH) { + forensic_write_msgs(FORENSIC_CRIT_UNTIMELY_DEATH); + } + break; + + case PR_SESS_DISCONNECT_MODULE_ACL: + if (forensic_criteria & FORENSIC_CRIT_MODULE_CONFIG) { + forensic_write_msgs(FORENSIC_CRIT_MODULE_CONFIG); + } + break; + } + + return; +} + +static void forensic_log_ev(const void *event_data, void *user_data) { + const pr_log_event_t *le; + + le = event_data; + forensic_add_msg(le->log_type, le->log_level, le->log_msg, le->log_msglen); +} + +#if defined(PR_SHARED_MODULE) +static void forensic_mod_unload_ev(const void *event_data, void *user_data) { + if (strcmp("mod_log_forensic.c", (const char *) event_data) == 0) { + pr_event_unregister(&log_forensic_module, NULL, NULL); + } +} +#endif /* PR_SHARED_MODULE */ + +/* Module Initialization + */ + +static int forensic_init(void) { +#if defined(PR_SHARED_MODULE) + pr_event_register(&log_forensic_module, "core.module-unload", + forensic_mod_unload_ev, NULL); +#endif /* PR_SHARED_MODULE */ + + return 0; +} + +static int forensic_sess_init(void) { + config_rec *c; + int unspec_listen = TRUE; + int xferlog_listen = TRUE; + int syslog_listen = TRUE; + int systemlog_listen = TRUE; + int extlog_listen = TRUE; + int tracelog_listen = TRUE; + int res, xerrno; + + /* Is this module enabled? */ + c = find_config(main_server->conf, CONF_PARAM, "ForensicLogEngine", FALSE); + if (c == NULL) { + return 0; + + } else { + forensic_engine = *((int *) c->argv[0]); + if (forensic_engine != TRUE) { + return 0; + } + } + + /* Do we have the required path for our logging? */ + c = find_config(main_server->conf, CONF_PARAM, "ForensicLogFile", FALSE); + if (c == NULL) { + pr_log_debug(DEBUG1, MOD_LOG_FORENSIC_VERSION + ": missing required ForensicLogFile setting, disabling module"); + return 0; + } + + pr_signals_block(); + PRIVS_ROOT + res = pr_log_openfile((const char *) c->argv[0], &forensic_logfd, 0640); + xerrno = errno; + PRIVS_RELINQUISH + pr_signals_unblock(); + + if (res < 0) { + const char *path; + + path = c->argv[0]; + + if (res == -1) { + pr_log_pri(PR_LOG_NOTICE, MOD_LOG_FORENSIC_VERSION + ": notice: unable to open ForensicLogFile '%s': %s", path, + strerror(xerrno)); + + } else if (res == PR_LOG_WRITABLE_DIR) { + pr_log_pri(PR_LOG_NOTICE, MOD_LOG_FORENSIC_VERSION + ": notice: unable to open ForensicLogFile '%s': parent directory is " + "world-writable", path); + + } else if (res == PR_LOG_SYMLINK) { + pr_log_pri(PR_LOG_NOTICE, MOD_LOG_FORENSIC_VERSION + ": notice: unable to open ForensicLogFile '%s': " + "cannot log to a symlink", path); + } + + pr_log_debug(DEBUG1, MOD_LOG_FORENSIC_VERSION + ": unable to ForensicLogFile '%s', disabling module", path); + return 0; + } + + /* Are there any log types for which we shouldn't be listening? */ + c = find_config(main_server->conf, CONF_PARAM, "ForensicLogCapture", FALSE); + if (c) { + unspec_listen = *((int *) c->argv[0]); + xferlog_listen = *((int *) c->argv[1]); + syslog_listen = *((int *) c->argv[2]); + systemlog_listen = *((int *) c->argv[3]); + extlog_listen = *((int *) c->argv[4]); + tracelog_listen = *((int *) c->argv[5]); + } + + /* What criteria are we to use for logging our captured log messages */ + c = find_config(main_server->conf, CONF_PARAM, "ForensicLogCriteria", FALSE); + if (c) { + forensic_criteria = *((unsigned long *) c->argv[0]); + } + + forensic_pool = make_sub_pool(session.pool); + pr_pool_tag(forensic_pool, MOD_LOG_FORENSIC_VERSION); + + c = find_config(main_server->conf, CONF_PARAM, "ForensicLogBufferSize", + FALSE); + if (c) { + forensic_nmsgs = *((unsigned int *) c->argv[0]); + + if (forensic_nmsgs < forensic_msgs_per_pool) { + forensic_msgs_per_pool = forensic_nmsgs; + } + } + + forensic_msgs = pcalloc(forensic_pool, + sizeof(struct forensic_msg) * forensic_nmsgs); + forensic_subpool = pr_pool_create_sz(forensic_pool, 256); + + /* We register our event listeners as the last thing we do. */ + + if ((forensic_criteria & FORENSIC_CRIT_MODULE_CONFIG) || + (forensic_criteria & FORENSIC_CRIT_UNTIMELY_DEATH)) { + pr_event_register(&log_forensic_module, "core.exit", forensic_exit_ev, + NULL); + } + + if (unspec_listen) { + pr_event_register(&log_forensic_module, "core.log.unspec", forensic_log_ev, + NULL); + } + + if (xferlog_listen) { + pr_event_register(&log_forensic_module, "core.log.xferlog", forensic_log_ev, + NULL); + } + + if (syslog_listen) { + pr_event_register(&log_forensic_module, "core.log.syslog", forensic_log_ev, + NULL); + } + + if (systemlog_listen) { + pr_event_register(&log_forensic_module, "core.log.systemlog", + forensic_log_ev, NULL); + } + + if (extlog_listen) { + pr_event_register(&log_forensic_module, "core.log.extlog", forensic_log_ev, + NULL); + } + + if (tracelog_listen) { + pr_event_register(&log_forensic_module, "core.log.tracelog", + forensic_log_ev, NULL); + } + + return 0; +} + +/* Module API tables + */ + +static conftable forensic_conftab[] = { + { "ForensicLogBufferSize", set_forensiclogbuffersize, NULL }, + { "ForensicLogCapture", set_forensiclogcapture, NULL }, + { "ForensicLogCriteria", set_forensiclogcriteria, NULL }, + { "ForensicLogEngine", set_forensiclogengine, NULL }, + { "ForensicLogFile", set_forensiclogfile, NULL }, + + { NULL, NULL, NULL } +}; + +static cmdtable forensic_cmdtab[] = { + { LOG_CMD_ERR, C_PASS, G_NONE, forensic_pass_err, FALSE, FALSE }, + + { 0, NULL } +}; + +module log_forensic_module = { + /* Always NULL */ + NULL, NULL, + + /* Module API version */ + 0x20, + + /* Module name */ + "log_forensic", + + /* Module configuration directive table */ + forensic_conftab, + + /* Module command handler table */ + forensic_cmdtab, + + /* Module auth handler table */ + NULL, + + /* Module initialization */ + forensic_init, + + /* Session initialization */ + forensic_sess_init, + + /* Module version */ + MOD_LOG_FORENSIC_VERSION +}; + diff --git a/doc/contrib/index.html b/doc/contrib/index.html index faaf96b3a5..c70f53aab3 100644 --- a/doc/contrib/index.html +++ b/doc/contrib/index.html @@ -1,4 +1,4 @@ - + @@ -61,6 +61,11 @@
For executing external commands based on configurable criteria
+

+

The mod_geoip module +
For looking up geographic information based on client IP address +
+

The mod_ifsession module
For adding per-user/group/Class sections to your @@ -78,6 +83,11 @@
For configuring server availability based on system load
+

+

The mod_log_forensic module +
For writing log messages only when configurable criteria are met +
+

The mod_qos module
For configuring site-specific Quality of Service (QOS) packet values @@ -235,7 +245,7 @@


-Last Updated: $Date: 2012-05-14 20:23:20 $
+Last Updated: $Date: 2013-01-25 00:04:14 $

diff --git a/doc/contrib/mod_log_forensic.html b/doc/contrib/mod_log_forensic.html new file mode 100644 index 0000000000..faea381cfa --- /dev/null +++ b/doc/contrib/mod_log_forensic.html @@ -0,0 +1,302 @@ + + + + + +ProFTPD module mod_log_forensic + + + + +
+
+

ProFTPD module mod_log_forensic

+
+

+ +

+The mod_log_forensic module "captures" log messages generated +by proftpd, for all sorts of log destinations, even if those +log messages would otherwise not be written out, and buffers them +in memory. When certain criteria are met (e.g. failed logins, +segfaults, etc), the mod_log_forensic module will flush +the buffered log messages out to a file. Installation instructions are +discussed here. + +

+The most current version of mod_log_forensic can be found at: +

+  http://www.castaglia.org/proftpd/
+
+ +

Author

+

+Please contact TJ Saunders <tj at castaglia.org> with any +questions, concerns, or suggestions regarding this module. + +

Directives

+ + +

+


+

ForensicLogBufferSize

+Syntax: ForensicLogBufferSize count
+Default: 1024
+Context: server config, <VirtualHost>, <Global>
+Module: mod_log_forensic
+Compatibility: 1.3.4rc3 and later + +

+The ForensicLogBufferSize directives configures the count +of log messages that mod_log_forensic will buffer. It is +effectively the count of the last count log messages you wish to +see logged, when one of the +ForensicLogCriteria are met. + +

+


+

ForensicLogCapture

+Syntax: ForensicLogCapture log-type1 ...
+Default: Unspec TransferLog syslog SystemLog ExtendedLog TraceLog
+Context: server config, <VirtualHost>, <Global>
+Module: mod_log_forensic
+Compatibility: 1.3.4rc3 and later + +

+The ForensicLogCapture directive configures which log types +the mod_log_forensic module "captures" for later writing. By +default, mod_log_forensic captures messages for all log types. + +

+The supported log types are: +

    +
  • Unspec +

    + This "type" covers any unspecified/unknown log, e.g. module-specific + log files such as SFTPLog, SQLLogFile, + TLSLog, etc. +

  • + +

    +

  • TransferLog +
  • syslog +
  • SystemLog +
  • ExtendedLog +
  • TraceLog +
+ +

+


+

ForensicLogCriteria

+Syntax: ForensicLogCriteria criterion1 ...
+Default: FailedLogin UntimelyDeath
+Context: server config, <VirtualHost>, <Global>
+Module: mod_log_forensic
+Compatibility: 1.3.4rc3 and later + +

+The ForensicLogCriteria directive configures the criteria +which determine when mod_log_forensic will flush its buffered +log messages out to the configured +ForensicLogFile. Multiple +criteria can be specified. + +

+The currently supported criteria are: +

    +
  • FailedLogin +

    + The buffered log messages will be written to the + ForensicLogFile if the login fails for any reason. +

  • + +

    +

  • UntimelyDeath +

    + If a session dies prematurely, e.g. due to a segfault or other + internal error, the buffered log messages will be written to the + ForensicLogFile. +

  • + +

    +

  • ModuleConfig +

    + If a session dies due to a module-specific policy, the buffered log + messages will be written to the ForensicLogFile. +

  • +
+ +

+


+

ForensicLogEngine

+Syntax: ForensicLogEngine on|off
+Default: off
+Context: server config, <VirtualHost>, <Global>
+Module: mod_log_forensic
+Compatibility: 1.3.4rc3 and later + +

+The ForensicLogEngine directive enables or disables the +mod_log_forensic module. + +

+


+

ForensicLogFile

+Syntax: ForensicLogFile file
+Default: None
+Context: server config, <VirtualHost>, <Global>
+Module: mod_log_forensic
+Compatibility: 1.3.4rc3 and later + +

+The ForensicLogFile directive configures a file used for logging +by mod_log_forensic. The configured file must be an +absolute path. + +

+Note that this directive is required for +mod_log_forensic to function properly. + +

+


+

Installation

+To install mod_log_forensic, copy the +mod_log_forensic.c into the third-party module directory in +the proftpd source code: +
+  # cp mod_log_forensic.c proftpd-dir/contrib/
+
+after unpacking the latest proftpd-1.3.x source code. For including +mod_log_forensic as a staticly linked module: +
+  ./configure --with-modules=mod_log_forensic ...
+
+Alternatively, mod_log_forensic can be built as a DSO module: +
+  ./configure --enable-dso --with-shared=mod_log_forensic ...
+
+Then follow the usual steps: +
+  make
+  make install
+
+ +

+


+

Usage

+ +

+Example mod_log_forensic configuration: +

+  <IfModule mod_log_forensic.c>
+    ForensicLogEngine on
+    ForensicLogFile /path/to/forensic.log
+  </IfModule>
+
+ +

+For a failed login, the configured ForensicLogFile will +contain a block of log lines, e.g.: +

+  -----BEGIN FAILED LOGIN FORENSICS-----
+  Client-Address: 127.0.0.1
+  Server-Address: ::ffff:127.0.0.1:5376
+  Raw-Bytes-In: 46
+  Raw-Bytes-Out: 158
+  Total-Bytes-In: 0
+  Total-Bytes-Out: 0
+  Total-Files-In: 0
+  Total-Files-Out: 0
+
+  ...
+  [syslog:7, PID 16044] dispatching CMD command 'PASS (hidden)' to mod_auth
+  [syslog:7, PID 16044] retrieved UID 1000 for user 'tj'
+  [syslog:7, PID 16044] retrieved group IDs: 1000, 0, 4, 20, 24, 46, 108, 109, 110
+  [syslog:7, PID 16044] retrieved group names: tj, root, adm, dialout, cdrom, plugdev, lpadmin, sambashare, admin
+  [syslog:7, PID 16044] ROOT PRIVS at mod_auth_pam.c:312
+  [syslog:7, PID 16044] RELINQUISH PRIVS at mod_auth_pam.c:482
+  [syslog:7, PID 16044] ROOT PRIVS at mod_auth_unix.c:467
+  [syslog:7, PID 16044] RELINQUISH PRIVS at mod_auth_unix.c:548
+  [SystemLog:5] familiar proftpd[15509] localhost (localhost[127.0.0.1]): USER tj (Login failed): Incorrect password.
+  [syslog:7, PID 16044] dispatching POST_CMD_ERR command 'PASS (hidden)' to mod_delay
+  [syslog:7, PID 16044] dispatching LOG_CMD_ERR command 'PASS (hidden)' to mod_log
+  -----END FAILED LOGIN FORENSICS-----
+
+ +For sessions which suffer an "untimely death", the begin/end markers in the +ForensicLogFile are: +
+  -----BEGIN UNTIMELY DEATH FORENSICS-----
+  Client-Address: 127.0.0.1
+  Server-Address: ::ffff:127.0.0.1:5376
+  Raw-Bytes-In: 46
+  Raw-Bytes-Out: 158
+  Total-Bytes-In: 0
+  Total-Bytes-Out: 0
+  Total-Files-In: 0
+  Total-Files-Out: 0
+
+  ...
+  -----END UNTIMELY DEATH FORENSICS-----
+
+ +

+Advantages
+What's the big deal with this module? What advantage does it provide +over the normal proftpd logging? The advantage is that with +mod_log_forensic, you do not have to configure ProFTPD to use +verbose logging (i.e. high DebugLevel and/or +Trace levels). If ProFTPD generated a log message internally +but that log message was filtered, then that log message would not +normally be written to disk -- but mod_log_forensic buffers +that log message anyway. + +

+To see this, simply use the following in your proftpd.conf: +

+  TraceLog /path/to/ftpd/trace.log
+  Trace DEFAULT:0
+
+  <IfModule mod_log_forensic.c>
+    ForensicLogEngine on
+    ForensicLogFile /path/to/ftpd/forensic.log
+  </IfModule>
+
+This configured proftpd for trace logging, but turns the +trace logging levels down to zero so that normally, nothing would be written +in the configured TraceLog file. + +

+Now attempt to log into proftpd, deliberately using a wrong/bad +password (or unknown user). The mod_log_forensic module will +write out all of the trace logging messages +(and the other SystemLog/syslog messages) to the +ForensicLogFile, even though the debug level is at the default +level of zero, and the trace levels are all zero. Thus you get the verbose +logging needed to help diagnose failed logins and such, without having +the verbose logging enabled all of the time. + +

+



+ +Author: $Author: castaglia $
+Last Updated: $Date: 2013-01-25 00:04:14 $
+ +

+ + +© Copyright 2011 TJ Saunders
+ All Rights Reserved
+
+ +

+ + + +