Skip to content

Commit

Permalink
Try fallback on IPv4 ANYADDR when IPv6 ANYADDR fails
Browse files Browse the repository at this point in the history
https://bugs.php.net/bug.php?id=74166
A host system with no/limited IPv6 support will fail at binding
the IPv6 ANYADDR address (::) as the address family is unsupported.

Deal with this by handling failure to implicitly bind to ::
as a soft failure, falling back to 0.0.0.0.

If binding to :: failed for some other reason (e.g. port in use)
then binding to 0.0.0.0 will likely fail as well, but we'll
get appropriate warnings for that.
  • Loading branch information
sgolemon committed Mar 8, 2017
1 parent 2ba2b76 commit b63c45c
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 29 deletions.
86 changes: 57 additions & 29 deletions sapi/fpm/fpm/fpm_sockets.c
Expand Up @@ -246,64 +246,92 @@ enum fpm_address_domain fpm_sockets_domain_from_address(char *address) /* {{{ */
}
/* }}} */

static int fpm_socket_af_inet_listening_socket(struct fpm_worker_pool_s *wp) /* {{{ */
static int fpm_socket_af_inet_socket_by_addr(struct fpm_worker_pool_s *wp, const char *addr, const char *port) /* {{{ */
{
struct addrinfo hints, *servinfo, *p;
char tmpbuf[INET6_ADDRSTRLEN];
int status;
int sock = -1;

memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

if ((status = getaddrinfo(addr, port, &hints, &servinfo)) != 0) {
zlog(ZLOG_ERROR, "getaddrinfo: %s\n", gai_strerror(status));
return -1;
}

for (p = servinfo; p != NULL; p = p->ai_next) {
inet_ntop(p->ai_family, fpm_get_in_addr(p->ai_addr), tmpbuf, INET6_ADDRSTRLEN);
if (sock < 0) {
if ((sock = fpm_sockets_get_listening_socket(wp, p->ai_addr, p->ai_addrlen)) != -1) {
zlog(ZLOG_DEBUG, "Found address for %s, socket opened on %s", addr, tmpbuf);
}
} else {
zlog(ZLOG_WARNING, "Found multiple addresses for %s, %s ignored", addr, tmpbuf);
}
}

freeaddrinfo(servinfo);

return sock;
}
/* }}} */

static int fpm_socket_af_inet_listening_socket(struct fpm_worker_pool_s *wp) /* {{{ */
{
char *dup_address = strdup(wp->config->listen_address);
char *port_str = strrchr(dup_address, ':');
char *addr = NULL;
char tmpbuf[INET6_ADDRSTRLEN];
int addr_len;
int port = 0;
int sock = -1;
int status;

if (port_str) { /* this is host:port pair */
*port_str++ = '\0';
port = atoi(port_str);
addr = dup_address;

/* strip brackets from address for getaddrinfo */
addr_len = strlen(addr);
if (addr[0] == '[' && addr[addr_len - 1] == ']') {
addr[addr_len - 1] = '\0';
addr++;
}

} else if (strlen(dup_address) == strspn(dup_address, "0123456789")) { /* this is port */
port = atoi(dup_address);
port_str = dup_address;
/* IPv6 catch-all + IPv4-mapped */
addr = "::";
}

if (port == 0) {
zlog(ZLOG_ERROR, "invalid port value '%s'", port_str);
return -1;
}

/* strip brackets from address for getaddrinfo */
addr_len = strlen(addr);
if (addr[0] == '[' && addr[addr_len - 1] == ']') {
addr[addr_len - 1] = '\0';
addr++;
}
if (addr) {
/* Bind a specific address */
sock = fpm_socket_af_inet_socket_by_addr(wp, addr, port_str);
} else {
/* Bind ANYADDR
*
* Try "::" first as that covers IPv6 ANYADDR and mapped IPv4 ANYADDR
* silencing warnings since failure is an option
*
* If that fails (because AF_INET6 is unsupported) retry with 0.0.0.0
*/
int old_level = zlog_set_level(ZLOG_ALERT);
sock = fpm_socket_af_inet_socket_by_addr(wp, "::", port_str);
zlog_set_level(old_level);

memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

if ((status = getaddrinfo(addr, port_str, &hints, &servinfo)) != 0) {
zlog(ZLOG_ERROR, "getaddrinfo: %s\n", gai_strerror(status));
free(dup_address);
return -1;
}

for (p = servinfo; p != NULL; p = p->ai_next) {
inet_ntop(p->ai_family, fpm_get_in_addr(p->ai_addr), tmpbuf, INET6_ADDRSTRLEN);
if (sock < 0) {
if ((sock = fpm_sockets_get_listening_socket(wp, p->ai_addr, p->ai_addrlen)) != -1) {
zlog(ZLOG_DEBUG, "Found address for %s, socket opened on %s", addr, tmpbuf);
}
} else {
zlog(ZLOG_WARNING, "Found multiple addresses for %s, %s ignored", addr, tmpbuf);
zlog(ZLOG_NOTICE, "Failed implicitly binding to ::, retrying with 0.0.0.0");
sock = fpm_socket_af_inet_socket_by_addr(wp, "0.0.0.0", port_str);
}
}

free(dup_address);
freeaddrinfo(servinfo);

return sock;
}
Expand Down
57 changes: 57 additions & 0 deletions sapi/fpm/tests/023.phpt
@@ -0,0 +1,57 @@
--TEST--
FPM: Test already bound address
--SKIPIF--
<?php include "skipif.inc"; ?>
--FILE--
<?php

include "include.inc";

$logfile = dirname(__FILE__).'/php-fpm.log.tmp';
$port = 9000+PHP_INT_SIZE;

$cfg = <<<EOT
[global]
log_level = debug
error_log = $logfile
[unconfined]
listen = $port
ping.path = /ping
ping.response = pong
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
EOT;

// Occupy our port and let things fail
$busy = stream_socket_server("tcp://[::]:$port");

$fpm = run_fpm($cfg, $tail);
if (is_resource($fpm)) {
/* Expect two specific lines of log output and show them
* If we get any different number of those patterns, display whole log
*/
$out = $all = '';
$count = 0;
while (!feof($tail)) {
$line = fgets($tail);
$all .= $line;
if ((false !== strpos($line, 'retrying with 0.0.0.0')) ||
(false !== strpos($line, 'unable to bind'))) {
$out .= $line;
++$count;
}
}
echo ($count == 2) ? $out : $all;
}
?>
--EXPECTF--
[%d-%s-%d %d:%d:%f] NOTICE: pid %d, fpm_socket_af_inet_listening_socket(), line %d: Failed implicitly binding to ::, retrying with 0.0.0.0
[%d-%s-%d %d:%d:%f] ERROR: pid %d, fpm_sockets_new_listening_socket(), line %d: unable to bind listening socket for address '%d': %s
--CLEAN--
<?php
$logfile = dirname(__FILE__).'/php-fpm.log.tmp';
@unlink($logfile);
?>

0 comments on commit b63c45c

Please sign in to comment.