X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/68f71b94cc10f8b0453597d0f92b4bd7d995eeef..cd8cb71deb2d653228cc037cf91ecab980acdcd0:/src/src/smtp_out.c diff --git a/src/src/smtp_out.c b/src/src/smtp_out.c index 86c3e4127..62557072e 100644 --- a/src/src/smtp_out.c +++ b/src/src/smtp_out.c @@ -2,9 +2,10 @@ * Exim - an Internet mail transport agent * *************************************************/ +/* Copyright (c) The Exim Maintainers 2020 - 2023 */ /* Copyright (c) University of Cambridge 1995 - 2018 */ -/* Copyright (c) The Exim Maintainers 2020 */ /* See the file NOTICE for conditions of use and distribution. */ +/* SPDX-License-Identifier: GPL-2.0-or-later */ /* A number of functions for driving outgoing SMTP calls. */ @@ -27,7 +28,7 @@ Arguments: which case the function does nothing host_af AF_INET or AF_INET6 for the outgoing IP address addr the mail address being handled (for setting errors) - interface point this to the interface + interface point this to the interface if there is one defined msg to add to any error message Returns: TRUE on success, FALSE on failure, with error message @@ -67,10 +68,10 @@ if (is_tainted(expint)) Uskip_whitespace(&expint); if (!*expint) return TRUE; -while ((iface = string_nextinlist(&expint, &sep, big_buffer, - big_buffer_size))) +while ((iface = string_nextinlist(&expint, &sep, NULL, 0))) { - if (string_is_ip_address(iface, NULL) == 0) + int if_af = string_is_ip_address(iface, NULL); + if (if_af == 0) { addr->transport_return = PANIC; addr->message = string_sprintf("\"%s\" is not a valid IP " @@ -79,11 +80,11 @@ while ((iface = string_nextinlist(&expint, &sep, big_buffer, return FALSE; } - if (((Ustrchr(iface, ':') == NULL)? AF_INET:AF_INET6) == host_af) + if ((if_af == 4 ? AF_INET : AF_INET6) == host_af) break; } -if (iface) *interface = string_copy(iface); +*interface = iface; return TRUE; } @@ -153,25 +154,44 @@ return TRUE; #ifdef TCP_FASTOPEN +/* Try to record if TFO was attmepted and if it was successfully used. */ + static void tfo_out_check(int sock) { +static BOOL done_once = FALSE; + +if (done_once) return; +done_once = TRUE; + # ifdef __FreeBSD__ struct tcp_info tinfo; -int val; -socklen_t len = sizeof(val); +socklen_t len = sizeof(tinfo); -/* The observability as of 12.1 is not useful as a client, only telling us that -a TFO option was used on SYN. It could have been a TFO-R, or ignored by the -server. */ +/* A getsockopt TCP_FASTOPEN unfortunately returns "was-used" for a TFO/R as +well as a TFO/C. Use what we can of the Linux hack below; reliability issues ditto. */ +switch (tcp_out_fastopen) + { + case TFO_ATTEMPTED_NODATA: + if ( getsockopt(sock, IPPROTO_TCP, TCP_INFO, &tinfo, &len) == 0 + && tinfo.tcpi_state == TCPS_SYN_SENT + && tinfo.__tcpi_unacked > 0 + ) + { + DEBUG(D_transport|D_v) + debug_printf("TCP_FASTOPEN tcpi_unacked %d\n", tinfo.__tcpi_unacked); + tcp_out_fastopen = TFO_USED_NODATA; + } + break; + /* + case TFO_ATTEMPTED_DATA: + case TFO_ATTEMPTED_DATA: + if (tinfo.tcpi_options & TCPI_OPT_SYN_DATA) XXX no equvalent as of 12.2 + */ + } -/* -if (tcp_out_fastopen == TFO_ATTEMPTED_NODATA || tcp_out_fastopen == TFO_ATTEMPTED_DATA) - if (getsockopt(sock, IPPROTO_TCP, TCP_FASTOPEN, &val, &len) == 0 && val != 0) {} -*/ switch (tcp_out_fastopen) { - case TFO_ATTEMPTED_NODATA: tcp_out_fastopen = TFO_USED_NODATA; break; case TFO_ATTEMPTED_DATA: tcp_out_fastopen = TFO_USED_DATA; break; default: break; /* compiler quietening */ } @@ -233,35 +253,21 @@ switch (tcp_out_fastopen) #endif -/* Arguments as for smtp_connect(), plus - early_data if non-NULL, idenmpotent data to be sent - - preferably in the TCP SYN segment - -Returns: connected socket number, or -1 with errno set +/* Create and bind a socket, given the connect-args. +Update those with the state. Return the fd, or -1 with errno set. */ int -smtp_sock_connect(host_item * host, int host_af, int port, uschar * interface, - transport_instance * tb, int timeout, const blob * early_data) +smtp_boundsock(smtp_connect_args * sc) { +transport_instance * tb = sc->tblock; smtp_transport_options_block * ob = (smtp_transport_options_block *)tb->options_block; const uschar * dscp = ob->dscp; -int dscp_value; -int dscp_level; -int dscp_option; -int sock; -int save_errno = 0; -const blob * fastopen_blob = NULL; - +int sock, dscp_value, dscp_level, dscp_option; -#ifndef DISABLE_EVENT -deliver_host_address = host->address; -deliver_host_port = port; -if (event_raise(tb->event_action, US"tcp:connect", NULL)) return -1; -#endif - -if ((sock = ip_socket(SOCK_STREAM, host_af)) < 0) return -1; +if ((sock = ip_socket(SOCK_STREAM, sc->host_af)) < 0) + return -1; /* Set TCP_NODELAY; Exim does its own buffering. */ @@ -272,7 +278,7 @@ if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, US &on, sizeof(on))) /* Set DSCP value, if we can. For now, if we fail to set the value, we don't bomb out, just log it and continue in default traffic class. */ -if (dscp && dscp_lookup(dscp, host_af, &dscp_level, &dscp_option, &dscp_value)) +if (dscp && dscp_lookup(dscp, sc->host_af, &dscp_level, &dscp_option, &dscp_value)) { HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("DSCP \"%s\"=%x ", dscp, dscp_value); @@ -281,7 +287,7 @@ if (dscp && dscp_lookup(dscp, host_af, &dscp_level, &dscp_option, &dscp_value)) debug_printf_indent("failed to set DSCP: %s ", strerror(errno)); /* If the kernel supports IPv4 and IPv6 on an IPv6 socket, we need to set the option for both; ignore failures here */ - if (host_af == AF_INET6 && + if (sc->host_af == AF_INET6 && dscp_lookup(dscp, AF_INET, &dscp_level, &dscp_option, &dscp_value)) (void) setsockopt(sock, dscp_level, dscp_option, &dscp_value, sizeof(dscp_value)); } @@ -289,64 +295,122 @@ if (dscp && dscp_lookup(dscp, host_af, &dscp_level, &dscp_option, &dscp_value)) /* Bind to a specific interface if requested. Caller must ensure the interface is the same type (IPv4 or IPv6) as the outgoing address. */ -if (interface && ip_bind(sock, host_af, interface, 0) < 0) +if (sc->interface) { - save_errno = errno; - HDEBUG(D_transport|D_acl|D_v) - debug_printf_indent("unable to bind outgoing SMTP call to %s: %s", interface, - strerror(errno)); + union sockaddr_46 interface_sock; + EXIM_SOCKLEN_T size = sizeof(interface_sock); + + if ( ip_bind(sock, sc->host_af, sc->interface, 0) < 0 + || getsockname(sock, (struct sockaddr *) &interface_sock, &size) < 0 + ) + { + HDEBUG(D_transport|D_acl|D_v) + debug_printf_indent("unable to bind outgoing SMTP call to %s: %s\n", sc->interface, + strerror(errno)); + close(sock); + return -1; + } + sending_ip_address = host_ntoa(-1, &interface_sock, NULL, &sending_port); } +sc->sock = sock; +return sock; +} + + +/* Arguments: + host host item containing name and address and port + host_af AF_INET or AF_INET6 + port TCP port number + interface outgoing interface address or NULL + tb transport + timeout timeout value or 0 + early_data if non-NULL, idempotent data to be sent - + preferably in the TCP SYN segment + Special case: non-NULL but with NULL blob.data - caller is + client-data-first (eg. TLS-on-connect) and a lazy-TCP-connect is + acceptable. + +Returns: connected socket number, or -1 with errno set +*/ + +int +smtp_sock_connect(smtp_connect_args * sc, int timeout, const blob * early_data) +{ +smtp_transport_options_block * ob = + (smtp_transport_options_block *)sc->tblock->options_block; +int sock; +int save_errno = 0; +const blob * fastopen_blob = NULL; + + +#ifndef DISABLE_EVENT +deliver_host_address = sc->host->address; +deliver_host_port = sc->host->port; +if (event_raise(sc->tblock->event_action, US"tcp:connect", NULL, &errno)) return -1; +#endif + +if ( (sock = sc->sock) < 0 + && (sock = smtp_boundsock(sc)) < 0) + save_errno = errno; +sc->sock = -1; + /* Connect to the remote host, and add keepalive to the socket before returning it, if requested. If the build supports TFO, request it - and if the caller requested some early-data then include that in the TFO request. If there is early-data but no TFO support, send it after connecting. */ -else +if (!save_errno) { #ifdef TCP_FASTOPEN - if (verify_check_given_host(CUSS &ob->hosts_try_fastopen, host) == OK) - fastopen_blob = early_data ? early_data : &tcp_fastopen_nodata; + /* See if TCP Fast Open usable. Default is a traditional 3WHS connect */ + if (verify_check_given_host(CUSS &ob->hosts_try_fastopen, sc->host) == OK) + { + if (!early_data) + fastopen_blob = &tcp_fastopen_nodata; /* TFO, with no data */ + else if (early_data->data) + fastopen_blob = early_data; /* TFO, with data */ +# ifdef TCP_FASTOPEN_CONNECT + else + { /* expecting client data */ + DEBUG(D_transport|D_acl|D_v) debug_printf(" set up lazy-connect\n"); + setsockopt(sock, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, US &on, sizeof(on)); + /* fastopen_blob = NULL; lazy TFO, triggered by data write */ + } +# endif + } #endif - if (ip_connect(sock, host_af, host->address, port, timeout, fastopen_blob) < 0) + if (ip_connect(sock, sc->host_af, sc->host->address, sc->host->port, timeout, fastopen_blob) < 0) save_errno = errno; else if (early_data && !fastopen_blob && early_data->data && early_data->len) { + /* We had some early-data to send, but couldn't do TFO */ HDEBUG(D_transport|D_acl|D_v) debug_printf("sending %ld nonTFO early-data\n", (long)early_data->len); -#ifdef TCP_QUICKACK +#ifdef TCP_QUICKACK_notdef (void) setsockopt(sock, IPPROTO_TCP, TCP_QUICKACK, US &off, sizeof(off)); #endif if (send(sock, early_data->data, early_data->len, 0) < 0) save_errno = errno; } +#ifdef TCP_QUICKACK_notdef + /* Under TFO (with openssl & pipe-conn; testcase 4069, as of + 5.10.8-100.fc32.x86_64) this seems to be inop. + Perhaps overwritten when we (client) go -> ESTABLISHED on seeing the 3rd-ACK? + For that case, added at smtp_reap_banner(). */ + (void) setsockopt(sock, IPPROTO_TCP, TCP_QUICKACK, US &off, sizeof(off)); +#endif } -/* Either bind() or connect() failed */ - -if (save_errno != 0) - { - HDEBUG(D_transport|D_acl|D_v) - { - debug_printf_indent(" failed: %s", CUstrerror(save_errno)); - if (save_errno == ETIMEDOUT) - debug_printf(" (timeout=%s)", readconf_printtime(timeout)); - debug_printf("\n"); - } - (void)close(sock); - errno = save_errno; - return -1; - } - -/* Both bind() and connect() succeeded, and any early-data */ - -else +if (!save_errno) { union sockaddr_46 interface_sock; EXIM_SOCKLEN_T size = sizeof(interface_sock); + /* Both bind() and connect() succeeded, and any early-data */ + HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" connected\n"); if (getsockname(sock, (struct sockaddr *)(&interface_sock), &size) == 0) sending_ip_address = host_ntoa(-1, &interface_sock, NULL, &sending_port); @@ -358,12 +422,25 @@ else return -1; } - if (ob->keepalive) ip_keepalive(sock, host->address, TRUE); + if (ob->keepalive) ip_keepalive(sock, sc->host->address, TRUE); #ifdef TCP_FASTOPEN tfo_out_check(sock); #endif return sock; } + +/* Either bind() or connect() failed */ + +HDEBUG(D_transport|D_acl|D_v) + { + debug_printf_indent(" failed: %s", CUstrerror(save_errno)); + if (save_errno == ETIMEDOUT) + debug_printf(" (timeout=%s)", readconf_printtime(timeout)); + debug_printf("\n"); + } +(void)close(sock); +errno = save_errno; +return -1; } @@ -375,7 +452,7 @@ smtp_port_for_connect(host_item * host, int port) { if (host->port != PORT_NONE) { - HDEBUG(D_transport|D_acl|D_v) + HDEBUG(D_transport|D_acl|D_v) if (port != host->port) debug_printf_indent("Transport port=%d replaced by host-specific port=%d\n", port, host->port); port = host->port; @@ -396,6 +473,9 @@ host->address will always be an IPv4 address. Arguments: sc details for making connection: host, af, interface, transport early_data if non-NULL, data to be sent - preferably in the TCP SYN segment + Special case: non-NULL but with NULL blob.data - caller is + client-data-first (eg. TLS-on-connect) and a lazy-TCP-connect is + acceptable. Returns: connected socket number, or -1 with errno set */ @@ -448,8 +528,7 @@ if (ob->socks_proxy) } #endif -return smtp_sock_connect(sc->host, sc->host_af, port, sc->interface, - sc->tblock, ob->connect_timeout, early_data); +return smtp_sock_connect(sc, ob->connect_timeout, early_data); } @@ -474,13 +553,21 @@ flush_buffer(smtp_outblock * outblock, int mode) int rc; int n = outblock->ptr - outblock->buffer; BOOL more = mode == SCMD_MORE; +client_conn_ctx * cctx; HDEBUG(D_transport|D_acl) debug_printf_indent("cmd buf flush %d bytes%s\n", n, more ? " (more expected)" : ""); +if (!(cctx = outblock->cctx)) + { + log_write(0, LOG_MAIN|LOG_PANIC, "null conn-context pointer"); + errno = 0; + return FALSE; + } + #ifndef DISABLE_TLS -if (outblock->cctx->tls_ctx) - rc = tls_write(outblock->cctx->tls_ctx, outblock->buffer, n, more); +if (cctx->tls_ctx) /*XXX have seen a null cctx here, rvfy sending QUIT, hence check above */ + rc = tls_write(cctx->tls_ctx, outblock->buffer, n, more); else #endif @@ -494,7 +581,7 @@ else requirement: TFO with data can, in rare cases, replay the data to the receiver. */ - if ( (outblock->cctx->sock = smtp_connect(outblock->conn_args, &early_data)) + if ( (cctx->sock = smtp_connect(outblock->conn_args, &early_data)) < 0) return FALSE; outblock->conn_args = NULL; @@ -502,7 +589,7 @@ else } else { - rc = send(outblock->cctx->sock, outblock->buffer, n, + rc = send(cctx->sock, outblock->buffer, n, #ifdef MSG_MORE more ? MSG_MORE : 0 #else @@ -517,7 +604,7 @@ else https://bugzilla.redhat.com/show_bug.cgi?id=1803806 */ if (!more) - setsockopt(outblock->cctx->sock, IPPROTO_TCP, TCP_CORK, &off, sizeof(off)); + setsockopt(cctx->sock, IPPROTO_TCP, TCP_CORK, &off, sizeof(off)); #endif } } @@ -535,6 +622,22 @@ return TRUE; +/* This might be called both due to callout and then from delivery. +Use memory that will not be released between those phases. +*/ +static void +smtp_debug_resp(const uschar * buf) +{ +#ifndef DISABLE_CLIENT_CMD_LOG +int old_pool = store_pool; +store_pool = POOL_PERM; +client_cmd_log = string_append_listele_n(client_cmd_log, ':', buf, + buf[3] == ' ' ? 3 : 4); +store_pool = old_pool; +#endif +} + + /************************************************* * Write SMTP command * *************************************************/ @@ -546,7 +649,7 @@ Arguments: sx SMTP connection, contains buffer for pipelining, and socket mode buffer, write-with-more-likely, write format a format, starting with one of - of HELO, MAIL FROM, RCPT TO, DATA, ".", or QUIT. + of HELO, MAIL FROM, RCPT TO, DATA, BDAT, ".", or QUIT. If NULL, flush pipeline buffer only. ... data for the format @@ -556,7 +659,7 @@ Returns: 0 if command added to pipelining buffer, with nothing transmitted */ int -smtp_write_command(void * sx, int mode, const char *format, ...) +smtp_write_command(void * sx, int mode, const char * format, ...) { smtp_outblock * outblock = &((smtp_context *)sx)->outblock; int rc = 0; @@ -577,7 +680,6 @@ if (format) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "overlong write_command in outgoing " "SMTP"); va_end(ap); - string_from_gstring(&gs); if (gs.ptr > outblock->buffersize) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "overlong write_command in outgoing " @@ -609,10 +711,10 @@ if (format) while (!isspace(*p)) p++; while (isspace(*p)) p++; } - while (*p != 0) *p++ = '*'; + while (*p) *p++ = '*'; } - HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP>> %s\n", big_buffer); + smtp_debug_cmd(big_buffer, mode); } if (mode != SCMD_BUFFER) @@ -643,7 +745,7 @@ Arguments: timelimit deadline for reading the lime, seconds past epoch Returns: length of a line that has been put in the buffer - -1 otherwise, with errno set + -1 otherwise, with errno set, and inblock->ptr adjusted */ static int @@ -684,6 +786,7 @@ for (;;) { *p = 0; /* Leave malformed line for error message */ errno = ERRNO_SMTPFORMAT; + inblock->ptr = ptr; return -1; } } @@ -709,6 +812,7 @@ for (;;) /* Get here if there has been some kind of recv() error; errno is set, but we ensure that the result buffer is empty before returning. */ +inblock->ptr = inblock->ptrend = inblock->buffer; *buffer = 0; return -1; } @@ -748,8 +852,10 @@ smtp_context * sx = sx0; uschar * ptr = buffer; int count = 0; time_t timelimit = time(NULL) + timeout; +BOOL yield = FALSE; errno = 0; /* Ensure errno starts out zero */ +buffer[0] = '\0'; #ifndef DISABLE_PIPE_CONNECT if (sx->pending_BANNER || sx->pending_EHLO) @@ -758,9 +864,8 @@ if (sx->pending_BANNER || sx->pending_EHLO) if ((rc = smtp_reap_early_pipe(sx, &count)) != OK) { DEBUG(D_transport) debug_printf("failed reaping pipelined cmd responsess\n"); - buffer[0] = '\0'; if (rc == DEFER) errno = ERRNO_TLSFAILURE; - return FALSE; + goto out; } } #endif @@ -791,7 +896,7 @@ for (;;) (ptr[3] != '-' && ptr[3] != ' ' && ptr[3] != 0)) { errno = ERRNO_SMTPFORMAT; /* format error */ - return FALSE; + goto out; } /* If the line we have just read is a terminal line, line, we are done. @@ -809,7 +914,7 @@ for (;;) } #ifdef TCP_FASTOPEN - tfo_out_check(sx->cctx.sock); +tfo_out_check(sx->cctx.sock); #endif /* Return a value that depends on the SMTP return code. On some systems a @@ -819,7 +924,11 @@ distinguish between an unexpected return code and other errors such as timeouts, lost connections, etc. */ errno = 0; -return buffer[0] == okdigit; +yield = buffer[0] == okdigit; + +out: + smtp_debug_resp(buffer); + return yield; } /* End of smtp_out.c */