aboutsummaryrefslogtreecommitdiff
path: root/src/cmd/upas/smtp/greylist.c
diff options
context:
space:
mode:
authorrsc <devnull@localhost>2005-10-29 16:26:44 +0000
committerrsc <devnull@localhost>2005-10-29 16:26:44 +0000
commit5cdb17983ae6e6367ad7a940cb219eab247a9304 (patch)
tree8ca1ef49af2a96e7daebe624d91fdf679814a057 /src/cmd/upas/smtp/greylist.c
parentcd3745196389579fb78b9b01ef1daefb5a57aa71 (diff)
downloadplan9port-5cdb17983ae6e6367ad7a940cb219eab247a9304.tar.gz
plan9port-5cdb17983ae6e6367ad7a940cb219eab247a9304.tar.bz2
plan9port-5cdb17983ae6e6367ad7a940cb219eab247a9304.zip
Thanks to John Cummings.
Diffstat (limited to 'src/cmd/upas/smtp/greylist.c')
-rw-r--r--src/cmd/upas/smtp/greylist.c274
1 files changed, 274 insertions, 0 deletions
diff --git a/src/cmd/upas/smtp/greylist.c b/src/cmd/upas/smtp/greylist.c
new file mode 100644
index 00000000..915e688e
--- /dev/null
+++ b/src/cmd/upas/smtp/greylist.c
@@ -0,0 +1,274 @@
+#include "common.h"
+#include "smtpd.h"
+#include "smtp.h"
+#include <ctype.h>
+#include <ip.h>
+#include <ndb.h>
+
+typedef struct {
+ int existed; /* these two are distinct to cope with errors */
+ int created;
+ int noperm;
+ long mtime; /* mod time, iff it already existed */
+} Greysts;
+
+/*
+ * There's a bit of a problem with yahoo; they apparently have a vast
+ * pool of machines that all run the same queue(s), so a 451 retry can
+ * come from a different IP address for many, many retries, and it can
+ * take ~5 hours for the same IP to call us back. Various other goofballs,
+ * notably the IEEE, try to send mail just before 9 AM, then refuse to try
+ * again until after 5 PM. Doh!
+ */
+enum {
+ Nonspammax = 14*60*60, /* must call back within this time if real */
+};
+static char whitelist[] = "/mail/lib/whitelist";
+
+/*
+ * matches ip addresses or subnets in whitelist against nci->rsys.
+ * ignores comments and blank lines in /mail/lib/whitelist.
+ */
+static int
+onwhitelist(void)
+{
+ int lnlen;
+ char *line, *parse;
+ char input[128];
+ uchar ip[IPaddrlen], ipmasked[IPaddrlen];
+ uchar mask4[IPaddrlen], addr4[IPaddrlen];
+ uchar mask[IPaddrlen], addr[IPaddrlen], addrmasked[IPaddrlen];
+ Biobuf *wl;
+ static int beenhere;
+ static allzero[IPaddrlen];
+
+ if (!beenhere) {
+ beenhere = 1;
+ fmtinstall('I', eipfmt);
+ }
+
+ parseip(ip, nci->rsys);
+ wl = Bopen(whitelist, OREAD);
+ if (wl == nil)
+ return 1;
+ while ((line = Brdline(wl, '\n')) != nil) {
+ if (line[0] == '#' || line[0] == '\n')
+ continue;
+ lnlen = Blinelen(wl);
+ line[lnlen-1] = '\0'; /* clobber newline */
+
+ /* default mask is /32 (v4) or /128 (v6) for bare IP */
+ parse = line;
+ if (strchr(line, '/') == nil) {
+ strncpy(input, line, sizeof input - 5);
+ if (strchr(line, '.') != nil)
+ strcat(input, "/32");
+ else
+ strcat(input, "/128");
+ parse = input;
+ }
+ /* sorry, dave; where's parsecidr for v4 or v6? */
+ v4parsecidr(addr4, mask4, parse);
+ v4tov6(addr, addr4);
+ v4tov6(mask, mask4);
+
+ maskip(addr, mask, addrmasked);
+ maskip(ip, mask, ipmasked);
+ if (memcmp(ipmasked, addrmasked, IPaddrlen) == 0)
+ break;
+ }
+ Bterm(wl);
+ return line != nil;
+}
+
+static int mkdirs(char *);
+
+/*
+ * if any directories leading up to path don't exist, create them.
+ * modifies but restores path.
+ */
+static int
+mkpdirs(char *path)
+{
+ int rv = 0;
+ char *sl = strrchr(path, '/');
+
+ if (sl != nil) {
+ *sl = '\0';
+ rv = mkdirs(path);
+ *sl = '/';
+ }
+ return rv;
+}
+
+/*
+ * if path or any directories leading up to it don't exist, create them.
+ * modifies but restores path.
+ */
+static int
+mkdirs(char *path)
+{
+ int fd;
+
+ if (access(path, AEXIST) >= 0)
+ return 0;
+
+ /* make presumed-missing intermediate directories */
+ if (mkpdirs(path) < 0)
+ return -1;
+
+ /* make final directory */
+ fd = create(path, OREAD, 0777|DMDIR);
+ if (fd < 0)
+ /*
+ * we may have lost a race; if the directory now exists,
+ * it's okay.
+ */
+ return access(path, AEXIST) < 0? -1: 0;
+ close(fd);
+ return 0;
+}
+
+static long
+getmtime(char *file)
+{
+ long mtime = -1;
+ Dir *ds = dirstat(file);
+
+ if (ds != nil) {
+ mtime = ds->mtime;
+ free(ds);
+ }
+ return mtime;
+}
+
+static void
+tryaddgrey(char *file, Greysts *gsp)
+{
+ int fd = create(file, OWRITE|OEXCL, 0444|DMEXCL);
+
+ gsp->created = (fd >= 0);
+ if (fd >= 0) {
+ close(fd);
+ gsp->existed = 0; /* just created; couldn't have existed */
+ } else {
+ /*
+ * why couldn't we create file? it must have existed
+ * (or we were denied perm on parent dir.).
+ * if it existed, fill in gsp->mtime; otherwise
+ * make presumed-missing intermediate directories.
+ */
+ gsp->existed = access(file, AEXIST) >= 0;
+ if (gsp->existed)
+ gsp->mtime = getmtime(file);
+ else if (mkpdirs(file) < 0)
+ gsp->noperm = 1;
+ }
+}
+
+static void
+addgreylist(char *file, Greysts *gsp)
+{
+ tryaddgrey(file, gsp);
+ if (!gsp->created && !gsp->existed && !gsp->noperm)
+ /* retry the greylist entry with parent dirs created */
+ tryaddgrey(file, gsp);
+}
+
+static int
+recentcall(Greysts *gsp)
+{
+ long delay = time(0) - gsp->mtime;
+
+ if (!gsp->existed)
+ return 0;
+ /* reject immediate call-back; spammers are doing that now */
+ return delay >= 30 && delay <= Nonspammax;
+}
+
+/*
+ * policy: if (caller-IP, my-IP, rcpt) is not on the greylist,
+ * reject this message as "451 temporary failure". if the caller is real,
+ * he'll retry soon, otherwise he's a spammer.
+ * at the first rejection, create a greylist entry for (my-ip, caller-ip,
+ * rcpt, time), where time is the file's mtime. if they call back and there's
+ * already a greylist entry, and it's within the allowed interval,
+ * add their IP to the append-only whitelist.
+ *
+ * greylist files can be removed at will; at worst they'll cause a few
+ * extra retries.
+ */
+
+static int
+isrcptrecent(char *rcpt)
+{
+ char *user;
+ char file[256];
+ Greysts gs;
+ Greysts *gsp = &gs;
+
+ if (rcpt[0] == '\0' || strchr(rcpt, '/') != nil ||
+ strcmp(rcpt, ".") == 0 || strcmp(rcpt, "..") == 0)
+ return 0;
+
+ /* shorten names to fit pre-fossil or pre-9p2000 file servers */
+ user = strrchr(rcpt, '!');
+ if (user == nil)
+ user = rcpt;
+ else
+ user++;
+
+ /* check & try to update the grey list entry */
+ snprint(file, sizeof file, "/mail/grey/%s/%s/%s",
+ nci->lsys, nci->rsys, user);
+ memset(gsp, 0, sizeof *gsp);
+ addgreylist(file, gsp);
+
+ /* if on greylist already and prior call was recent, add to whitelist */
+ if (gsp->existed && recentcall(gsp)) {
+ syslog(0, "smtpd",
+ "%s/%s was grey; adding IP to white", nci->rsys, rcpt);
+ return 1;
+ } else if (gsp->existed)
+ syslog(0, "smtpd", "call for %s/%s was seconds ago or long ago",
+ nci->rsys, rcpt);
+ else
+ syslog(0, "smtpd", "no call registered for %s/%s; registering",
+ nci->rsys, rcpt);
+ return 0;
+}
+
+void
+vfysenderhostok(void)
+{
+ char *fqdn;
+ int recent = 0;
+ Link *l;
+
+ if (onwhitelist())
+ return;
+
+ for (l = rcvers.first; l; l = l->next)
+ if (isrcptrecent(s_to_c(l->p)))
+ recent = 1;
+
+ /* if on greylist already and prior call was recent, add to whitelist */
+ if (recent) {
+ int fd = create(whitelist, OWRITE, 0666|DMAPPEND);
+
+ if (fd >= 0) {
+ seek(fd, 0, 2); /* paranoia */
+ if ((fqdn = csgetvalue(nil, "ip", nci->rsys, "dom", nil)) != nil)
+ fprint(fd, "# %s\n%s\n\n", fqdn, nci->rsys);
+ else
+ fprint(fd, "# unknown\n%s\n\n", nci->rsys);
+ close(fd);
+ }
+ } else {
+ syslog(0, "smtpd",
+ "no recent call from %s for a rcpt; rejecting with temporary failure",
+ nci->rsys);
+ reply("451 please try again soon from the same IP.\r\n");
+ exits("no recent call for a rcpt");
+ }
+}