diff options
author | 2004-02-26 07:28:54 +0000 | |
---|---|---|
committer | 2004-02-26 07:28:54 +0000 | |
commit | 1f68c1d423a18ba467ea103f8d8bc128a0f56830 (patch) | |
tree | 29833b387cb6e2affeb37dea6a12cde9cbae3d49 | |
parent | sync (diff) | |
download | wireguard-openbsd-1f68c1d423a18ba467ea103f8d8bc128a0f56830.tar.xz wireguard-openbsd-1f68c1d423a18ba467ea103f8d8bc128a0f56830.zip |
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@
-rw-r--r-- | libexec/Makefile | 4 | ||||
-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 | ||||
-rw-r--r-- | libexec/spamlogd/Makefile | 9 | ||||
-rw-r--r-- | libexec/spamlogd/spamlogd.8 | 78 | ||||
-rw-r--r-- | libexec/spamlogd/spamlogd.c | 234 | ||||
-rw-r--r-- | usr.sbin/Makefile | 4 | ||||
-rw-r--r-- | usr.sbin/spamdb/Makefile | 9 | ||||
-rw-r--r-- | usr.sbin/spamdb/spamdb.8 | 61 | ||||
-rw-r--r-- | usr.sbin/spamdb/spamdb.c | 263 |
14 files changed, 1462 insertions, 92 deletions
diff --git a/libexec/Makefile b/libexec/Makefile index d2411215786..71fde178848 100644 --- a/libexec/Makefile +++ b/libexec/Makefile @@ -1,12 +1,12 @@ # from: @(#)Makefile 5.7 (Berkeley) 4/1/91 -# $OpenBSD: Makefile,v 1.37 2003/07/22 17:15:13 brad Exp $ +# $OpenBSD: Makefile,v 1.38 2004/02/26 07:28:54 beck Exp $ .include <bsd.own.mk> SUBDIR= comsat fingerd ftpd ftp-proxy getNAME getty identd lockspool \ mail.local makewhatis \ rpc.rquotad rpc.rstatd rpc.rusersd rpc.rwalld rpc.sprayd spamd \ - spamd-setup rshd talkd tcpd telnetd tftpd uucpd + spamlogd spamd-setup rshd talkd tcpd telnetd tftpd uucpd SUBDIR+=login_passwd login_skey login_reject \ login_chpass login_lchpass login_token login_radius 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) { diff --git a/libexec/spamlogd/Makefile b/libexec/spamlogd/Makefile new file mode 100644 index 00000000000..ce40ac8143f --- /dev/null +++ b/libexec/spamlogd/Makefile @@ -0,0 +1,9 @@ +# $OpenBSD: Makefile,v 1.1 2004/02/26 07:28:55 beck Exp $ + +PROG= spamlogd +SRCS= spamlogd.c +MAN= spamlogd.8 + +CFLAGS+= -Wall -Wstrict-prototypes -ansi -I ${.CURDIR}/../spamd + +.include <bsd.prog.mk> diff --git a/libexec/spamlogd/spamlogd.8 b/libexec/spamlogd/spamlogd.8 new file mode 100644 index 00000000000..61e411efb2a --- /dev/null +++ b/libexec/spamlogd/spamlogd.8 @@ -0,0 +1,78 @@ +.\" $OpenBSD: spamlogd.8,v 1.1 2004/02/26 07:28:55 beck Exp $ +.\" +.\" 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. +.\" +.Dd February 16, 2004 +.Dt SPAMLOGD 8 +.Os +.Sh NAME +.Nm spamlogd +.Nd spamd whitelist updating daemon +.Sh SYNOPSIS +.Nm spamlogd +.Op Fl i Ar interface +.Sh DESCRIPTION +.Nm +manipulates the +.Xr spamd 8 +database in +.Pa /var/db/spamd +used for +.Xr spamd 8 +greylisting. +.Nm +updates the +.Pa /var/db/spamd +whitelist entries whenever a connection +to port 25 is logged to the +.Xr pflog 4 +interface. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl i Ar interface +Specify a network interface on which packets must arrive. +The default is to watch for connections logged from any interfaces. +.El +.Pp +It is important to be sure to log any connections to your real +MTA in order for +.Nm +to update the whitelist entries. +An example +.Xr pf.conf 5 +configuration for logging such connections is as follows: +.Bd -literal -offset 4n +$EXT_IF = "fxp0" +$MAILHOSTS = "{129.128.11.10, 129.128.11.43}" +pass in log on $EXT_IF inet proto tcp to $MAILHOSTS \e + port smtp keep state +.Ed +.Sh FILES +/var/db/spamd +.Sh SEE ALSO +.Xr pflog 4 , +.Xr spamd.conf 5 , +.Xr pflogd 8 , +.Xr spamd 8 , +.Xr spamd-setup 8 , +.Xr spamdb 8 , +.Xr tcpdump 8 +.Sh HISTORY +The +.Nm +command +appeared in +.Ox 3.5 . diff --git a/libexec/spamlogd/spamlogd.c b/libexec/spamlogd/spamlogd.c new file mode 100644 index 00000000000..23e6057e7b7 --- /dev/null +++ b/libexec/spamlogd/spamlogd.c @@ -0,0 +1,234 @@ +/* + * 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. + */ + +/* watch pf log for mail connections, update whitelist entries. */ + +#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 <time.h> +#include <unistd.h> + +#include "grey.h" +#define PATH_TCPDUMP "/usr/sbin/tcpdump" + +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.. */ +static 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 +dbupdate(char *dbname, char *ip) +{ + struct gdata gd; + time_t now; + int r; + + now = time(NULL); + memset(&btreeinfo, 0, sizeof(btreeinfo)); + db = dbopen(dbname, O_EXLOCK|O_RDWR, 0600, DB_BTREE, &btreeinfo); + if (db == NULL) + return(-1); + if (!address_valid_v4(ip)) { + warnx("invalid ip address %s\n", ip); + goto bad; + } + memset(&dbk, 0, sizeof(dbk)); + dbk.size = strlen(ip); + dbk.data = ip; + memset(&dbd, 0, sizeof(dbd)); + /* add or update whitelist entry */ + r = db->get(db, &dbk, &dbd, 0); + if (r == -1) { + warn("db->get failed"); + goto bad; + } + if (r) { + /* new entry */ + memset(&gd, 0, sizeof(gd)); + gd.first = now; + gd.bcount = 1; + gd.pass = now; + gd.expire = now + WHITEEXP; + memset(&dbk, 0, sizeof(dbk)); + dbk.size = strlen(ip); + dbk.data = ip; + memset(&dbd, 0, sizeof(dbd)); + dbd.size = sizeof(gd); + dbd.data = &gd; + r = db->put(db, &dbk, &dbd, 0); + if (r) { + warn("db->put failed"); + goto bad; + } + } else { + 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.pcount++; + gd.expire = now + WHITEEXP; + memset(&dbk, 0, sizeof(dbk)); + dbk.size = strlen(ip); + dbk.data = ip; + memset(&dbd, 0, sizeof(dbd)); + dbd.size = sizeof(gd); + dbd.data = &gd; + r = db->put(db, &dbk, &dbd, 0); + if (r) { + warn("db->put failed"); + goto bad; + } + } + db->sync(db, 0); + db->close(db); + return (0); + bad: + db->sync(db, 0); + db->close(db); + return(-1); +} + +static int +usage(void) +{ + fprintf(stderr, "usage: spamlogd [-i netif]\n"); + exit(-1); +} + +char *targv[19] = { + "tcpdump", "-l", "-n", "-e", "-i", "pflog0", "-q", + "-t", "inbound", "and", "port", "25", "and", "action", "pass", + NULL, NULL, NULL, NULL +}; + +int +main(int argc, char **argv) +{ + int ch, p[2]; + char *buf, *lbuf; + size_t len; + pid_t pid; + FILE *f; + + while ((ch = getopt(argc, argv, "i:")) != -1) { + switch (ch) { + case 'i': + targv[15] = "and"; + targv[16] = "on"; + targv[17] = optarg; + break; + default: + usage(); + break; + } + } + + if (daemon(1, 1) == -1) + err(1, "daemon"); + if (pipe(p) == -1) + err(1, "pipe"); + switch (pid = fork()) { + case -1: + err(1, "fork"); + case 0: + /* child */ + close(p[0]); + close(STDERR_FILENO); + if (dup2(p[1], STDOUT_FILENO) == -1) { + warn("dup2"); + _exit(1); + } + close(p[1]); + execvp(PATH_TCPDUMP, targv); + warn("exec of %s failed", PATH_TCPDUMP); + _exit(1); + } + + /* parent */ + f = fdopen(p[0], "r"); + if (f == NULL) + err(1, "fdopen"); + lbuf = NULL; + while ((buf = fgetln(f, &len))) { + char *cp; + + if (buf[len - 1] == '\n') + buf[len - 1] = '\0'; + else { + if ((lbuf = (char *)malloc(len + 1)) == NULL) + err(1, NULL); + memcpy(lbuf, buf, len); + lbuf[len] = '\0'; + buf = lbuf; + } + if ((cp = (strchr(buf, '>'))) != NULL) { + /* XXX replace this grot with an sscanf */ + while (*cp != '.' && cp >= buf) { + *cp = '\0'; + cp--; + } + *cp ='\0'; + while (*cp != ' ' && cp >= buf) + cp--; + cp++; + dbupdate(PATH_SPAMD_DB, cp); + } + if (lbuf != NULL) { + free(lbuf); + lbuf = NULL; + } + } + exit(0); +} diff --git a/usr.sbin/Makefile b/usr.sbin/Makefile index 16973d56da7..3b1811ffc41 100644 --- a/usr.sbin/Makefile +++ b/usr.sbin/Makefile @@ -1,4 +1,4 @@ -# $OpenBSD: Makefile,v 1.104 2004/02/23 19:55:35 tedu Exp $ +# $OpenBSD: Makefile,v 1.105 2004/02/26 07:28:55 beck Exp $ # not yet done: catman @@ -15,7 +15,7 @@ SUBDIR= ac accton adduser amd arp authpf \ procmap pstat pwd_mkdb quot quotaon rarpd rbootd \ rdconfig rdate repquota rmt \ rpc.bootparamd rpc.lockd rwhod \ - sa sensorsd sesd sliplogin slstats spppcontrol spray \ + sa sensorsd sesd sliplogin slstats spamdb spppcontrol spray \ syslogc syslogd tcpdump timed tokenadm tokeninit traceroute trpt \ usbdevs user vipw vnconfig wsmoused zdump zic diff --git a/usr.sbin/spamdb/Makefile b/usr.sbin/spamdb/Makefile new file mode 100644 index 00000000000..1574b921827 --- /dev/null +++ b/usr.sbin/spamdb/Makefile @@ -0,0 +1,9 @@ +# $OpenBSD: Makefile,v 1.1 2004/02/26 07:28:55 beck Exp $ + +PROG= spamdb +SRCS= spamdb.c +MAN= spamdb.8 + +CFLAGS+= -Wall -Wstrict-prototypes -ansi -I ${.CURDIR}/../../libexec/spamd + +.include <bsd.prog.mk> diff --git a/usr.sbin/spamdb/spamdb.8 b/usr.sbin/spamdb/spamdb.8 new file mode 100644 index 00000000000..85d676ef119 --- /dev/null +++ b/usr.sbin/spamdb/spamdb.8 @@ -0,0 +1,61 @@ +.\" $OpenBSD: spamdb.8,v 1.1 2004/02/26 07:28:55 beck Exp $ +.\" +.\" 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. +.\" +.Dd February 16, 2004 +.Dt SPAMDB 8 +.Os +.Sh NAME +.Nm spamdb +.Nd spamd database tool +.Sh SYNOPSIS +.Nm spamdb +.Bk -words +.Op Fl a Ar ip +.Op Fl d Ar ip +.Ek +.Sh DESCRIPTION +.Nm +manipulates the spamd database in +.Pa /var/db/spamd +used for +.Xr spamd 8 +greylisting. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl a Ar ip +Add or update a whitelist entry for IP address "ip". +Updates time last seen to now. +.It Fl d Ar ip +Delete a whitelist entry for IP address "ip". +.El +.Pp +If invoked without any arguments, +.Nm +lists the contents of the database +in a text format. +.Sh FILES +/var/db/spamd +.Sh SEE ALSO +.Xr spamd.conf 5 , +.Xr spamd 8 , +.Xr spamd-setup 8 +.Sh HISTORY +The +.Nm +command +appeared in +.Ox 3.5 . diff --git a/usr.sbin/spamdb/spamdb.c b/usr.sbin/spamdb/spamdb.c new file mode 100644 index 00000000000..930b131f06a --- /dev/null +++ b/usr.sbin/spamdb/spamdb.c @@ -0,0 +1,263 @@ +/* + * 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 <time.h> +#include <unistd.h> + +#include "grey.h" +#define PATH_SPAMD_DB "/var/db/spamd" + +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.. */ +static 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 +dbupdate(char *dbname, char *ip, int add) +{ + struct gdata gd; + time_t now; + int r; + + now = time(NULL); + memset(&btreeinfo, 0, sizeof(btreeinfo)); + db = dbopen(dbname, O_EXLOCK|O_RDWR, 0600, DB_BTREE, &btreeinfo); + if (db == NULL) + return(-1); + if (!address_valid_v4(ip)) { + warnx("invalid ip address %s\n", ip); + goto bad; + } + memset(&dbk, 0, sizeof(dbk)); + dbk.size = strlen(ip); + dbk.data = ip; + memset(&dbd, 0, sizeof(dbd)); + if (!add) { + /* remove whitelist entry */ + r = db->get(db, &dbk, &dbd, 0); + if (r == -1) { + warn("db->get failed"); + goto bad; + } + if (r) { + warnx("no entry for %s", ip); + goto bad; + } else if (db->del(db, &dbk, 0)) { + warn("db->del failed"); + goto bad; + } + } else { + /* add or update whitelist entry */ + r = db->get(db, &dbk, &dbd, 0); + if (r == -1) { + warn("db->get failed"); + goto bad; + } + if (r) { + /* new entry */ + memset(&gd, 0, sizeof(gd)); + gd.first = now; + gd.bcount = 1; + gd.pass = now; + gd.expire = now + WHITEEXP; + memset(&dbk, 0, sizeof(dbk)); + dbk.size = strlen(ip); + dbk.data = ip; + memset(&dbd, 0, sizeof(dbd)); + dbd.size = sizeof(gd); + dbd.data = &gd; + r = db->put(db, &dbk, &dbd, 0); + if (r) { + warn("db->put failed"); + goto bad; + } + } else { + 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.pcount++; + gd.expire = now + WHITEEXP; + memset(&dbk, 0, sizeof(dbk)); + dbk.size = strlen(ip); + dbk.data = ip; + memset(&dbd, 0, sizeof(dbd)); + dbd.size = sizeof(gd); + dbd.data = &gd; + r = db->put(db, &dbk, &dbd, 0); + if (r) { + warn("db->put failed"); + goto bad; + } + } + } + db->sync(db, 0); + db->close(db); + return (0); + bad: + db->sync(db, 0); + db->close(db); + return(-1); +} + + + +int +dblist(char *dbname) +{ + struct gdata gd; + int r; + + /* walk db, list in text format */ + memset(&btreeinfo, 0, sizeof(btreeinfo)); + db = dbopen(dbname, O_EXLOCK|O_RDWR, 0600, DB_BTREE, &btreeinfo); + if (db == NULL) + err(1, "dbopen"); + 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, *cp; + + if ((dbk.size < 1) || dbd.size != sizeof(struct gdata)) { + db->close(db); + errx(1, "bogus size db entry - bad db file?"); + } + memcpy(&gd, dbd.data, sizeof(gd)); + a = malloc(dbk.size + 1); + if (a == NULL) + err(1, "malloc"); + memcpy(a, dbk.data, dbk.size); + a[dbk.size]='\0'; + cp = strchr(a, '\n'); + if (cp == NULL) + /* this is a whitelist entry */ + printf("WHITE:%s:%d:%d:%d:%d:%d\n", a, gd.first, + gd.pass, gd.expire, gd.bcount, gd.pcount); + else { + char *from, *to; + + /* greylist entry */ + *cp = '\0'; + from = cp + 1; + to = strchr(from, '\n'); + if (to == NULL) { + warnx("No from part in grey key %s", a); + goto bad; + } + *to = '\0'; + to++; + printf("GREY:%s:%s:%s:%d:%d:%d:%d:%d\n", + a, from, to, gd.first, gd.pass, gd.expire, + gd.bcount, gd.pcount); + } + } + db->sync(db, 0); + db->close(db); + return(0); + bad: + db->sync(db, 0); + db->close(db); + errx(1, "incorrect db format entry"); + /* NOTREACHED */ + return(-1); +} + + +static int +usage(void) +{ + fprintf(stderr, "usage: spamdb [-a ip] [-d ip]\n"); + exit(-1); +} + +int +main(int argc, char **argv) +{ + int ch, action = 0; + char *ip = NULL; + + while ((ch = getopt(argc, argv, "a:d:")) != -1) { + switch (ch) { + case 'a': + action = 1; + ip = optarg; + break; + case 'd': + action = 2; + ip = optarg; + break; + default: + usage(); + break; + } + } + + switch (action) { + case 0: + dblist("/var/db/spamd"); + break; + case 1: + dbupdate("/var/db/spamd", ip, 1); + break; + case 2: + dbupdate("/var/db/spamd", ip, 0); + break; + default: + errx(-1, "bad action"); + } + return(0); +} |