X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/fb05276aaee4c27b6f20fb1f32290ee40a929064..d6ffd8ef9ebcb2dc913c75ee255459e8ff4fb4da:/src/src/smtp_out.c diff --git a/src/src/smtp_out.c b/src/src/smtp_out.c index 0c655e260..86c3e4127 100644 --- a/src/src/smtp_out.c +++ b/src/src/smtp_out.c @@ -2,7 +2,8 @@ * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) University of Cambridge 1995 - 2016 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 */ /* See the file NOTICE for conditions of use and distribution. */ /* A number of functions for driving outgoing SMTP calls. */ @@ -45,15 +46,26 @@ if (!istring) return TRUE; if (!(expint = expand_string(istring))) { - if (expand_string_forcedfail) return TRUE; + if (f.expand_string_forcedfail) return TRUE; addr->transport_return = PANIC; addr->message = string_sprintf("failed to expand \"interface\" " "option for %s: %s", msg, expand_string_message); return FALSE; } -while (isspace(*expint)) expint++; -if (*expint == 0) return TRUE; +if (is_tainted(expint)) + { + log_write(0, LOG_MAIN|LOG_PANIC, + "attempt to use tainted value '%s' from '%s' for interface", + expint, istring); + addr->transport_return = PANIC; + addr->message = string_sprintf("failed to expand \"interface\" " + "option for %s: configuration error", msg); + return FALSE; + } + +Uskip_whitespace(&expint); +if (!*expint) return TRUE; while ((iface = string_nextinlist(&expint, &sep, big_buffer, big_buffer_size))) @@ -100,7 +112,7 @@ smtp_get_port(uschar *rstring, address_item *addr, int *port, uschar *msg) { uschar *pstring = expand_string(rstring); -if (pstring == NULL) +if (!pstring) { addr->transport_return = PANIC; addr->message = string_sprintf("failed to expand \"%s\" (\"port\" option) " @@ -124,7 +136,7 @@ if (isdigit(*pstring)) else { struct servent *smtp_service = getservbyname(CS pstring, "tcp"); - if (smtp_service == NULL) + if (!smtp_service) { addr->transport_return = PANIC; addr->message = string_sprintf("TCP port \"%s\" is not defined for %s", @@ -140,9 +152,97 @@ return TRUE; +#ifdef TCP_FASTOPEN +static void +tfo_out_check(int sock) +{ +# ifdef __FreeBSD__ +struct tcp_info tinfo; +int val; +socklen_t len = sizeof(val); + +/* 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. */ + +/* +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 */ + } + +# else /* Linux & Apple */ +# if defined(TCP_INFO) && defined(EXIM_HAVE_TCPI_UNACKED) +struct tcp_info tinfo; +socklen_t len = sizeof(tinfo); + +switch (tcp_out_fastopen) + { + /* This is a somewhat dubious detection method; totally undocumented so likely + to fail in future kernels. There seems to be no documented way. What we really + want to know is if the server sent smtp-banner data before our ACK of his SYN,ACK + hit him. What this (possibly?) detects is whether we sent a TFO cookie with our + SYN, as distinct from a TFO request. This gets a false-positive when the server + key is rotated; we send the old one (which this test sees) but the server returns + the new one and does not send its SMTP banner before we ACK his SYN,ACK. + To force that rotation case: + '# echo -n "00000000-00000000-00000000-0000000" >/proc/sys/net/ipv4/tcp_fastopen_key' + The kernel seems to be counting unack'd packets. */ + + case TFO_ATTEMPTED_NODATA: + if ( getsockopt(sock, IPPROTO_TCP, TCP_INFO, &tinfo, &len) == 0 + && tinfo.tcpi_state == TCP_SYN_SENT + && tinfo.tcpi_unacked > 1 + ) + { + DEBUG(D_transport|D_v) + debug_printf("TCP_FASTOPEN tcpi_unacked %d\n", tinfo.tcpi_unacked); + tcp_out_fastopen = TFO_USED_NODATA; + } + break; + + /* When called after waiting for received data we should be able + to tell if data we sent was accepted. */ + + case TFO_ATTEMPTED_DATA: + if ( getsockopt(sock, IPPROTO_TCP, TCP_INFO, &tinfo, &len) == 0 + && tinfo.tcpi_state == TCP_ESTABLISHED + ) + if (tinfo.tcpi_options & TCPI_OPT_SYN_DATA) + { + DEBUG(D_transport|D_v) debug_printf("TFO: data was acked\n"); + tcp_out_fastopen = TFO_USED_DATA; + } + else + { + DEBUG(D_transport|D_v) debug_printf("TFO: had to retransmit\n"); + tcp_out_fastopen = TFO_NOT_USED; + } + break; + + default: break; /* compiler quietening */ + } +# endif +# endif /* Linux & Apple */ +} +#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 +*/ + int smtp_sock_connect(host_item * host, int host_af, int port, uschar * interface, - transport_instance * tb, int timeout) + transport_instance * tb, int timeout, const blob * early_data) { smtp_transport_options_block * ob = (smtp_transport_options_block *)tb->options_block; @@ -152,7 +252,8 @@ int dscp_level; int dscp_option; int sock; int save_errno = 0; -BOOL fastopen = FALSE; +const blob * fastopen_blob = NULL; + #ifndef DISABLE_EVENT deliver_host_address = host->address; @@ -166,7 +267,7 @@ if ((sock = ip_socket(SOCK_STREAM, host_af)) < 0) return -1; if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, US &on, sizeof(on))) HDEBUG(D_transport|D_acl|D_v) - debug_printf("failed to set NODELAY: %s ", strerror(errno)); + debug_printf_indent("failed to set NODELAY: %s ", strerror(errno)); /* 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. */ @@ -174,10 +275,10 @@ bomb out, just log it and continue in default traffic class. */ if (dscp && dscp_lookup(dscp, host_af, &dscp_level, &dscp_option, &dscp_value)) { HDEBUG(D_transport|D_acl|D_v) - debug_printf("DSCP \"%s\"=%x ", dscp, dscp_value); + debug_printf_indent("DSCP \"%s\"=%x ", dscp, dscp_value); if (setsockopt(sock, dscp_level, dscp_option, &dscp_value, sizeof(dscp_value)) < 0) HDEBUG(D_transport|D_acl|D_v) - debug_printf("failed to set DSCP: %s ", strerror(errno)); + 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 && @@ -185,10 +286,6 @@ if (dscp && dscp_lookup(dscp, host_af, &dscp_level, &dscp_option, &dscp_value)) (void) setsockopt(sock, dscp_level, dscp_option, &dscp_value, sizeof(dscp_value)); } -#ifdef TCP_FASTOPEN -if (verify_check_given_host (&ob->hosts_try_fastopen, host) == OK) fastopen = TRUE; -#endif - /* Bind to a specific interface if requested. Caller must ensure the interface is the same type (IPv4 or IPv6) as the outgoing address. */ @@ -196,15 +293,36 @@ if (interface && ip_bind(sock, host_af, interface, 0) < 0) { save_errno = errno; HDEBUG(D_transport|D_acl|D_v) - debug_printf("unable to bind outgoing SMTP call to %s: %s", interface, + debug_printf_indent("unable to bind outgoing SMTP call to %s: %s", interface, strerror(errno)); } /* Connect to the remote host, and add keepalive to the socket before returning -it, if requested. */ +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 (ip_connect(sock, host_af, host->address, port, timeout, fastopen) < 0) - save_errno = errno; +else + { +#ifdef TCP_FASTOPEN + if (verify_check_given_host(CUSS &ob->hosts_try_fastopen, host) == OK) + fastopen_blob = early_data ? early_data : &tcp_fastopen_nodata; +#endif + + if (ip_connect(sock, host_af, host->address, port, timeout, fastopen_blob) < 0) + save_errno = errno; + else if (early_data && !fastopen_blob && early_data->data && early_data->len) + { + HDEBUG(D_transport|D_acl|D_v) + debug_printf("sending %ld nonTFO early-data\n", (long)early_data->len); + +#ifdef TCP_QUICKACK + (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; + } + } /* Either bind() or connect() failed */ @@ -212,7 +330,7 @@ if (save_errno != 0) { HDEBUG(D_transport|D_acl|D_v) { - debug_printf("failed: %s", CUstrerror(save_errno)); + debug_printf_indent(" failed: %s", CUstrerror(save_errno)); if (save_errno == ETIMEDOUT) debug_printf(" (timeout=%s)", readconf_printtime(timeout)); debug_printf("\n"); @@ -222,13 +340,14 @@ if (save_errno != 0) return -1; } -/* Both bind() and connect() succeeded */ +/* Both bind() and connect() succeeded, and any early-data */ else { union sockaddr_46 interface_sock; EXIM_SOCKLEN_T size = sizeof(interface_sock); - HDEBUG(D_transport|D_acl|D_v) debug_printf("connected\n"); + + 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); else @@ -238,11 +357,33 @@ else close(sock); return -1; } + if (ob->keepalive) ip_keepalive(sock, host->address, TRUE); +#ifdef TCP_FASTOPEN + tfo_out_check(sock); +#endif return sock; } } + + + + +void +smtp_port_for_connect(host_item * host, int port) +{ +if (host->port != PORT_NONE) + { + HDEBUG(D_transport|D_acl|D_v) + debug_printf_indent("Transport port=%d replaced by host-specific port=%d\n", port, + host->port); + port = host->port; + } +else host->port = port; /* Set the port actually used */ +} + + /************************************************* * Connect to remote host * *************************************************/ @@ -252,60 +393,63 @@ detected by checking for a colon in the address. AF_INET6 is defined even on non-IPv6 systems, to enable the code to be less messy. However, on such systems host->address will always be an IPv4 address. -The port field in the host item is used if it is set (usually router from SRV -records or elsewhere). In other cases, the default passed as an argument is -used, and the host item is updated with its value. - Arguments: - host host item containing name and address (and sometimes port) - host_af AF_INET or AF_INET6 - port default remote port to connect to, in host byte order, for those - hosts whose port setting is PORT_NONE - interface outgoing interface address or NULL - timeout timeout value or 0 - tb transport + sc details for making connection: host, af, interface, transport + early_data if non-NULL, data to be sent - preferably in the TCP SYN segment Returns: connected socket number, or -1 with errno set */ int -smtp_connect(host_item *host, int host_af, int port, uschar *interface, - int timeout, transport_instance * tb) +smtp_connect(smtp_connect_args * sc, const blob * early_data) { -#ifdef SUPPORT_SOCKS -smtp_transport_options_block * ob = - (smtp_transport_options_block *)tb->options_block; -#endif - -if (host->port != PORT_NONE) - { - HDEBUG(D_transport|D_acl|D_v) - debug_printf("Transport port=%d replaced by host-specific port=%d\n", port, - host->port); - port = host->port; - } -else host->port = port; /* Set the port actually used */ +int port = sc->host->port; +smtp_transport_options_block * ob = sc->ob; -callout_address = string_sprintf("[%s]:%d", host->address, port); +callout_address = string_sprintf("[%s]:%d", sc->host->address, port); HDEBUG(D_transport|D_acl|D_v) { uschar * s = US" "; - if (interface) s = string_sprintf(" from %s ", interface); + if (sc->interface) s = string_sprintf(" from %s ", sc->interface); #ifdef SUPPORT_SOCKS if (ob->socks_proxy) s = string_sprintf("%svia proxy ", s); #endif - debug_printf("Connecting to %s %s%s... ", host->name, callout_address, s); + debug_printf_indent("Connecting to %s %s%s... ", sc->host->name, callout_address, s); } /* Create and connect the socket */ #ifdef SUPPORT_SOCKS if (ob->socks_proxy) - return socks_sock_connect(host, host_af, port, interface, tb, timeout); + { + int sock = socks_sock_connect(sc->host, sc->host_af, port, sc->interface, + sc->tblock, ob->connect_timeout); + + if (sock >= 0) + { + if (early_data && early_data->data && early_data->len) + if (send(sock, early_data->data, early_data->len, 0) < 0) + { + int save_errno = errno; + 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(ob->connect_timeout)); + debug_printf("\n"); + } + (void)close(sock); + sock = -1; + errno = save_errno; + } + } + return sock; + } #endif -return smtp_sock_connect(host, host_af, port, interface, tb, timeout); +return smtp_sock_connect(sc->host, sc->host_af, port, sc->interface, + sc->tblock, ob->connect_timeout, early_data); } @@ -319,27 +463,68 @@ pipelining. Argument: outblock the SMTP output block + mode further data expected, or plain Returns: TRUE if OK, FALSE on error, with errno set */ static BOOL -flush_buffer(smtp_outblock *outblock) +flush_buffer(smtp_outblock * outblock, int mode) { int rc; int n = outblock->ptr - outblock->buffer; +BOOL more = mode == SCMD_MORE; + +HDEBUG(D_transport|D_acl) debug_printf_indent("cmd buf flush %d bytes%s\n", n, + more ? " (more expected)" : ""); -HDEBUG(D_transport|D_acl) debug_printf("cmd buf flush %d bytes\n", n); -#ifdef SUPPORT_TLS -if (tls_out.active == outblock->sock) - rc = tls_write(FALSE, outblock->buffer, n); +#ifndef DISABLE_TLS +if (outblock->cctx->tls_ctx) + rc = tls_write(outblock->cctx->tls_ctx, outblock->buffer, n, more); else #endif - rc = send(outblock->sock, outblock->buffer, n, 0); + + { + if (outblock->conn_args) + { + blob early_data = { .data = outblock->buffer, .len = n }; + + /* We ignore the more-flag if we're doing a connect with early-data, which + means we won't get BDAT+data. A pity, but wise due to the idempotency + 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)) + < 0) + return FALSE; + outblock->conn_args = NULL; + rc = n; + } + else + { + rc = send(outblock->cctx->sock, outblock->buffer, n, +#ifdef MSG_MORE + more ? MSG_MORE : 0 +#else + 0 +#endif + ); + +#if defined(__linux__) + /* This is a workaround for a current linux kernel bug: as of + 5.6.8-200.fc31.x86_64 small (cctx->sock, IPPROTO_TCP, TCP_CORK, &off, sizeof(off)); +#endif + } + } if (rc <= 0) { - HDEBUG(D_transport|D_acl) debug_printf("send failed: %s\n", strerror(errno)); + HDEBUG(D_transport|D_acl) debug_printf_indent("send failed: %s\n", strerror(errno)); return FALSE; } @@ -358,8 +543,8 @@ return TRUE; any error message. Arguments: - outblock contains buffer for pipelining, and socket - noflush if TRUE, save the command in the output buffer, for pipelining + 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. If NULL, flush pipeline buffer only. @@ -371,36 +556,43 @@ Returns: 0 if command added to pipelining buffer, with nothing transmitted */ int -smtp_write_command(smtp_outblock *outblock, BOOL noflush, const char *format, ...) +smtp_write_command(void * sx, int mode, const char *format, ...) { -int count; +smtp_outblock * outblock = &((smtp_context *)sx)->outblock; int rc = 0; -va_list ap; if (format) { + gstring gs = { .size = big_buffer_size, .ptr = 0, .s = big_buffer }; + va_list ap; + + /* Use taint-unchecked routines for writing into big_buffer, trusting that + we'll never expand the results. Actually, the error-message use - leaving + the results in big_buffer for potential later use - is uncomfortably distant. + XXX Would be better to assume all smtp commands are short, use normal pool + alloc rather than big_buffer, and another global for the data-for-error. */ + va_start(ap, format); - if (!string_vformat(big_buffer, big_buffer_size, CS format, ap)) + if (!string_vformat(&gs, SVFMT_TAINT_NOCHK, CS format, ap)) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "overlong write_command in outgoing " "SMTP"); va_end(ap); - count = Ustrlen(big_buffer); + string_from_gstring(&gs); - if (count > outblock->buffersize) + if (gs.ptr > outblock->buffersize) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "overlong write_command in outgoing " "SMTP"); - if (count > outblock->buffersize - (outblock->ptr - outblock->buffer)) + if (gs.ptr > outblock->buffersize - (outblock->ptr - outblock->buffer)) { rc = outblock->cmd_count; /* flush resets */ - if (!flush_buffer(outblock)) return -1; + if (!flush_buffer(outblock, SCMD_FLUSH)) return -1; } - Ustrncpy(CS outblock->ptr, big_buffer, count); - outblock->ptr += count; + Ustrncpy(outblock->ptr, gs.s, gs.ptr); + outblock->ptr += gs.ptr; outblock->cmd_count++; - count -= 2; - big_buffer[count] = 0; /* remove \r\n for error message */ + gs.ptr -= 2; string_from_gstring(&gs); /* remove \r\n for error message */ /* We want to hide the actual data sent in AUTH transactions from reflections and logs. While authenticating, a flag is set in the outblock to enable this. @@ -420,13 +612,13 @@ if (format) while (*p != 0) *p++ = '*'; } - HDEBUG(D_transport|D_acl|D_v) debug_printf(" SMTP>> %s\n", big_buffer); + HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP>> %s\n", big_buffer); } -if (!noflush) +if (mode != SCMD_BUFFER) { rc += outblock->cmd_count; /* flush resets */ - if (!flush_buffer(outblock)) return -1; + if (!flush_buffer(outblock, mode)) return -1; } return rc; @@ -448,19 +640,19 @@ Arguments: inblock the SMTP input block (contains holding buffer, socket, etc.) buffer where to put the line size space available for the line - timeout the timeout to use when reading a packet + 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 */ static int -read_response_line(smtp_inblock *inblock, uschar *buffer, int size, int timeout) +read_response_line(smtp_inblock *inblock, uschar *buffer, int size, time_t timelimit) { uschar *p = buffer; uschar *ptr = inblock->ptr; uschar *ptrend = inblock->ptrend; -int sock = inblock->sock; +client_conn_ctx * cctx = inblock->cctx; /* Loop for reading multiple packets or reading another packet after emptying a previously-read one. */ @@ -498,15 +690,20 @@ for (;;) /* Need to read a new input packet. */ - rc = ip_recv(sock, inblock->buffer, inblock->buffersize, timeout); - if (rc <= 0) break; + if((rc = ip_recv(cctx, inblock->buffer, inblock->buffersize, timelimit)) <= 0) + { + DEBUG(D_deliver|D_transport|D_acl|D_v) + debug_printf_indent(errno ? " SMTP(%s)<<\n" : " SMTP(closed)<<\n", + strerror(errno)); + break; + } /* Another block of data has been successfully read. Set up the pointers and let the loop continue. */ ptrend = inblock->ptrend = inblock->buffer + rc; ptr = inblock->buffer; - DEBUG(D_transport|D_acl) debug_printf("read response data: size=%d\n", rc); + DEBUG(D_transport|D_acl) debug_printf_indent("read response data: size=%d\n", rc); } /* Get here if there has been some kind of recv() error; errno is set, but we @@ -532,34 +729,52 @@ also returned after a reading error. In this case buffer[0] will be zero, and the error code will be in errno. Arguments: - inblock the SMTP input block (contains holding buffer, socket, etc.) + sx the SMTP connection (contains input block with holding buffer, + socket, etc.) buffer where to put the response size the size of the buffer okdigit the expected first digit of the response - timeout the timeout to use + timeout the timeout to use, in seconds Returns: TRUE if a valid, non-error response was received; else FALSE */ +/*XXX could move to smtp transport; no other users */ BOOL -smtp_read_response(smtp_inblock *inblock, uschar *buffer, int size, int okdigit, +smtp_read_response(void * sx0, uschar * buffer, int size, int okdigit, int timeout) { -uschar *ptr = buffer; -int count; +smtp_context * sx = sx0; +uschar * ptr = buffer; +int count = 0; +time_t timelimit = time(NULL) + timeout; errno = 0; /* Ensure errno starts out zero */ -/* This is a loop to read and concatentate the lines that make up a multi-line +#ifndef DISABLE_PIPE_CONNECT +if (sx->pending_BANNER || sx->pending_EHLO) + { + int rc; + 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; + } + } +#endif + +/* This is a loop to read and concatenate the lines that make up a multi-line response. */ for (;;) { - if ((count = read_response_line(inblock, ptr, size, timeout)) < 0) + if ((count = read_response_line(&sx->inblock, ptr, size, timelimit)) < 0) return FALSE; HDEBUG(D_transport|D_acl|D_v) - debug_printf(" %s %s\n", (ptr == buffer)? "SMTP<<" : " ", ptr); + debug_printf_indent(" %s %s\n", ptr == buffer ? "SMTP<<" : " ", ptr); /* Check the format of the response: it must start with three digits; if these are followed by a space or end of line, the response is complete. If @@ -593,6 +808,10 @@ for (;;) size -= count + 1; } +#ifdef TCP_FASTOPEN + tfo_out_check(sx->cctx.sock); +#endif + /* Return a value that depends on the SMTP return code. On some systems a non-zero value of errno has been seen at this point, so ensure it is zero, because the caller of this function looks at errno when FALSE is returned, to