static void psc_send_socket_close_event(int event, char *context) { const char *myname = "psc_send_socket_close_event"; PSC_STATE *state = (PSC_STATE *) context; if (msg_verbose > 1) msg_info("%s: sq=%d cq=%d event %d on send socket %d from [%s]:%s", myname, psc_post_queue_length, psc_check_queue_length, event, state->smtp_server_fd, state->smtp_client_addr, state->smtp_client_port); /* * The real SMTP server has closed the local IPC channel, or we have * reached the limit of our patience. In the latter case it is still * possible that the real SMTP server will receive the socket so we * should not interfere. */ PSC_CLEAR_EVENT_REQUEST(state->smtp_server_fd, psc_send_socket_close_event, context); if (event == EVENT_TIME) msg_warn("timeout sending connection to service %s", psc_smtpd_service_name); psc_free_session_state(state); }
static void psc_dnsbl_receive(int event, void *context) { const char *myname = "psc_dnsbl_receive"; VSTREAM *stream = (VSTREAM *) context; PSC_DNSBL_SCORE *score; PSC_DNSBL_HEAD *head; PSC_DNSBL_SITE *site; ARGV *reply_argv; int request_id; int dnsbl_ttl; PSC_CLEAR_EVENT_REQUEST(vstream_fileno(stream), psc_dnsbl_receive, context); /* * Receive the DNSBL lookup result. * * This is preliminary code to explore the field. Later, DNSBL lookup will * be handled by an UDP-based DNS client that is built directly into some * Postfix daemon. * * Don't bother looking up the blocklist score when the client IP address is * not listed at the DNSBL. * * Don't panic when the blocklist score no longer exists. It may be deleted * when the client triggers a "drop" action after pregreet, when the * client does not pregreet and the DNSBL reply arrives late, or when the * client triggers a "drop" action after hanging up. */ if (event == EVENT_READ && attr_scan(stream, ATTR_FLAG_STRICT, RECV_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, reply_dnsbl), RECV_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, reply_client), RECV_ATTR_INT(MAIL_ATTR_LABEL, &request_id), RECV_ATTR_STR(MAIL_ATTR_RBL_ADDR, reply_addr), RECV_ATTR_INT(MAIL_ATTR_TTL, &dnsbl_ttl), ATTR_TYPE_END) == 5 && (score = (PSC_DNSBL_SCORE *) htable_find(dnsbl_score_cache, STR(reply_client))) != 0 && score->request_id == request_id) { /* * Run this response past all applicable DNSBL filters and update the * blocklist score for this client IP address. * * Don't panic when the DNSBL domain name is not found. The DNSBLOG * server may be messed up. */ if (msg_verbose > 1) msg_info("%s: client=\"%s\" score=%d domain=\"%s\" reply=\"%d %s\"", myname, STR(reply_client), score->total, STR(reply_dnsbl), dnsbl_ttl, STR(reply_addr)); head = (PSC_DNSBL_HEAD *) htable_find(dnsbl_site_cache, STR(reply_dnsbl)); if (head == 0) { /* Bogus domain. Do nothing. */ } else if (*STR(reply_addr) != 0) { /* DNS reputation record(s) found. */ reply_argv = 0; for (site = head->first; site != 0; site = site->next) { if (site->byte_codes == 0 || psc_dnsbl_match(site->byte_codes, reply_argv ? reply_argv : (reply_argv = argv_split(STR(reply_addr), " ")))) { if (score->dnsbl_name == 0 || score->dnsbl_weight < site->weight) { score->dnsbl_name = head->safe_dnsbl; score->dnsbl_weight = site->weight; } score->total += site->weight; if (msg_verbose > 1) msg_info("%s: filter=\"%s\" weight=%d score=%d", myname, site->filter ? site->filter : "null", site->weight, score->total); } /* As with dnsblog(8), a value < 0 means no reply TTL. */ if (site->weight > 0) { if (score->fail_ttl < 0 || score->fail_ttl > dnsbl_ttl) score->fail_ttl = dnsbl_ttl; } else { if (score->pass_ttl < 0 || score->pass_ttl > dnsbl_ttl) score->pass_ttl = dnsbl_ttl; } } if (reply_argv != 0) argv_free(reply_argv); } else { /* No DNS reputation record found. */ for (site = head->first; site != 0; site = site->next) { /* As with dnsblog(8), a value < 0 means no reply TTL. */ if (site->weight > 0) { if (score->pass_ttl < 0 || score->pass_ttl > dnsbl_ttl) score->pass_ttl = dnsbl_ttl; } else { if (score->fail_ttl < 0 || score->fail_ttl > dnsbl_ttl) score->fail_ttl = dnsbl_ttl; } } } /* * Notify the requestor(s) that the result is ready to be picked up. * If this call isn't made, clients have to sit out the entire * pre-handshake delay. */ score->pending_lookups -= 1; if (score->pending_lookups == 0) PSC_CALL_BACK_NOTIFY(score, PSC_NULL_EVENT); } else if (event == EVENT_TIME) { msg_warn("dnsblog reply timeout %ds for %s", var_psc_dnsbl_tmout, (char *) vstream_context(stream)); } /* Here, score may be a null pointer. */ vstream_fclose(stream); }
static void psc_early_event(int event, char *context) { const char *myname = "psc_early_event"; PSC_STATE *state = (PSC_STATE *) context; 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) { state->pregr_stamp = 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 whitelist 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); if (var_psc_dnsbl_wthresh < 0) psc_whitelist_non_dnsbl(state); } if (state->dnsbl_score < var_psc_dnsbl_thresh) { state->dnsbl_stamp = event_time() + var_psc_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); /* 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); 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; } }
static void psc_endpt_haproxy_event(int event, char *context) { const char *myname = "psc_endpt_haproxy_event"; PSC_HAPROXY_STATE *state = (PSC_HAPROXY_STATE *) context; int status = 0; MAI_HOSTADDR_STR smtp_client_addr; MAI_SERVPORT_STR smtp_client_port; MAI_HOSTADDR_STR smtp_server_addr; MAI_SERVPORT_STR smtp_server_port; int last_char = 0; const char *err; VSTRING *escape_buf; char read_buf[HAPROXY_MAX_LEN]; ssize_t read_len; char *cp; /* * We must not read(2) past the <CR><LF> that terminates the haproxy * line. For efficiency reasons we read the entire haproxy line in one * read(2) call when we know that the line is unfragmented. In the rare * case that the line is fragmented, we fall back and read(2) it one * character at a time. */ switch (event) { case EVENT_TIME: msg_warn("haproxy read: time limit exceeded"); status = -1; break; case EVENT_READ: /* Determine the initial VSTREAM read(2) buffer size. */ if (VSTRING_LEN(state->buffer) == 0) { if ((read_len = recv(vstream_fileno(state->stream), read_buf, sizeof(read_buf) - 1, MSG_PEEK)) > 0 && ((cp = memchr(read_buf, '\n', read_len)) != 0)) { read_len = cp - read_buf + 1; } else { read_len = 1; } vstream_control(state->stream, VSTREAM_CTL_BUFSIZE, read_len, VSTREAM_CTL_END); } /* Drain the VSTREAM buffer, otherwise this pseudo-thread will hang. */ do { if ((last_char = VSTREAM_GETC(state->stream)) == VSTREAM_EOF) { if (vstream_ferror(state->stream)) msg_warn("haproxy read: %m"); else msg_warn("haproxy read: lost connection"); status = -1; break; } if (VSTRING_LEN(state->buffer) >= HAPROXY_MAX_LEN) { msg_warn("haproxy read: line too long"); status = -1; break; } VSTRING_ADDCH(state->buffer, last_char); } while (vstream_peek(state->stream) > 0); break; } /* * Parse the haproxy line. Note: the haproxy_srvr_parse() routine * performs address protocol checks, address and port syntax checks, and * converts IPv4-in-IPv6 address string syntax (:ffff::1.2.3.4) to IPv4 * syntax where permitted by the main.cf:inet_protocols setting. */ if (status == 0 && last_char == '\n') { VSTRING_TERMINATE(state->buffer); if ((err = haproxy_srvr_parse(vstring_str(state->buffer), &smtp_client_addr, &smtp_client_port, &smtp_server_addr, &smtp_server_port)) != 0) { escape_buf = vstring_alloc(HAPROXY_MAX_LEN + 2); escape(escape_buf, vstring_str(state->buffer), VSTRING_LEN(state->buffer)); msg_warn("haproxy read: %s: %s", err, vstring_str(escape_buf)); status = -1; vstring_free(escape_buf); } } /* * Are we done yet? */ if (status < 0 || last_char == '\n') { PSC_CLEAR_EVENT_REQUEST(vstream_fileno(state->stream), psc_endpt_haproxy_event, context); vstream_control(state->stream, VSTREAM_CTL_BUFSIZE, (ssize_t) VSTREAM_BUFSIZE, VSTREAM_CTL_END); state->notify(status, state->stream, &smtp_client_addr, &smtp_client_port, &smtp_server_addr, &smtp_server_port); /* Note: the stream may be closed at this point. */ vstring_free(state->buffer); myfree((char *) state); } }