Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ PHP NEWS
. Fixed bug GH-19926 (reset internal pointer earlier while splicing array
while COW violation flag is still set). (alexandre-daubois)

- Streams:
. Added so_reuseaddr streams context socket option that allows disabling
address resuse.

- Zip:
. Fixed ZipArchive callback being called after executor has shut down.
(ilutov)
Expand Down
6 changes: 6 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ PHP 8.6 UPGRADE NOTES
IntlNumberRangeFormatter::IDENTITY_FALLBACK_SINGLE_VALUE, IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE, IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY and
IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE identity fallbacks.
It is supported from icu 63.

- Streams:
. Added stream socket context option so_reuseaddr that allows disabling
address reuse (SO_REUSEADDR) and explicitly uses SO_EXCLUSIVEADDRUSE on
Windows.

========================================
3. Changes in SAPI modules
========================================
Expand Down
73 changes: 73 additions & 0 deletions ext/standard/tests/network/so_reuseaddr.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
--TEST--
stream_socket_server() SO_REUSEADDR context option test
--FILE--
<?php
$is_win = substr(PHP_OS, 0, 3) == "WIN";
// Test default behavior (SO_REUSEADDR enabled)
$server1 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
if (!$server1) {
die('Unable to create server3');
}

$addr = stream_socket_get_name($server1, false);
$port = (int)substr(strrchr($addr, ':'), 1);

$client1 = stream_socket_client("tcp://127.0.0.1:$port");
$accepted = stream_socket_accept($server1, 1);

// Force real TCP connection with data
fwrite($client1, "test");
fread($accepted, 4);
fwrite($accepted, "response");
fread($client1, 8);

fclose($client1);
if (!$is_win) { // Windows would always succeed if server is closed
fclose($server1);
}

$server2 = @stream_socket_server("tcp://127.0.0.1:$port", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
if ($server2) {
echo "Default: Server restart succeeded\n";
fclose($server2);
} else {
echo "Default: Server restart failed\n";
}

// Test with SO_REUSEADDR explicitly disabled
$context = stream_context_create(['socket' => ['so_reuseaddr' => false]]);
$server3 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context);
if (!$server3) {
die('Unable to create server3');
}

$addr = stream_socket_get_name($server3, false);
$port = (int)substr(strrchr($addr, ':'), 1);

$client3 = stream_socket_client("tcp://127.0.0.1:$port");
$accepted = stream_socket_accept($server3, 1);

// Force real TCP connection with data
fwrite($client3, "test");
fread($accepted, 4);
fwrite($accepted, "response");
fread($client3, 8);

// Client closes first (becomes active closer)
fclose($client3); // This enters TIME_WAIT
if (!$is_win) { // Windows would always succeed if server is closed
fclose($server3);
}

// Try immediate bind with SO_REUSEADDR disabled (should fail)
$server4 = @stream_socket_server("tcp://127.0.0.1:$port", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context);
if ($server4) {
echo "Disabled: Server restart succeeded\n";
fclose($server4);
} else {
echo "Disabled: Server restart failed\n";
}
?>
--EXPECT--
Default: Server restart succeeded
Disabled: Server restart failed
136 changes: 136 additions & 0 deletions ext/standard/tests/network/so_reuseport.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
--TEST--
stream_socket_server() SO_REUSEPORT context option test
--SKIPIF--
<?php
if (substr(PHP_OS, 0, 3) == "WIN") {
die('skip SO_REUSEPORT not available on Windows');
}
?>
--FILE--
<?php
// SO_REUSEPORT enabled - should allow multiple servers on same port
$context1 = stream_context_create(['socket' => ['so_reuseport' => true]]);
$server1 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context1);

if (!$server1) {
die('Unable to create server1');
}

$addr = stream_socket_get_name($server1, false);
$port = (int)substr(strrchr($addr, ':'), 1);

// Establish actual connection on server1
$client1 = stream_socket_client("tcp://127.0.0.1:$port");
$accepted1 = stream_socket_accept($server1, 1);

// Force real TCP connection with data
fwrite($client1, "test");
fread($accepted1, 4);
fwrite($accepted1, "response");
fread($client1, 8);

// Try to bind second server to SAME port with SO_REUSEPORT (while server1 still active)
$context2 = stream_context_create(['socket' => ['so_reuseport' => true]]);
$server2 = @stream_socket_server("tcp://127.0.0.1:$port", $errno, $errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context2);

if ($server2) {
echo "SO_REUSEPORT enabled: Multiple servers succeeded\n";

// Verify server2 can also accept connections
$client2 = stream_socket_client("tcp://127.0.0.1:$port");
$accepted2_on_srv1 = @stream_socket_accept($server1, 0.1);
$accepted2_on_srv2 = @stream_socket_accept($server2, 0.1);

if ($accepted2_on_srv1 || $accepted2_on_srv2) {
echo "SO_REUSEPORT enabled: Connections accepted\n";
}

if ($accepted2_on_srv1) fclose($accepted2_on_srv1);
if ($accepted2_on_srv2) fclose($accepted2_on_srv2);
if ($client2) fclose($client2);

fclose($server2);
} else {
echo "SO_REUSEPORT enabled: Multiple servers failed\n";
}

