Skip to content

Commit aead67d

Browse files
committed
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
1 parent f00a301 commit aead67d

File tree

6 files changed

+101
-3
lines changed

6 files changed

+101
-3
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ PHP NEWS
3333
. Fixed bug GH-19926 (reset internal pointer earlier while splicing array
3434
while COW violation flag is still set). (alexandre-daubois)
3535

36+
- Streams:
37+
. Added so_reuseaddr streams context socket option that allows disabling
38+
address resuse.
39+
3640
- Zip:
3741
. Fixed ZipArchive callback being called after executor has shut down.
3842
(ilutov)

UPGRADING

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ PHP 8.6 UPGRADE NOTES
3636
IntlNumberRangeFormatter::IDENTITY_FALLBACK_SINGLE_VALUE, IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE, IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY and
3737
IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE identity fallbacks.
3838
It is supported from icu 63.
39+
40+
- Streams:
41+
. Added stream socket context option so_reuseaddr that allows disabling
42+
address reuse (SO_REUSEADDR) and explicitly uses SO_EXCLUSIVEADDRUSE on
43+
Windows.
44+
3945
========================================
4046
3. Changes in SAPI modules
4147
========================================
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
--TEST--
2+
stream_socket_server() SO_REUSEADDR context option test
3+
--FILE--
4+
<?php
5+
$is_win = substr(PHP_OS, 0, 3) == "WIN";
6+
// Test default behavior (SO_REUSEADDR enabled)
7+
$server1 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
8+
if (!$server1) {
9+
die('Unable to create server3');
10+
}
11+
12+
$addr = stream_socket_get_name($server1, false);
13+
$port = (int)substr(strrchr($addr, ':'), 1);
14+
15+
$client1 = stream_socket_client("tcp://127.0.0.1:$port");
16+
$accepted = stream_socket_accept($server1, 1);
17+
18+
// Force real TCP connection with data
19+
fwrite($client1, "test");
20+
fread($accepted, 4);
21+
fwrite($accepted, "response");
22+
fread($client1, 8);
23+
24+
fclose($client1);
25+
if (!$is_win) { // Windows would always succeed if server is closed
26+
fclose($server1);
27+
}
28+
29+
$server2 = @stream_socket_server("tcp://127.0.0.1:$port", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
30+
if ($server2) {
31+
echo "Default: Server restart succeeded\n";
32+
fclose($server2);
33+
} else {
34+
echo "Default: Server restart failed\n";
35+
}
36+
37+
// Test with SO_REUSEADDR explicitly disabled
38+
$context = stream_context_create(['socket' => ['so_reuseaddr' => false]]);
39+
$server3 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context);
40+
if (!$server3) {
41+
die('Unable to create server3');
42+
}
43+
44+
$addr = stream_socket_get_name($server3, false);
45+
$port = (int)substr(strrchr($addr, ':'), 1);
46+
47+
$client3 = stream_socket_client("tcp://127.0.0.1:$port");
48+
$accepted = stream_socket_accept($server3, 1);
49+
50+
// Force real TCP connection with data
51+
fwrite($client3, "test");
52+
fread($accepted, 4);
53+
fwrite($accepted, "response");
54+
fread($client3, 8);
55+
56+
// Client closes first (becomes active closer)
57+
fclose($client3); // This enters TIME_WAIT
58+
if (!$is_win) { // Windows would always succeed if server is closed
59+
fclose($server3);
60+
}
61+
62+
// Try immediate bind with SO_REUSEADDR disabled (should fail)
63+
$server4 = @stream_socket_server("tcp://127.0.0.1:$port", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context);
64+
if ($server4) {
65+
echo "Disabled: Server restart succeeded\n";
66+
fclose($server4);
67+
} else {
68+
echo "Disabled: Server restart failed\n";
69+
}
70+
?>
71+
--EXPECT--
72+
Default: Server restart succeeded
73+
Disabled: Server restart failed

main/network.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,8 +496,13 @@ php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned po
496496

497497
/* attempt to bind */
498498

499-
#ifdef SO_REUSEADDR
500-
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&sockoptval, sizeof(sockoptval));
499+
if (sockopts & STREAM_SOCKOP_SO_REUSEADDR) {
500+
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&sockoptval, sizeof(sockoptval));
501+
}
502+
#ifdef PHP_WIN32
503+
else {
504+
setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char*)&sockoptval, sizeof(sockoptval));
505+
}
501506
#endif
502507
#ifdef IPV6_V6ONLY
503508
if (sockopts & STREAM_SOCKOP_IPV6_V6ONLY) {

main/php_network.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ typedef int php_socket_t;
123123
#define STREAM_SOCKOP_IPV6_V6ONLY (1 << 3)
124124
#define STREAM_SOCKOP_IPV6_V6ONLY_ENABLED (1 << 4)
125125
#define STREAM_SOCKOP_TCP_NODELAY (1 << 5)
126-
126+
#define STREAM_SOCKOP_SO_REUSEADDR (1 << 6)
127127

128128
/* uncomment this to debug poll(2) emulation on systems that have poll(2) */
129129
/* #define PHP_USE_POLL_2_EMULATION 1 */

main/streams/xp_socket.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,16 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t *
718718
}
719719
#endif
720720

721+
#ifdef SO_REUSEADDR
722+
/* SO_REUSEADDR is enabled by default so this option is just to disable it if set to false. */
723+
if (!PHP_STREAM_CONTEXT(stream)
724+
|| (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_reuseaddr")) == NULL
725+
|| zend_is_true(tmpzval)
726+
) {
727+
sockopts |= STREAM_SOCKOP_SO_REUSEADDR;
728+
}
729+
#endif
730+
721731
#ifdef SO_BROADCAST
722732
if (stream->ops == &php_stream_udp_socket_ops /* SO_BROADCAST is only applicable for UDP */
723733
&& PHP_STREAM_CONTEXT(stream)

0 commit comments

Comments
 (0)