TFO: use IPPROTO_TCP not SOL_TCL for setsockopt, being present on more platforms
[exim.git] / src / src / ip.c
index 98eed1b934777be9e1d29a527df9eb7500acda28..ee70cf469085294575624b1a87b5d1fac8e9e5f1 100644 (file)
@@ -2,7 +2,7 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2009 */
+/* Copyright (c) University of Cambridge 1995 - 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Functions for doing things with sockets. With the advent of IPv6 this has
@@ -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,17 +127,34 @@ 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);
   }
+}
+
+
+
+/* This function binds a socket to a local interface address and port. For a
+wildcard IPv6 bind, the address is ":".
 
-/* Now we can call the bind() function */
+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);
 }
 
@@ -172,13 +174,15 @@ 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    TRUE iff TCP_FASTOPEN can be used
 
 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,
+  BOOL fastopen)
 {
 struct sockaddr_in s_in4;
 struct sockaddr *s_ptr;
@@ -208,7 +212,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,9 +220,34 @@ 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 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.  Is there any usage where the former might be?
+We might extend the ip_connect() args for data if so.  For now,
+connect in FASTOPEN mode but with zero data.
+*/
+
+if (fastopen)
+  {
+  if (  (rc = sendto(sock, NULL, 0, MSG_FASTOPEN, s_ptr, s_len)) < 0
+     && errno == EOPNOTSUPP
+     )
+    {
+    DEBUG(D_transport)
+      debug_printf("Tried TCP Fast Open but apparently not enabled by sysctl");
+    rc = connect(sock, s_ptr, s_len);
+    }
+  }
+else
+#endif
+  rc = connect(sock, s_ptr, s_len);
+
 save_errno = errno;
 alarm(0);
 
@@ -226,29 +255,188 @@ alarm(0);
 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 (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
+  address       the remote address, in text form
+  portlo,porthi the remote port range
+  timeout       a timeout
+  connhost     if not NULL, host_item filled in with connection details
+  errstr        pointer for allocated string on error
+
+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)
+{
+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, type == SOCK_STREAM) == 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;
+}
+
+
+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);
+}
+
+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,7 +452,7 @@ 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,
@@ -279,39 +467,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 = { time_left, 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,28 +505,52 @@ 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:
+  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
+*/
+
+int
+ip_recv(int sock, uschar *buffer, int buffsize, int timeout)
+{
+int rc;
+
+if (!fd_ready(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. */
@@ -464,7 +673,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 +729,9 @@ while (last > first)
     return TRUE;
     }
   else if (c > 0)
-    {
     first = middle + 1;
-    }
   else
-    {
     last = middle;
-    }
   }
 return FALSE;
 }
@@ -541,3 +746,5 @@ for (i=0; i < dscp_table_size; ++i)
 
 
 /* End of ip.c */
+/* vi: aw ai sw=2
+*/