xref: /plan9/sys/src/cmd/upas/smtp/greylist.c (revision db328f6c7c73a5910bf8af87a2c263ed92e7d421)
1*db328f6cSDavid du Colombier /*
2*db328f6cSDavid du Colombier  * greylisting is the practice of making unknown callers call twice, with
3*db328f6cSDavid du Colombier  * a pause between them, before accepting their mail and adding them to a
4*db328f6cSDavid du Colombier  * whitelist of known callers.
5*db328f6cSDavid du Colombier  *
6*db328f6cSDavid du Colombier  * There's a bit of a problem with yahoo and other large sources of mail;
7*db328f6cSDavid du Colombier  * they have a vast pool of machines that all run the same queue(s), so a
8*db328f6cSDavid du Colombier  * 451 retry can come from a different IP address for many, many retries,
9*db328f6cSDavid du Colombier  * and it can take ~5 hours for the same IP to call us back.  To cope
10*db328f6cSDavid du Colombier  * better with this, we immediately accept mail from any system on the
11*db328f6cSDavid du Colombier  * same class C subnet (IPv4 /24) as anybody on our whitelist, since the
12*db328f6cSDavid du Colombier  * mail-sending machines tend to be clustered within a class C subnet.
13*db328f6cSDavid du Colombier  *
14*db328f6cSDavid du Colombier  * Various other goofballs, notably the IEEE, try to send mail just
15*db328f6cSDavid du Colombier  * before 9 AM, then refuse to try again until after 5 PM. D'oh!
16*db328f6cSDavid du Colombier  */
17e288d156SDavid du Colombier #include "common.h"
18e288d156SDavid du Colombier #include "smtpd.h"
19e288d156SDavid du Colombier #include "smtp.h"
20e288d156SDavid du Colombier #include <ctype.h>
21e288d156SDavid du Colombier #include <ip.h>
22fd597ed8SDavid du Colombier #include <ndb.h>
23e288d156SDavid du Colombier 
24*db328f6cSDavid du Colombier enum {
25*db328f6cSDavid du Colombier 	Nonspammax = 14*60*60,  /* must call back within this time if real */
26*db328f6cSDavid du Colombier 	Nonspammin = 5*60,	/* must wait this long to retry */
27*db328f6cSDavid du Colombier };
28*db328f6cSDavid du Colombier 
2908863f9aSDavid du Colombier typedef struct {
3008863f9aSDavid du Colombier 	int	existed;	/* these two are distinct to cope with errors */
3108863f9aSDavid du Colombier 	int	created;
3208863f9aSDavid du Colombier 	int	noperm;
3308863f9aSDavid du Colombier 	long	mtime;		/* mod time, iff it already existed */
3408863f9aSDavid du Colombier } Greysts;
3508863f9aSDavid du Colombier 
363b56890dSDavid du Colombier static char whitelist[] = "/mail/grey/whitelist";
37e288d156SDavid du Colombier 
38e288d156SDavid du Colombier /*
39e288d156SDavid du Colombier  * matches ip addresses or subnets in whitelist against nci->rsys.
403b56890dSDavid du Colombier  * ignores comments and blank lines in /mail/grey/whitelist.
41e288d156SDavid du Colombier  */
42e288d156SDavid du Colombier static int
onwhitelist(void)43e288d156SDavid du Colombier onwhitelist(void)
44e288d156SDavid du Colombier {
45e288d156SDavid du Colombier 	int lnlen;
463b56890dSDavid du Colombier 	char *line, *parse, *p;
47e288d156SDavid du Colombier 	char input[128];
48*db328f6cSDavid du Colombier 	uchar *mask;
49e288d156SDavid du Colombier 	uchar mask4[IPaddrlen], addr4[IPaddrlen];
50*db328f6cSDavid du Colombier 	uchar rmask[IPaddrlen], addr[IPaddrlen];
51*db328f6cSDavid du Colombier 	uchar ipmasked[IPaddrlen], addrmasked[IPaddrlen];
52e288d156SDavid du Colombier 	Biobuf *wl;
53e288d156SDavid du Colombier 
54e288d156SDavid du Colombier 	wl = Bopen(whitelist, OREAD);
55e288d156SDavid du Colombier 	if (wl == nil)
56e288d156SDavid du Colombier 		return 1;
57e288d156SDavid du Colombier 	while ((line = Brdline(wl, '\n')) != nil) {
58e288d156SDavid du Colombier 		lnlen = Blinelen(wl);
59e288d156SDavid du Colombier 		line[lnlen-1] = '\0';		/* clobber newline */
60e288d156SDavid du Colombier 
613b56890dSDavid du Colombier 		p = strpbrk(line, " \t");
623b56890dSDavid du Colombier 		if (p)
633b56890dSDavid du Colombier 			*p = 0;
643b56890dSDavid du Colombier 		if (line[0] == '#' || line[0] == 0)
653b56890dSDavid du Colombier 			continue;
663b56890dSDavid du Colombier 
67*db328f6cSDavid du Colombier 		/* default mask is /24 (v4) or /128 (v6) for bare IP */
68e288d156SDavid du Colombier 		parse = line;
69e288d156SDavid du Colombier 		if (strchr(line, '/') == nil) {
703b56890dSDavid du Colombier 			strecpy(input, input + sizeof input - 5, line);
71*db328f6cSDavid du Colombier 			if (strchr(line, ':') != nil)	/* v6? */
72e288d156SDavid du Colombier 				strcat(input, "/128");
73*db328f6cSDavid du Colombier 			else if (strchr(line, '.') != nil)
74*db328f6cSDavid du Colombier 				strcat(input, "/24");	/* was /32 */
75e288d156SDavid du Colombier 			parse = input;
76e288d156SDavid du Colombier 		}
77*db328f6cSDavid du Colombier 		mask = rmask;
78*db328f6cSDavid du Colombier 		if (strchr(line, ':') != nil) {		/* v6? */
79*db328f6cSDavid du Colombier 			parseip(addr, parse);
80*db328f6cSDavid du Colombier 			p = strchr(parse, '/');
81*db328f6cSDavid du Colombier 			if (p != nil)
82*db328f6cSDavid du Colombier 				parseipmask(mask, p);
83*db328f6cSDavid du Colombier 			else
84*db328f6cSDavid du Colombier 				mask = IPallbits;
85*db328f6cSDavid du Colombier 		} else {
86e288d156SDavid du Colombier 			v4parsecidr(addr4, mask4, parse);
87e288d156SDavid du Colombier 			v4tov6(addr, addr4);
88e288d156SDavid du Colombier 			v4tov6(mask, mask4);
89*db328f6cSDavid du Colombier 		}
90e288d156SDavid du Colombier 		maskip(addr, mask, addrmasked);
9146595261SDavid du Colombier 		maskip(rsysip, mask, ipmasked);
92*db328f6cSDavid du Colombier 		if (equivip6(ipmasked, addrmasked))
93e288d156SDavid du Colombier 			break;
94e288d156SDavid du Colombier 	}
95e288d156SDavid du Colombier 	Bterm(wl);
96e288d156SDavid du Colombier 	return line != nil;
97e288d156SDavid du Colombier }
98e288d156SDavid du Colombier 
99e288d156SDavid du Colombier static int mkdirs(char *);
100e288d156SDavid du Colombier 
101e288d156SDavid du Colombier /*
102e288d156SDavid du Colombier  * if any directories leading up to path don't exist, create them.
103e288d156SDavid du Colombier  * modifies but restores path.
104e288d156SDavid du Colombier  */
105e288d156SDavid du Colombier static int
mkpdirs(char * path)106e288d156SDavid du Colombier mkpdirs(char *path)
107e288d156SDavid du Colombier {
108e288d156SDavid du Colombier 	int rv = 0;
109e288d156SDavid du Colombier 	char *sl = strrchr(path, '/');
110e288d156SDavid du Colombier 
111e288d156SDavid du Colombier 	if (sl != nil) {
112e288d156SDavid du Colombier 		*sl = '\0';
113e288d156SDavid du Colombier 		rv = mkdirs(path);
114e288d156SDavid du Colombier 		*sl = '/';
115e288d156SDavid du Colombier 	}
116e288d156SDavid du Colombier 	return rv;
117e288d156SDavid du Colombier }
118e288d156SDavid du Colombier 
119e288d156SDavid du Colombier /*
120e288d156SDavid du Colombier  * if path or any directories leading up to it don't exist, create them.
121e288d156SDavid du Colombier  * modifies but restores path.
122e288d156SDavid du Colombier  */
123e288d156SDavid du Colombier static int
mkdirs(char * path)124e288d156SDavid du Colombier mkdirs(char *path)
125e288d156SDavid du Colombier {
126e288d156SDavid du Colombier 	int fd;
127e288d156SDavid du Colombier 
128e288d156SDavid du Colombier 	if (access(path, AEXIST) >= 0)
129e288d156SDavid du Colombier 		return 0;
130e288d156SDavid du Colombier 
131e288d156SDavid du Colombier 	/* make presumed-missing intermediate directories */
132e288d156SDavid du Colombier 	if (mkpdirs(path) < 0)
133e288d156SDavid du Colombier 		return -1;
134e288d156SDavid du Colombier 
135e288d156SDavid du Colombier 	/* make final directory */
136e288d156SDavid du Colombier 	fd = create(path, OREAD, 0777|DMDIR);
137e288d156SDavid du Colombier 	if (fd < 0)
138e288d156SDavid du Colombier 		/*
139e288d156SDavid du Colombier 		 * we may have lost a race; if the directory now exists,
140e288d156SDavid du Colombier 		 * it's okay.
141e288d156SDavid du Colombier 		 */
142e288d156SDavid du Colombier 		return access(path, AEXIST) < 0? -1: 0;
143e288d156SDavid du Colombier 	close(fd);
144e288d156SDavid du Colombier 	return 0;
145e288d156SDavid du Colombier }
146e288d156SDavid du Colombier 
14708863f9aSDavid du Colombier static long
getmtime(char * file)14808863f9aSDavid du Colombier getmtime(char *file)
149e288d156SDavid du Colombier {
15046595261SDavid du Colombier 	int fd;
15108863f9aSDavid du Colombier 	long mtime = -1;
15246595261SDavid du Colombier 	Dir *ds;
153e288d156SDavid du Colombier 
15446595261SDavid du Colombier 	fd = open(file, ORDWR);
15546595261SDavid du Colombier 	if (fd < 0)
15646595261SDavid du Colombier 		return mtime;
15746595261SDavid du Colombier 	ds = dirfstat(fd);
15808863f9aSDavid du Colombier 	if (ds != nil) {
15908863f9aSDavid du Colombier 		mtime = ds->mtime;
1607e254d1cSDavid du Colombier 		/*
1617e254d1cSDavid du Colombier 		 * new twist: update file's mtime after reading it,
1627e254d1cSDavid du Colombier 		 * so each call resets the future time after which
1637e254d1cSDavid du Colombier 		 * we'll accept calls.  thus spammers who keep pounding
1647e254d1cSDavid du Colombier 		 * us lose, but just pausing for a few minutes and retrying
1657e254d1cSDavid du Colombier 		 * will succeed.
1667e254d1cSDavid du Colombier 		 */
16746595261SDavid du Colombier 		if (0) {
16846595261SDavid du Colombier 			/*
16946595261SDavid du Colombier 			 * apparently none can't do this wstat
17046595261SDavid du Colombier 			 * (permission denied);
17146595261SDavid du Colombier 			 * more undocumented whacky none behaviour.
17246595261SDavid du Colombier 			 */
1737e254d1cSDavid du Colombier 			ds->mtime = time(0);
17446595261SDavid du Colombier 			if (dirfwstat(fd, ds) < 0)
17546595261SDavid du Colombier 				syslog(0, "smtpd", "dirfwstat %s: %r", file);
17608863f9aSDavid du Colombier 		}
17746595261SDavid du Colombier 		free(ds);
17846595261SDavid du Colombier 		write(fd, "x", 1);
17946595261SDavid du Colombier 	}
18046595261SDavid du Colombier 	close(fd);
18108863f9aSDavid du Colombier 	return mtime;
18208863f9aSDavid du Colombier }
183e288d156SDavid du Colombier 
18408863f9aSDavid du Colombier static void
tryaddgrey(char * file,Greysts * gsp)18508863f9aSDavid du Colombier tryaddgrey(char *file, Greysts *gsp)
18608863f9aSDavid du Colombier {
18746595261SDavid du Colombier 	int fd = create(file, OWRITE|OEXCL, 0666);
188e288d156SDavid du Colombier 
18908863f9aSDavid du Colombier 	gsp->created = (fd >= 0);
19008863f9aSDavid du Colombier 	if (fd >= 0) {
19108863f9aSDavid du Colombier 		close(fd);
19208863f9aSDavid du Colombier 		gsp->existed = 0;  /* just created; couldn't have existed */
1937e254d1cSDavid du Colombier 		gsp->mtime = time(0);
19408863f9aSDavid du Colombier 	} else {
19508863f9aSDavid du Colombier 		/*
19608863f9aSDavid du Colombier 		 * why couldn't we create file? it must have existed
19708863f9aSDavid du Colombier 		 * (or we were denied perm on parent dir.).
19808863f9aSDavid du Colombier 		 * if it existed, fill in gsp->mtime; otherwise
19908863f9aSDavid du Colombier 		 * make presumed-missing intermediate directories.
20008863f9aSDavid du Colombier 		 */
20108863f9aSDavid du Colombier 		gsp->existed = access(file, AEXIST) >= 0;
20208863f9aSDavid du Colombier 		if (gsp->existed)
20308863f9aSDavid du Colombier 			gsp->mtime = getmtime(file);
20408863f9aSDavid du Colombier 		else if (mkpdirs(file) < 0)
20508863f9aSDavid du Colombier 			gsp->noperm = 1;
20608863f9aSDavid du Colombier 	}
20708863f9aSDavid du Colombier }
20808863f9aSDavid du Colombier 
20908863f9aSDavid du Colombier static void
addgreylist(char * file,Greysts * gsp)21008863f9aSDavid du Colombier addgreylist(char *file, Greysts *gsp)
21108863f9aSDavid du Colombier {
21208863f9aSDavid du Colombier 	tryaddgrey(file, gsp);
21308863f9aSDavid du Colombier 	if (!gsp->created && !gsp->existed && !gsp->noperm)
21408863f9aSDavid du Colombier 		/* retry the greylist entry with parent dirs created */
21508863f9aSDavid du Colombier 		tryaddgrey(file, gsp);
216e288d156SDavid du Colombier }
217e288d156SDavid du Colombier 
218e288d156SDavid du Colombier static int
recentcall(Greysts * gsp)21908863f9aSDavid du Colombier recentcall(Greysts *gsp)
220e288d156SDavid du Colombier {
22108863f9aSDavid du Colombier 	long delay = time(0) - gsp->mtime;
22208863f9aSDavid du Colombier 
22308863f9aSDavid du Colombier 	if (!gsp->existed)
22408863f9aSDavid du Colombier 		return 0;
22508863f9aSDavid du Colombier 	/* reject immediate call-back; spammers are doing that now */
226f097827aSDavid du Colombier 	return delay >= Nonspammin && delay <= Nonspammax;
227e288d156SDavid du Colombier }
228e288d156SDavid du Colombier 
229e288d156SDavid du Colombier /*
230e288d156SDavid du Colombier  * policy: if (caller-IP, my-IP, rcpt) is not on the greylist,
231e288d156SDavid du Colombier  * reject this message as "451 temporary failure".  if the caller is real,
232e288d156SDavid du Colombier  * he'll retry soon, otherwise he's a spammer.
233e288d156SDavid du Colombier  * at the first rejection, create a greylist entry for (my-ip, caller-ip,
234e288d156SDavid du Colombier  * rcpt, time), where time is the file's mtime.  if they call back and there's
235e288d156SDavid du Colombier  * already a greylist entry, and it's within the allowed interval,
236e288d156SDavid du Colombier  * add their IP to the append-only whitelist.
237e288d156SDavid du Colombier  *
238e288d156SDavid du Colombier  * greylist files can be removed at will; at worst they'll cause a few
239e288d156SDavid du Colombier  * extra retries.
240e288d156SDavid du Colombier  */
241e288d156SDavid du Colombier 
242e288d156SDavid du Colombier static int
isrcptrecent(char * rcpt)243e288d156SDavid du Colombier isrcptrecent(char *rcpt)
244e288d156SDavid du Colombier {
245e288d156SDavid du Colombier 	char *user;
246e288d156SDavid du Colombier 	char file[256];
24708863f9aSDavid du Colombier 	Greysts gs;
24808863f9aSDavid du Colombier 	Greysts *gsp = &gs;
249e288d156SDavid du Colombier 
250e288d156SDavid du Colombier 	if (rcpt[0] == '\0' || strchr(rcpt, '/') != nil ||
251e288d156SDavid du Colombier 	    strcmp(rcpt, ".") == 0 || strcmp(rcpt, "..") == 0)
252e288d156SDavid du Colombier 		return 0;
253e288d156SDavid du Colombier 
254e288d156SDavid du Colombier 	/* shorten names to fit pre-fossil or pre-9p2000 file servers */
255e288d156SDavid du Colombier 	user = strrchr(rcpt, '!');
256e288d156SDavid du Colombier 	if (user == nil)
257e288d156SDavid du Colombier 		user = rcpt;
258e288d156SDavid du Colombier 	else
259e288d156SDavid du Colombier 		user++;
260e288d156SDavid du Colombier 
26108863f9aSDavid du Colombier 	/* check & try to update the grey list entry */
2623b56890dSDavid du Colombier 	snprint(file, sizeof file, "/mail/grey/tmp/%s/%s/%s",
263e288d156SDavid du Colombier 		nci->lsys, nci->rsys, user);
26408863f9aSDavid du Colombier 	memset(gsp, 0, sizeof *gsp);
26508863f9aSDavid du Colombier 	addgreylist(file, gsp);
266e288d156SDavid du Colombier 
267e288d156SDavid du Colombier 	/* if on greylist already and prior call was recent, add to whitelist */
26808863f9aSDavid du Colombier 	if (gsp->existed && recentcall(gsp)) {
269e288d156SDavid du Colombier 		syslog(0, "smtpd",
270e288d156SDavid du Colombier 			"%s/%s was grey; adding IP to white", nci->rsys, rcpt);
271e288d156SDavid du Colombier 		return 1;
27208863f9aSDavid du Colombier 	} else if (gsp->existed)
27346595261SDavid du Colombier 		syslog(0, "smtpd", "call for %s/%s was just minutes ago "
27446595261SDavid du Colombier 			"or long ago", nci->rsys, rcpt);
275e288d156SDavid du Colombier 	else
27608863f9aSDavid du Colombier 		syslog(0, "smtpd", "no call registered for %s/%s; registering",
277e288d156SDavid du Colombier 			nci->rsys, rcpt);
278e288d156SDavid du Colombier 	return 0;
279e288d156SDavid du Colombier }
280e288d156SDavid du Colombier 
281e288d156SDavid du Colombier void
vfysenderhostok(void)282e288d156SDavid du Colombier vfysenderhostok(void)
283e288d156SDavid du Colombier {
284fd597ed8SDavid du Colombier 	char *fqdn;
285e288d156SDavid du Colombier 	int recent = 0;
286e288d156SDavid du Colombier 	Link *l;
287e288d156SDavid du Colombier 
288e288d156SDavid du Colombier 	if (onwhitelist())
289e288d156SDavid du Colombier 		return;
290e288d156SDavid du Colombier 
291e288d156SDavid du Colombier 	for (l = rcvers.first; l; l = l->next)
292e288d156SDavid du Colombier 		if (isrcptrecent(s_to_c(l->p)))
293e288d156SDavid du Colombier 			recent = 1;
294e288d156SDavid du Colombier 
295e288d156SDavid du Colombier 	/* if on greylist already and prior call was recent, add to whitelist */
296e288d156SDavid du Colombier 	if (recent) {
29708863f9aSDavid du Colombier 		int fd = create(whitelist, OWRITE, 0666|DMAPPEND);
298e288d156SDavid du Colombier 
299e288d156SDavid du Colombier 		if (fd >= 0) {
300e288d156SDavid du Colombier 			seek(fd, 0, 2);			/* paranoia */
301e06f534bSDavid du Colombier 			fqdn = csgetvalue(nci->root, "ip", nci->rsys, "dom",
302e06f534bSDavid du Colombier 				nil);
303e06f534bSDavid du Colombier 			if (fqdn != nil)
3043b56890dSDavid du Colombier 				fprint(fd, "%s %s\n", nci->rsys, fqdn);
305fd597ed8SDavid du Colombier 			else
3063b56890dSDavid du Colombier 				fprint(fd, "%s\n", nci->rsys);
307e06f534bSDavid du Colombier 			free(fqdn);
308e288d156SDavid du Colombier 			close(fd);
309e288d156SDavid du Colombier 		}
310e288d156SDavid du Colombier 	} else {
311e288d156SDavid du Colombier 		syslog(0, "smtpd",
312e288d156SDavid du Colombier 	"no recent call from %s for a rcpt; rejecting with temporary failure",
313e288d156SDavid du Colombier 			nci->rsys);
314e288d156SDavid du Colombier 		reply("451 please try again soon from the same IP.\r\n");
315e288d156SDavid du Colombier 		exits("no recent call for a rcpt");
316e288d156SDavid du Colombier 	}
317e288d156SDavid du Colombier }
318