diff options
author | Bob Beck <beck@cvs.openbsd.org> | 2004-02-26 07:28:56 +0000 |
---|---|---|
committer | Bob Beck <beck@cvs.openbsd.org> | 2004-02-26 07:28:56 +0000 |
commit | c879576d51d8099395bf9286c8e939a3c6484839 (patch) | |
tree | 873df009cb315b2bdd3132a80971aee555209401 /libexec/spamd | |
parent | 68ca8b3e82a4a5094091027feddcb51262302e73 (diff) |
Add -g option for greylisting support for spamd. The greylisting techinque
originates from a paper by Evan Harris which can be found at
http://projects.puremagic.com/greylisting/. This implementation makes
spamd allow for non-blacklisted addresses to be treated as "greylisted".
where they are tracked in a db file, and whitelisted by addition to a
pf table when the same envelope from and to are retried from the same
source IP address. Testing by many, ok deraadt@
Diffstat (limited to 'libexec/spamd')
-rw-r--r-- | libexec/spamd/Makefile | 4 | ||||
-rw-r--r-- | libexec/spamd/grey.c | 456 | ||||
-rw-r--r-- | libexec/spamd/grey.h | 18 | ||||
-rw-r--r-- | libexec/spamd/sdl.c | 16 | ||||
-rw-r--r-- | libexec/spamd/spamd.8 | 137 | ||||
-rw-r--r-- | libexec/spamd/spamd.c | 261 |
6 files changed, 804 insertions, 88 deletions
diff --git a/libexec/spamd/Makefile b/libexec/spamd/Makefile index 84abfe0221e..b36239de0f2 100644 --- a/libexec/spamd/Makefile +++ b/libexec/spamd/Makefile @@ -1,7 +1,7 @@ -# $OpenBSD: Makefile,v 1.6 2003/07/02 22:44:11 deraadt Exp $ +# $OpenBSD: Makefile,v 1.7 2004/02/26 07:28:55 beck Exp $ PROG= spamd -SRCS= spamd.c sdl.c +SRCS= spamd.c sdl.c grey.c MAN= spamd.8 CFLAGS+= -Wall -Wstrict-prototypes -ansi diff --git a/libexec/spamd/grey.c b/libexec/spamd/grey.c new file mode 100644 index 00000000000..de17ad2d0bc --- /dev/null +++ b/libexec/spamd/grey.c @@ -0,0 +1,456 @@ +/* + * Copyright (c) 2004 Bob Beck. All rights reserved. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/ioctl.h> +#include <sys/fcntl.h> +#include <sys/wait.h> +#include <net/if.h> +#include <netinet/in.h> +#include <net/pfvar.h> +#include <arpa/inet.h> +#include <db.h> +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <time.h> +#include <unistd.h> + +#include "grey.h" + +extern struct syslog_data sdata; +extern struct passwd *pw; +extern FILE * grey; +extern int debug; + +size_t whitecount, whitealloc; +char **whitelist; +int pfdev; + +DB *db; +DBT dbk, dbd; +BTREEINFO btreeinfo; + +/* borrowed from dhartmei.. */ +int +address_valid_v4(const char *a) +{ + if (!*a) + return (0); + while (*a) + if ((*a >= '0' && *a <= '9') || *a == '.') + a++; + else + return (0); + return (1); +} + +int +address_valid_v6(const char *a) +{ + if (!*a) + return (0); + while (*a) + if ((*a >= '0' && *a <= '9') || + (*a >= 'a' && *a <= 'f') || + (*a >= 'A' && *a <= 'F') || + *a == ':') + a++; + else + return (0); + return (1); +} + +int +configure_pf(char **addrs, int count) +{ + static char *argv[11]= {"pfctl", "-p", "/dev/pf", "-q", "-t", + "spamd-white", "-T", "replace", "-f" "-", NULL}; + FILE *pf = NULL; + int i, pdes[2]; + pid_t pid; + char *fdpath; + + if (debug) + fprintf(stderr, "configure_pf - device on fd %d\n", pfdev); + if (pfdev < 1 || pfdev > 63) + return(-1); + if (asprintf(&fdpath, "/dev/fd/%d", pfdev) == -1) + return(-1); + argv[2] = fdpath; + if (pipe(pdes) != 0) { + syslog_r(LOG_INFO, &sdata, "pipe failed (%m)"); + free(fdpath); + return(-1); + } + switch (pid = fork()) { + case -1: + syslog_r(LOG_INFO, &sdata, "fork failed (%m)"); + free(fdpath); + return(-1); + case 0: + /* child */ + close(pdes[1]); + if (pdes[0] != STDIN_FILENO) { + dup2(pdes[0], STDIN_FILENO); + close(pdes[0]); + } + execvp(PATH_PFCTL, argv); + syslog_r(LOG_ERR, &sdata, "can't exec %s:%m", PATH_PFCTL); + _exit(1); + } + + /* parent */ + free(fdpath); + close(pdes[0]); + pf = fdopen(pdes[1], "w"); + if (pf == NULL) { + syslog_r(LOG_INFO, &sdata, "fdopen failed (%m)"); + return(-1); + } + for (i = 0; i < count; i++) + if (addrs[i] != NULL) { + fprintf(pf, "%s/32\n", addrs[i]); + free(addrs[i]); + addrs[i] = NULL; + } + fclose(pf); + waitpid(pid, NULL, 0); + return(0); +} + +/* validate, then add to list of addrs to whitelist */ +int +addwhiteaddr(char *addr) +{ + struct in_addr ia; + + if (address_valid_v4(addr)) { + if (inet_aton(addr, &ia) == 1) { + if (whitecount == whitealloc) { + char **tmp; + + tmp = realloc(whitelist, + (whitealloc + 1024) * sizeof(char **)); + if (tmp == NULL) + return(-1); + whitelist = tmp; + whitealloc += 1024; + } + whitelist[whitecount] = strdup(addr); + if (whitelist[whitecount] == NULL) + return(-1); + whitecount++; + } + } else if (address_valid_v6(addr)) { + /* XXX deal with v6 later */ + return(-1); + } else + return(-1); + return(0); +} + +int +greyscan(char *dbname) +{ + time_t now = time(NULL); + struct gdata gd; + int r; + + /* walk db, expire, and whitelist */ + + memset(&btreeinfo, 0, sizeof(btreeinfo)); + db = dbopen(dbname, O_EXLOCK|O_RDWR, 0600, DB_BTREE, &btreeinfo); + if (db == NULL) { + syslog_r(LOG_INFO, &sdata, "dbopen failed (%m)"); + return(-1); + } + memset(&dbk, 0, sizeof(dbk)); + memset(&dbd, 0, sizeof(dbd)); + for (r = db->seq(db, &dbk, &dbd, R_FIRST); !r; + r = db->seq(db, &dbk, &dbd, R_NEXT)) { + char a[128]; + + if ((dbk.size < 1) || dbd.size != sizeof(struct gdata)) { + db->close(db); + return(-1); + } + memcpy(&gd, dbd.data, sizeof(gd)); + if (gd.expire < now) { + /* get rid of entry */ + if (debug) { + memset(a, 0, sizeof(a)); + memcpy(a, dbk.data, MIN(sizeof(a), + dbk.size)); + syslog_r(LOG_DEBUG, &sdata, + "deleting %s from %s", a, dbname); + } + if (db->del(db, &dbk, 0)) { + db->sync(db, 0); + db->close(db); + return(-1); + } + } else if (gd.pass < now) { + int tuple = 0; + char *cp; + + /* + * remove this tuple-keyed entry from db + * add address to whitelist + * add an address-keyed entry to db + */ + memset(a, 0, sizeof(a)); + memcpy(a, dbk.data, MIN(sizeof(a) - 1, dbk.size)); + cp = strchr(a, '\n'); + if (cp != NULL) { + tuple = 1; + *cp = '\0'; + } + if ((addwhiteaddr(a) == -1) && db->del(db, &dbk, 0)) + goto bad; + if (tuple) { + if (db->del(db, &dbk, 0)) + goto bad; + /* re-add entry, keyed only by ip */ + memset(&dbk, 0, sizeof(dbk)); + dbk.size = strlen(a); + dbk.data = a; + memset(&dbd, 0, sizeof(dbd)); + dbd.size = sizeof(gd); + dbd.data = &gd; + if (db->put(db, &dbk, &dbd, 0)) + goto bad; + syslog_r(LOG_DEBUG, &sdata, + "whitelisting %s in %s", a, dbname); + } + if (debug) + fprintf(stderr, "whitelisted %s\n", a); + } + } + configure_pf(whitelist, whitecount); + db->sync(db, 0); + db->close(db); + return(0); + bad: + db->sync(db, 0); + db->close(db); + return(-1); +} + +int +greyupdate(char *dbname, char *ip, char *from, char *to) +{ + char *key = NULL; + struct gdata gd; + time_t now = time(NULL); + int r; + + /* open with lock, find record, update, close, unlock */ + memset(&btreeinfo, 0, sizeof(btreeinfo)); + db = dbopen(dbname, O_EXLOCK|O_RDWR, 0600, DB_BTREE, &btreeinfo); + if (db == NULL) + return(-1); + if (asprintf(&key, "%s\n%s\n%s", ip, from, to) == -1) + goto bad; + memset(&dbk, 0, sizeof(dbk)); + dbk.size = strlen(key); + dbk.data = key; + memset(&dbd, 0, sizeof(dbd)); + r = db->get(db, &dbk, &dbd, 0); + if (r == -1) + goto bad; + if (r) { + /* new entry */ + memset(&gd, 0, sizeof(gd)); + gd.first = now; + gd.bcount = 1; + gd.pass = now + GREYEXP; + gd.expire = now + GREYEXP; + memset(&dbk, 0, sizeof(dbk)); + dbk.size = strlen(key); + dbk.data = key; + memset(&dbd, 0, sizeof(dbd)); + dbd.size = sizeof(gd); + dbd.data = &gd; + r = db->put(db, &dbk, &dbd, 0); + if (r) + goto bad; + if (debug) + fprintf(stderr, "added %s\n", key); + } else { + /* existing entry */ + if (dbd.size != sizeof(gd)) { + /* whatever this is, it doesn't belong */ + db->del(db, &dbk, 0); + goto bad; + } + memcpy(&gd, dbd.data, sizeof(gd)); + gd.bcount++; + if (gd.first + PASSTIME < now) { + gd.pass = now; + gd.expire = now + WHITEEXP; + } + memset(&dbk, 0, sizeof(dbk)); + dbk.size = strlen(key); + dbk.data = key; + memset(&dbd, 0, sizeof(dbd)); + dbd.size = sizeof(gd); + dbd.data = &gd; + r = db->put(db, &dbk, &dbd, 0); + if (r) + goto bad; + if (debug) + fprintf(stderr, "updated %s\n", key); + } + db->close(db); + free(key); + return(0); + bad: + free(key); + db->close(db); + return(-1); +} + +int +greyreader(void) +{ + char ip[32], from[MAX_MAIL], to[MAX_MAIL], *buf; + size_t len; + int state; + + state = 0; + if (grey == NULL) + errx(-1, "No greylist pipe stream!\n"); + while ((buf = fgetln(grey, &len))) { + if (buf[len - 1] == '\n') + buf[len - 1] = '\0'; + else + /* all valid lines end in \n */ + continue; + if (strlen(buf) < 4) + continue; + + switch(state) { + case 0: + if (strncmp(buf, "IP:", 3) != 0) + break; + strlcpy(ip, buf+3, sizeof(ip)); + if (address_valid_v4(ip)) + state = 1; + else + state = 0; + break; + case 1: + if (strncmp(buf, "FR:", 3) != 0) { + state = 0; + break; + } + strlcpy(from, buf+3, sizeof(from)); + state = 2; + break; + case 2: + if (strncmp(buf, "TO:", 3) != 0) { + state = 0; + break; + } + strlcpy(to, buf+3, sizeof(to)); + if (debug) + fprintf(stderr, + "Got Grey IP %s from %s to %s\n", + ip, from, to); + greyupdate(PATH_SPAMD_DB, ip, from, to); + state = 0; + break; + } + } + return (0); +} + +void +greyscanner(void) +{ + for (;;) { + sleep(DB_SCAN_INTERVAL); + greyscan(PATH_SPAMD_DB); + } + /* NOTREACHED */ +} + +int +greywatcher(void) +{ + pid_t pid; + + pfdev = open("/dev/pf", O_RDWR); + if (pfdev == -1) + err(1, "open of /dev/pf failed"); + + /* check to see if /var/db/spamd exists, if not, create it */ + if (open(PATH_SPAMD_DB, O_RDWR, 0) == -1 && errno == ENOENT) { + int i; + i = open(PATH_SPAMD_DB, O_RDWR|O_CREAT, 0644); + if (i == -1) + err(1, "can't create %s", PATH_SPAMD_DB); + /* if we are dropping privs, chown to that user */ + if (pw && (fchown(i, pw->pw_uid, pw->pw_gid) == -1)) + err(1, "can't chown %s", PATH_SPAMD_DB); + } + + /* + * lose root, continue as non-root user + * XXX Should not be _spamd - as it currently is. + */ + if (pw) { + setgroups(1, &pw->pw_gid); + setegid(pw->pw_gid); + setgid(pw->pw_gid); + seteuid(pw->pw_uid); + setuid(pw->pw_uid); + } + + if (!debug) { + if (daemon(1, 1) == -1) + err(1, "fork"); + } + + pid = fork(); + if (pid == -1) + err(1, "fork"); + if (pid == 0) { + /* + * child, talks to jailed spamd over greypipe, + * updates db. has no access to pf. + */ + close(pfdev); + setproctitle("(%s update)", PATH_SPAMD_DB); + greyreader(); + } else { + /* + * parent, scans db periodically for changes and updates + * pf whitelist table accordingly. + */ + fclose(grey); + setproctitle("(pf <spamd-white> update)"); + greyscanner(); + } + return(0); +} diff --git a/libexec/spamd/grey.h b/libexec/spamd/grey.h new file mode 100644 index 00000000000..1384f906050 --- /dev/null +++ b/libexec/spamd/grey.h @@ -0,0 +1,18 @@ + +#define MAX_MAIL 1024 /* how big an email address will we consider */ +#define PASSTIME (60 * 30) /* pass after first retry seen after 30 mins */ +#define GREYEXP (60 * 60 * 4) /* remove grey entries after 4 hours */ +#define WHITEEXP (60 * 60 * 24 * 36) /* remove white entries after 36 days */ +#define PATH_PFCTL "/sbin/pfctl" +#define DB_SCAN_INTERVAL 60 +#define PATH_SPAMD_DB "/var/db/spamd" + +struct gdata { + time_t first; /* when did we see it first */ + time_t pass; /* when was it whitelisted */ + time_t expire; /* when will we get rid of this entry */ + int bcount; /* how many times have we blocked it */ + int pcount; /* how many good connections have we seen after wl */ +}; + +extern int greywatcher(void); diff --git a/libexec/spamd/sdl.c b/libexec/spamd/sdl.c index f9c1a9fd0b8..c5b90b0e826 100644 --- a/libexec/spamd/sdl.c +++ b/libexec/spamd/sdl.c @@ -1,4 +1,4 @@ -/* $OpenBSD: sdl.c,v 1.10 2003/09/26 16:07:29 deraadt Exp $ */ +/* $OpenBSD: sdl.c,v 1.11 2004/02/26 07:28:55 beck Exp $ */ /* * Copyright (c) 2003 Bob Beck. All rights reserved. * @@ -188,13 +188,13 @@ match_addr(struct sdaddr *a, struct sdaddr *m, struct sdaddr *b, break; case AF_INET6: if (((a->addr32[0]) == - (b->addr32[0] & m->addr32[0])) && + (b->addr32[0] & m->addr32[0])) && ((a->addr32[1]) == - (b->addr32[1] & m->addr32[1])) && + (b->addr32[1] & m->addr32[1])) && ((a->addr32[2]) == - (b->addr32[2] & m->addr32[2])) && + (b->addr32[2] & m->addr32[2])) && ((a->addr32[3]) == - (b->addr32[3] & m->addr32[3]))) + (b->addr32[3] & m->addr32[3]))) match++; break; } @@ -213,8 +213,8 @@ sdl_lookup(struct sdlist *head, int af, void * src) struct sdlist *sdl; struct sdentry *sda; struct sdaddr *source = (struct sdaddr *) src; - static int sdnewlen = 0; - static struct sdlist **sdnew = NULL; + int sdnewlen = 0; + struct sdlist **sdnew = NULL; if (head == NULL) return (NULL); @@ -229,7 +229,7 @@ sdl_lookup(struct sdlist *head, int af, void * src) tmp = realloc(sdnew, (sdnewlen + 128) * - sizeof(struct sdlist *)); + sizeof(struct sdlist *)); if (tmp == NULL) /* * XXX out of memory - diff --git a/libexec/spamd/spamd.8 b/libexec/spamd/spamd.8 index 57bfa284ead..ce60564946c 100644 --- a/libexec/spamd/spamd.8 +++ b/libexec/spamd/spamd.8 @@ -1,4 +1,4 @@ -.\" $OpenBSD: spamd.8,v 1.42 2004/01/21 01:55:10 deraadt Exp $ +.\" $OpenBSD: spamd.8,v 1.43 2004/02/26 07:28:55 beck Exp $ .\" .\" Copyright (c) 2002 Theo de Raadt. All rights reserved. .\" @@ -31,8 +31,9 @@ .Sh SYNOPSIS .Nm spamd .Bk -words -.Op Fl 45dv +.Op Fl 45dgv .Op Fl c Ar maxcon +.Op Fl G Ar passtime:greyexp:whiteexp .Op Fl n Ar name .Op Fl p Ar port .Op Fl r Ar reply @@ -64,6 +65,14 @@ Debug mode. does not .Xr fork 2 into the background. +.It Fl g +Greylisting mode; see +.Sx GREYLISTING +below. +.It Fl G Ar passtime:greyexp:whiteexp +Adjust the three time parameters for greylisting; see +.Sx GREYLISTING +below. .It Fl n Ar name The SMTP version banner that is reported upon initial connection. .It Fl p Ar port @@ -138,7 +147,7 @@ The rules can be loaded into a to simplify handling. .Bd -literal -offset 4n table <spamd> persist -rdr pass inet proto tcp from <spamd> to any \\ +rdr pass inet proto tcp from <spamd> to any \e port smtp -> 127.0.0.1 port 8025 .Ed .Pp @@ -200,6 +209,112 @@ will reject mail by displaying all the messages from all blacklists in which a connecting address is matched. .Xr spamd-setup 8 is normally used to configure this information. +.Sh GREYLISTING +When run in greylisting mode, +.Nm +will run in the normal mode for any addresses blacklisted by +.Xr spamd-setup 8 . +Connections from addresses not blacklisted by +.Xr spamd-setup 8 +will be considered for greylisting. +Such connections will not be stuttered at or delayed, +and will receive the pleasantly innocuous temporary failure of: +.Bd -literal -offset 4n +450 Temporary failure, please try again later. +.Ed +.Pp +in the SMTP dialogue immediately after the recipient is specified. +.Nm +will use the db file in +.Pa /var/db/spamd +to track these non-blacklisted connections to +.Nm +by connecting IP address, envelope-from, and envelope-to, or "tuple" for +short. +.Pp +A previously unseen tuple is added to the +.Pa /var/db/spamd +database, recording the time an initial connection attempt was seen. +After +.Em passtime +minutes (by default 30) if +.Nm +sees a retried attempt to deliver mail for the same tuple, +.Nm +will whitelist the connecting address by adding it as a +whitelist entry to to +.Pa /var/db/spamd . +.Pp +.Nm +regularly scans the +.Pa /var/db/spamd +database and configures all whitelist addresses as the +.Em spamd-white +.Xr pf 4 +table. +The +.Em spamd-white +table must be used to allow connections to pass to the +real MTA as in the following +.Xr pf.conf 5 +example: +.Bd -literal -offset 4n +table <spamd> persist +table <spamd-white> persist +rdr pass inet proto tcp from <spamd> to any \e + port smtp -> 127.0.0.1 port 8025 +rdr pass inet proto tcp from !<spamd-white> to any port smtp \e + -> 127.0.0.1 port 8025 +.Ed +.Pp +With this configuration, +.Xr spamd-setup 8 +should be used to configure blacklists in +.Nm +and add them to the +.Em spamd +.Xr pf 4 +table. +These connections will be stuttered at by +.Nm . +All other connections not in the +.Em spamd-white +table are redirected to +.Nm +but will not be stuttered at. +Such connections will be +considered for greylisting and eventual whitelisting (by addition +to the +.Em spamd-white +table so they are not redirected) if they retry mail delivery. +.Pp +.Nm +removes tuple entries from the +.Pa /var/db/spamd +database if delivery has not been retried within +.Em greyexp +hours (by default 4) from the initial time a connection is seen. +The default is 4 hours as this is the most common setting after which +MTA's will give up attempting to retry delivery of a message. +.Pp +.Nm +removes whitelist entries from the +.Pa /var/db/spamd +database if no mail delivery activity has been seen from the +whitelisted address by +.Xr spamlogd 8 +within +.Em whiteexp +hours (by default 864, or 36 days) from the initial time an address +is whitelisted. +The default is 36 days to allow for the delivery of +monthly mailing list digests without greylist delays every time. +.Xr spamlogd 8 +should be used to update the whitelist entries in +.Pa /var/db/spamd +when connections are seen to pass to the real MTA on the +.Em smtp +port. .Sh LOGGING .Nm sends log messages to @@ -225,6 +340,8 @@ daemon.err;daemon.warn;daemon.info /var/log/spamd .Xr syslog.conf 5 , .Xr pfctl 8 , .Xr spamd-setup 8 , +.Xr spamdb 8 , +.Xr spamlog 8 , .Xr syslogd 8 .Sh HISTORY The @@ -232,3 +349,17 @@ The command appeared in .Ox 3.3 . +.Sh BUGS +.Nm +currently uses the user +.Dq _spamd +outside a chroot jail when running in greylisting mode, and requires +the greylisting database in +.Pa /var/db/spamd +to be owned by the +.Dq _spamd +user. +This is wrong and should change to a distinct user from the +one used by the chrooted +.Nm +process. diff --git a/libexec/spamd/spamd.c b/libexec/spamd/spamd.c index 16ddcf8834c..b2cb5a93ca1 100644 --- a/libexec/spamd/spamd.c +++ b/libexec/spamd/spamd.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spamd.c,v 1.52 2003/11/09 07:35:25 dhartmei Exp $ */ +/* $OpenBSD: spamd.c,v 1.53 2004/02/26 07:28:55 beck Exp $ */ /* * Copyright (c) 2002 Theo de Raadt. All rights reserved. @@ -48,6 +48,7 @@ #include <machine/endian.h> #include "sdl.h" +#include "grey.h" struct con { int fd; @@ -57,7 +58,8 @@ struct con { struct sockaddr_in sin; void *ia; char addr[32]; - char mail[64], rcpt[64]; + char mail[MAX_MAIL], rcpt[MAX_MAIL]; + struct sdlist **blacklists; /* * we will do stuttering by changing these to time_t's of @@ -80,6 +82,8 @@ struct con { int ol; int data_lines; int data_body; + int stutter; + int sr; } *con; void usage(void); @@ -88,8 +92,8 @@ int parse_configline(char *); void parse_configs(void); void do_config(void); int append_error_string (struct con *, size_t, char *, int, void *); -char *build_reply(struct con *); -char *doreply(struct con *); +void build_reply(struct con *); +void doreply(struct con *); void setlog(char *, size_t, char *); void initcon(struct con *, int, struct sockaddr_in *); void closecon(struct con *); @@ -103,6 +107,12 @@ struct syslog_data sdata = SYSLOG_DATA_INIT; char *reply = NULL; char *nreply = "450"; char *spamd = "spamd IP-based SPAM blocker"; +int greypipe[2]; +FILE *grey; +time_t passtime = PASSTIME; +time_t greyexp = GREYEXP; +time_t whiteexp = WHITEEXP; +struct passwd *pw; extern struct sdlist *blacklists; @@ -116,18 +126,18 @@ time_t t; int maxcon = MAXCON; int clients; int debug; +int greylist; int verbose; int stutter = 1; int window; #define MAXTIME 400 - void usage(void) { fprintf(stderr, - "usage: spamd [-45dv] [-c maxcon] [-n name] [-p port] [-r reply] " - "[-s secs]\n"); + "usage: spamd [-45dgv] [-c maxcon] [-G mins:hours:hours] [-n name]" + "[-p port] [-r reply] [-s secs]\n"); fprintf(stderr, " [-w window]\n"); exit(1); @@ -152,10 +162,9 @@ grow_obuf(struct con *cp, int off) cp->obuf = tmp; cp->obufalloc = 1; return (cp->obuf + off); - } + } } - int parse_configline(char *line) { @@ -315,7 +324,6 @@ configdone: conffd = -1; } - int append_error_string(struct con *cp, size_t off, char *fmt, int af, void *ia) { @@ -404,17 +412,39 @@ append_error_string(struct con *cp, size_t off, char *fmt, int af, void *ia) return (i); } - char * +loglists(struct con *cp) { + static char matchlists[80]; + struct sdlist **matches; + int s = sizeof(matchlists) - 4; + + matchlists[0] = '\0'; + matches = cp->blacklists; + if (matches == NULL) + return(NULL); + for (; *matches; matches++) { + + /* don't report an insane amount of lists in the logs. + * just truncate and indicate with ... + */ + if (strlen(matchlists) + strlen(matches[0]->tag) + 1 + >= s) + strlcat(matchlists, " ...", sizeof(matchlists)); + else { + strlcat(matchlists, " ", s); + strlcat(matchlists, matches[0]->tag, s); + } + } + return matchlists; +} + +void build_reply(struct con *cp) { struct sdlist **matches; - static char matchlists[80]; int off = 0; - matchlists[0] = '\0'; - - matches = sdl_lookup(blacklists, cp->af, cp->ia); + matches = cp->blacklists; if (matches == NULL) { if (cp->osize) free(cp->obuf); @@ -423,20 +453,10 @@ build_reply(struct con *cp) goto bad; } for (; *matches; matches++) { - int used = 0, s = sizeof(matchlists) - 4; + int used = 0; char *c = cp->obuf + off; int left = cp->osize - off; - /* don't report an insane amount of lists in the logs. - * just truncate and indicate with ... - */ - if (strlen(matchlists) + strlen(matches[0]->tag) + 1 - >= s) - strlcat(matchlists, " ...", sizeof(matchlists)); - else { - strlcat(matchlists, " ", s); - strlcat(matchlists, matches[0]->tag, s); - } used = append_error_string(cp, off, matches[0]->string, cp->af, cp->ia); if (used == -1) @@ -453,33 +473,37 @@ build_reply(struct con *cp) cp->obuf[off] = '\0'; } } - return matchlists; bad: /* Out of memory, or no match. give generic reply */ - asprintf(&cp->obuf, - "%s-Sorry %s\n" - "%s-You are trying to send mail from an address listed by one\n" - "%s or more IP-based registries as being a SPAM source.\n", - nreply, cp->addr, nreply, nreply); + if (cp->obuf != NULL && cp->obufalloc) + free(cp->obuf); + if (cp->blacklists != NULL) + asprintf(&cp->obuf, + "%s-Sorry %s\n" + "%s-You are trying to send mail from an address" + "listed by one\n" + "%s or more IP-based registries as being a SPAM source.\n", + nreply, cp->addr, nreply, nreply); + else + asprintf(&cp->obuf, + "450 Temporary failure, please try again later.\r\n"); if (cp->obuf == NULL) { /* we're having a really bad day.. */ cp->obufalloc = 0; /* know not to free or mangle */ cp->obuf = "450 Try again\n"; } else cp->osize = strlen(cp->obuf) + 1; - return matchlists; } -char * +void doreply(struct con *cp) { if (reply) { if (!cp->obufalloc) errx(1, "shouldn't happen"); snprintf(cp->obuf, cp->osize, "%s %s\n", nreply, reply); - return(""); } - return (build_reply(cp)); + build_reply(cp); } void @@ -508,12 +532,15 @@ void initcon(struct con *cp, int fd, struct sockaddr_in *sin) { time_t t; + char *tmp; time(&t); if (cp->obufalloc) { free(cp->obuf); cp->obuf = NULL; } + if (cp->blacklists) + free(cp->blacklists); bzero(cp, sizeof(struct con)); if (grow_obuf(cp, 0) == NULL) err(1, "malloc"); @@ -521,13 +548,26 @@ initcon(struct con *cp, int fd, struct sockaddr_in *sin) memcpy(&cp->sin, sin, sizeof(struct sockaddr_in)); cp->af = sin->sin_family; cp->ia = (void *) &cp->sin.sin_addr; + cp->blacklists = sdl_lookup(blacklists, cp->af, cp->ia); + cp->stutter = (greylist && cp->blacklists == NULL) ? 0 : stutter; + if (cp->blacklists != NULL) + cp->lists = strdup(loglists(cp)); + else + cp->lists = NULL; strlcpy(cp->addr, inet_ntoa(sin->sin_addr), sizeof(cp->addr)); + tmp = strdup(ctime(&t)); + if (tmp == NULL) + tmp = "some time"; + else + tmp[strlen(tmp) - 1] = '\0'; /* nuke newline */ snprintf(cp->obuf, cp->osize, - "220 %s ESMTP %s; %s", - hostname, spamd, ctime(&t)); + "220 %s ESMTP %s; %s\r\n", + hostname, spamd, tmp); + if (tmp != NULL) + free(tmp); cp->op = cp->obuf; cp->ol = strlen(cp->op); - cp->w = t + stutter; + cp->w = t + cp->stutter; cp->s = t; strlcpy(cp->rend, "\n", sizeof cp->rend); clients++; @@ -539,8 +579,10 @@ closecon(struct con *cp) time_t t; time(&t); - syslog_r(LOG_INFO, &sdata, "%s: disconnected after %ld seconds.", - cp->addr, (long)(t - cp->s)); + syslog_r(LOG_INFO, &sdata, "%s: disconnected after %ld seconds.%s%s", + cp->addr, (long)(t - cp->s), + ((cp->lists == NULL) ? "" : " lists:"), + ((cp->lists == NULL) ? "": cp->lists)); if (debug > 0) printf("%s connected for %ld seconds.\n", cp->addr, (long)(t - cp->s)); @@ -548,6 +590,10 @@ closecon(struct con *cp) free(cp->lists); cp->lists = NULL; } + if (cp->blacklists != NULL) { + free(cp->blacklists); + cp->blacklists = NULL; + } if (cp->osize > 0 && cp->obufalloc) { free(cp->obuf); cp->obuf = NULL; @@ -582,12 +628,12 @@ nextstate(struct con *cp) match(cp->ibuf, "EHLO")) { snprintf(cp->obuf, cp->osize, "250 Hello, spam sender. " - "Pleased to be wasting your time.\n"); + "Pleased to be wasting your time.\r\n"); cp->op = cp->obuf; cp->ol = strlen(cp->op); cp->laststate = cp->state; cp->state = 2; - cp->w = t + stutter; + cp->w = t + cp->stutter; break; } goto mail; @@ -605,12 +651,12 @@ nextstate(struct con *cp) setlog(cp->mail, sizeof cp->mail, cp->ibuf); snprintf(cp->obuf, cp->osize, "250 You are about to try to deliver spam. " - "Your time will be spent, for nothing.\n"); + "Your time will be spent, for nothing.\r\n"); cp->op = cp->obuf; cp->ol = strlen(cp->op); cp->laststate = cp->state; cp->state = 4; - cp->w = t + stutter; + cp->w = t + cp->stutter; break; } goto rcpt; @@ -628,15 +674,33 @@ nextstate(struct con *cp) setlog(cp->rcpt, sizeof(cp->rcpt), cp->ibuf); snprintf(cp->obuf, cp->osize, "250 This is hurting you more than it is " - "hurting me.\n"); + "hurting me.\r\n"); cp->op = cp->obuf; cp->ol = strlen(cp->op); cp->laststate = cp->state; cp->state = 6; - cp->w = t + stutter; - if (cp->mail[0] && cp->rcpt[0]) - syslog_r(LOG_INFO, &sdata, "%s: %s -> %s", + cp->w = t + cp->stutter; + if (cp->mail[0] && cp->rcpt[0]) { + if (verbose) + syslog_r(LOG_DEBUG, &sdata, + "(%s) %s: %s -> %s", + cp->blacklists ? "BLACK" : "GREY", + cp->addr, cp->mail, + cp->rcpt); + if(debug) + fprintf(stderr, "(%s) %s: %s -> %s\n", + cp->blacklists ? "BLACK" : "GREY", cp->addr, cp->mail, cp->rcpt); + if (greylist && cp->blacklists == NULL) { + /* send this info to the greylister */ + fprintf(grey, "IP:%s\nFR:%s\nTO:%s\n", + cp->addr, cp->mail, cp->rcpt); + fflush(grey); + cp->laststate = cp->state; + cp->state = 98; + goto done; + } + } break; } goto spam; @@ -654,23 +718,23 @@ nextstate(struct con *cp) if (match(cp->ibuf, "DATA")) { snprintf(cp->obuf, cp->osize, "354 Enter spam, end with \".\" on a line by " - "itself\n"); + "itself\r\n"); cp->state = 60; } else { snprintf(cp->obuf, cp->osize, - "500 5.5.1 Command unrecognized\n"); + "500 5.5.1 Command unrecognized\r\n"); cp->state = cp->laststate; } cp->ip = cp->ibuf; cp->il = sizeof(cp->ibuf) - 1; cp->op = cp->obuf; cp->ol = strlen(cp->op); - cp->w = t + stutter; + cp->w = t + cp->stutter; break; case 60: if (!strcmp(cp->ibuf, ".") || (cp->data_body && ++cp->data_lines >= 10)) { - cp->laststate = cp->state; + cp->laststate = cp->state; cp->state = 98; goto done; } @@ -679,8 +743,8 @@ nextstate(struct con *cp) if (verbose && cp->data_body && *cp->ibuf) syslog_r(LOG_DEBUG, &sdata, "%s: Body: %s", cp->addr, cp->ibuf); - else if (verbose && (match(cp->ibuf, "FROM:") || - match(cp->ibuf, "TO:") || match(cp->ibuf, "SUBJECT:"))) + else if (verbose && (match(cp->ibuf, "FROM:") || + match(cp->ibuf, "TO:") || match(cp->ibuf, "SUBJECT:"))) syslog_r(LOG_INFO, &sdata, "%s: %s", cp->addr, cp->ibuf); cp->ip = cp->ibuf; @@ -689,13 +753,10 @@ nextstate(struct con *cp) break; done: case 98: - cp->lists = strdup(doreply(cp)); - if (cp->lists != NULL) - syslog_r(LOG_INFO, &sdata, "%s: matched lists: %s", - cp->addr, cp->lists); + doreply(cp); cp->op = cp->obuf; cp->ol = strlen(cp->op); - cp->w = t + stutter; + cp->w = t + cp->stutter; cp->laststate = cp->state; cp->state = 99; break; @@ -716,9 +777,9 @@ handler(struct con *cp) if (cp->r) { n = read(cp->fd, cp->ip, cp->il); - if (n == 0) { + if (n == 0) closecon(cp); - } else if (n == -1) { + else if (n == -1) { if (debug > 0) perror("read()"); closecon(cp); @@ -750,7 +811,7 @@ handlew(struct con *cp, int one) int n; if (cp->w) { - if (*cp->op == '\n') { + if (*cp->op == '\n' && !cp->sr) { /* insert \r before \n */ n = write(cp->fd, "\r", 1); if (n == 0) { @@ -763,10 +824,14 @@ handlew(struct con *cp, int one) goto handled; } } - n = write(cp->fd, cp->op, one ? 1 : cp->ol); - if (n == 0) { + if (*cp->op == '\r') + cp->sr = 1; + else + cp->sr = 0; + n = write(cp->fd, cp->op, (one && cp->stutter) ? 1 : cp->ol); + if (n == 0) closecon(cp); - } else if (n == -1) { + else if (n == -1) { if (debug > 0 && errno != EPIPE) perror("write()"); closecon(cp); @@ -776,7 +841,7 @@ handlew(struct con *cp, int one) } } handled: - cp->w = t + stutter; + cp->w = t + cp->stutter; if (cp->ol == 0) { cp->w = 0; nextstate(cp); @@ -789,12 +854,12 @@ main(int argc, char *argv[]) fd_set *fdsr = NULL, *fdsw = NULL; struct sockaddr_in sin; struct sockaddr_in lin; - struct passwd *pw; - int ch, s, s2, conflisten = 0, i, omax = 0; - int sinlen, one = 1; + int ch, s, s2, conflisten = 0, i, omax = 0, one = 1; + socklen_t sinlen; u_short port, cfg_port; struct servent *ent; struct rlimit rlp; + pid_t pid; tzset(); openlog_r("spamd", LOG_PID | LOG_NDELAY, LOG_DAEMON, &sdata); @@ -809,7 +874,7 @@ main(int argc, char *argv[]) if (gethostname(hostname, sizeof hostname) == -1) err(1, "gethostname"); - while ((ch = getopt(argc, argv, "45c:p:dr:s:n:vw:")) != -1) { + while ((ch = getopt(argc, argv, "45c:p:dgG:r:s:n:vw:")) != -1) { switch (ch) { case '4': nreply = "450"; @@ -830,6 +895,20 @@ main(int argc, char *argv[]) case 'd': debug = 1; break; + case 'g': + greylist = 1; + break; + case 'G': + if (sscanf(optarg, "%d:%d:%d", &passtime, &greyexp, + &whiteexp) != 3) + usage(); + /* convert to seconds from minutes */ + passtime *= 60; + /* convert to seconds from hours */ + whiteexp *= (60 * 60); + /* convert to seconds from hours */ + greyexp *= (60 * 60); + break; case 'r': reply = optarg; break; @@ -855,7 +934,7 @@ main(int argc, char *argv[]) } } - rlp.rlim_cur = rlp.rlim_max = maxcon + 7; + rlp.rlim_cur = rlp.rlim_max = maxcon + 15; if (setrlimit(RLIMIT_NOFILE, &rlp) == -1) err(1, "setrlimit"); @@ -918,6 +997,33 @@ main(int argc, char *argv[]) if (!pw) pw = getpwnam("nobody"); + if (greylist) { + /* open pipe to talk to greylister */ + if (pipe(greypipe) == -1) + err(1, "pipe"); + + pid = fork(); + if (pid == -1) + err(1, "fork"); + if (pid != 0) { + /* parent - run greylister */ + close(greypipe[1]); + grey = fdopen(greypipe[0], "r"); + if (grey == NULL) + err(1, "fdopen"); + return(greywatcher()); + /* NOTREACHED */ + } else { + /* child - continue */ + close(greypipe[0]); + grey = fdopen(greypipe[1], "w"); + if (grey == NULL) { + warn("fdopen"); + _exit(1); + } + } + } + if (chroot("/var/empty") == -1 || chdir("/") == -1) { syslog(LOG_ERR, "cannot chdir to /var/empty."); exit(1); @@ -1039,15 +1145,20 @@ main(int argc, char *argv[]) close(s2); else { initcon(&con[i], s2, &sin); - syslog_r(LOG_INFO, &sdata, "%s: connected (%d)", - con[i].addr, clients); + syslog_r(LOG_INFO, &sdata, + "%s: connected (%d)%s%s", + con[i].addr, clients, + ((con[i].lists == NULL) ? "" : + ", lists:"), + ((con[i].lists == NULL) ? "": + con[i].lists)); } } if (FD_ISSET(conflisten, fdsr)) { sinlen = sizeof(lin); conffd = accept(conflisten, (struct sockaddr *)&lin, &sinlen); - if (conffd == -1) + if (conffd == -1) /* accept failed, they may try again */ continue; else if (ntohs(lin.sin_port) >= IPPORT_RESERVED) { |