/* * greylisting is the practice of making unknown callers call twice, with * a pause between them, before accepting their mail and adding them to a * whitelist of known callers. * * There's a bit of a problem with yahoo and other large sources of mail; * they 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. To cope * better with this, we immediately accept mail from any system on the * same class C subnet (IPv4 /24) as anybody on our whitelist, since the * mail-sending machines tend to be clustered within a class C subnet. * * Various other goofballs, notably the IEEE, try to send mail just * before 9 AM, then refuse to try again until after 5 PM. D'oh! */ #include "common.h" #include "smtpd.h" #include "smtp.h" #include #include #include enum { Nonspammax = 14*60*60, /* must call back within this time if real */ Nonspammin = 5*60, /* must wait this long to retry */ }; 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; static char whitelist[] = "/mail/grey/whitelist"; /* * matches ip addresses or subnets in whitelist against nci->rsys. * ignores comments and blank lines in /mail/grey/whitelist. */ static int onwhitelist(void) { int lnlen; char *line, *parse, *p; char input[128]; uchar *mask; uchar mask4[IPaddrlen], addr4[IPaddrlen]; uchar rmask[IPaddrlen], addr[IPaddrlen]; uchar ipmasked[IPaddrlen], addrmasked[IPaddrlen]; Biobuf *wl; wl = Bopen(whitelist, OREAD); if (wl == nil) return 1; while ((line = Brdline(wl, '\n')) != nil) { lnlen = Blinelen(wl); line[lnlen-1] = '\0'; /* clobber newline */ p = strpbrk(line, " \t"); if (p) *p = 0; if (line[0] == '#' || line[0] == 0) continue; /* default mask is /24 (v4) or /128 (v6) for bare IP */ parse = line; if (strchr(line, '/') == nil) { strecpy(input, input + sizeof input - 5, line); if (strchr(line, ':') != nil) /* v6? */ strcat(input, "/128"); else if (strchr(line, '.') != nil) strcat(input, "/24"); /* was /32 */ parse = input; } mask = rmask; if (strchr(line, ':') != nil) { /* v6? */ parseip(addr, parse); p = strchr(parse, '/'); if (p != nil) parseipmask(mask, p); else mask = IPallbits; } else { v4parsecidr(addr4, mask4, parse); v4tov6(addr, addr4); v4tov6(mask, mask4); } maskip(addr, mask, addrmasked); maskip(rsysip, mask, ipmasked); if (equivip6(ipmasked, addrmasked)) 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) { int fd; long mtime = -1; Dir *ds; fd = open(file, ORDWR); if (fd < 0) return mtime; ds = dirfstat(fd); if (ds != nil) { mtime = ds->mtime; /* * new twist: update file's mtime after reading it, * so each call resets the future time after which * we'll accept calls. thus spammers who keep pounding * us lose, but just pausing for a few minutes and retrying * will succeed. */ if (0) { /* * apparently none can't do this wstat * (permission denied); * more undocumented whacky none behaviour. */ ds->mtime = time(0); if (dirfwstat(fd, ds) < 0) syslog(0, "smtpd", "dirfwstat %s: %r", file); } free(ds); write(fd, "x", 1); } close(fd); return mtime; } static void tryaddgrey(char *file, Greysts *gsp) { int fd = create(file, OWRITE|OEXCL, 0666); gsp->created = (fd >= 0); if (fd >= 0) { close(fd); gsp->existed = 0; /* just created; couldn't have existed */ gsp->mtime = time(0); } 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 >= Nonspammin && 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/tmp/%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 just minutes 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 */ fqdn = csgetvalue(nci->root, "ip", nci->rsys, "dom", nil); if (fqdn != nil) fprint(fd, "%s %s\n", nci->rsys, fqdn); else fprint(fd, "%s\n", nci->rsys); free(fqdn); 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"); } }