X-Git-Url: https://vcs.fsf.org/?p=exim.git;a=blobdiff_plain;f=src%2Fsrc%2Fip.c;h=fa688be644cdc1208aab75130501ae8ced76d81b;hp=ae629b0513d47a577e407f218f954630c43e9fff;hb=7df6163adc1837c48842471307ecb18aaded4530;hpb=bc338899110ea22098559081f77cbd0f7a8044bd diff --git a/src/src/ip.c b/src/src/ip.c index ae629b051..fa688be64 100644 --- a/src/src/ip.c +++ b/src/src/ip.c @@ -2,12 +2,12 @@ * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) University of Cambridge 1995 - 2013 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ /* See the file NOTICE for conditions of use and distribution. */ /* Functions for doing things with sockets. With the advent of IPv6 this has got messier, so that it's worth pulling out the code into separate functions -that other parts of Exim can call, expecially as there are now several +that other parts of Exim can call, especially as there are now several different places in the code where sockets are used. */ @@ -64,11 +64,11 @@ Returns: nothing - failure provokes a panic-die */ static void -ip_addrinfo(uschar *address, struct sockaddr_in6 *saddr) +ip_addrinfo(const uschar *address, struct sockaddr_in6 *saddr) { #ifdef IPV6_USE_INET_PTON - if (inet_pton(AF_INET6, CS address, &saddr->sin6_addr) != 1) + if (inet_pton(AF_INET6, CCS address, &saddr->sin6_addr) != 1) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "unable to parse \"%s\" as an " "IP address", address); saddr->sin6_family = AF_INET6; @@ -81,7 +81,7 @@ ip_addrinfo(uschar *address, struct sockaddr_in6 *saddr) hints.ai_family = AF_INET6; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_NUMERICHOST; - if ((rc = getaddrinfo(CS address, NULL, &hints, &res)) != 0 || res == NULL) + if ((rc = getaddrinfo(CCS address, NULL, &hints, &res)) != 0 || res == NULL) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "unable to parse \"%s\" as an " "IP address: %s", address, (rc == 0)? "NULL result returned" : gai_strerror(rc)); @@ -97,24 +97,11 @@ ip_addrinfo(uschar *address, struct sockaddr_in6 *saddr) * Bind socket to interface and port * *************************************************/ -/* This function binds a socket to a local interface address and port. For a -wildcard IPv6 bind, the address is ":". - -Arguments: - sock the socket - af AF_INET or AF_INET6 - the socket type - address the IP address, in text form - port the IP port (host order) - -Returns: the result of bind() -*/ - int -ip_bind(int sock, int af, uschar *address, int port) +ip_addr(void * sin_, int af, const uschar * address, int port) { -int s_len; -union sockaddr_46 sin; -memset(&sin, 0, sizeof(sin)); +union sockaddr_46 * sin = sin_; +memset(sin, 0, sizeof(*sin)); /* Setup code when using an IPv6 socket. The wildcard address is ":", to ensure an IPv6 socket is used. */ @@ -124,15 +111,13 @@ if (af == AF_INET6) { if (address[0] == ':' && address[1] == 0) { - sin.v6.sin6_family = AF_INET6; - sin.v6.sin6_addr = in6addr_any; + sin->v6.sin6_family = AF_INET6; + sin->v6.sin6_addr = in6addr_any; } else - { - ip_addrinfo(address, &sin.v6); /* Panic-dies on error */ - } - sin.v6.sin6_port = htons(port); - s_len = sizeof(sin.v6); + ip_addrinfo(address, &sin->v6); /* Panic-dies on error */ + sin->v6.sin6_port = htons(port); + return sizeof(sin->v6); } else #else /* HAVE_IPv6 */ @@ -142,22 +127,59 @@ af = af; /* Avoid compiler warning */ /* Setup code when using IPv4 socket. The wildcard address is "". */ { - sin.v4.sin_family = AF_INET; - sin.v4.sin_port = htons(port); - s_len = sizeof(sin.v4); - if (address[0] == 0) - sin.v4.sin_addr.s_addr = (S_ADDR_TYPE)INADDR_ANY; - else - sin.v4.sin_addr.s_addr = (S_ADDR_TYPE)inet_addr(CS address); + sin->v4.sin_family = AF_INET; + sin->v4.sin_port = htons(port); + sin->v4.sin_addr.s_addr = address[0] == 0 + ? (S_ADDR_TYPE)INADDR_ANY + : (S_ADDR_TYPE)inet_addr(CS address); + return sizeof(sin->v4); } +} -/* Now we can call the bind() function */ + +/* This function binds a socket to a local interface address and port. For a +wildcard IPv6 bind, the address is ":". + +Arguments: + sock the socket + af AF_INET or AF_INET6 - the socket type + address the IP address, in text form + port the IP port (host order) + +Returns: the result of bind() +*/ + +int +ip_bind(int sock, int af, uschar *address, int port) +{ +union sockaddr_46 sin; +int s_len = ip_addr(&sin, af, address, port); return bind(sock, (struct sockaddr *)&sin, s_len); } +/************************************************* +*************************************************/ + +#ifdef EXIM_TFO_PROBE +void +tfo_probe(void) +{ +# ifdef TCP_FASTOPEN +int sock, backlog = 5; + +if ( (sock = socket(SOCK_STREAM, AF_INET, 0)) < 0 + && setsockopt(sock, IPPROTO_TCP, TCP_FASTOPEN, &backlog, sizeof(backlog)) + ) + f.tcp_fastopen_ok = TRUE; +close(sock); +# endif +} +#endif + + /************************************************* * Connect socket to remote host * *************************************************/ @@ -172,13 +194,16 @@ Arguments: af AF_INET6 or AF_INET for the socket type address the remote address, in text form port the remote port - timeout a timeout + timeout a timeout (zero for indefinite timeout) + fastopen_blob non-null iff TCP_FASTOPEN can be used; may indicate early-data to + be sent in SYN segment Returns: 0 on success; -1 on failure, with errno set */ int -ip_connect(int sock, int af, uschar *address, int port, int timeout) +ip_connect(int sock, int af, const uschar *address, int port, int timeout, + const blob * fastopen_blob) { struct sockaddr_in s_in4; struct sockaddr *s_ptr; @@ -208,7 +233,7 @@ IPv6 support. */ memset(&s_in4, 0, sizeof(s_in4)); s_in4.sin_family = AF_INET; s_in4.sin_port = htons(port); - s_in4.sin_addr.s_addr = (S_ADDR_TYPE)inet_addr(CS address); + s_in4.sin_addr.s_addr = (S_ADDR_TYPE)inet_addr(CCS address); s_ptr = (struct sockaddr *)&s_in4; s_len = sizeof(s_in4); } @@ -216,39 +241,263 @@ IPv6 support. */ /* If no connection timeout is set, just call connect() without setting a timer, thereby allowing the inbuilt OS timeout to operate. */ +callout_address = string_sprintf("[%s]:%d", address, port); sigalrm_seen = FALSE; -if (timeout > 0) alarm(timeout); -rc = connect(sock, s_ptr, s_len); +if (timeout > 0) ALARM(timeout); + +#if defined(TCP_FASTOPEN) && defined(MSG_FASTOPEN) +/* TCP Fast Open, if the system has a cookie from a previous call to +this peer, can send data in the SYN packet. The peer can send data +before it gets our ACK of its SYN,ACK - the latter is useful for +the SMTP banner. Other (than SMTP) cases of TCP connections can +possibly use the data-on-syn, so support that too. + +This is a Linux implementation. It might be useable on FreeBSD; I have +not checked. I think MacOS has a "connectx" call for this purpose, +rather than using "sendto" ? */ + +if (fastopen_blob && f.tcp_fastopen_ok) + { + if ((rc = sendto(sock, fastopen_blob->data, fastopen_blob->len, + MSG_FASTOPEN | MSG_DONTWAIT, s_ptr, s_len)) >= 0) + /* seen for with-data, experimental TFO option, with-cookie case */ + /* seen for with-data, proper TFO opt, with-cookie case */ + { + DEBUG(D_transport|D_v) + debug_printf("non-TFO mode connection attempt to %s, %lu data\n", + address, (unsigned long)fastopen_blob->len); + /*XXX also seen on successful TFO, sigh */ + tcp_out_fastopen = fastopen_blob->len > 0 ? TFO_USED : TFO_ATTEMPTED; + } + else if (errno == EINPROGRESS) /* expected if we had no cookie for peer */ + /* seen for no-data, proper TFO option, both cookie-request and with-cookie cases */ + /* apparently no visibility of the diffference at this point */ + /* seen for with-data, proper TFO opt, cookie-req */ + /* with netwk delay, post-conn tcp_info sees unacked 1 for R, 2 for C; code in smtp_out.c */ + /* ? older Experimental TFO option behaviour ? */ + { /* queue unsent data */ + DEBUG(D_transport|D_v) debug_printf("TFO mode sendto, %s data: EINPROGRESS\n", + fastopen_blob->len > 0 ? "with" : "no"); + if (!fastopen_blob->data) + { + tcp_out_fastopen = TFO_ATTEMPTED; /* we tried; unknown if useful yet */ + rc = 0; + } + else + rc = send(sock, fastopen_blob->data, fastopen_blob->len, 0); + } + else if(errno == EOPNOTSUPP) + { + DEBUG(D_transport) + debug_printf("Tried TCP Fast Open but apparently not enabled by sysctl\n"); + goto legacy_connect; + } + } +else +#endif + { +legacy_connect: + DEBUG(D_transport|D_v) if (fastopen_blob) + debug_printf("non-TFO mode connection attempt to %s, %lu data\n", + address, (unsigned long)fastopen_blob->len); + if ((rc = connect(sock, s_ptr, s_len)) >= 0) + if ( fastopen_blob && fastopen_blob->data && fastopen_blob->len + && send(sock, fastopen_blob->data, fastopen_blob->len, 0) < 0) + rc = -1; + } + save_errno = errno; -alarm(0); +ALARM_CLR(0); /* There is a testing facility for simulating a connection timeout, as I can't think of any other way of doing this. It converts a connection refused into a timeout if the timeout is set to 999999. */ -if (running_in_test_harness) +if (f.running_in_test_harness && save_errno == ECONNREFUSED && timeout == 999999) { - if (save_errno == ECONNREFUSED && timeout == 999999) - { - rc = -1; - save_errno = EINTR; - sigalrm_seen = TRUE; - } + rc = -1; + save_errno = EINTR; + sigalrm_seen = TRUE; } /* Success */ -if (rc >= 0) return 0; +if (rc >= 0) + return 0; /* A failure whose error code is "Interrupted system call" is in fact an externally applied timeout if the signal handler has been run. */ -errno = (save_errno == EINTR && sigalrm_seen)? ETIMEDOUT : save_errno; +errno = save_errno == EINTR && sigalrm_seen ? ETIMEDOUT : save_errno; return -1; } +/************************************************* +* Create connected socket to remote host * +*************************************************/ + +/* Create a socket and connect to host (name or number, ipv6 ok) + at one of port-range. + +Arguments: + type SOCK_DGRAM or SOCK_STREAM + af AF_INET6 or AF_INET for the socket type + hostname host name, or ip address (as text) + portlo,porthi the remote port range + timeout a timeout + connhost if not NULL, host_item to be filled in with connection details + errstr pointer for allocated string on error + fastopen_blob with SOCK_STREAM, if non-null, request TCP Fast Open. + Additionally, optional early-data to send + +Return: + socket fd, or -1 on failure (having allocated an error string) +*/ +int +ip_connectedsocket(int type, const uschar * hostname, int portlo, int porthi, + int timeout, host_item * connhost, uschar ** errstr, const blob * fastopen_blob) +{ +int namelen, port; +host_item shost; +host_item *h; +int af = 0, fd, fd4 = -1, fd6 = -1; + +shost.next = NULL; +shost.address = NULL; +shost.port = portlo; +shost.mx = -1; + +namelen = Ustrlen(hostname); + +/* Anything enclosed in [] must be an IP address. */ + +if (hostname[0] == '[' && + hostname[namelen - 1] == ']') + { + uschar * host = string_copyn(hostname+1, namelen-2); + if (string_is_ip_address(host, NULL) == 0) + { + *errstr = string_sprintf("malformed IP address \"%s\"", hostname); + return -1; + } + shost.name = shost.address = host; + } + +/* Otherwise check for an unadorned IP address */ + +else if (string_is_ip_address(hostname, NULL) != 0) + shost.name = shost.address = string_copyn(hostname, namelen); + +/* Otherwise lookup IP address(es) from the name */ + +else + { + shost.name = string_copyn(hostname, namelen); + if (host_find_byname(&shost, NULL, HOST_FIND_QUALIFY_SINGLE, + NULL, FALSE) != HOST_FOUND) + { + *errstr = string_sprintf("no IP address found for host %s", shost.name); + return -1; + } + } + +/* Try to connect to the server - test each IP till one works */ + +for (h = &shost; h; h = h->next) + { + fd = Ustrchr(h->address, ':') != 0 + ? fd6 < 0 ? (fd6 = ip_socket(type, af = AF_INET6)) : fd6 + : fd4 < 0 ? (fd4 = ip_socket(type, af = AF_INET )) : fd4; + + if (fd < 0) + { + *errstr = string_sprintf("failed to create socket: %s", strerror(errno)); + goto bad; + } + + for(port = portlo; port <= porthi; port++) + if (ip_connect(fd, af, h->address, port, timeout, fastopen_blob) == 0) + { + if (fd != fd6) close(fd6); + if (fd != fd4) close(fd4); + if (connhost) + { + h->port = port; + *connhost = *h; + connhost->next = NULL; + } + return fd; + } + } + +*errstr = string_sprintf("failed to connect to any address for %s: %s", + hostname, strerror(errno)); + +bad: + close(fd4); close(fd6); return -1; +} + + +/*XXX TFO? */ +int +ip_tcpsocket(const uschar * hostport, uschar ** errstr, int tmo) +{ +int scan; +uschar hostname[256]; +unsigned int portlow, porthigh; + +/* extract host and port part */ +scan = sscanf(CS hostport, "%255s %u-%u", hostname, &portlow, &porthigh); +if (scan != 3) + { + if (scan != 2) + { + *errstr = string_sprintf("invalid socket '%s'", hostport); + return -1; + } + porthigh = portlow; + } + +return ip_connectedsocket(SOCK_STREAM, hostname, portlow, porthigh, + tmo, NULL, errstr, NULL); +} + +int +ip_unixsocket(const uschar * path, uschar ** errstr) +{ +int sock; +struct sockaddr_un server; + +if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) + { + *errstr = US"can't open UNIX socket."; + return -1; + } + +callout_address = string_copy(path); +server.sun_family = AF_UNIX; +Ustrncpy(server.sun_path, path, sizeof(server.sun_path)-1); +server.sun_path[sizeof(server.sun_path)-1] = '\0'; +if (connect(sock, (struct sockaddr *) &server, sizeof(server)) < 0) + { + int err = errno; + (void)close(sock); + *errstr = string_sprintf("unable to connect to UNIX socket (%s): %s", + path, strerror(err)); + return -1; + } +return sock; +} + +int +ip_streamsocket(const uschar * spec, uschar ** errstr, int tmo) +{ +return *spec == '/' + ? ip_unixsocket(spec, errstr) : ip_tcpsocket(spec, errstr, tmo); +} + /************************************************* * Set keepalive on a socket * *************************************************/ @@ -264,11 +513,11 @@ Returns: nothing */ void -ip_keepalive(int sock, uschar *address, BOOL torf) +ip_keepalive(int sock, const uschar *address, BOOL torf) { int fodder = 1; if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, - (uschar *)(&fodder), sizeof(fodder)) != 0) + US (&fodder), sizeof(fodder)) != 0) log_write(0, LOG_MAIN, "setsockopt(SO_KEEPALIVE) on connection %s %s " "failed: %s", torf? "to":"from", address, strerror(errno)); } @@ -279,39 +528,36 @@ if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, * Receive from a socket with timeout * *************************************************/ -/* The timeout is implemented using select(), and we loop to cover select() -getting interrupted, and the possibility of select() returning with a positive -result but no ready descriptor. Is this in fact possible? - +/* Arguments: - sock the socket - buffer to read into - bufsize the buffer size - timeout the timeout - -Returns: > 0 => that much data read - <= 0 on error or EOF; errno set - zero for EOF + fd the file descriptor + timeout the timeout, seconds +Returns: TRUE => ready for i/o + FALSE => timed out, or other error */ - -int -ip_recv(int sock, uschar *buffer, int buffsize, int timeout) +BOOL +fd_ready(int fd, int timeout) { fd_set select_inset; -struct timeval tv; -int start_recv = time(NULL); +time_t start_recv = time(NULL); +int time_left = timeout; int rc; +if (time_left <= 0) + { + errno = ETIMEDOUT; + return FALSE; + } /* Wait until the socket is ready */ -for (;;) +do { + struct timeval tv = { .tv_sec = time_left, .tv_usec = 0 }; FD_ZERO (&select_inset); - FD_SET (sock, &select_inset); - tv.tv_sec = timeout; - tv.tv_usec = 0; + FD_SET (fd, &select_inset); - DEBUG(D_transport) debug_printf("waiting for data on socket\n"); - rc = select(sock + 1, (SELECT_ARG2_TYPE *)&select_inset, NULL, NULL, &tv); + /*DEBUG(D_transport) debug_printf("waiting for data on fd\n");*/ + rc = select(fd + 1, (SELECT_ARG2_TYPE *)&select_inset, NULL, NULL, &tv); /* If some interrupt arrived, just retry. We presume this to be rare, but it can happen (e.g. the SIGUSR1 signal sent by exiwhat causes @@ -320,40 +566,64 @@ for (;;) Aug 2004: Somebody set up a cron job that ran exiwhat every 2 minutes, making the interrupt not at all rare. Since the timeout is typically more than 2 minutes, the effect was to block the timeout completely. To prevent this - happening again, we do an explicit time test. */ + happening again, we do an explicit time test and adjust the timeout + accordingly */ if (rc < 0 && errno == EINTR) { DEBUG(D_transport) debug_printf("EINTR while waiting for socket data\n"); - if (time(NULL) - start_recv < timeout) continue; - DEBUG(D_transport) debug_printf("total wait time exceeds timeout\n"); - } - /* Handle a timeout, and treat any other select error as a timeout, including - an EINTR when we have been in this loop for longer than timeout. */ + /* Watch out, 'continue' jumps to the condition, not to the loops top */ + time_left = timeout - (time(NULL) - start_recv); + if (time_left > 0) continue; + } if (rc <= 0) { errno = ETIMEDOUT; - return -1; + return FALSE; } - /* If the socket is ready, break out of the loop. */ - - if (FD_ISSET(sock, &select_inset)) break; + /* Checking the FD_ISSET is not enough, if we're interrupted, the + select_inset may still contain the 'input'. */ } +while (rc < 0 || !FD_ISSET(fd, &select_inset)); +return TRUE; +} + +/* The timeout is implemented using select(), and we loop to cover select() +getting interrupted, and the possibility of select() returning with a positive +result but no ready descriptor. Is this in fact possible? + +Arguments: + cctx the connection context (socket fd, possibly TLS context) + buffer to read into + bufsize the buffer size + timeout the timeout + +Returns: > 0 => that much data read + <= 0 on error or EOF; errno set - zero for EOF +*/ + +int +ip_recv(client_conn_ctx * cctx, uschar * buffer, int buffsize, int timeout) +{ +int rc; + +if (!fd_ready(cctx->sock, timeout)) + return -1; /* The socket is ready, read from it (via TLS if it's active). On EOF (i.e. close down of the connection), set errno to zero; otherwise leave it alone. */ #ifdef SUPPORT_TLS -if (tls_out.active == sock) - rc = tls_read(FALSE, buffer, buffsize); -else if (tls_in.active == sock) - rc = tls_read(TRUE, buffer, buffsize); +if (cctx->tls_ctx) /* client TLS */ + rc = tls_read(cctx->tls_ctx, buffer, buffsize); +else if (tls_in.active.sock == cctx->sock) /* server TLS */ + rc = tls_read(NULL, buffer, buffsize); else #endif - rc = recv(sock, buffer, buffsize, 0); + rc = recv(cctx->sock, buffer, buffsize, 0); if (rc > 0) return rc; if (rc == 0) errno = 0; @@ -464,7 +734,7 @@ if (af == AF_INET) *level = IPPROTO_IP; *optname = IP_TOS; } -#if HAVE_IPV6 +#if HAVE_IPV6 && defined(IPV6_TCLASS) else if (af == AF_INET6) { *level = IPPROTO_IPV6; @@ -520,13 +790,9 @@ while (last > first) return TRUE; } else if (c > 0) - { first = middle + 1; - } else - { last = middle; - } } return FALSE; } @@ -541,3 +807,5 @@ for (i=0; i < dscp_table_size; ++i) /* End of ip.c */ +/* vi: aw ai sw=2 +*/