#include "common.h" #include "smtpd.h" #include "smtp.h" #include #include 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) { 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 */ fprint(fd, "%s\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"); } }