fclose($accepted1);
fclose($client1);
fclose($server1);

// SO_REUSEPORT disabled (default) - should NOT allow multiple servers
$server3 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);

if (!$server3) {
die('Unable to create server3');
}

$addr = stream_socket_get_name($server3, false);
$port = (int)substr(strrchr($addr, ':'), 1);

$client3 = stream_socket_client("tcp://127.0.0.1:$port");
$accepted3 = stream_socket_accept($server3, 1);

fwrite($client3, "test");
fread($accepted3, 4);
fwrite($accepted3, "response");
fread($client3, 8);

// Try to bind second server WITHOUT SO_REUSEPORT (while server3 still active)
$server4 = @stream_socket_server("tcp://127.0.0.1:$port", $errno, $errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);

if ($server4) {
echo "SO_REUSEPORT disabled: Multiple servers succeeded\n";
fclose($server4);
} else {
echo "SO_REUSEPORT disabled: Multiple servers failed\n";
}

fclose($accepted3);
fclose($client3);
fclose($server3);

// SO_REUSEPORT explicitly disabled
$context3 = stream_context_create(['socket' => ['so_reuseport' => false]]);
$server5 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context3);

if (!$server5) {
die('Unable to create server5');
}

$addr = stream_socket_get_name($server5, false);
$port = (int)substr(strrchr($addr, ':'), 1);

$client5 = stream_socket_client("tcp://127.0.0.1:$port");
$accepted5 = stream_socket_accept($server5, 1);

fwrite($client5, "test");
fread($accepted5, 4);
fwrite($accepted5, "response");
fread($client5, 8);

$context4 = stream_context_create(['socket' => ['so_reuseport' => false]]);
$server6 = @stream_socket_server("tcp://127.0.0.1:$port", $errno, $errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context4);

if ($server6) {
echo "SO_REUSEPORT explicitly disabled: Multiple servers succeeded\n";
fclose($server6);
} else {
echo "SO_REUSEPORT explicitly disabled: Multiple servers failed\n";
}

fclose($accepted5);
fclose($client5);
fclose($server5);
?>
--EXPECT--
SO_REUSEPORT enabled: Multiple servers succeeded
SO_REUSEPORT enabled: Connections accepted
SO_REUSEPORT disabled: Multiple servers failed
SO_REUSEPORT explicitly disabled: Multiple servers failed
15 changes: 13 additions & 2 deletions main/network.c
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,13 @@ php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned po

/* attempt to bind */

#ifdef SO_REUSEADDR
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&sockoptval, sizeof(sockoptval));
if (sockopts & STREAM_SOCKOP_SO_REUSEADDR) {
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&sockoptval, sizeof(sockoptval));
}
#ifdef PHP_WIN32
else {
setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char*)&sockoptval, sizeof(sockoptval));
}
#endif
#ifdef IPV6_V6ONLY
if (sockopts & STREAM_SOCKOP_IPV6_V6ONLY) {
Expand All @@ -507,7 +512,13 @@ php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned po
#endif
#ifdef SO_REUSEPORT
if (sockopts & STREAM_SOCKOP_SO_REUSEPORT) {
# ifdef SO_REUSEPORT_LB
/* Historically, SO_REUSEPORT on FreeBSD predates Linux version, however does not
* involve load balancing grouping thus SO_REUSEPORT_LB is the genuine equivalent.*/
setsockopt(sock, SOL_SOCKET, SO_REUSEPORT_LB, (char*)&sockoptval, sizeof(sockoptval));
# else
setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, (char*)&sockoptval, sizeof(sockoptval));
# endif
}
#endif
#ifdef SO_BROADCAST
Expand Down
2 changes: 1 addition & 1 deletion main/php_network.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ typedef int php_socket_t;
#define STREAM_SOCKOP_IPV6_V6ONLY (1 << 3)
#define STREAM_SOCKOP_IPV6_V6ONLY_ENABLED (1 << 4)
#define STREAM_SOCKOP_TCP_NODELAY (1 << 5)

#define STREAM_SOCKOP_SO_REUSEADDR (1 << 6)

/* uncomment this to debug poll(2) emulation on systems that have poll(2) */
/* #define PHP_USE_POLL_2_EMULATION 1 */
Expand Down
10 changes: 10 additions & 0 deletions main/streams/xp_socket.c
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,16 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t *
}
#endif

#ifdef SO_REUSEADDR
/* SO_REUSEADDR is enabled by default so this option is just to disable it if set to false. */
if (!PHP_STREAM_CONTEXT(stream)
|| (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_reuseaddr")) == NULL
|| zend_is_true(tmpzval)
) {
sockopts |= STREAM_SOCKOP_SO_REUSEADDR;
}
#endif

#ifdef SO_BROADCAST
if (stream->ops == &php_stream_udp_socket_ops /* SO_BROADCAST is only applicable for UDP */
&& PHP_STREAM_CONTEXT(stream)
Expand Down