diff options
Diffstat (limited to 'src/smtpd/smtpd_proxy.c')
-rw-r--r-- | src/smtpd/smtpd_proxy.c | 1171 |
1 files changed, 1171 insertions, 0 deletions
diff --git a/src/smtpd/smtpd_proxy.c b/src/smtpd/smtpd_proxy.c new file mode 100644 index 0000000..b2e765b --- /dev/null +++ b/src/smtpd/smtpd_proxy.c @@ -0,0 +1,1171 @@ +/*++ +/* NAME +/* smtpd_proxy 3 +/* SUMMARY +/* SMTP server pass-through proxy client +/* SYNOPSIS +/* #include <smtpd.h> +/* #include <smtpd_proxy.h> +/* +/* typedef struct { +/* .in +4 +/* VSTREAM *stream; /* SMTP proxy or replay log */ +/* VSTRING *buffer; /* last SMTP proxy response */ +/* /* other fields... */ +/* .in -4 +/* } SMTPD_PROXY; +/* +/* int smtpd_proxy_create(state, flags, service, timeout, +/* ehlo_name, mail_from) +/* SMTPD_STATE *state; +/* int flags; +/* const char *service; +/* int timeout; +/* const char *ehlo_name; +/* const char *mail_from; +/* +/* int proxy->cmd(state, expect, format, ...) +/* SMTPD_PROXY *proxy; +/* SMTPD_STATE *state; +/* int expect; +/* const char *format; +/* +/* void smtpd_proxy_free(state) +/* SMTPD_STATE *state; +/* +/* int smtpd_proxy_parse_opts(param_name, param_val) +/* const char *param_name; +/* const char *param_val; +/* RECORD-LEVEL ROUTINES +/* int proxy->rec_put(proxy->stream, rec_type, data, len) +/* SMTPD_PROXY *proxy; +/* int rec_type; +/* const char *data; +/* ssize_t len; +/* +/* int proxy->rec_fprintf(proxy->stream, rec_type, format, ...) +/* SMTPD_PROXY *proxy; +/* int rec_type; +/* cont char *format; +/* DESCRIPTION +/* The functions in this module implement a pass-through proxy +/* client. +/* +/* In order to minimize the intrusiveness of pass-through +/* proxying, 1) the proxy server must support the same MAIL +/* FROM/RCPT syntax that Postfix supports, 2) the record-level +/* routines for message content proxying have the same interface +/* as the routines that are used for non-proxied mail. +/* +/* smtpd_proxy_create() takes a description of a before-queue +/* filter. Depending on flags, it either arranges to buffer +/* up commands and message content until the entire message +/* is received, or it immediately connects to the proxy service, +/* sends EHLO, sends client information with the XFORWARD +/* command if possible, sends the MAIL FROM command, and +/* receives the reply. +/* A non-zero result value means trouble: either the proxy is +/* unavailable, or it did not send the expected reply. +/* All results are reported via the proxy->buffer field in a +/* form that can be sent to the SMTP client. An unexpected +/* 2xx or 3xx proxy server response is replaced by a generic +/* error response to avoid support problems. +/* In case of error, smtpd_proxy_create() updates the +/* state->error_mask and state->err fields, and leaves the +/* SMTPD_PROXY handle in an unconnected state. Destroy the +/* handle after reporting the error reply in the proxy->buffer +/* field. +/* +/* proxy->cmd() formats and either buffers up the command and +/* expected response until the entire message is received, or +/* it immediately sends the specified command to the proxy +/* server, and receives the proxy server reply. +/* A non-zero result value means trouble: either the proxy is +/* unavailable, or it did not send the expected reply. +/* All results are reported via the proxy->buffer field in a +/* form that can be sent to the SMTP client. An unexpected +/* 2xx or 3xx proxy server response is replaced by a generic +/* error response to avoid support problems. +/* In case of error, proxy->cmd() updates the state->error_mask +/* and state->err fields. +/* +/* smtpd_proxy_free() destroys a proxy server handle and resets +/* the state->proxy field. +/* +/* smtpd_proxy_parse_opts() parses main.cf processing options. +/* +/* proxy->rec_put() is a rec_put() clone that either buffers +/* up arbitrary message content records until the entire message +/* is received, or that immediately sends it to the proxy +/* server. +/* All data is expected to be in SMTP dot-escaped form. +/* All errors are reported as a REC_TYPE_ERROR result value, +/* with the state->error_mask, state->err and proxy-buffer +/* fields given appropriate values. +/* +/* proxy->rec_fprintf() is a rec_fprintf() clone that formats +/* message content and either buffers up the record until the +/* entire message is received, or that immediately sends it +/* to the proxy server. +/* All data is expected to be in SMTP dot-escaped form. +/* All errors are reported as a REC_TYPE_ERROR result value, +/* with the state->error_mask, state->err and proxy-buffer +/* fields given appropriate values. +/* +/* Arguments: +/* .IP flags +/* Zero, or SMTPD_PROXY_FLAG_SPEED_ADJUST to buffer up the entire +/* message before contacting a before-queue content filter. +/* Note: when this feature is requested, the before-queue +/* filter MUST use the same 2xx, 4xx or 5xx reply code for all +/* recipients of a multi-recipient message. +/* .IP server +/* The SMTP proxy server host:port. The host or host: part is optional. +/* This argument is not duplicated. +/* .IP timeout +/* Time limit for connecting to the proxy server and for +/* sending and receiving proxy server commands and replies. +/* .IP ehlo_name +/* The EHLO Hostname that will be sent to the proxy server. +/* This argument is not duplicated. +/* .IP mail_from +/* The MAIL FROM command. This argument is not duplicated. +/* .IP state +/* SMTP server state. +/* .IP expect +/* Expected proxy server reply status code range. A warning is logged +/* when an unexpected reply is received. Specify one of the following: +/* .RS +/* .IP SMTPD_PROX_WANT_OK +/* The caller expects a reply in the 200 range. +/* .IP SMTPD_PROX_WANT_MORE +/* The caller expects a reply in the 300 range. +/* .IP SMTPD_PROX_WANT_ANY +/* The caller has no expectation. Do not warn for unexpected replies. +/* .IP SMTPD_PROX_WANT_NONE +/* Do not bother waiting for a reply. +/* .RE +/* .IP format +/* A format string. +/* .IP stream +/* Connection to proxy server. +/* .IP data +/* Pointer to the content of one message content record. +/* .IP len +/* The length of a message content record. +/* SEE ALSO +/* smtpd(8) Postfix smtp server +/* DIAGNOSTICS +/* Panic: internal API violations. +/* +/* Fatal errors: memory allocation problem. +/* +/* Warnings: unexpected response from proxy server, unable +/* to connect to proxy server, proxy server read/write error, +/* proxy speed-adjust buffer read/write error. +/* 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 +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <ctype.h> +#include <unistd.h> + +#ifdef STRCASECMP_IN_STRINGS_H +#include <strings.h> +#endif + +/* Utility library. */ + +#include <msg.h> +#include <vstream.h> +#include <vstring.h> +#include <stringops.h> +#include <connect.h> +#include <name_code.h> +#include <mymalloc.h> + +/* Global library. */ + +#include <mail_error.h> +#include <smtp_stream.h> +#include <cleanup_user.h> +#include <mail_params.h> +#include <rec_type.h> +#include <mail_proto.h> +#include <xtext.h> +#include <record.h> +#include <mail_queue.h> + +/* Application-specific. */ + +#include <smtpd.h> +#include <smtpd_proxy.h> + + /* + * XFORWARD server features, recognized by the pass-through proxy client. + */ +#define SMTPD_PROXY_XFORWARD_NAME (1<<0) /* client name */ +#define SMTPD_PROXY_XFORWARD_ADDR (1<<1) /* client address */ +#define SMTPD_PROXY_XFORWARD_PROTO (1<<2) /* protocol */ +#define SMTPD_PROXY_XFORWARD_HELO (1<<3) /* client helo */ +#define SMTPD_PROXY_XFORWARD_IDENT (1<<4) /* message identifier */ +#define SMTPD_PROXY_XFORWARD_DOMAIN (1<<5) /* origin type */ +#define SMTPD_PROXY_XFORWARD_PORT (1<<6) /* client port */ + + /* + * Spead-matching: we use an unlinked file for transient storage. + */ +static VSTREAM *smtpd_proxy_replay_stream; + + /* + * Forward declarations. + */ +static void smtpd_proxy_fake_server_reply(SMTPD_STATE *, int); +static int smtpd_proxy_rdwr_error(SMTPD_STATE *, int); +static int PRINTFLIKE(3, 4) smtpd_proxy_cmd(SMTPD_STATE *, int, const char *,...); +static int smtpd_proxy_rec_put(VSTREAM *, int, const char *, ssize_t); + + /* + * SLMs. + */ +#define STR(x) vstring_str(x) +#define LEN(x) VSTRING_LEN(x) +#define STREQ(x, y) (strcmp((x), (y)) == 0) + +/* smtpd_proxy_xforward_flush - flush forwarding information */ + +static int smtpd_proxy_xforward_flush(SMTPD_STATE *state, VSTRING *buf) +{ + int ret; + + if (VSTRING_LEN(buf) > 0) { + ret = smtpd_proxy_cmd(state, SMTPD_PROX_WANT_OK, + XFORWARD_CMD "%s", STR(buf)); + VSTRING_RESET(buf); + return (ret); + } + return (0); +} + +/* smtpd_proxy_xforward_send - send forwarding information */ + +static int smtpd_proxy_xforward_send(SMTPD_STATE *state, VSTRING *buf, + const char *name, + int value_available, + const char *value) +{ + size_t new_len; + int ret; + +#define CONSTR_LEN(s) (sizeof(s) - 1) +#define PAYLOAD_LIMIT (512 - CONSTR_LEN("250 " XFORWARD_CMD "\r\n")) + + if (!value_available) + value = XFORWARD_UNAVAILABLE; + + /* + * Encode the attribute value. + */ + if (state->expand_buf == 0) + state->expand_buf = vstring_alloc(100); + xtext_quote(state->expand_buf, value, ""); + + /* + * How much space does this attribute need? SPACE name = value. + */ + new_len = strlen(name) + strlen(STR(state->expand_buf)) + 2; + if (new_len > PAYLOAD_LIMIT) + msg_warn("%s command payload %s=%.10s... exceeds SMTP protocol limit", + XFORWARD_CMD, name, value); + + /* + * Flush the buffer if we need to, and store the attribute. + */ + if (VSTRING_LEN(buf) > 0 && VSTRING_LEN(buf) + new_len > PAYLOAD_LIMIT) + if ((ret = smtpd_proxy_xforward_flush(state, buf)) < 0) + return (ret); + vstring_sprintf_append(buf, " %s=%s", name, STR(state->expand_buf)); + + return (0); +} + +/* smtpd_proxy_connect - open proxy connection */ + +static int smtpd_proxy_connect(SMTPD_STATE *state) +{ + SMTPD_PROXY *proxy = state->proxy; + int fd; + char *lines; + char *words; + VSTRING *buf; + int bad; + char *word; + static const NAME_CODE known_xforward_features[] = { + XFORWARD_NAME, SMTPD_PROXY_XFORWARD_NAME, + XFORWARD_ADDR, SMTPD_PROXY_XFORWARD_ADDR, + XFORWARD_PORT, SMTPD_PROXY_XFORWARD_PORT, + XFORWARD_PROTO, SMTPD_PROXY_XFORWARD_PROTO, + XFORWARD_HELO, SMTPD_PROXY_XFORWARD_HELO, + XFORWARD_IDENT, SMTPD_PROXY_XFORWARD_IDENT, + XFORWARD_DOMAIN, SMTPD_PROXY_XFORWARD_DOMAIN, + 0, 0, + }; + int server_xforward_features; + int (*connect_fn) (const char *, int, int); + const char *endpoint; + + /* + * Find connection method (default inet) + */ + if (strncasecmp("unix:", proxy->service_name, 5) == 0) { + endpoint = proxy->service_name + 5; + connect_fn = unix_connect; + } else { + if (strncasecmp("inet:", proxy->service_name, 5) == 0) + endpoint = proxy->service_name + 5; + else + endpoint = proxy->service_name; + connect_fn = inet_connect; + } + + /* + * Connect to proxy. + */ + if ((fd = connect_fn(endpoint, BLOCKING, proxy->timeout)) < 0) { + msg_warn("connect to proxy filter %s: %m", proxy->service_name); + return (smtpd_proxy_rdwr_error(state, 0)); + } + proxy->service_stream = vstream_fdopen(fd, O_RDWR); + /* Needed by our DATA-phase record emulation routines. */ + vstream_control(proxy->service_stream, + CA_VSTREAM_CTL_CONTEXT((void *) state), + CA_VSTREAM_CTL_END); + /* Avoid poor performance when TCP MSS > VSTREAM_BUFSIZE. */ + if (connect_fn == inet_connect) + vstream_tweak_tcp(proxy->service_stream); + smtp_timeout_setup(proxy->service_stream, proxy->timeout); + + /* + * Get server greeting banner. + * + * If this fails then we have a problem because the proxy should always + * accept our connection. Make up our own response instead of passing + * back a negative greeting banner: the proxy open is delayed to the + * point that the client expects a MAIL FROM or RCPT TO reply. + */ + if (smtpd_proxy_cmd(state, SMTPD_PROX_WANT_OK, "%s", "")) { + smtpd_proxy_fake_server_reply(state, CLEANUP_STAT_PROXY); + smtpd_proxy_close(state); + return (-1); + } + + /* + * Send our own EHLO command. If this fails then we have a problem + * because the proxy should always accept our EHLO command. Make up our + * own response instead of passing back a negative EHLO reply: the proxy + * open is delayed to the point that the remote SMTP client expects a + * MAIL FROM or RCPT TO reply. + */ + if (smtpd_proxy_cmd(state, SMTPD_PROX_WANT_OK, "EHLO %s", + proxy->ehlo_name)) { + smtpd_proxy_fake_server_reply(state, CLEANUP_STAT_PROXY); + smtpd_proxy_close(state); + return (-1); + } + + /* + * Parse the EHLO reply and see if we can forward logging information. + */ + server_xforward_features = 0; + lines = STR(proxy->reply); + while ((words = mystrtok(&lines, "\n")) != 0) { + if (mystrtok(&words, "- ") && (word = mystrtok(&words, " \t")) != 0) { + if (strcasecmp(word, XFORWARD_CMD) == 0) + while ((word = mystrtok(&words, " \t")) != 0) + server_xforward_features |= + name_code(known_xforward_features, + NAME_CODE_FLAG_NONE, word); + } + } + + /* + * Send XFORWARD attributes. For robustness, explicitly specify what SMTP + * session attributes are known and unknown. Make up our own response + * instead of passing back a negative XFORWARD reply: the proxy open is + * delayed to the point that the remote SMTP client expects a MAIL FROM + * or RCPT TO reply. + */ + if (server_xforward_features) { + buf = vstring_alloc(100); + bad = + (((server_xforward_features & SMTPD_PROXY_XFORWARD_NAME) + && smtpd_proxy_xforward_send(state, buf, XFORWARD_NAME, + IS_AVAIL_CLIENT_NAME(FORWARD_NAME(state)), + FORWARD_NAME(state))) + || ((server_xforward_features & SMTPD_PROXY_XFORWARD_ADDR) + && smtpd_proxy_xforward_send(state, buf, XFORWARD_ADDR, + IS_AVAIL_CLIENT_ADDR(FORWARD_ADDR(state)), + FORWARD_ADDR(state))) + || ((server_xforward_features & SMTPD_PROXY_XFORWARD_PORT) + && smtpd_proxy_xforward_send(state, buf, XFORWARD_PORT, + IS_AVAIL_CLIENT_PORT(FORWARD_PORT(state)), + FORWARD_PORT(state))) + || ((server_xforward_features & SMTPD_PROXY_XFORWARD_HELO) + && smtpd_proxy_xforward_send(state, buf, XFORWARD_HELO, + IS_AVAIL_CLIENT_HELO(FORWARD_HELO(state)), + FORWARD_HELO(state))) + || ((server_xforward_features & SMTPD_PROXY_XFORWARD_IDENT) + && smtpd_proxy_xforward_send(state, buf, XFORWARD_IDENT, + IS_AVAIL_CLIENT_IDENT(FORWARD_IDENT(state)), + FORWARD_IDENT(state))) + || ((server_xforward_features & SMTPD_PROXY_XFORWARD_PROTO) + && smtpd_proxy_xforward_send(state, buf, XFORWARD_PROTO, + IS_AVAIL_CLIENT_PROTO(FORWARD_PROTO(state)), + FORWARD_PROTO(state))) + || ((server_xforward_features & SMTPD_PROXY_XFORWARD_DOMAIN) + && smtpd_proxy_xforward_send(state, buf, XFORWARD_DOMAIN, 1, + STREQ(FORWARD_DOMAIN(state), MAIL_ATTR_RWR_LOCAL) ? + XFORWARD_DOM_LOCAL : XFORWARD_DOM_REMOTE)) + || smtpd_proxy_xforward_flush(state, buf)); + vstring_free(buf); + if (bad) { + smtpd_proxy_fake_server_reply(state, CLEANUP_STAT_PROXY); + smtpd_proxy_close(state); + return (-1); + } + } + + /* + * Pass-through the remote SMTP client's MAIL FROM command. If this + * fails, then we have a problem because the proxy should always accept + * any MAIL FROM command that was accepted by us. + */ + if (smtpd_proxy_cmd(state, SMTPD_PROX_WANT_OK, "%s", + proxy->mail_from) != 0) { + /* NOT: smtpd_proxy_fake_server_reply(state, CLEANUP_STAT_PROXY); */ + smtpd_proxy_close(state); + return (-1); + } + return (0); +} + +/* smtpd_proxy_fake_server_reply - produce generic error response */ + +static void smtpd_proxy_fake_server_reply(SMTPD_STATE *state, int status) +{ + const CLEANUP_STAT_DETAIL *detail; + + /* + * Either we have no server reply (connection refused), or we have an + * out-of-protocol server reply, so we make up a generic server error + * response instead. + */ + detail = cleanup_stat_detail(status); + vstring_sprintf(state->proxy->reply, + "%d %s Error: %s", + detail->smtp, detail->dsn, detail->text); +} + +/* smtpd_proxy_replay_rdwr_error - report replay log I/O error */ + +static int smtpd_proxy_replay_rdwr_error(SMTPD_STATE *state) +{ + + /* + * Log an appropriate warning message. + */ + msg_warn("proxy speed-adjust log I/O error: %m"); + + /* + * Set the appropriate flags and server reply. + */ + state->error_mask |= MAIL_ERROR_RESOURCE; + /* Update state->err in case we are past the client's DATA command. */ + state->err |= CLEANUP_STAT_PROXY; + smtpd_proxy_fake_server_reply(state, CLEANUP_STAT_PROXY); + return (-1); +} + +/* smtpd_proxy_rdwr_error - report proxy communication error */ + +static int smtpd_proxy_rdwr_error(SMTPD_STATE *state, int err) +{ + const char *myname = "smtpd_proxy_rdwr_error"; + SMTPD_PROXY *proxy = state->proxy; + + /* + * Sanity check. + */ + if (err != 0 && err != SMTP_ERR_NONE && proxy == 0) + msg_panic("%s: proxy error %d without proxy handle", myname, err); + + /* + * Log an appropriate warning message. + */ + switch (err) { + case 0: + case SMTP_ERR_NONE: + break; + case SMTP_ERR_EOF: + msg_warn("lost connection with proxy %s", proxy->service_name); + break; + case SMTP_ERR_TIME: + msg_warn("timeout talking to proxy %s", proxy->service_name); + break; + default: + msg_panic("%s: unknown proxy %s error %d", + myname, proxy->service_name, err); + } + + /* + * Set the appropriate flags and server reply. + */ + state->error_mask |= MAIL_ERROR_SOFTWARE; + /* Update state->err in case we are past the client's DATA command. */ + state->err |= CLEANUP_STAT_PROXY; + smtpd_proxy_fake_server_reply(state, CLEANUP_STAT_PROXY); + return (-1); +} + +/* smtpd_proxy_replay_send - replay saved SMTP session from speed-match log */ + +static int smtpd_proxy_replay_send(SMTPD_STATE *state) +{ + const char *myname = "smtpd_proxy_replay_send"; + static VSTRING *replay_buf = 0; + SMTPD_PROXY *proxy = state->proxy; + int rec_type; + int expect = SMTPD_PROX_WANT_BAD; + + /* + * Sanity check. + */ + if (smtpd_proxy_replay_stream == 0) + msg_panic("%s: no before-queue filter speed-adjust log", myname); + + /* + * Errors first. + */ + if (vstream_ferror(smtpd_proxy_replay_stream) + || vstream_feof(smtpd_proxy_replay_stream) + || rec_put(smtpd_proxy_replay_stream, REC_TYPE_END, "", 0) != REC_TYPE_END + || vstream_fflush(smtpd_proxy_replay_stream)) + /* NOT: fsync(vstream_fileno(smtpd_proxy_replay_stream)) */ + return (smtpd_proxy_replay_rdwr_error(state)); + + /* + * Delayed connection to the before-queue filter. + */ + if (smtpd_proxy_connect(state) < 0) + return (-1); + + /* + * Replay the speed-match log. We do sanity check record content, but we + * don't implement a protocol state engine here, since we are reading + * from a file that we just wrote ourselves. + * + * This is different than the MailChannels patented solution that + * multiplexes a large number of slowed-down inbound connections over a + * small number of fast connections to a local MTA. + * + * - MailChannels receives mail directly from the Internet. It uses one + * connection to the local MTA to reject invalid recipients before + * receiving the entire email message at reduced bit rates, and then uses + * a different connection to quickly deliver the message to the local + * MTA. + * + * - Postfix receives mail directly from the Internet. The Postfix SMTP + * server rejects invalid recipients before receiving the entire message + * over the Internet, and then delivers the message quickly to a local + * SMTP-based content filter. + */ + if (replay_buf == 0) + replay_buf = vstring_alloc(100); + if (vstream_fseek(smtpd_proxy_replay_stream, (off_t) 0, SEEK_SET) < 0) + return (smtpd_proxy_replay_rdwr_error(state)); + + for (;;) { + switch (rec_type = rec_get(smtpd_proxy_replay_stream, replay_buf, + REC_FLAG_NONE)) { + + /* + * Message content. + */ + case REC_TYPE_NORM: + case REC_TYPE_CONT: + if (smtpd_proxy_rec_put(proxy->service_stream, rec_type, + STR(replay_buf), LEN(replay_buf)) < 0) + return (-1); + break; + + /* + * Expected server reply type. + */ + case REC_TYPE_RCPT: + if (!alldig(STR(replay_buf)) + || (expect = atoi(STR(replay_buf))) == SMTPD_PROX_WANT_BAD) + msg_panic("%s: malformed server reply type: %s", + myname, STR(replay_buf)); + break; + + /* + * Client command, or void. Bail out on the first negative proxy + * response. This is OK, because the filter must use the same + * reply code for all recipients of a multi-recipient message. + */ + case REC_TYPE_FROM: + if (expect == SMTPD_PROX_WANT_BAD) + msg_panic("%s: missing server reply type", myname); + if (smtpd_proxy_cmd(state, expect, "%s", STR(replay_buf)) < 0) + return (-1); + expect = SMTPD_PROX_WANT_BAD; + break; + + /* + * Explicit end marker, instead of implicit EOF. + */ + case REC_TYPE_END: + return (0); + + /* + * Errors. + */ + case REC_TYPE_ERROR: + return (smtpd_proxy_replay_rdwr_error(state)); + default: + msg_panic("%s: unexpected record type; %d", myname, rec_type); + } + } +} + +/* smtpd_proxy_save_cmd - save SMTP command + expected response to replay log */ + +static int PRINTFLIKE(3, 4) smtpd_proxy_save_cmd(SMTPD_STATE *state, int expect, const char *fmt,...) +{ + va_list ap; + + /* + * Errors first. + */ + if (vstream_ferror(smtpd_proxy_replay_stream) + || vstream_feof(smtpd_proxy_replay_stream)) + return (smtpd_proxy_replay_rdwr_error(state)); + + /* + * Save the expected reply first, so that the replayer can safely + * overwrite the input buffer with the command. + */ + rec_fprintf(smtpd_proxy_replay_stream, REC_TYPE_RCPT, "%d", expect); + + /* + * The command can be omitted at the start of an SMTP session. This is + * not documented as part of the official interface because it is used + * only internally to this module. + */ + + /* + * Save the command to the replay log, and send it to the before-queue + * filter after we have received the entire message. + */ + va_start(ap, fmt); + rec_vfprintf(smtpd_proxy_replay_stream, REC_TYPE_FROM, fmt, ap); + va_end(ap); + + /* + * If we just saved the "." command, replay the log. + */ + return (strcmp(fmt, ".") ? 0 : smtpd_proxy_replay_send(state)); +} + +/* smtpd_proxy_cmd - send command to proxy, receive reply */ + +static int smtpd_proxy_cmd(SMTPD_STATE *state, int expect, const char *fmt,...) +{ + SMTPD_PROXY *proxy = state->proxy; + va_list ap; + char *cp; + int last_char; + int err = 0; + static VSTRING *buffer = 0; + + /* + * Errors first. Be prepared for delayed errors from the DATA phase. + */ + if (vstream_ferror(proxy->service_stream) + || vstream_feof(proxy->service_stream) + || (err = vstream_setjmp(proxy->service_stream)) != 0) { + return (smtpd_proxy_rdwr_error(state, err)); + } + + /* + * Format the command. + */ + va_start(ap, fmt); + vstring_vsprintf(proxy->request, fmt, ap); + va_end(ap); + + /* + * The command can be omitted at the start of an SMTP session. This is + * not documented as part of the official interface because it is used + * only internally to this module. + */ + if (LEN(proxy->request) > 0) { + + /* + * Optionally log the command first, so that we can see in the log + * what the program is trying to do. + */ + if (msg_verbose) + msg_info("> %s: %s", proxy->service_name, STR(proxy->request)); + + /* + * Send the command to the proxy server. Since we're going to read a + * reply immediately, there is no need to flush buffers. + */ + smtp_fputs(STR(proxy->request), LEN(proxy->request), + proxy->service_stream); + } + + /* + * Early return if we don't want to wait for a server reply (such as + * after sending QUIT). + */ + if (expect == SMTPD_PROX_WANT_NONE) + return (0); + + /* + * Censor out non-printable characters in server responses and save + * complete multi-line responses if possible. + * + * We can't parse or store input that exceeds var_line_limit, so we just + * skip over it to simplify the remainder of the code below. + */ + VSTRING_RESET(proxy->reply); + if (buffer == 0) + buffer = vstring_alloc(10); + for (;;) { + last_char = smtp_get(buffer, proxy->service_stream, var_line_limit, + SMTP_GET_FLAG_SKIP); + printable(STR(buffer), '?'); + if (last_char != '\n') + msg_warn("%s: response longer than %d: %.30s...", + proxy->service_name, var_line_limit, + STR(buffer)); + if (msg_verbose) + msg_info("< %s: %.100s", proxy->service_name, STR(buffer)); + + /* + * Defend against a denial of service attack by limiting the amount + * of multi-line text that we are willing to store. + */ + if (LEN(proxy->reply) < var_line_limit) { + if (VSTRING_LEN(proxy->reply)) + vstring_strcat(proxy->reply, "\r\n"); + vstring_strcat(proxy->reply, STR(buffer)); + } + + /* + * Parse the response into code and text. Ignore unrecognized + * garbage. This means that any character except space (or end of + * line) will have the same effect as the '-' line continuation + * character. + */ + for (cp = STR(buffer); *cp && ISDIGIT(*cp); cp++) + /* void */ ; + if (cp - STR(buffer) == 3) { + if (*cp == '-') + continue; + if (*cp == ' ' || *cp == 0) + break; + } + msg_warn("received garbage from proxy %s: %.100s", + proxy->service_name, STR(buffer)); + } + + /* + * Log a warning in case the proxy does not send the expected response. + * Silently accept any response when the client expressed no expectation. + * + * Starting with Postfix 2.6 we don't pass through unexpected 2xx or 3xx + * proxy replies. They are a source of support problems, so we replace + * them by generic server error replies. + */ + if (expect != SMTPD_PROX_WANT_ANY && expect != *STR(proxy->reply)) { + msg_warn("proxy %s rejected \"%s\": \"%s\"", + proxy->service_name, LEN(proxy->request) == 0 ? + "connection request" : STR(proxy->request), + STR(proxy->reply)); + if (*STR(proxy->reply) == SMTPD_PROX_WANT_OK + || *STR(proxy->reply) == SMTPD_PROX_WANT_MORE) { + smtpd_proxy_rdwr_error(state, 0); + } + return (-1); + } else { + return (0); + } +} + +/* smtpd_proxy_save_rec_put - save message content to replay log */ + +static int smtpd_proxy_save_rec_put(VSTREAM *stream, int rec_type, + const char *data, ssize_t len) +{ + const char *myname = "smtpd_proxy_save_rec_put"; + int ret; + +#define VSTREAM_TO_SMTPD_STATE(s) ((SMTPD_STATE *) vstream_context(s)) + + /* + * Sanity check. + */ + if (stream == 0) + msg_panic("%s: attempt to use closed stream", myname); + + /* + * Send one content record. Errors and results must be as with rec_put(). + */ + if (rec_type == REC_TYPE_NORM || rec_type == REC_TYPE_CONT) + ret = rec_put(stream, rec_type, data, len); + else + msg_panic("%s: need REC_TYPE_NORM or REC_TYPE_CONT", myname); + + /* + * Errors last. + */ + if (ret != rec_type) { + (void) smtpd_proxy_replay_rdwr_error(VSTREAM_TO_SMTPD_STATE(stream)); + return (REC_TYPE_ERROR); + } + return (rec_type); +} + +/* smtpd_proxy_rec_put - send message content, rec_put() clone */ + +static int smtpd_proxy_rec_put(VSTREAM *stream, int rec_type, + const char *data, ssize_t len) +{ + const char *myname = "smtpd_proxy_rec_put"; + int err = 0; + + /* + * Errors first. + */ + if (vstream_ferror(stream) || vstream_feof(stream) + || (err = vstream_setjmp(stream)) != 0) { + (void) smtpd_proxy_rdwr_error(VSTREAM_TO_SMTPD_STATE(stream), err); + return (REC_TYPE_ERROR); + } + + /* + * Send one content record. Errors and results must be as with rec_put(). + */ + if (rec_type == REC_TYPE_NORM) + smtp_fputs(data, len, stream); + else if (rec_type == REC_TYPE_CONT) + smtp_fwrite(data, len, stream); + else + msg_panic("%s: need REC_TYPE_NORM or REC_TYPE_CONT", myname); + return (rec_type); +} + +/* smtpd_proxy_save_rec_fprintf - save message content to replay log */ + +static int smtpd_proxy_save_rec_fprintf(VSTREAM *stream, int rec_type, + const char *fmt,...) +{ + const char *myname = "smtpd_proxy_save_rec_fprintf"; + va_list ap; + int ret; + + /* + * Sanity check. + */ + if (stream == 0) + msg_panic("%s: attempt to use closed stream", myname); + + /* + * Save one content record. Errors and results must be as with + * rec_fprintf(). + */ + va_start(ap, fmt); + if (rec_type == REC_TYPE_NORM) + ret = rec_vfprintf(stream, rec_type, fmt, ap); + else + msg_panic("%s: need REC_TYPE_NORM", myname); + va_end(ap); + + /* + * Errors last. + */ + if (ret != rec_type) { + (void) smtpd_proxy_replay_rdwr_error(VSTREAM_TO_SMTPD_STATE(stream)); + return (REC_TYPE_ERROR); + } + return (rec_type); +} + +/* smtpd_proxy_rec_fprintf - send message content, rec_fprintf() clone */ + +static int smtpd_proxy_rec_fprintf(VSTREAM *stream, int rec_type, + const char *fmt,...) +{ + const char *myname = "smtpd_proxy_rec_fprintf"; + va_list ap; + int err = 0; + + /* + * Errors first. + */ + if (vstream_ferror(stream) || vstream_feof(stream) + || (err = vstream_setjmp(stream)) != 0) { + (void) smtpd_proxy_rdwr_error(VSTREAM_TO_SMTPD_STATE(stream), err); + return (REC_TYPE_ERROR); + } + + /* + * Send one content record. Errors and results must be as with + * rec_fprintf(). + */ + va_start(ap, fmt); + if (rec_type == REC_TYPE_NORM) + smtp_vprintf(stream, fmt, ap); + else + msg_panic("%s: need REC_TYPE_NORM", myname); + va_end(ap); + return (rec_type); +} + +#ifndef NO_TRUNCATE + +/* smtpd_proxy_replay_setup - prepare the replay logfile */ + +static int smtpd_proxy_replay_setup(SMTPD_STATE *state) +{ + const char *myname = "smtpd_proxy_replay_setup"; + off_t file_offs; + + /* + * Where possible reuse an existing replay logfile, because creating a + * file is expensive compared to reading or writing. For security reasons + * we must truncate the file before reuse. For performance reasons we + * should truncate the file immediately after the end of a mail + * transaction. We enforce the security guarantee upon reuse, by + * requiring that no I/O happened since the file was truncated. This is + * less expensive than truncating the file redundantly. + */ + if (smtpd_proxy_replay_stream != 0) { + /* vstream_ftell() won't invoke the kernel, so all errors are mine. */ + if ((file_offs = vstream_ftell(smtpd_proxy_replay_stream)) != 0) + msg_panic("%s: bad before-queue filter speed-adjust log offset %lu", + myname, (unsigned long) file_offs); + vstream_clearerr(smtpd_proxy_replay_stream); + if (msg_verbose) + msg_info("%s: reuse speed-adjust stream fd=%d", myname, + vstream_fileno(smtpd_proxy_replay_stream)); + /* Here, smtpd_proxy_replay_stream != 0 */ + } + + /* + * Create a new replay logfile. + */ + if (smtpd_proxy_replay_stream == 0) { + smtpd_proxy_replay_stream = mail_queue_enter(MAIL_QUEUE_INCOMING, 0, + (struct timeval *) 0); + if (smtpd_proxy_replay_stream == 0) + return (smtpd_proxy_replay_rdwr_error(state)); + if (unlink(VSTREAM_PATH(smtpd_proxy_replay_stream)) < 0) + msg_warn("remove before-queue filter speed-adjust log %s: %m", + VSTREAM_PATH(smtpd_proxy_replay_stream)); + if (msg_verbose) + msg_info("%s: new speed-adjust stream fd=%d", myname, + vstream_fileno(smtpd_proxy_replay_stream)); + } + + /* + * Needed by our DATA-phase record emulation routines. + */ + vstream_control(smtpd_proxy_replay_stream, + CA_VSTREAM_CTL_CONTEXT((void *) state), + CA_VSTREAM_CTL_END); + return (0); +} + +#endif + +/* smtpd_proxy_create - set up smtpd proxy handle */ + +int smtpd_proxy_create(SMTPD_STATE *state, int flags, const char *service, + int timeout, const char *ehlo_name, + const char *mail_from) +{ + SMTPD_PROXY *proxy; + + /* + * When an operation has many arguments it is safer to use named + * parameters, and have the compiler enforce the argument count. + */ +#define SMTPD_PROXY_ALLOC(p, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12) \ + ((p) = (SMTPD_PROXY *) mymalloc(sizeof(*(p))), (p)->a1, (p)->a2, \ + (p)->a3, (p)->a4, (p)->a5, (p)->a6, (p)->a7, (p)->a8, (p)->a9, \ + (p)->a10, (p)->a11, (p)->a12, (p)) + + /* + * Sanity check. + */ + if (state->proxy != 0) + msg_panic("smtpd_proxy_create: handle still exists"); + + /* + * Connect to the before-queue filter immediately. + */ + if ((flags & SMTPD_PROXY_FLAG_SPEED_ADJUST) == 0) { + state->proxy = + SMTPD_PROXY_ALLOC(proxy, stream = 0, request = vstring_alloc(10), + reply = vstring_alloc(10), + cmd = smtpd_proxy_cmd, + rec_fprintf = smtpd_proxy_rec_fprintf, + rec_put = smtpd_proxy_rec_put, + flags = flags, service_stream = 0, + service_name = service, timeout = timeout, + ehlo_name = ehlo_name, mail_from = mail_from); + if (smtpd_proxy_connect(state) < 0) { + /* NOT: smtpd_proxy_free(state); we still need proxy->reply. */ + return (-1); + } + proxy->stream = proxy->service_stream; + return (0); + } + + /* + * Connect to the before-queue filter after we receive the entire + * message. Open the replay logfile early to simplify code. The file is + * reused for multiple mail transactions, so there is no need to minimize + * its life time. + */ + else { +#ifdef NO_TRUNCATE + msg_panic("smtpd_proxy_create: speed-adjust support is not available"); +#else + if (smtpd_proxy_replay_setup(state) < 0) + return (-1); + state->proxy = + SMTPD_PROXY_ALLOC(proxy, stream = smtpd_proxy_replay_stream, + request = vstring_alloc(10), + reply = vstring_alloc(10), + cmd = smtpd_proxy_save_cmd, + rec_fprintf = smtpd_proxy_save_rec_fprintf, + rec_put = smtpd_proxy_save_rec_put, + flags = flags, service_stream = 0, + service_name = service, timeout = timeout, + ehlo_name = ehlo_name, mail_from = mail_from); + return (0); +#endif + } +} + +/* smtpd_proxy_close - close proxy connection without destroying handle */ + +void smtpd_proxy_close(SMTPD_STATE *state) +{ + SMTPD_PROXY *proxy = state->proxy; + + /* + * Specify SMTPD_PROX_WANT_NONE so that the server reply will not clobber + * the END-OF-DATA reply. + */ + if (proxy->service_stream != 0) { + if (vstream_feof(proxy->service_stream) == 0 + && vstream_ferror(proxy->service_stream) == 0) + (void) smtpd_proxy_cmd(state, SMTPD_PROX_WANT_NONE, + SMTPD_CMD_QUIT); + (void) vstream_fclose(proxy->service_stream); + if (proxy->stream == proxy->service_stream) + proxy->stream = 0; + proxy->service_stream = 0; + } +} + +/* smtpd_proxy_free - destroy smtpd proxy handle */ + +void smtpd_proxy_free(SMTPD_STATE *state) +{ + SMTPD_PROXY *proxy = state->proxy; + + /* + * Clean up. + */ + if (proxy->service_stream != 0) + (void) smtpd_proxy_close(state); + if (proxy->request != 0) + vstring_free(proxy->request); + if (proxy->reply != 0) + vstring_free(proxy->reply); + myfree((void *) proxy); + state->proxy = 0; + + /* + * Reuse the replay logfile if possible. For security reasons we must + * truncate the replay logfile before reuse. For performance reasons we + * should truncate the replay logfile immediately after the end of a mail + * transaction. We truncate the file here, and enforce the security + * guarantee by requiring that no I/O happens before the file is reused. + */ + if (smtpd_proxy_replay_stream == 0) + return; + if (vstream_ferror(smtpd_proxy_replay_stream)) { + /* Errors are already reported. */ + (void) vstream_fclose(smtpd_proxy_replay_stream); + smtpd_proxy_replay_stream = 0; + return; + } + /* Flush output from aborted transaction before truncating the file!! */ + if (vstream_fseek(smtpd_proxy_replay_stream, (off_t) 0, SEEK_SET) < 0) { + msg_warn("seek before-queue filter speed-adjust log: %m"); + (void) vstream_fclose(smtpd_proxy_replay_stream); + smtpd_proxy_replay_stream = 0; + return; + } + if (ftruncate(vstream_fileno(smtpd_proxy_replay_stream), (off_t) 0) < 0) { + msg_warn("truncate before-queue filter speed-adjust log: %m"); + (void) vstream_fclose(smtpd_proxy_replay_stream); + smtpd_proxy_replay_stream = 0; + return; + } +} + +/* smtpd_proxy_parse_opts - parse main.cf options */ + +int smtpd_proxy_parse_opts(const char *param_name, const char *param_val) +{ + static const NAME_MASK proxy_opts_table[] = { + SMTPD_PROXY_NAME_SPEED_ADJUST, SMTPD_PROXY_FLAG_SPEED_ADJUST, + 0, 0, + }; + int flags; + + /* + * The optional before-filter speed-adjust buffers use disk space. + * However, we don't know if they compete for storage space with the + * after-filter queue, so we can't simply bump up the free space + * requirement to 2.5 * message_size_limit. + */ + flags = name_mask(param_name, proxy_opts_table, param_val); + if (flags & SMTPD_PROXY_FLAG_SPEED_ADJUST) { +#ifdef NO_TRUNCATE + msg_warn("smtpd_proxy %s support is not available", + SMTPD_PROXY_NAME_SPEED_ADJUST); + flags &= ~SMTPD_PROXY_FLAG_SPEED_ADJUST; +#endif + } + return (flags); +} |