From aead67d0bb2f6a3903325b68f274e6b1bceb7bce Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 25 Sep 2025 20:33:10 +0200 Subject: [PATCH 1/3] Add so_reuseaddr stream socket context option This is to allow disabling of SO_REUSEADDR that is enabled by default. To achieve better compatibility on Windows SO_EXCLUSIVEADDRUSE is set if so_reuseaddr is false. Closes GH-19967 --- NEWS | 4 ++ UPGRADING | 6 ++ ext/standard/tests/network/so_reuseaddr.phpt | 73 ++++++++++++++++++++ main/network.c | 9 ++- main/php_network.h | 2 +- main/streams/xp_socket.c | 10 +++ 6 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 ext/standard/tests/network/so_reuseaddr.phpt diff --git a/NEWS b/NEWS index 92b404f3a68a1..938aa1f72edb6 100644 --- a/NEWS +++ b/NEWS @@ -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) diff --git a/UPGRADING b/UPGRADING index eb158460cefb7..f4a455caefa1f 100644 --- a/UPGRADING +++ b/UPGRADING @@ -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 ======================================== diff --git a/ext/standard/tests/network/so_reuseaddr.phpt b/ext/standard/tests/network/so_reuseaddr.phpt new file mode 100644 index 0000000000000..d400a81ac33d3 --- /dev/null +++ b/ext/standard/tests/network/so_reuseaddr.phpt @@ -0,0 +1,73 @@ +--TEST-- +stream_socket_server() SO_REUSEADDR context option test +--FILE-- + ['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 diff --git a/main/network.c b/main/network.c index 7be64c2c4e1f1..0dadf0bb4dc3a 100644 --- a/main/network.c +++ b/main/network.c @@ -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) { diff --git a/main/php_network.h b/main/php_network.h index 6df73cf7af0d0..45e1e1902631d 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -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 */ diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index e3e81323c3fc7..db35a9b7952c8 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -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) From f900035b273c17c67948d89dcad76e03e885b60e Mon Sep 17 00:00:00 2001 From: David Carlier Date: Mon, 20 Oct 2025 22:42:56 +0000 Subject: [PATCH 2/3] network: on freebsd using SO_REUSEPORT_LB for a better distribution. SO_REUSEPORT on FreeBSD acts differently as the underlying semantic is different (as it predates Linux) since it s more for UDP/multicasts. The SO_REUSEPORT_LB flag, however, uses load balancing for group of address:port combinations which is how Linux is implemented. Co-authored-by: Jakub Zelenka --- main/network.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main/network.c b/main/network.c index 0dadf0bb4dc3a..6c43321cf2e8a 100644 --- a/main/network.c +++ b/main/network.c @@ -512,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 From 5151b9d8d6606dbb3eccaef45d2390511fbc91dd Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 3 Nov 2025 12:40:50 +0100 Subject: [PATCH 3/3] Add test for so_reuseport stream socket option --- ext/standard/tests/network/so_reuseport.phpt | 136 +++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 ext/standard/tests/network/so_reuseport.phpt diff --git a/ext/standard/tests/network/so_reuseport.phpt b/ext/standard/tests/network/so_reuseport.phpt new file mode 100644 index 0000000000000..12a07bbd42364 --- /dev/null +++ b/ext/standard/tests/network/so_reuseport.phpt @@ -0,0 +1,136 @@ +--TEST-- +stream_socket_server() SO_REUSEPORT context option test +--SKIPIF-- + +--FILE-- + ['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