/*
* Copyright (C) 2004 Liam Widdowson (liam@inodes.org)
*
* smtptrapd - The SMTP Trap Daemon - Version 1.3
*
* Last updated: Tue Jul 4 00:02:47 EDT 2006
*
* 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.
*/
#include <stdio.h>
#include <pthread.h>
#include <pwd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <errno.h>
#include <string.h>
#include <sysexits.h>
#include <syslog.h>
#include <signal.h>
#ifdef __linux__
#include <getopt.h>
#endif
#define LISTEN_PORT 25
#define NUM_CONSUMER 10
#define UNPRIV_USER "nobody"
#define LOG_SIZ 80
#define SMTP_STATE_NONE 0x0
#define SMTP_STATE_HELO 0x1
#define SMTP_STATE_FROM 0x2
#define SMTP_STATE_RCPT 0x4
#define SMTP_STATE_QUIT 0x8
#define SMTP_CMD_MAX 50
#define SMTP_CMD_TOUT 60
#define SMTP_TIME_MAX 120
#define MAX_QUEUE_LEN 100
#define SMTP_MSG_BANNERBYE "421 %s Service Unavailable\r\n"
#define SMTP_MSG_BANNER "220 %s ESMTP Service Ready\r\n"
#define SMTP_MSG_GOODBYE "221 Closing connection\r\n"
#define SMTP_MSG_BADBYE "421 Closing connection\r\n"
#define SMTP_MSG_OK "250 OK\r\n"
#define SMTP_MSG_TRYAGAIN "451 Try again later\r\n"
#define SMTP_MSG_INVALID "501 Invalid syntax\r\n"
typedef struct {
int **fd; /* array of queue elements to be worked upon */
int fd_num; /* number of elements in the queue array */
int work; /* predicate to indicate is work to be done */
pthread_cond_t cond; /* the condition for consumers to wait upon */
pthread_mutex_t mutex; /* the associated mutex */
} cqueue_t;
typedef struct {
char *chroot_dir;
char *banner;
char *username;
struct in_addr local_ip;
short bind_local;
int port;
int max_queue_len;
int threads;
} config_t;
cqueue_t queue;
config_t config;
void chomp(char *buf, size_t len) {
size_t x;
for (x = 0; x < len; x++) {
if ((buf[x] == '\r') || (buf[x] == '\n')) buf[x] = '\0';
}
}
/* a function to bind to a particular TCP port */
int bind_proc(void) {
struct sockaddr_in saddr;
int s;
int one = 1;
saddr.sin_family = AF_INET;
if (config.bind_local) {
saddr.sin_addr.s_addr = config.local_ip.s_addr;
} else {
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
}
saddr.sin_port = htons(config.port);
if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1)
return -1;
if (setsockopt(s, SOL_SOCKET,SO_REUSEADDR, &one, sizeof(one)) == -1)
return -1;
if (bind(s, (struct sockaddr *) &saddr, sizeof(saddr)) == -1)
return -1;
if (listen(s, 20) == -1)
return -1;
return s;
}
int drop_priv(void) {
struct passwd *p;
p = getpwnam((config.username == NULL) ?
UNPRIV_USER : config.username);
if (p == NULL) {
syslog(LOG_CRIT, "error: could not find uid for %s",
(config.username == NULL) ? UNPRIV_USER : config.username);
return -1;
}
if (config.chroot_dir != NULL) {
if (chdir(config.chroot_dir) != 0)
{
syslog(LOG_CRIT, "error: could not chdir to %s",
config.chroot_dir);
return -1;
}
if (chroot(config.chroot_dir) != 0)
{
syslog(LOG_CRIT, "error: could not chroot to %s",
config.chroot_dir);
return -1;
}
}
if (setgid(p->pw_gid) != 0) {
syslog(LOG_CRIT, "error: could not find setgid for %s",
(config.username == NULL) ? UNPRIV_USER : config.username);
return -1;
}
if (setuid(p->pw_uid) != 0) {
syslog(LOG_CRIT, "error: could not find setgid for %s",
(config.username == NULL) ? UNPRIV_USER : config.username);
return -1;
}
return 0;
}
void producer(int s) {
struct sockaddr_in caddr;
size_t clen = sizeof(struct sockaddr_in);
int ns = 0;
do {
while ((ns = accept(s, (struct sockaddr *) &caddr, &clen)) != -1) {
/* lock the queue to add an item */
pthread_mutex_lock(&queue.mutex);
/* check to see if the queue is large and if so, simply
* politely drop the connection to avoid DoS attacks */
if (queue.fd_num > config.max_queue_len) {
char buf[BUFSIZ];
struct sockaddr_in addr;
int addrlen = sizeof(addr);
pthread_mutex_unlock(&queue.mutex);
snprintf(buf, BUFSIZ-1, SMTP_MSG_BANNERBYE,
config.banner);
buf[BUFSIZ-1] = '\0';
if (getpeername(ns, (struct sockaddr *) &addr,
&addrlen) < 0)
{
syslog(LOG_INFO,
"dropping inbound connection due to"
" excessive queue size");
} else {
syslog(LOG_INFO,
"dropping inbound connection from: %s"
" due to excessive queue size",
inet_ntoa(addr.sin_addr));
}
write(ns, buf, strlen(buf));
close(ns);
continue;
}
/* increase the number of items */
queue.fd_num++;
/* indiciate that there is work to be done */
queue.work++;
/* add an item to the queue */
queue.fd = realloc(queue.fd, (queue.fd_num * sizeof(int *))+1);
if (queue.fd == NULL) {
syslog(LOG_CRIT, "error: failed to allocate memory");
exit(EX_OSERR);
}
queue.fd[queue.fd_num] = malloc(sizeof(int));
if (queue.fd[queue.fd_num] == NULL) {
syslog(LOG_CRIT, "error: failed to allocate memory");
exit(EX_OSERR);
}
/* set the value of the queue element to the fd */
*queue.fd[queue.fd_num] = ns;
/* unlock the queue as the addition has finished */
pthread_mutex_unlock(&queue.mutex);
/* wake a thread */
pthread_cond_signal(&queue.cond);
}
} while (errno == ECONNABORTED);
syslog(LOG_CRIT, "error: accept failed: %s; shutting down",
strerror(errno));
return;
}
int read_t(int fd, char *buf, size_t len) {
int maxfdp1 = fd + 1;
struct timeval tm = { SMTP_CMD_TOUT, 0 };
fd_set rset;
FD_ZERO(&rset);
FD_SET(fd, &rset);
if (select(maxfdp1, &rset, NULL, NULL, &tm) != -1) {
if (FD_ISSET(fd, &rset)) {
return recv(fd, buf, len, 0);
} else {
return -2;
}
}
return 0;
}
void smtp_close(int fd, char *type) {
write(fd, type, strlen(type));
close(fd);
}
void smtp_process(int fd) {
int n;
int state = SMTP_STATE_NONE;
char buf[BUFSIZ], from[LOG_SIZ], rcpt[LOG_SIZ];
struct sockaddr_in addr;
int addrlen = sizeof(addr);
int i = 0, etime = 0, itime = time(NULL);
/* some nice initialisation of strings */
strcpy(from, "none");
strcpy(rcpt, "none");
if (getpeername(fd, (struct sockaddr *) &addr, &addrlen) < 0) {
smtp_close(fd, SMTP_MSG_BADBYE);
return;
}
/* send the banner */
snprintf(buf, BUFSIZ-1, SMTP_MSG_BANNER, config.banner);
buf[BUFSIZ-1] = '\0';
if (write(fd, buf, strlen(buf)) <= 0) {
smtp_close(fd, SMTP_MSG_BADBYE);
return;
}
while (state != SMTP_STATE_QUIT) {
/* do not let the spammers tie up a thread forever */
if (i > SMTP_CMD_MAX) {
smtp_close(fd, SMTP_MSG_BADBYE);
break;
}
etime = time(NULL);
if (etime > (itime + SMTP_TIME_MAX)) {
smtp_close(fd, SMTP_MSG_BADBYE);
break;
}
/* wait for a smtp command */
n = read_t(fd, buf, BUFSIZ-1);
if (n <= 0) {
smtp_close(fd, SMTP_MSG_BADBYE);
break;
}
if (n < 4) {
/* string can not possibly be a real SMTP verb */
if (write(fd, SMTP_MSG_INVALID, strlen(SMTP_MSG_INVALID)) <= 0) {
smtp_close(fd, SMTP_MSG_BADBYE);
break;
}
continue;
}
/* strip out the CRLF characters and NULL terminate
* the string */
buf[n] = '\0';
chomp(buf, n);
i++;
if ((strncasecmp(buf, "HELO", 4) == 0 ||
strncasecmp(buf, "EHLO", 4) == 0) &&
state == SMTP_STATE_NONE)
{
state = SMTP_STATE_HELO;
if (write(fd, SMTP_MSG_OK, strlen(SMTP_MSG_OK)) <= 0) {
smtp_close(fd, SMTP_MSG_BADBYE);
break;
}
}
else if (strncasecmp(buf, "MAIL FROM", 9) == 0 &&
state == SMTP_STATE_HELO)
{
state = SMTP_STATE_FROM;
if (write(fd, SMTP_MSG_OK, strlen(SMTP_MSG_OK)) <= 0) {
smtp_close(fd, SMTP_MSG_BADBYE);
break;
}
/* for interesting logging */
strncpy(from, buf, LOG_SIZ-1);
from[LOG_SIZ-1] = '\0';
}
else if (strncasecmp(buf, "RCPT TO", 7) == 0 &&
(state == SMTP_STATE_FROM || state == SMTP_STATE_RCPT))
{
state = SMTP_STATE_RCPT;
if (write(fd, SMTP_MSG_TRYAGAIN, strlen(SMTP_MSG_TRYAGAIN)) <= 0) {
smtp_close(fd, SMTP_MSG_BADBYE);
break;
}
/* for interesting logging; only the last one will
* be recorded */
strncpy(rcpt, buf, LOG_SIZ-1);
rcpt[LOG_SIZ-1] = '\0';
}
else if (strncasecmp(buf, "QUIT", 4) == 0)
{
smtp_close(fd, SMTP_MSG_GOODBYE);
break;
}
else if (strncasecmp(buf, "DATA", 4) == 0 &&
state == SMTP_STATE_RCPT)
{
if (write(fd, SMTP_MSG_TRYAGAIN, strlen(SMTP_MSG_TRYAGAIN)) <= 0) {
smtp_close(fd, SMTP_MSG_BADBYE);
break;
}
}
else if (strncasecmp(buf, "NOOP", 4) == 0) {
if (write(fd, SMTP_MSG_OK, strlen(SMTP_MSG_OK)) <= 0) {
smtp_close(fd, SMTP_MSG_BADBYE);
break;
}
}
else {
if (write(fd, SMTP_MSG_INVALID, strlen(SMTP_MSG_INVALID)) <= 0) {
smtp_close(fd, SMTP_MSG_BADBYE);
break;
}
}
}
syslog(LOG_INFO, "info: connection [ip: %s; f: %s; r: %s]",
inet_ntoa(addr.sin_addr), from, rcpt);
return;
}
void *consumer(void) {
int fd;
while (1) {
/* mutex must be locked before waiting on the condition */
pthread_mutex_lock(&queue.mutex);
/* test predicate to ensure that there is work to be done */
while (queue.work < 1) {
/* wait to be called into duty */
pthread_cond_wait(&queue.cond, &queue.mutex);
}
/* returns with mutex locked */
/* grab the value of the file descriptor from the queue */
fd = *queue.fd[queue.fd_num];
/* remove the queue element */
free(queue.fd[queue.fd_num]);
queue.fd_num--;
/* indicate that there is now less work to be done */
queue.work--;
/* unlock the queue mutex */
pthread_mutex_unlock(&queue.mutex);
smtp_process(fd);
}
}
int main(int argc, char **argv) {
pthread_t t;
int s, x, c;
/* do some config init */
config.bind_local = 0;
config.chroot_dir = NULL;
config.username = NULL;
config.banner = NULL;
config.port = LISTEN_PORT;
config.threads = NUM_CONSUMER;
config.max_queue_len = MAX_QUEUE_LEN;
while ((c = getopt(argc, argv, "hc:l:p:b:u:m:t:")) != -1) {
switch (c) {
case 'h':
printf("%s: %s\n", argv[0],
"-c [chroot dir] "
"-l [tcp listen address] "
"-b [smtp banner hostname] "
"-u [username] "
"-t [number of threads] "
"-p [listen port] "
"-m [max accept queue length]");
exit(EX_USAGE);
case 'l':
if (inet_aton(optarg, &config.local_ip) == 0)
{
syslog(LOG_CRIT,
"error: invalid bind address %s",
optarg);
exit(EX_USAGE);
}
config.bind_local = 1;
break;
case 'c':
config.chroot_dir = strdup(optarg);
break;
case 'b':
config.banner = strdup(optarg);
break;
case 'u':
config.username = strdup(optarg);
break;
case 'p':
config.port = atoi(optarg);
break;
case 't':
config.threads = atoi(optarg);
break;
case 'm':
config.max_queue_len = atoi(optarg);
break;
}
}
if (config.banner == NULL) {
char hname[BUFSIZ];
memset(hname, 0, BUFSIZ);
if (gethostname(hname, BUFSIZ-1) != 0) {
syslog(LOG_CRIT,
"error: could not work out my own hostname");
exit(EX_OSERR);
}
config.banner = strdup(hname);
if (config.banner == NULL) {
syslog(LOG_CRIT, "error: unable to duplicate string");
exit(EX_OSERR);
}
}
/* try to bind to the port here as we will most likely need
* to be root */
if ((s = bind_proc()) == -1) {
syslog(LOG_CRIT, "error: failed to bind to port %d: %s",
config.port, strerror(errno));
exit(EX_OSERR);
}
/* Patch from Shane DeRidder: ignore SIGPIPE from abnormally
* closed connections
*/
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
/* background the process */
if (fork()) exit(0);
if (fork()) exit(0);
#ifdef __FreeBSD__
setpgrp(0, 0);
#else
setpgrp();
#endif
openlog("smtptrapd", LOG_PID, LOG_MAIL);
/* drop privileges */
if (drop_priv() == -1) {
close(s);
exit(EX_OSERR);
}
/* thread pool initialisation */
if (pthread_mutex_init(&queue.mutex, NULL) != 0) {
syslog(LOG_CRIT, "error: failed to initialise mutex");
close(s);
exit(EX_OSERR);
}
if (pthread_cond_init(&queue.cond, NULL) != 0) {
syslog(LOG_CRIT, "error: failed to initialise condition");
close(s);
exit(EX_OSERR);
}
queue.fd = malloc(sizeof(int *));
if (queue.fd == NULL) {
syslog(LOG_CRIT, "error: failed to allocate memory");
close(s);
exit(EX_OSERR);
}
/* initialise queue values */
queue.fd_num = -1;
queue.work = 0;
#ifdef __sun__
pthread_setconcurrency(config.threads);
#endif
/* create a number of consumer threads */
for (x = 0; x < config.threads; x++) {
if (pthread_create(&t, NULL, (void *) consumer, NULL) != 0) {
syslog(LOG_CRIT, "error: failed to create thread");
exit(EX_OSERR);
}
}
/* give the thread pool something to do */
producer(s);
return 0;
}
syntax highlighted by Code2HTML, v. 0.9.1