xref: /plan9/sys/src/cmd/upas/smtp/greylist.c (revision db328f6c7c73a5910bf8af87a2c263ed92e7d421)
1 /*
2  * greylisting is the practice of making unknown callers call twice, with
3  * a pause between them, before accepting their mail and adding them to a
4  * whitelist of known callers.
5  *
6  * There's a bit of a problem with yahoo and other large sources of mail;
7  * they have a vast pool of machines that all run the same queue(s), so a
8  * 451 retry can come from a different IP address for many, many retries,
9  * and it can take ~5 hours for the same IP to call us back.  To cope
10  * better with this, we immediately accept mail from any system on the
11  * same class C subnet (IPv4 /24) as anybody on our whitelist, since the
12  * mail-sending machines tend to be clustered within a class C subnet.
13  *
14  * Various other goofballs, notably the IEEE, try to send mail just
15  * before 9 AM, then refuse to try again until after 5 PM. D'oh!
16  */
17 #include "common.h"
18 #include "smtpd.h"
19 #include "smtp.h"
20 #include <ctype.h>
21 #include <ip.h>
22 #include <ndb.h>
23 
24 enum {
25 	Nonspammax = 14*60*60,  /* must call back within this time if real */
26 	Nonspammin = 5*60,	/* must wait this long to retry */
27 };
28 
29 typedef struct {
30 	int	existed;	/* these two are distinct to cope with errors */
31 	int	created;
32 	int	noperm;
33 	long	mtime;		/* mod time, iff it already existed */
34 } Greysts;
35 
36 static char whitelist[] = "/mail/grey/whitelist";
37 
38 /*
39  * matches ip addresses or subnets in whitelist against nci->rsys.
40  * ignores comments and blank lines in /mail/grey/whitelist.
41  */
42 static int
onwhitelist(void)43 onwhitelist(void)
44 {
45 	int lnlen;
46 	char *line, *parse, *p;
47 	char input[128];
48 	uchar *mask;
49 	uchar mask4[IPaddrlen], addr4[IPaddrlen];
50 	uchar rmask[IPaddrlen], addr[IPaddrlen];
51 	uchar ipmasked[IPaddrlen], addrmasked[IPaddrlen];
52 	Biobuf *wl;
53 
54 	wl = Bopen(whitelist, OREAD);
55 	if (wl == nil)
56 		return 1;
57 	while ((line = Brdline(wl, '\n')) != nil) {
58 		lnlen = Blinelen(wl);
59 		line[lnlen-1] = '\0';		/* clobber newline */
60 
61 		p = strpbrk(line, " \t");
62 		if (p)
63 			*p = 0;
64 		if (line[0] == '#' || line[0] == 0)
65 			continue;
66 
67 		/* default mask is /24 (v4) or /128 (v6) for bare IP */
68 		parse = line;
69 		if (strchr(line, '/') == nil) {
70 			strecpy(input, input + sizeof input - 5, line);
71 			if (strchr(line, ':') != nil)	/* v6? */
72 				strcat(input, "/128");
73 			else if (strchr(line, '.') != nil)
74 				strcat(input, "/24");	/* was /32 */
75 			parse = input;
76 		}
77 		mask = rmask;
78 		if (strchr(line, ':') != nil) {		/* v6? */
79 			parseip(addr, parse);
80 			p = strchr(parse, '/');
81 			if (p != nil)
82 				parseipmask(mask, p);
83 			else
84 				mask = IPallbits;
85 		} else {
86 			v4parsecidr(addr4, mask4, parse);
87 			v4tov6(addr, addr4);
88 			v4tov6(mask, mask4);
89 		}
90 		maskip(addr, mask, addrmasked);
91 		maskip(rsysip, mask, ipmasked);
92 		if (equivip6(ipmasked, addrmasked))
93 			break;
94 	}
95 	Bterm(wl);
96 	return line != nil;
97 }
98 
99 static int mkdirs(char *);
100 
101 /*
102  * if any directories leading up to path don't exist, create them.
103  * modifies but restores path.
104  */
105 static int
mkpdirs(char * path)106 mkpdirs(char *path)
107 {
108 	int rv = 0;
109 	char *sl = strrchr(path, '/');
110 
111 	if (sl != nil) {
112 		*sl = '\0';
113 		rv = mkdirs(path);
114 		*sl = '/';
115 	}
116 	return rv;
117 }
118 
119 /*
120  * if path or any directories leading up to it don't exist, create them.
121  * modifies but restores path.
122  */
123 static int
mkdirs(char * path)124 mkdirs(char *path)
125 {
126 	int fd;
127 
128 	if (access(path, AEXIST) >= 0)
129 		return 0;
130 
131 	/* make presumed-missing intermediate directories */
132 	if (mkpdirs(path) < 0)
133 		return -1;
134 
135 	/* make final directory */
136 	fd = create(path, OREAD, 0777|DMDIR);
137 	if (fd < 0)
138 		/*
139 		 * we may have lost a race; if the directory now exists,
140 		 * it's okay.
141 		 */
142 		return access(path, AEXIST) < 0? -1: 0;
143 	close(fd);
144 	return 0;
145 }
146 
147 static long
getmtime(char * file)148 getmtime(char *file)
149 {
150 	int fd;
151 	long mtime = -1;
152 	Dir *ds;
153 
154 	fd = open(file, ORDWR);
155 	if (fd < 0)
156 		return mtime;
157 	ds = dirfstat(fd);
158 	if (ds != nil) {
159 		mtime = ds->mtime;
160 		/*
161 		 * new twist: update file's mtime after reading it,
162 		 * so each call resets the future time after which
163 		 * we'll accept calls.  thus spammers who keep pounding
164 		 * us lose, but just pausing for a few minutes and retrying
165 		 * will succeed.
166 		 */
167 		if (0) {
168 			/*
169 			 * apparently none can't do this wstat
170 			 * (permission denied);
171 			 * more undocumented whacky none behaviour.
172 			 */
173 			ds->mtime = time(0);
174 			if (dirfwstat(fd, ds) < 0)
175 				syslog(0, "smtpd", "dirfwstat %s: %r", file);
176 		}
177 		free(ds);
178 		write(fd, "x", 1);
179 	}
180 	close(fd);
181 	return mtime;
182 }
183 
184 static void
tryaddgrey(char * file,Greysts * gsp)185 tryaddgrey(char *file, Greysts *gsp)
186 {
187 	int fd = create(file, OWRITE|OEXCL, 0666);
188 
189 	gsp->created = (fd >= 0);
190 	if (fd >= 0) {
191 		close(fd);
192 		gsp->existed = 0;  /* just created; couldn't have existed */
193 		gsp->mtime = time(0);
194 	} else {
195 		/*
196 		 * why couldn't we create file? it must have existed
197 		 * (or we were denied perm on parent dir.).
198 		 * if it existed, fill in gsp->mtime; otherwise
199 		 * make presumed-missing intermediate directories.
200 		 */
201 		gsp->existed = access(file, AEXIST) >= 0;
202 		if (gsp->existed)
203 			gsp->mtime = getmtime(file);
204 		else if (mkpdirs(file) < 0)
205 			gsp->noperm = 1;
206 	}
207 }
208 
209 static void
addgreylist(char * file,Greysts * gsp)210 addgreylist(char *file, Greysts *gsp)
211 {
212 	tryaddgrey(file, gsp);
213 	if (!gsp->created && !gsp->existed && !gsp->noperm)
214 		/* retry the greylist entry with parent dirs created */
215 		tryaddgrey(file, gsp);
216 }
217 
218 static int
recentcall(Greysts * gsp)219 recentcall(Greysts *gsp)
220 {
221 	long delay = time(0) - gsp->mtime;
222 
223 	if (!gsp->existed)
224 		return 0;
225 	/* reject immediate call-back; spammers are doing that now */
226 	return delay >= Nonspammin && delay <= Nonspammax;
227 }
228 
229 /*
230  * policy: if (caller-IP, my-IP, rcpt) is not on the greylist,
231  * reject this message as "451 temporary failure".  if the caller is real,
232  * he'll retry soon, otherwise he's a spammer.
233  * at the first rejection, create a greylist entry for (my-ip, caller-ip,
234  * rcpt, time), where time is the file's mtime.  if they call back and there's
235  * already a greylist entry, and it's within the allowed interval,
236  * add their IP to the append-only whitelist.
237  *
238  * greylist files can be removed at will; at worst they'll cause a few
239  * extra retries.
240  */
241 
242 static int
isrcptrecent(char * rcpt)243 isrcptrecent(char *rcpt)
244 {
245 	char *user;
246 	char file[256];
247 	Greysts gs;
248 	Greysts *gsp = &gs;
249 
250 	if (rcpt[0] == '\0' || strchr(rcpt, '/') != nil ||
251 	    strcmp(rcpt, ".") == 0 || strcmp(rcpt, "..") == 0)
252 		return 0;
253 
254 	/* shorten names to fit pre-fossil or pre-9p2000 file servers */
255 	user = strrchr(rcpt, '!');
256 	if (user == nil)
257 		user = rcpt;
258 	else
259 		user++;
260 
261 	/* check & try to update the grey list entry */
262 	snprint(file, sizeof file, "/mail/grey/tmp/%s/%s/%s",
263 		nci->lsys, nci->rsys, user);
264 	memset(gsp, 0, sizeof *gsp);
265 	addgreylist(file, gsp);
266 
267 	/* if on greylist already and prior call was recent, add to whitelist */
268 	if (gsp->existed && recentcall(gsp)) {
269 		syslog(0, "smtpd",
270 			"%s/%s was grey; adding IP to white", nci->rsys, rcpt);
271 		return 1;
272 	} else if (gsp->existed)
273 		syslog(0, "smtpd", "call for %s/%s was just minutes ago "
274 			"or long ago", nci->rsys, rcpt);
275 	else
276 		syslog(0, "smtpd", "no call registered for %s/%s; registering",
277 			nci->rsys, rcpt);
278 	return 0;
279 }
280 
281 void
vfysenderhostok(void)282 vfysenderhostok(void)
283 {
284 	char *fqdn;
285 	int recent = 0;
286 	Link *l;
287 
288 	if (onwhitelist())
289 		return;
290 
291 	for (l = rcvers.first; l; l = l->next)
292 		if (isrcptrecent(s_to_c(l->p)))
293 			recent = 1;
294 
295 	/* if on greylist already and prior call was recent, add to whitelist */
296 	if (recent) {
297 		int fd = create(whitelist, OWRITE, 0666|DMAPPEND);
298 
299 		if (fd >= 0) {
300 			seek(fd, 0, 2);			/* paranoia */
301 			fqdn = csgetvalue(nci->root, "ip", nci->rsys, "dom",
302 				nil);
303 			if (fqdn != nil)
304 				fprint(fd, "%s %s\n", nci->rsys, fqdn);
305 			else
306 				fprint(fd, "%s\n", nci->rsys);
307 			free(fqdn);
308 			close(fd);
309 		}
310 	} else {
311 		syslog(0, "smtpd",
312 	"no recent call from %s for a rcpt; rejecting with temporary failure",
313 			nci->rsys);
314 		reply("451 please try again soon from the same IP.\r\n");
315 		exits("no recent call for a rcpt");
316 	}
317 }
318