7 /* SMTP server pass-through proxy client
10 /* #include <smtpd_proxy.h>
14 /* /* other fields... */
15 /* VSTREAM *proxy; /* connection to SMTP proxy */
16 /* VSTRING *proxy_buffer; /* last SMTP proxy response */
17 /* /* other fields... */
21 /* int smtpd_proxy_open(state, service, timeout, ehlo_name, mail_from)
22 /* SMTPD_STATE *state;
23 /* const char *service;
25 /* const char *ehlo_name;
26 /* const char *mail_from;
28 /* int smtpd_proxy_cmd(state, expect, format, ...)
29 /* SMTPD_STATE *state;
33 /* void smtpd_proxy_close(state)
34 /* SMTPD_STATE *state;
35 /* RECORD-LEVEL ROUTINES
36 /* int smtpd_proxy_rec_put(stream, rec_type, data, len)
42 /* int smtpd_proxy_rec_fprintf(stream, rec_type, format, ...)
47 /* The functions in this module implement a pass-through proxy
50 /* In order to minimize the intrusiveness of pass-through proxying, 1) the
51 /* proxy server must support the same MAIL FROM/RCPT syntax that Postfix
52 /* supports, 2) the record-level routines for message content proxying
53 /* have the same interface as the routines that are used for non-proxied
56 /* smtpd_proxy_open() connects to the proxy service, sends EHLO, sends
57 /* client information with the XFORWARD command if possible, sends
58 /* the MAIL FROM command, and receives the reply. A non-zero result means
59 /* trouble: either the proxy is unavailable, or it did not send the
61 /* The result is reported via the state->proxy_buffer field in a form
62 /* that can be sent to the SMTP client. In case of error, the
63 /* state->error_mask and state->err fields are updated.
64 /* A state->proxy_buffer field is created automatically; this field
65 /* persists beyond the end of a proxy session.
67 /* smtpd_proxy_cmd() formats and sends the specified command to the
68 /* proxy server, and receives the proxy server reply. A non-zero result
69 /* means trouble: either the proxy is unavailable, or it did not send the
71 /* All results are reported via the state->proxy_buffer field in a form
72 /* that can be sent to the SMTP client. In case of error, the
73 /* state->error_mask and state->err fields are updated.
75 /* smtpd_proxy_close() disconnects from a proxy server and resets
76 /* the state->proxy field. The last proxy server reply or error
77 /* description remains available via state->proxy-reply.
79 /* smtpd_proxy_rec_put() is a rec_put() clone that passes arbitrary
80 /* message content records to the proxy server. The data is expected
81 /* to be in SMTP dot-escaped form. All errors are reported as a
82 /* REC_TYPE_ERROR result value.
84 /* smtpd_proxy_rec_fprintf() is a rec_fprintf() clone that formats
85 /* message content and sends it to the proxy server. Leading dots are
86 /* not escaped. All errors are reported as a REC_TYPE_ERROR result
91 /* The SMTP proxy server host:port. The host or host: part is optional.
93 /* Time limit for connecting to the proxy server and for
94 /* sending and receiving proxy server commands and replies.
96 /* The EHLO Hostname that will be sent to the proxy server.
98 /* The MAIL FROM command.
100 /* SMTP server state.
102 /* Expected proxy server reply status code range. A warning is logged
103 /* when an unexpected reply is received. Specify one of the following:
105 /* .IP SMTPD_PROX_WANT_OK
106 /* The caller expects a reply in the 200 range.
107 /* .IP SMTPD_PROX_WANT_MORE
108 /* The caller expects a reply in the 300 range.
109 /* .IP SMTPD_PROX_WANT_ANY
110 /* The caller has no expectation. Do not warn for unexpected replies.
111 /* .IP SMTPD_PROX_WANT_NONE
112 /* Do not bother waiting for a reply.
117 /* Connection to proxy server.
119 /* Pointer to the content of one message content record.
121 /* The length of a message content record.
123 /* smtpd(8) Postfix smtp server
125 /* Fatal errors: memory allocation problem.
127 /* Warnings: unexpected response from proxy server, unable
128 /* to connect to proxy server, proxy server read/write error.
132 /* The Secure Mailer license must be distributed with this software.
135 /* IBM T.J. Watson Research
137 /* Yorktown Heights, NY 10598, USA
140 /* System library. */
142 #include <sys_defs.h>
145 #ifdef STRCASECMP_IN_STRINGS_H
149 /* Utility library. */
154 #include <stringops.h>
156 #include <name_code.h>
158 /* Global library. */
160 #include <mail_error.h>
161 #include <smtp_stream.h>
162 #include <cleanup_user.h>
163 #include <mail_params.h>
164 #include <rec_type.h>
165 #include <mail_proto.h>
166 #include <mail_params.h> /* null_format_string */
169 /* Application-specific. */
172 #include <smtpd_proxy.h>
175 * XFORWARD server features, recognized by the pass-through proxy client.
177 #define SMTPD_PROXY_XFORWARD_NAME (1<<0) /* client name */
178 #define SMTPD_PROXY_XFORWARD_ADDR (1<<1) /* client address */
179 #define SMTPD_PROXY_XFORWARD_PROTO (1<<2) /* protocol */
180 #define SMTPD_PROXY_XFORWARD_HELO (1<<3) /* client helo */
181 #define SMTPD_PROXY_XFORWARD_IDENT (1<<4) /* message identifier */
182 #define SMTPD_PROXY_XFORWARD_DOMAIN (1<<5) /* origin type */
183 #define SMTPD_PROXY_XFORWARD_PORT (1<<6) /* client port */
188 #define STR(x) vstring_str(x)
189 #define LEN(x) VSTRING_LEN(x)
190 #define SMTPD_PROXY_CONNECT null_format_string
191 #define STREQ(x, y) (strcmp((x), (y)) == 0)
193 /* smtpd_xforward_flush - flush forwarding information */
195 static int smtpd_xforward_flush(SMTPD_STATE
*state
, VSTRING
*buf
)
199 if (VSTRING_LEN(buf
) > 0) {
200 ret
= smtpd_proxy_cmd(state
, SMTPD_PROX_WANT_OK
,
201 XFORWARD_CMD
"%s", STR(buf
));
208 /* smtpd_xforward - send forwarding information */
210 static int smtpd_xforward(SMTPD_STATE
*state
, VSTRING
*buf
, const char *name
,
211 int value_available
, const char *value
)
216 #define CONSTR_LEN(s) (sizeof(s) - 1)
217 #define PAYLOAD_LIMIT (512 - CONSTR_LEN("250 " XFORWARD_CMD "\r\n"))
219 if (!value_available
)
220 value
= XFORWARD_UNAVAILABLE
;
223 * Encode the attribute value.
225 if (state
->expand_buf
== 0)
226 state
->expand_buf
= vstring_alloc(100);
227 xtext_quote(state
->expand_buf
, value
, "");
230 * How much space does this attribute need? SPACE name = value.
232 new_len
= strlen(name
) + strlen(STR(state
->expand_buf
)) + 2;
233 if (new_len
> PAYLOAD_LIMIT
)
234 msg_warn("%s command payload %s=%.10s... exceeds SMTP protocol limit",
235 XFORWARD_CMD
, name
, value
);
238 * Flush the buffer if we need to, and store the attribute.
240 if (VSTRING_LEN(buf
) > 0 && VSTRING_LEN(buf
) + new_len
> PAYLOAD_LIMIT
)
241 if ((ret
= smtpd_xforward_flush(state
, buf
)) < 0)
243 vstring_sprintf_append(buf
, " %s=%s", name
, STR(state
->expand_buf
));
248 /* smtpd_proxy_open - open proxy connection */
250 int smtpd_proxy_open(SMTPD_STATE
*state
, const char *service
,
251 int timeout
, const char *ehlo_name
,
252 const char *mail_from
)
260 static const NAME_CODE xforward_features
[] = {
261 XFORWARD_NAME
, SMTPD_PROXY_XFORWARD_NAME
,
262 XFORWARD_ADDR
, SMTPD_PROXY_XFORWARD_ADDR
,
263 XFORWARD_PORT
, SMTPD_PROXY_XFORWARD_PORT
,
264 XFORWARD_PROTO
, SMTPD_PROXY_XFORWARD_PROTO
,
265 XFORWARD_HELO
, SMTPD_PROXY_XFORWARD_HELO
,
266 XFORWARD_DOMAIN
, SMTPD_PROXY_XFORWARD_DOMAIN
,
269 const CLEANUP_STAT_DETAIL
*detail
;
270 int (*connect_fn
) (const char *, int, int);
271 const char *endpoint
;
274 * This buffer persists beyond the end of a proxy session so we can
275 * inspect the last command's reply.
277 if (state
->proxy_buffer
== 0)
278 state
->proxy_buffer
= vstring_alloc(10);
281 * Find connection method (default inet)
283 if (strncasecmp("unix:", service
, 5) == 0) {
284 endpoint
= service
+ 5;
285 connect_fn
= unix_connect
;
287 if (strncasecmp("inet:", service
, 5) == 0)
288 endpoint
= service
+ 5;
291 connect_fn
= inet_connect
;
297 if ((fd
= connect_fn(endpoint
, BLOCKING
, timeout
)) < 0) {
298 state
->error_mask
|= MAIL_ERROR_SOFTWARE
;
299 state
->err
|= CLEANUP_STAT_PROXY
;
300 msg_warn("connect to proxy service %s: %m", service
);
301 detail
= cleanup_stat_detail(CLEANUP_STAT_PROXY
);
302 vstring_sprintf(state
->proxy_buffer
,
304 detail
->smtp
, detail
->dsn
, detail
->text
);
307 state
->proxy
= vstream_fdopen(fd
, O_RDWR
);
308 vstream_control(state
->proxy
, VSTREAM_CTL_PATH
, service
, VSTREAM_CTL_END
);
309 smtp_timeout_setup(state
->proxy
, timeout
);
312 * Get server greeting banner.
314 * If this fails then we have a problem because the proxy should always
315 * accept our connection. Make up our own response instead of passing
316 * back the greeting banner: the proxy open might be delayed to the point
317 * that the client expects a MAIL FROM or RCPT TO reply.
319 if (smtpd_proxy_cmd(state
, SMTPD_PROX_WANT_OK
, SMTPD_PROXY_CONNECT
) != 0) {
320 detail
= cleanup_stat_detail(CLEANUP_STAT_PROXY
);
321 vstring_sprintf(state
->proxy_buffer
,
323 detail
->smtp
, detail
->dsn
, detail
->text
);
324 smtpd_proxy_close(state
);
329 * Send our own EHLO command. If this fails then we have a problem
330 * because the proxy should always accept our EHLO command. Make up our
331 * own response instead of passing back the EHLO reply: the proxy open
332 * might be delayed to the point that the client expects a MAIL FROM or
335 if (smtpd_proxy_cmd(state
, SMTPD_PROX_WANT_OK
, "EHLO %s", ehlo_name
) != 0) {
336 detail
= cleanup_stat_detail(CLEANUP_STAT_PROXY
);
337 vstring_sprintf(state
->proxy_buffer
,
339 detail
->smtp
, detail
->dsn
, detail
->text
);
340 smtpd_proxy_close(state
);
345 * Parse the EHLO reply and see if we can forward logging information.
347 state
->proxy_xforward_features
= 0;
348 lines
= STR(state
->proxy_buffer
);
349 while ((words
= mystrtok(&lines
, "\n")) != 0) {
350 if (mystrtok(&words
, "- ") && (word
= mystrtok(&words
, " \t")) != 0) {
351 if (strcasecmp(word
, XFORWARD_CMD
) == 0)
352 while ((word
= mystrtok(&words
, " \t")) != 0)
353 state
->proxy_xforward_features
|=
354 name_code(xforward_features
, NAME_CODE_FLAG_NONE
, word
);
359 * Send XFORWARD attributes. For robustness, explicitly specify what SMTP
360 * session attributes are known and unknown.
362 if (state
->proxy_xforward_features
) {
363 buf
= vstring_alloc(100);
365 if (state
->proxy_xforward_features
& SMTPD_PROXY_XFORWARD_NAME
)
366 bad
= smtpd_xforward(state
, buf
, XFORWARD_NAME
,
367 IS_AVAIL_CLIENT_NAME(FORWARD_NAME(state
)),
368 FORWARD_NAME(state
));
370 && (state
->proxy_xforward_features
& SMTPD_PROXY_XFORWARD_ADDR
))
371 bad
= smtpd_xforward(state
, buf
, XFORWARD_ADDR
,
372 IS_AVAIL_CLIENT_ADDR(FORWARD_ADDR(state
)),
373 FORWARD_ADDR(state
));
375 && (state
->proxy_xforward_features
& SMTPD_PROXY_XFORWARD_PORT
))
376 bad
= smtpd_xforward(state
, buf
, XFORWARD_PORT
,
377 IS_AVAIL_CLIENT_PORT(FORWARD_PORT(state
)),
378 FORWARD_PORT(state
));
380 && (state
->proxy_xforward_features
& SMTPD_PROXY_XFORWARD_HELO
))
381 bad
= smtpd_xforward(state
, buf
, XFORWARD_HELO
,
382 IS_AVAIL_CLIENT_HELO(FORWARD_HELO(state
)),
383 FORWARD_HELO(state
));
385 && (state
->proxy_xforward_features
& SMTPD_PROXY_XFORWARD_PROTO
))
386 bad
= smtpd_xforward(state
, buf
, XFORWARD_PROTO
,
387 IS_AVAIL_CLIENT_PROTO(FORWARD_PROTO(state
)),
388 FORWARD_PROTO(state
));
390 && (state
->proxy_xforward_features
& SMTPD_PROXY_XFORWARD_DOMAIN
))
391 bad
= smtpd_xforward(state
, buf
, XFORWARD_DOMAIN
, 1,
392 STREQ(FORWARD_DOMAIN(state
), MAIL_ATTR_RWR_LOCAL
) ?
393 XFORWARD_DOM_LOCAL
: XFORWARD_DOM_REMOTE
);
395 bad
= smtpd_xforward_flush(state
, buf
);
398 detail
= cleanup_stat_detail(CLEANUP_STAT_PROXY
);
399 vstring_sprintf(state
->proxy_buffer
,
401 detail
->smtp
, detail
->dsn
, detail
->text
);
402 smtpd_proxy_close(state
);
408 * Pass-through the client's MAIL FROM command. If this fails, then we
409 * have a problem because the proxy should always accept any MAIL FROM
410 * command that was accepted by us.
412 if (smtpd_proxy_cmd(state
, SMTPD_PROX_WANT_OK
, "%s", mail_from
) != 0) {
413 smtpd_proxy_close(state
);
419 /* smtpd_proxy_rdwr_error - report proxy communication error */
421 static int smtpd_proxy_rdwr_error(VSTREAM
*stream
, int err
)
425 msg_warn("lost connection with proxy %s", VSTREAM_PATH(stream
));
428 msg_warn("timeout talking to proxy %s", VSTREAM_PATH(stream
));
431 msg_panic("smtpd_proxy_rdwr_error: unknown proxy %s stream error %d",
432 VSTREAM_PATH(stream
), err
);
436 /* smtpd_proxy_cmd_error - report unexpected proxy reply */
438 static void smtpd_proxy_cmd_error(SMTPD_STATE
*state
, const char *fmt
,
444 * The command can be omitted at the start of an SMTP session. A null
445 * format string is not documented as part of the official interface
446 * because it is used only internally to this module.
448 buf
= vstring_alloc(100);
449 vstring_vsprintf(buf
, fmt
== SMTPD_PROXY_CONNECT
?
450 "connection request" : fmt
, ap
);
451 msg_warn("proxy %s rejected \"%s\": \"%s\"", VSTREAM_PATH(state
->proxy
),
452 STR(buf
), STR(state
->proxy_buffer
));
456 /* smtpd_proxy_cmd - send command to proxy, receive reply */
458 int smtpd_proxy_cmd(SMTPD_STATE
*state
, int expect
, const char *fmt
,...)
464 static VSTRING
*buffer
= 0;
465 const CLEANUP_STAT_DETAIL
*detail
;
468 * Errors first. Be prepared for delayed errors from the DATA phase.
470 if (vstream_ferror(state
->proxy
)
471 || vstream_feof(state
->proxy
)
472 || ((err
= vstream_setjmp(state
->proxy
)) != 0
473 && smtpd_proxy_rdwr_error(state
->proxy
, err
))) {
474 state
->error_mask
|= MAIL_ERROR_SOFTWARE
;
475 state
->err
|= CLEANUP_STAT_PROXY
;
476 detail
= cleanup_stat_detail(CLEANUP_STAT_PROXY
);
477 vstring_sprintf(state
->proxy_buffer
,
479 detail
->smtp
, detail
->dsn
, detail
->text
);
484 * The command can be omitted at the start of an SMTP session. This is
485 * not documented as part of the official interface because it is used
486 * only internally to this module.
488 if (fmt
!= SMTPD_PROXY_CONNECT
) {
491 * Format the command.
494 vstring_vsprintf(state
->proxy_buffer
, fmt
, ap
);
498 * Optionally log the command first, so that we can see in the log
499 * what the program is trying to do.
502 msg_info("> %s: %s", VSTREAM_PATH(state
->proxy
),
503 STR(state
->proxy_buffer
));
506 * Send the command to the proxy server. Since we're going to read a
507 * reply immediately, there is no need to flush buffers.
509 smtp_fputs(STR(state
->proxy_buffer
), LEN(state
->proxy_buffer
),
514 * Early return if we don't want to wait for a server reply (such as
515 * after sending QUIT.
517 if (expect
== SMTPD_PROX_WANT_NONE
)
521 * Censor out non-printable characters in server responses and save
522 * complete multi-line responses if possible.
524 VSTRING_RESET(state
->proxy_buffer
);
526 buffer
= vstring_alloc(10);
528 last_char
= smtp_get(buffer
, state
->proxy
, var_line_limit
);
529 printable(STR(buffer
), '?');
530 if (last_char
!= '\n')
531 msg_warn("%s: response longer than %d: %.30s...",
532 VSTREAM_PATH(state
->proxy
), var_line_limit
,
535 msg_info("< %s: %.100s", VSTREAM_PATH(state
->proxy
),
539 * Defend against a denial of service attack by limiting the amount
540 * of multi-line text that we are willing to store.
542 if (LEN(state
->proxy_buffer
) < var_line_limit
) {
543 if (VSTRING_LEN(state
->proxy_buffer
))
544 VSTRING_ADDCH(state
->proxy_buffer
, '\n');
545 vstring_strcat(state
->proxy_buffer
, STR(buffer
));
549 * Parse the response into code and text. Ignore unrecognized
550 * garbage. This means that any character except space (or end of
551 * line) will have the same effect as the '-' line continuation
554 for (cp
= STR(buffer
); *cp
&& ISDIGIT(*cp
); cp
++)
556 if (cp
- STR(buffer
) == 3) {
559 if (*cp
== ' ' || *cp
== 0)
562 msg_warn("received garbage from proxy %s: %.100s",
563 VSTREAM_PATH(state
->proxy
), STR(buffer
));
567 * Log a warning in case the proxy does not send the expected response.
568 * Silently accept any response when the client expressed no expectation.
570 * Don't pass through misleading 2xx replies. it confuses naive users and
571 * SMTP clients, and creates support problems.
573 if (expect
!= SMTPD_PROX_WANT_ANY
&& expect
!= *STR(state
->proxy_buffer
)) {
575 smtpd_proxy_cmd_error(state
, fmt
, ap
);
577 if (*STR(state
->proxy_buffer
) == SMTPD_PROX_WANT_OK
578 || *STR(state
->proxy_buffer
) == SMTPD_PROX_WANT_MORE
) {
579 state
->error_mask
|= MAIL_ERROR_SOFTWARE
;
580 state
->err
|= CLEANUP_STAT_PROXY
;
581 detail
= cleanup_stat_detail(CLEANUP_STAT_PROXY
);
582 vstring_sprintf(state
->proxy_buffer
,
584 detail
->smtp
, detail
->dsn
, detail
->text
);
592 /* smtpd_proxy_rec_put - send message content, rec_put() clone */
594 int smtpd_proxy_rec_put(VSTREAM
*stream
, int rec_type
,
595 const char *data
, ssize_t len
)
602 if (vstream_ferror(stream
) || vstream_feof(stream
))
603 return (REC_TYPE_ERROR
);
604 if ((err
= vstream_setjmp(stream
)) != 0)
605 return (smtpd_proxy_rdwr_error(stream
, err
), REC_TYPE_ERROR
);
608 * Send one content record. Errors and results must be as with rec_put().
610 if (rec_type
== REC_TYPE_NORM
)
611 smtp_fputs(data
, len
, stream
);
612 else if (rec_type
== REC_TYPE_CONT
)
613 smtp_fwrite(data
, len
, stream
);
615 msg_panic("smtpd_proxy_rec_put: need REC_TYPE_NORM or REC_TYPE_CONT");
619 /* smtpd_proxy_rec_fprintf - send message content, rec_fprintf() clone */
621 int smtpd_proxy_rec_fprintf(VSTREAM
*stream
, int rec_type
,
630 if (vstream_ferror(stream
) || vstream_feof(stream
))
631 return (REC_TYPE_ERROR
);
632 if ((err
= vstream_setjmp(stream
)) != 0)
633 return (smtpd_proxy_rdwr_error(stream
, err
), REC_TYPE_ERROR
);
636 * Send one content record. Errors and results must be as with
640 if (rec_type
== REC_TYPE_NORM
)
641 smtp_vprintf(stream
, fmt
, ap
);
643 msg_panic("smtpd_proxy_rec_fprintf: need REC_TYPE_NORM");
648 /* smtpd_proxy_close - close proxy connection */
650 void smtpd_proxy_close(SMTPD_STATE
*state
)
652 (void) vstream_fclose(state
->proxy
);