/*++ /* NAME /* postscreen_early 3 /* SUMMARY /* postscreen pre-handshake tests /* SYNOPSIS /* #include /* /* 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 #include #include /* Utility library. */ #include #include #include #include /* Global library. */ #include /* Application-specific. */ #include 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); } }