summaryrefslogtreecommitdiffstats
path: root/src/postscreen/postscreen_early.c
blob: c9d8fafc14811307e25751d10e58145efc64dd85 (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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
/*++
/* NAME
/*	postscreen_early 3
/* SUMMARY
/*	postscreen pre-handshake tests
/* SYNOPSIS
/*	#include <postscreen.h>
/*
/*	void	psc_early_init(void)
/*
/*	void	psc_early_tests(state)
/*	PSC_STATE *state;
/* DESCRIPTION
/*	psc_early_tests() performs protocol tests before the SMTP
/*	handshake: the pregreet test and the DNSBL test. Control
/*	is passed to the psc_smtpd_tests() routine as appropriate.
/*
/*	psc_early_init() performs one-time initialization.
/* LICENSE
/* .ad
/* .fi
/*	The Secure Mailer license must be distributed with this software.
/* AUTHOR(S)
/*	Wietse Venema
/*	IBM T.J. Watson Research
/*	P.O. Box 704
/*	Yorktown Heights, NY 10598, USA
/*
/*	Wietse Venema
/*	Google, Inc.
/*	111 8th Avenue
/*	New York, NY 10011, USA
/*--*/

/* System library. */

#include <sys_defs.h>
#include <sys/socket.h>
#include <limits.h>

/* Utility library. */

#include <msg.h>
#include <stringops.h>
#include <mymalloc.h>
#include <vstring.h>

/* Global library. */

#include <mail_params.h>

/* Application-specific. */

#include <postscreen.h>

static char *psc_teaser_greeting;
static VSTRING *psc_escape_buf;

/* psc_allowlist_non_dnsbl - allowlist pending non-dnsbl tests */

static void psc_allowlist_non_dnsbl(PSC_STATE *state)
{
    time_t  now;
    int     tindx;

    /*
     * If no tests failed (we can't undo those), and if the allowlist
     * threshold is met, flag non-dnsbl tests that are pending or disabled as
     * successfully completed, and set their expiration times equal to the
     * DNSBL expiration time, except for tests that would expire later.
     * 
     * Why flag disabled tests as passed? When a disabled test is turned on,
     * postscreen should not apply that test to clients that are already
     * allowlisted based on their combined DNSBL score.
     */
    if ((state->flags & PSC_STATE_MASK_ANY_FAIL) == 0
	&& state->dnsbl_score < var_psc_dnsbl_thresh
	&& var_psc_dnsbl_althresh < 0
	&& state->dnsbl_score <= var_psc_dnsbl_althresh) {
	now = event_time();
	for (tindx = 0; tindx < PSC_TINDX_COUNT; tindx++) {
	    if (tindx == PSC_TINDX_DNSBL)
		continue;
	    if ((state->flags & PSC_STATE_FLAG_BYTINDX_TODO(tindx))
		&& !(state->flags & PSC_STATE_FLAG_BYTINDX_PASS(tindx))) {
		if (msg_verbose)
		    msg_info("skip %s test for [%s]:%s",
			 psc_test_name(tindx), PSC_CLIENT_ADDR_PORT(state));
		/* Wrong for deep protocol tests, but we disable those. */
		state->flags |= PSC_STATE_FLAG_BYTINDX_DONE(tindx);
		/* This also disables pending deep protocol tests. */
		state->flags |= PSC_STATE_FLAG_BYTINDX_PASS(tindx);
	    }
	    /* Update expiration even if the test was completed or disabled. */
	    if (state->client_info->expire_time[tindx] < now + state->dnsbl_ttl)
		state->client_info->expire_time[tindx] = now + state->dnsbl_ttl;
	}
    }
}

/* psc_early_event - handle pre-greet, EOF, and DNSBL results. */

static void psc_early_event(int event, void *context)
{
    const char *myname = "psc_early_event";
    PSC_STATE *state = (PSC_STATE *) context;
    time_t *expire_time = state->client_info->expire_time;
    char    read_buf[PSC_READ_BUF_SIZE];
    int     read_count;
    DELTA_TIME elapsed;

    if (msg_verbose > 1)
	msg_info("%s: sq=%d cq=%d event %d on smtp socket %d from [%s]:%s flags=%s",
		 myname, psc_post_queue_length, psc_check_queue_length,
		 event, vstream_fileno(state->smtp_client_stream),
		 state->smtp_client_addr, state->smtp_client_port,
		 psc_print_state_flags(state->flags, myname));

    PSC_CLEAR_EVENT_REQUEST(vstream_fileno(state->smtp_client_stream),
			    psc_early_event, context);

    /*
     * XXX Be sure to empty the DNSBL lookup buffer otherwise we have a
     * memory leak.
     * 
     * XXX We can avoid "forgetting" to do this by keeping a pointer to the
     * DNSBL lookup buffer in the PSC_STATE structure. This also allows us to
     * shave off a hash table lookup when retrieving the DNSBL result.
     * 
     * A direct pointer increases the odds of dangling pointers. Hash-table
     * lookup is safer, and that is why it's done that way.
     */
    switch (event) {

	/*
	 * We either reached the end of the early tests time limit, or all
	 * early tests completed before the pregreet timer would go off.
	 */
    case EVENT_TIME:

	/*
	 * Check if the SMTP client spoke before its turn.
	 */
	if ((state->flags & PSC_STATE_FLAG_PREGR_TODO) != 0
	    && (state->flags & PSC_STATE_MASK_PREGR_FAIL_DONE) == 0) {
	    expire_time[PSC_TINDX_PREGR] = event_time() + var_psc_pregr_ttl;
	    PSC_PASS_SESSION_STATE(state, "pregreet test",
				   PSC_STATE_FLAG_PREGR_PASS);
	}
	if ((state->flags & PSC_STATE_FLAG_PREGR_FAIL)
	    && psc_pregr_action == PSC_ACT_IGNORE) {
	    PSC_UNFAIL_SESSION_STATE(state, PSC_STATE_FLAG_PREGR_FAIL);
	    /* Not: PSC_PASS_SESSION_STATE. Repeat this test the next time. */
	}

	/*
	 * Collect the DNSBL score, and allowlist other tests if applicable.
	 * Note: this score will be partial when some DNS lookup did not
	 * complete before the pregreet timer expired.
	 * 
	 * If the client is DNS blocklisted, drop the connection, send the
	 * client to a dummy protocol engine, or continue to the next test.
	 */
#define PSC_DNSBL_FORMAT \
	"%s 5.7.1 Service unavailable; client [%s] blocked using %s\r\n"
#define NO_DNSBL_SCORE	INT_MAX

	if (state->flags & PSC_STATE_FLAG_DNSBL_TODO) {
	    if (state->dnsbl_score == NO_DNSBL_SCORE) {
		state->dnsbl_score =
		    psc_dnsbl_retrieve(state->smtp_client_addr,
				       &state->dnsbl_name,
				       state->dnsbl_index,
				       &state->dnsbl_ttl);
		if (var_psc_dnsbl_althresh < 0)
		    psc_allowlist_non_dnsbl(state);
	    }
	    if (state->dnsbl_score < var_psc_dnsbl_thresh) {
		expire_time[PSC_TINDX_DNSBL] = event_time() + state->dnsbl_ttl;
		PSC_PASS_SESSION_STATE(state, "dnsbl test",
				       PSC_STATE_FLAG_DNSBL_PASS);
	    } else {
		msg_info("DNSBL rank %d for [%s]:%s",
			 state->dnsbl_score, PSC_CLIENT_ADDR_PORT(state));
		PSC_FAIL_SESSION_STATE(state, PSC_STATE_FLAG_DNSBL_FAIL);
		switch (psc_dnsbl_action) {
		case PSC_ACT_DROP:
		    state->dnsbl_reply = vstring_sprintf(vstring_alloc(100),
						    PSC_DNSBL_FORMAT, "521",
						    state->smtp_client_addr,
							 state->dnsbl_name);
		    PSC_DROP_SESSION_STATE(state, STR(state->dnsbl_reply));
		    return;
		case PSC_ACT_ENFORCE:
		    state->dnsbl_reply = vstring_sprintf(vstring_alloc(100),
						    PSC_DNSBL_FORMAT, "550",
						    state->smtp_client_addr,
							 state->dnsbl_name);
		    PSC_ENFORCE_SESSION_STATE(state, STR(state->dnsbl_reply));
		    break;
		case PSC_ACT_IGNORE:
		    PSC_UNFAIL_SESSION_STATE(state, PSC_STATE_FLAG_DNSBL_FAIL);
		    /* Not: PSC_PASS_SESSION_STATE. Repeat this test. */
		    break;
		default:
		    msg_panic("%s: unknown dnsbl action value %d",
			      myname, psc_dnsbl_action);

		}
	    }
	}

	/*
	 * Pass the connection to a real SMTP server, or enter the dummy
	 * engine for deep tests.
	 */
	if ((state->flags & PSC_STATE_FLAG_NOFORWARD) != 0
	    || ((state->flags & PSC_STATE_MASK_SMTPD_PASS)
		!= PSC_STATE_FLAGS_TODO_TO_PASS(state->flags & PSC_STATE_MASK_SMTPD_TODO)))
	    psc_smtpd_tests(state);
	else
	    psc_conclude(state);
	return;

	/*
	 * EOF, or the client spoke before its turn. We simply drop the
	 * connection, or we continue waiting and allow DNS replies to
	 * trickle in.
	 */
    default:
	if ((read_count = recv(vstream_fileno(state->smtp_client_stream),
			  read_buf, sizeof(read_buf) - 1, MSG_PEEK)) <= 0) {
	    /* Avoid memory leak. */
	    if (state->dnsbl_score == NO_DNSBL_SCORE
		&& (state->flags & PSC_STATE_FLAG_DNSBL_TODO))
		(void) psc_dnsbl_retrieve(state->smtp_client_addr,
					  &state->dnsbl_name,
					  state->dnsbl_index,
					  &state->dnsbl_ttl);
	    /* XXX Wait for DNS replies to come in. */
	    psc_hangup_event(state);
	    return;
	}
	read_buf[read_count] = 0;
	escape(psc_escape_buf, read_buf, read_count);
	msg_info("PREGREET %d after %s from [%s]:%s: %.100s", read_count,
	       psc_format_delta_time(psc_temp, state->start_time, &elapsed),
		 PSC_CLIENT_ADDR_PORT(state), STR(psc_escape_buf));
	PSC_FAIL_SESSION_STATE(state, PSC_STATE_FLAG_PREGR_FAIL);
	switch (psc_pregr_action) {
	case PSC_ACT_DROP:
	    /* Avoid memory leak. */
	    if (state->dnsbl_score == NO_DNSBL_SCORE
		&& (state->flags & PSC_STATE_FLAG_DNSBL_TODO))
		(void) psc_dnsbl_retrieve(state->smtp_client_addr,
					  &state->dnsbl_name,
					  state->dnsbl_index,
					  &state->dnsbl_ttl);
	    PSC_DROP_SESSION_STATE(state, "521 5.5.1 Protocol error\r\n");
	    return;
	case PSC_ACT_ENFORCE:
	    /* We call psc_dnsbl_retrieve() when the timer expires. */
	    PSC_ENFORCE_SESSION_STATE(state, "550 5.5.1 Protocol error\r\n");
	    break;
	case PSC_ACT_IGNORE:
	    /* We call psc_dnsbl_retrieve() when the timer expires. */
	    /* We must handle this case after the timer expires. */
	    break;
	default:
	    msg_panic("%s: unknown pregreet action value %d",
		      myname, psc_pregr_action);
	}

	/*
	 * Terminate the greet delay if we're just waiting for the pregreet
	 * test to complete. It is safe to call psc_early_event directly,
	 * since we are already in that function.
	 * 
	 * XXX After this code passes all tests, swap around the two blocks in
	 * this switch statement and fall through from EVENT_READ into
	 * EVENT_TIME, instead of calling psc_early_event recursively.
	 */
	state->flags |= PSC_STATE_FLAG_PREGR_DONE;
	if (elapsed.dt_sec >= PSC_EFF_GREET_WAIT
	    || ((state->flags & PSC_STATE_MASK_EARLY_DONE)
		== PSC_STATE_FLAGS_TODO_TO_DONE(state->flags & PSC_STATE_MASK_EARLY_TODO)))
	    psc_early_event(EVENT_TIME, context);
	else
	    event_request_timer(psc_early_event, context,
				PSC_EFF_GREET_WAIT - elapsed.dt_sec);
	return;
    }
}

/* psc_early_dnsbl_event - cancel pregreet timer if waiting for DNS only */

static void psc_early_dnsbl_event(int unused_event, void *context)
{
    const char *myname = "psc_early_dnsbl_event";
    PSC_STATE *state = (PSC_STATE *) context;

    if (msg_verbose)
	msg_info("%s: notify [%s]:%s", myname, PSC_CLIENT_ADDR_PORT(state));

    /*
     * Collect the DNSBL score, and allowlist other tests if applicable.
     */
    state->dnsbl_score =
	psc_dnsbl_retrieve(state->smtp_client_addr, &state->dnsbl_name,
			   state->dnsbl_index, &state->dnsbl_ttl);
    if (var_psc_dnsbl_althresh < 0)
	psc_allowlist_non_dnsbl(state);

    /*
     * Terminate the greet delay if we're just waiting for DNSBL lookup to
     * complete. Don't call psc_early_event directly, that would result in a
     * dangling pointer.
     */
    state->flags |= PSC_STATE_FLAG_DNSBL_DONE;
    if ((state->flags & PSC_STATE_MASK_EARLY_DONE)
	== PSC_STATE_FLAGS_TODO_TO_DONE(state->flags & PSC_STATE_MASK_EARLY_TODO))
	event_request_timer(psc_early_event, context, EVENT_NULL_DELAY);
}

/* psc_early_tests - start the early (before protocol) tests */

void    psc_early_tests(PSC_STATE *state)
{
    const char *myname = "psc_early_tests";

    /*
     * Report errors and progress in the context of this test.
     */
    PSC_BEGIN_TESTS(state, "tests before SMTP handshake");

    /*
     * Run a PREGREET test. Send half the greeting banner, by way of teaser,
     * then wait briefly to see if the client speaks before its turn.
     */
    if ((state->flags & PSC_STATE_FLAG_PREGR_TODO) != 0
	&& psc_teaser_greeting != 0
	&& PSC_SEND_REPLY(state, psc_teaser_greeting) != 0) {
	psc_hangup_event(state);
	return;
    }

    /*
     * Run a DNS blocklist query.
     */
    if ((state->flags & PSC_STATE_FLAG_DNSBL_TODO) != 0)
	state->dnsbl_index =
	    psc_dnsbl_request(state->smtp_client_addr, psc_early_dnsbl_event,
			      (void *) state);
    else
	state->dnsbl_index = -1;
    state->dnsbl_score = NO_DNSBL_SCORE;

    /*
     * Wait for the client to respond or for DNS lookup to complete.
     */
    if ((state->flags & PSC_STATE_FLAG_PREGR_TODO) != 0)
	PSC_READ_EVENT_REQUEST(vstream_fileno(state->smtp_client_stream),
		       psc_early_event, (void *) state, PSC_EFF_GREET_WAIT);
    else
	event_request_timer(psc_early_event, (void *) state, PSC_EFF_GREET_WAIT);
}

/* psc_early_init - initialize early tests */

void    psc_early_init(void)
{
    if (*var_psc_pregr_banner) {
	vstring_sprintf(psc_temp, "220-%s\r\n", var_psc_pregr_banner);
	psc_teaser_greeting = mystrdup(STR(psc_temp));
	psc_escape_buf = vstring_alloc(100);
    }
}