diff options
author | rsc <devnull@localhost> | 2005-10-29 16:26:44 +0000 |
---|---|---|
committer | rsc <devnull@localhost> | 2005-10-29 16:26:44 +0000 |
commit | 5cdb17983ae6e6367ad7a940cb219eab247a9304 (patch) | |
tree | 8ca1ef49af2a96e7daebe624d91fdf679814a057 /src/cmd/upas/smtp/greylist.c | |
parent | cd3745196389579fb78b9b01ef1daefb5a57aa71 (diff) | |
download | plan9port-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.c | 274 |
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"); + } +} |