/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __linux__ #include #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; }