summaryrefslogtreecommitdiff
path: root/libexec/spamd
diff options
context:
space:
mode:
authorBob Beck <beck@cvs.openbsd.org>2004-02-26 07:28:56 +0000
committerBob Beck <beck@cvs.openbsd.org>2004-02-26 07:28:56 +0000
commitc879576d51d8099395bf9286c8e939a3c6484839 (patch)
tree873df009cb315b2bdd3132a80971aee555209401 /libexec/spamd
parent68ca8b3e82a4a5094091027feddcb51262302e73 (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/Makefile4
-rw-r--r--libexec/spamd/grey.c456
-rw-r--r--libexec/spamd/grey.h18
-rw-r--r--libexec/spamd/sdl.c16
-rw-r--r--libexec/spamd/spamd.8137
-rw-r--r--libexec/spamd/spamd.c261
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) {