Re: [Exim] Linux USAGI and the default bind/listen behaviour

Page principale
Supprimer ce message
Répondre à ce message
Auteur: Lionel Elie Mamane
Date:  
À: Philip Hazel
CC: exim-users
Sujet: Re: [Exim] Linux USAGI and the default bind/listen behaviour
--
On Fri, Jan 31, 2003 at 03:21:06PM +0000, Philip Hazel wrote:
> On Fri, 31 Jan 2003, Lionel Elie Mamane wrote:


>>> I'm not surprised. Many changes have been made in that area in Exim
>>> 4. Exim 3 is pretty obsolete now. I suggest you upgrade.


>> I tried exim4, it exhibits the same behaviour.


> Interesting. There is an extensive comment in the Exim 4 code,
> containing this:


It's the same comment as in the exim3 code

> So it is supposed to do exactly what you said:


>> EADDRINUSE. The following works, though:
>>
>> - bind(IPv6 any address)
>> - listen(IPv6 any address)
>> - bind(IPv4 any address), failure because EADDRINUSE -> goto end
>> - listen(IPv4 any address)
>> - :end:


No, what it does is:

- bind(IPv6 any address)
- bind(IPv4 any address)
- listen(IPv6 any address)
- listen(IPv4 any address)

Notice the different ordering. With USAGI Linux, this gives:

- bind(IPv6 any address): success
- bind(IPv4 any address): success
- listen(IPv6 any address): FAIL!!!


>> The bind(ipv4) will fail with EADDRINUSE.


> Ah! The difference is that the bind() is failing, not the listen().


Only if the listen(ipv6) is already done!

> The code expects the bind() to succeed and the listen() to fail,
> because that's what was experienced before.


> I will put in a check for EADDRINUSE on the bind().


It won't work: You need to move the listen() calls within the same for
loop as the binds. The code in daemon.c was:

- do all binds
- do smpt_accept_* sanity checks
- do all listens

It needs to look like

- do smpt_accept_* sanity checks
- do all bind/listen pairs

Something like (untested!):

/* Do a sanity check on the max connects value just to save us from getting
a huge amount of store. */

if (smtp_accept_max > 4095) smtp_accept_max = 4096;

/* There's no point setting smtp_accept_queue unless it is less than the max
connects limit. The configuration reader ensures that the max is set if the
queue-only option is set. */

if (smtp_accept_queue > smtp_accept_max) smtp_accept_queue = 0;

/* Get somewhere to keep the list of SMTP accepting pids if we are keeping
track of them for total number and queue/host limits. */

  if (smtp_accept_max > 0)
    {
    int i;
    smtp_slots = store_get(smtp_accept_max * sizeof(smtp_slot));
    for (i = 0; i < smtp_accept_max; i++) smtp_slots[i] = empty_smtp_slot;
    }


