/* * Copyright (c) 2008 Hypertriton, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../config/version.h" #include "../config/sysconfdir.h" #include "mailblockd.h" #include "ports.h" #include "config.h" #define MBD_TOTSCORE_MAX 3.0e+38 #define MBD_COUNT_MAX (0xffffffff-1) #define MBD_LOGFILE char *mbdHome, *mbdHost, *mbdPort, *pidFile, *logFile, *mbdPassword; char *pathHosts, *pathScores; FILE *mbdLog = NULL; float minScore, maxScore; u_int maxEntries, nExpireEntries; TBL *tblHosts; /* Allowed hosts */ TBL *tblScores; /* Spam score table */ TBL_Entry **expireEntries; /* For expiration process */ u_int *expireHashes; /* For expiration process */ static char mbdError[1024]; static int mbdSocks[4]; static int mbdSockCount; static volatile int die_flag = 0; static volatile int flush_flag = 0; static volatile int chld_flag = 0; static fd_set servfds; void MBD_SetError(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vsnprintf(mbdError, sizeof(mbdError), fmt, ap); va_end(ap); } char * MBD_GetError(void) { return (mbdError); } void MBD_Fatal(const char *fmt, ...) { char err[128]; va_list ap; va_start(ap, fmt); vsnprintf(err, sizeof(err), fmt, ap); va_end(ap); fputs(err, stderr); fputc('\n', stderr); exit(1); } void * MBD_Malloc(size_t size) { void *p; if ((p = malloc(size)) == NULL) { MBD_Fatal("Out of memory"); } return (p); } #ifdef DEBUG void MBD_Debug(const char *fmt, ...) { #ifdef MBD_LOGFILE static char msg[160]; va_list ap; va_start(ap, fmt); vsnprintf(msg, sizeof(msg), fmt, ap); va_end(ap); fprintf(mbdLog, "[%d] %s\n", getpid(), msg); fflush(mbdLog); #endif } #endif /* DEBUG */ static void ReportExit(const char *what, pid_t rpid, int code) { if (WIFSIGNALED(code)) { syslog(LOG_ERR, "[%d] %s exited (signal %d)", (int)rpid, what, WTERMSIG(code)); } else if (WIFEXITED(code) && WEXITSTATUS(code) != 0) { syslog(LOG_ERR, "[%d] %s exited due to failure (%d)", (int)rpid, what, WEXITSTATUS(code)); } } static void Reap(void) { int save_errno = errno; int rpid, code; do { rpid = waitpid(-1, &code, WNOHANG); if (rpid > 0) { ReportExit("mailblockd", rpid, code); } } while (rpid > 0 || (rpid == -1 && errno == EINTR)); errno = save_errno; } static void master_sig_die(int sigraised) { die_flag = 1; } static void master_sig_flush(int sigraised) { flush_flag = 1; } static void master_sig_chld(int sigraised) { chld_flag = 1; } static void CloseFiles(void) { int i; for (i = 0; i < mbdSockCount; i++) close(mbdSocks[i]); } #if 0 void MBD_EnterServerProc(const char *name) { struct sigaction sa; CloseFiles(); sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sa.sa_handler = SIG_DFL; sigaction(SIGCHLD, &sa, NULL); sa.sa_handler = SIG_IGN; sigaction(SIGURG, &sa, NULL); sigaction(SIGHUP, &sa, NULL); sigaction(SIGPIPE, &sa, NULL); sigfillset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sa.sa_handler = child_sig_die; sigaction(SIGINT, &sa, NULL); sigaction(SIGQUIT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); sigaction(SIGUSR1, &sa, NULL); closelog(); openlog(name, LOG_PID|LOG_NDELAY, LOG_LOCAL0); Setproctitle("%s", name); } void MBD_ExitServerProc(int code) { closelog(); exit(code); } #endif #ifdef MBD_LOGFILE static void OpenLog(void) { int fd; if (mbdLog != NULL) { fclose(mbdLog); } if ((mbdLog = fopen(logFile, "a")) == NULL) { syslog(LOG_ERR, "%s: %m", logFile); fprintf(stderr, "%s: %s\n", logFile, strerror(errno)); exit(1); } fd = fileno(mbdLog); fchmod(fd, 0640); fchown(fd, 0, 20); } #endif /* MBD_LOGFILE */ static __inline__ int ProcessSignals(void) { if (die_flag) { die_flag = 0; #if 0 kill(0, SIGUSR1); #endif return (1); } if (chld_flag) { chld_flag = 0; Reap(); } if (flush_flag) { flush_flag = 0; #ifdef MBD_LOGFILE OpenLog(); #endif if (TBL_Save(tblScores, pathScores, 0600) == -1) MBD_Fatal("Saving scores: %s", MBD_GetError()); } return (0); } static int ValidIPv4(const char *s) { const char *p; int nDigits = 0; int nBlocks = 0; size_t len = strlen(s); if (len < 7 || len > 15) { return (0); } for (p = &s[0]; *p != '\0'; p++) { if (isdigit(*p)) { if (++nDigits > 3) { return (0); } } else if (*p == '.') { if (++nBlocks > 4) { return (0); } nDigits = 0; } else { return (0); } } return (1); } static void ExpireOldScores(void) { TBL_Entry *ent; u_int nOldest = 0; u_int i, j; for (i = 0; i < nExpireEntries; i++) { expireEntries[i] = NULL; } for (i = 0; i < tblScores->nbuckets; i++) { SLIST_FOREACH(ent, &tblScores->buckets[i].ents, bents) { for (j = 0; j < nExpireEntries; j++) { if (expireEntries[j] == NULL || ent->stamp < expireEntries[j]->stamp) { expireEntries[j] = ent; expireHashes[j] = i; break; } } } } MBD_Debug("Expiring %d entries", (int)nExpireEntries); for (i = 0; i < nExpireEntries; i++) { if (expireEntries[i] == NULL) { continue; } MBD_Debug("Expiring: [%d] %s (hash=%u)", i, expireEntries[i]->key, expireHashes[i]); TBL_DeleteEntry(tblScores, expireHashes[i], expireEntries[i]); } if (TBL_Save(tblScores, pathScores, 0600) == -1) MBD_Debug("Flush failed: %s", MBD_GetError()); } static int ProcessScore(const char *addr, char *data) { TBL_Entry *ent; struct timeval tv; char *auth, *rcvd, *sScore; char *s; double score; u_int h; if (TBL_Lookup(tblHosts, addr, NULL) == -1) { MBD_SetError("Unknown host: %s", addr); return (-1); } s = data; auth = strsep(&s, " "); rcvd = strsep(&s, " "); sScore = strsep(&s, " "); if (auth == NULL || rcvd == NULL || sScore == NULL || auth[0] == '\0' || rcvd[0] == '\0' || sScore[0] == '\0') { MBD_SetError("Malformed packet"); return (-1); } if (strcmp(auth, mbdPassword) != 0) { MBD_SetError("Bad password: %s", auth); return (-1); } if (!ValidIPv4(rcvd)) { MBD_SetError("Invalid IP: %s", rcvd); return (-1); } score = strtod(sScore, NULL); if (score == -HUGE_VAL || score == HUGE_VAL || score < minScore || score >= maxScore) { MBD_SetError("Illegal score: %f", score); return (-1); } h = TBL_Hash(tblScores,rcvd); if ((ent = TBL_LookupHash(tblScores, rcvd, h)) == NULL) { MBD_Debug("N %s (%f)", rcvd, score); if (tblScores->count >= maxEntries) { MBD_Debug("Maximum entries (%d), expiring", maxEntries); ExpireOldScores(); } ent = TBL_Insert(tblScores, h, rcvd); ent->count = 1; ent->scoreTot = score; ent->scoreAvg = score; } else { MBD_Debug("E %s (%d/%f)", ent->key, ent->count, ent->scoreAvg); if (ent->count < MBD_COUNT_MAX) { ent->count++; } if ((ent->scoreTot + score) > MBD_TOTSCORE_MAX) { ent->scoreTot = MBD_TOTSCORE_MAX; ent->scoreAvg = MBD_TOTSCORE_MAX/ent->count; } else { ent->scoreTot += score; ent->scoreAvg = ent->scoreTot/ent->count; } } if (gettimeofday(&tv, NULL) != -1) { ent->stamp = tv.tv_sec; } else { ent->stamp = 0; } return (0); } int main(int argc, char **argv, char **env) { struct addrinfo hints, *res, *res0; const char *cause = NULL; int rv, i, maxfd = 0; struct sigaction sa; struct stat sb; FILE *f; CFG_File *cf; if ((cf = CFG_OpenFile(SYSCONFDIR, "mailblockd.conf")) == NULL) { fprintf(stderr, "Load configuration file: %s\n", MBD_GetError()); exit(1); } CFG_GetStr(cf, "home", &mbdHome, "/var/db/mailblockd/"); CFG_GetStr(cf, "pid-file", &pidFile, "/var/run/mailblockd.pid"); CFG_GetStr(cf, "log-file", &logFile, "/var/log/mailblockd"); CFG_GetStr(cf, "hosts-file", &pathHosts, "/var/db/mailblockd/hosts"); CFG_GetStr(cf, "scores-file", &pathScores, "/var/db/mailblockd/scores"); CFG_GetStr(cf, "host", &mbdHost, "localhost"); CFG_GetStr(cf, "port", &mbdPort, "925"); CFG_GetStr(cf, "password", &mbdPassword, "secret"); CFG_GetFlt(cf, "min-score", &minScore, -300.0); CFG_GetFlt(cf, "max-score", &maxScore, +300.0); CFG_GetUint(cf, "max-entries", &maxEntries, 50000); CFG_GetUint(cf, "expire-entries", &nExpireEntries, 100); CFG_CloseFile(cf); #ifdef MBD_LOGFILE OpenLog(); #endif MBD_Debug("mailblockd %s", VERSION); syslog(LOG_INFO, "Server startup (%s)", VERSION); /* Load the tables */ if (stat(mbdHome, &sb) != 0 && mkdir(mbdHome, 0755) == -1) { MBD_SetError("mkdir %s: %s", mbdHome, strerror(errno)); goto fatal; } if ((tblHosts = TBL_Load(pathHosts, -1)) == NULL || (tblScores = TBL_Load(pathScores, maxEntries)) == NULL) { goto fatal; } expireEntries = Malloc(nExpireEntries*sizeof(TBL_Entry *)); expireHashes = Malloc(nExpireEntries*sizeof(u_int)); FD_ZERO(&servfds); /* * Set up the server listening socket(s). */ memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; hints.ai_flags = AI_PASSIVE; if ((rv = getaddrinfo(mbdHost, mbdPort, &hints, &res0)) != 0) { syslog(LOG_ERR, "%s:%s: %s", mbdHost, mbdPort, gai_strerror(rv)); goto out; } for (mbdSockCount = 0, res = res0; res != NULL && mbdSockCount < 4; res = res->ai_next) { rv = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (rv == -1) { cause = "socket"; continue; } i = 1; setsockopt(rv, SOL_SOCKET, SO_REUSEADDR, &i, (my_socklen_t)sizeof(i)); if (bind(rv, res->ai_addr, res->ai_addrlen) == -1) { cause = "bind"; close(rv); continue; } mbdSocks[mbdSockCount++] = rv; FD_SET(rv, &servfds); if (rv > maxfd) { maxfd = rv; } } if (mbdSockCount == 0) { syslog(LOG_ERR, "%s: %m", cause); goto out; } freeaddrinfo(res0); sigemptyset(&sa.sa_mask); sa.sa_handler = master_sig_chld; sa.sa_flags = SA_RESTART; sigaction(SIGCHLD, &sa, NULL); sa.sa_handler = master_sig_flush; sa.sa_flags = SA_RESTART; sigaction(SIGHUP, &sa, NULL); sa.sa_handler = SIG_IGN; sigaction(SIGUSR1, &sa, NULL); sa.sa_handler = master_sig_die; sa.sa_flags = 0; sigaction(SIGINT, &sa, NULL); sigaction(SIGQUIT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); /* Write the PID file */ if (stat(pidFile, &sb) == 0) { MBD_Debug("WARNING: Overwriting existing PID file!"); syslog(LOG_WARNING, "Overwriting %s!", pidFile); } if ((f = fopen(pidFile, "w")) == NULL) { MBD_Debug("%s: %s", pidFile, strerror(errno)); syslog(LOG_ERR, "%s: %m", pidFile); goto out_close; } fprintf(f, "%lu\n", (unsigned long)getpid()); fclose(f); for (;;) { fd_set rfds = servfds; rv = select(maxfd+1, &rfds, NULL, NULL, NULL); if (rv == -1) { if (errno == EINTR) { if (ProcessSignals() == 1) { goto out_close; } continue; } else { syslog(LOG_ERR, "select: %m; shutdown"); exit(1); } } for (i = 0; i < mbdSockCount; i++) { char buf[40+12]; struct sockaddr_in siPeer; int siPeerLen = sizeof(siPeer); ssize_t rv; if (!FD_ISSET(mbdSocks[i], &rfds)) { continue; } if ((rv = recvfrom(mbdSocks[i], buf, sizeof(buf), 0, (struct sockaddr *)&siPeer, &siPeerLen)) == -1) { syslog(LOG_ERR, "recvfrom: %m; shutdown"); exit(1); } if (rv > 0 && buf[rv-1] == '\n') { buf[rv-1] = '\0'; } else { buf[rv] = '\0'; } if (ProcessScore(inet_ntoa(siPeer.sin_addr), buf) == -1) { MBD_Debug("ProcessScore: %s", MBD_GetError()); /* syslog(LOG_ERR, "%s", MBD_GetError()); */ } } if (ProcessSignals() == 1) break; } out_close: syslog(LOG_NOTICE, "Server shutdown (signal); saving %d entries", tblScores->count); if (TBL_Save(tblScores, pathScores, 0600) == -1) syslog(LOG_ERR, "Flush failed: %s", MBD_GetError()); out: TBL_Destroy(tblHosts); TBL_Destroy(tblScores); CloseFiles(); unlink(pidFile); return (0); fatal: fprintf(stderr, "FATAL: %s\n", MBD_GetError()); syslog(LOG_CRIT, "FATAL: %s", MBD_GetError()); return (1); }