aboutsummaryrefslogtreecommitdiff
path: root/src/cmd/upas/smtp/greylist.c
blob: 1efd145718db661f984f9fd6d5d254416983290a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
#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 = "#9/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;

	if (!beenhere) {
		beenhere = 1;
		fmtinstall('I', eipfmt);
		whitelist = unsharp(whitelist);
	}

	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, "%s/mail/grey/%s/%s/%s",
		get9root(), 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, "# 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");
	}
}