/* For each IP address, create a socket and bind it to the appropriate
port, and start listening on it.
See comments above about IPv6 sockets that may or may not accept IPv4
calls when listening on all interfaces. We also have to cope with the case of
a system with IPv6 libraries, but no IPv6 support in the kernel. In this
case, we must ignore failure to create an IPv6 socket for wildcard listening.
The second socket (IPv4) should then get used instead - we have to shuffle
it down into first place. */

  for (ipa = addresses, sk = 0; sk < listen_socket_count; ipa = ipa->next, sk++)
    {
    int retries = 9;
    int af = (Ustrchr(ipa->address, ':') != NULL)? AF_INET6 : AF_INET;


    /* Fill in the default port (host_find_interfaces() will have left zero
    if no port specified) */


    if (ipa->port == 0) ipa->port = default_smtp_port;


    listen_sockets[sk] = ip_socket(SOCK_STREAM, af);
    if (listen_sockets[sk] < 0)
      {
      /* Just log failure for an IPv6 wildcard socket */


      if (af == AF_INET6 && local_interfaces == NULL)
        {
        log_write(0, LOG_MAIN, "Failed to create IPv6 socket for wildcard "
          "listening (%s): falling back to IPv4", strerror(errno));


        addresses = addresses->next;   /* Chop IPv6 off the list */
        sk--;                          /* Back up the count */
        listen_socket_count--;         /* Reduce the total */
        continue;                      /* With the IPv4 socket */
        }


      /* Not a failure to create an IPv6 socket for wildcard listening */


      else
        log_write(0, LOG_PANIC_DIE, "IPv%c socket creation failed: %s",
          (af == AF_INET6)? '6' : '4', strerror(errno));
      }


    /* If this is an IPv6 wildcard socket, set IPV6_V6ONLY if that option is
    available. Just log failure (can get protocol not available, just like
    socket creation can). */


    #ifdef IPV6_V6ONLY
    if (local_interfaces == NULL && af == AF_INET6 &&
        setsockopt(listen_sockets[sk], IPPROTO_IPV6, IPV6_V6ONLY, (char *)(&on),
          sizeof(on)) < 0)
      log_write(0, LOG_MAIN, "Setting IPV6_V6ONLY on daemon's IPv6 wildcard "
        "socket failed (%s): carrying on without it", strerror(errno));
    #endif  /* IPV6_V6ONLY */


    /* Set SO_REUSEADDR so that the daemon can be restarted while a connection
    is being handled.  Without this, a connection will prevent reuse of the
    smtp port for listening. */


    if (setsockopt(listen_sockets[sk], SOL_SOCKET, SO_REUSEADDR,
                   (uschar *)(&on), sizeof(on)) < 0)
      log_write(0, LOG_MAIN|LOG_PANIC_DIE, "setting SO_REUSEADDR on socket "
        "failed when starting daemon: %s", strerror(errno));


    /* Set TCP_NODELAY; Exim does its own buffering. */


    setsockopt(listen_sockets[sk], IPPROTO_TCP, TCP_NODELAY, (uschar *)(&on),
      sizeof(on));


    /* Now bind the socket to the required port; if Exim is being restarted
    it may not always be possible to bind immediately, even with SO_REUSEADDR
    set, so try 10 times, waiting between each try. */


    DEBUG(D_any)
      {
      if (ipa->address[0] == 0 ||
         (ipa->address[0] == ':' && ipa->address[1] == 0))
        debug_printf("listening on all interfaces (IPv%c) port %d\n",
          (af == AF_INET6)? '6' : '4', ipa->port);
      else
        debug_printf("listening on %s port %d\n", ipa->address, ipa->port);
      }


    for(;;)
      {
      if (ip_bind(listen_sockets[sk], af, ipa->address, ipa->port) < 0)
        {
    if (errno == EADDRINUSE && local_interfaces == NULL && sk > 0)
      {
      DEBUG(blahblah) "Ignored IPv4 EADDRINUSE bind failure;
      }
    else
      {
          uschar *msg = US strerror(errno);
          uschar *addr = (ipa->address[0] == 0)? US"(any IPv4)" :
              (ipa->address[0] == ':' && ipa->address[1] == 0)? US"(any IPv6)" :
              ipa->address;
          if (retries-- <= 0)
            log_write(0, LOG_MAIN|LOG_PANIC_DIE,
              "socket bind() to port %d for address %s failed: %s: "
              "daemon abandoned", ipa->port, addr, msg);
          log_write(0, LOG_MAIN, "socket bind() to port %d for address %s "
            "failed: %s: waiting before trying again", ipa->port, addr, msg);
          sleep(30);
      }
        }
      else
        {
        /* Start listening on the bound socket, establishing the maximum backlog of
            connections that is allowed. In an IPv6 environment, if listen() fails with
        the error EADDRINUSE and we are doing wildcard listening and this is the
        second (i.e last) socket, ignore the error on the grounds that we must be
        in a system where the IPv6 socket accepts both kinds
        of call. */
    if (listen(listen_sockets[sk], smtp_connect_backlog) < 0)
          {
          if (errno == EADDRINUSE && local_interfaces == NULL && sk > 0)
            {
            DEBUG(D_any) debug_printf("wildcard IPv4 listen() failed after IPv6 "
              "listen() success; EADDRINUSE ignored\n");
            close(listen_sockets[sk]);
            listen_socket_count--;       /* OK because we know we are in the */
            break;                       /* last iteration of this loop */
            }
          else
            {
            int skf;
            for (ipa = addresses, skf = 0; skf < sk; ipa = ipa->next, skf++);
            log_write(0, LOG_PANIC_DIE, "listen() failed on interface %s: %s",
              (ipa->address[0] == 0)? US"(any IPv4)" :
      /* LIONEL NOTE: The test on ipa->address[1] was ':', must be '\0' */
              (ipa->address[0] == ':' && ipa->address[1] == '\0')? US"(any IPv6)" :
              ipa->address,
              strerror(errno));
            }
          }
    }
      }
    }



  }      /* End of setup for listening */



> And hope it works - because I can't test this.


Throw patches (or CVS checkout points) at me, I'll test it.

> Thanks for the report.


You're welcome.

--
Lionel
--
[ Content of type application/pgp-signature deleted ]
--