From 9582d8e6d746befac8b3aa84a2cb503d64030716 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 3 Nov 2025 22:22:01 +0100 Subject: [PATCH 1/2] Add stream socket keepalive context options This adds so_keepalive, tcp_keepidle, tcp_keepintvl and tcp_keepcnt stream socket context options that are used to set their upper case C macro variants by setsockopt function. The test requires sockets extension and just tests that the values are being set. This is because a real test would be slow and difficult to show that those options really work due to how they work internally. Closes GH-20381 --- NEWS | 2 + UPGRADING | 3 + ext/standard/tests/network/so_keepalive.phpt | 151 +++++++++++++++++++ main/network.c | 93 ++++++++++-- main/php_network.h | 23 +++ main/streams/xp_socket.c | 134 +++++++++++++--- 6 files changed, 378 insertions(+), 28 deletions(-) create mode 100644 ext/standard/tests/network/so_keepalive.phpt diff --git a/NEWS b/NEWS index 12740c65c3c89..3b20cbb026c44 100644 --- a/NEWS +++ b/NEWS @@ -73,6 +73,8 @@ PHP NEWS while COW violation flag is still set). (alexandre-daubois) - Streams: + . Added so_keepalive, tcp_keepidle, tcp_keepintvl and tcp_keepcnt stream + socket context options. . Added so_reuseaddr streams context socket option that allows disabling address resuse. . Fixed bug GH-20370 (User stream filters could violate typed property diff --git a/UPGRADING b/UPGRADING index d52827bf96154..f9e506235197a 100644 --- a/UPGRADING +++ b/UPGRADING @@ -46,6 +46,9 @@ PHP 8.6 UPGRADE NOTES . Added stream socket context option so_reuseaddr that allows disabling address reuse (SO_REUSEADDR) and explicitly uses SO_EXCLUSIVEADDRUSE on Windows. + . Added stream socket context options so_keepalive, tcp_keepidle, + tcp_keepintvl and tcp_keepcnt that allow setting socket keepalive + options. ======================================== 3. Changes in SAPI modules diff --git a/ext/standard/tests/network/so_keepalive.phpt b/ext/standard/tests/network/so_keepalive.phpt new file mode 100644 index 0000000000000..a437d16694b3b --- /dev/null +++ b/ext/standard/tests/network/so_keepalive.phpt @@ -0,0 +1,151 @@ +--TEST-- +stream_socket_server() and stream_socket_client() SO_KEEPALIVE context option test +--EXTENSIONS-- +sockets +--SKIPIF-- + +--FILE-- + [ + 'so_keepalive' => true, + 'tcp_keepidle' => 60, + 'tcp_keepintvl' => 10, + 'tcp_keepcnt' => 5, + ] +]); + +$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $server_context); + +if (!$server) { + die('Unable to create server'); +} + +$addr = stream_socket_get_name($server, false); +$port = (int)substr(strrchr($addr, ':'), 1); + +// Test client with SO_KEEPALIVE enabled +$client_context = stream_context_create([ + 'socket' => [ + 'so_keepalive' => true, + 'tcp_keepidle' => 30, + 'tcp_keepintvl' => 5, + 'tcp_keepcnt' => 3, + ] +]); + +$client = stream_socket_client("tcp://127.0.0.1:$port", $errno, $errstr, 30, + STREAM_CLIENT_CONNECT, $client_context); + +if (!$client) { + die('Unable to create client'); +} + +$accepted = stream_socket_accept($server, 1); + +if (!$accepted) { + die('Unable to accept connection'); +} + +// Verify server side (accepted connection) +$server_sock = socket_import_stream($accepted); +$server_keepalive = socket_get_option($server_sock, SOL_SOCKET, SO_KEEPALIVE); +echo "Server SO_KEEPALIVE: " . ($server_keepalive ? "enabled" : "disabled") . "\n"; + +if (defined('TCP_KEEPIDLE')) { + $server_idle = socket_get_option($server_sock, SOL_TCP, TCP_KEEPIDLE); + echo "Server TCP_KEEPIDLE: $server_idle\n"; +} else { + $server_idle = socket_get_option($server_sock, SOL_TCP, TCP_KEEPALIVE); + echo "Server TCP_KEEPIDLE: $server_idle\n"; +} + +$server_intvl = socket_get_option($server_sock, SOL_TCP, TCP_KEEPINTVL); +echo "Server TCP_KEEPINTVL: $server_intvl\n"; + +$server_cnt = socket_get_option($server_sock, SOL_TCP, TCP_KEEPCNT); +echo "Server TCP_KEEPCNT: $server_cnt\n"; + +// Verify client side +$client_sock = socket_import_stream($client); +$client_keepalive = socket_get_option($client_sock, SOL_SOCKET, SO_KEEPALIVE); +echo "Client SO_KEEPALIVE: " . ($client_keepalive ? "enabled" : "disabled") . "\n"; + +if (defined('TCP_KEEPIDLE')) { + $client_idle = socket_get_option($client_sock, SOL_TCP, TCP_KEEPIDLE); + echo "Client TCP_KEEPIDLE: $client_idle\n"; +} else { + $client_idle = socket_get_option($client_sock, SOL_TCP, TCP_KEEPALIVE); + echo "Client TCP_KEEPIDLE: $client_idle\n"; +} + +$client_intvl = socket_get_option($client_sock, SOL_TCP, TCP_KEEPINTVL); +echo "Client TCP_KEEPINTVL: $client_intvl\n"; + +$client_cnt = socket_get_option($client_sock, SOL_TCP, TCP_KEEPCNT); +echo "Client TCP_KEEPCNT: $client_cnt\n"; + +fclose($accepted); +fclose($client); +fclose($server); + +// Test server with SO_KEEPALIVE disabled +$server2_context = stream_context_create(['socket' => ['so_keepalive' => false]]); +$server2 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $server2_context); + +$addr2 = stream_socket_get_name($server2, false); +$port2 = (int)substr(strrchr($addr2, ':'), 1); + +$client2 = stream_socket_client("tcp://127.0.0.1:$port2"); +$accepted2 = stream_socket_accept($server2, 1); + +$server2_sock = socket_import_stream($accepted2); +$server2_keepalive = socket_get_option($server2_sock, SOL_SOCKET, SO_KEEPALIVE); +echo "Server disabled SO_KEEPALIVE: " . ($server2_keepalive ? "enabled" : "disabled") . "\n"; + +fclose($accepted2); +fclose($client2); +fclose($server2); + +// Test client with SO_KEEPALIVE disabled +$server3 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN); + +$addr3 = stream_socket_get_name($server3, false); +$port3 = (int)substr(strrchr($addr3, ':'), 1); + +$client3_context = stream_context_create(['socket' => ['so_keepalive' => false]]); +$client3 = stream_socket_client("tcp://127.0.0.1:$port3", $errno, $errstr, 30, + STREAM_CLIENT_CONNECT, $client3_context); + +$client3_sock = socket_import_stream($client3); +$client3_keepalive = socket_get_option($client3_sock, SOL_SOCKET, SO_KEEPALIVE); +echo "Client disabled SO_KEEPALIVE: " . ($client3_keepalive ? "enabled" : "disabled") . "\n"; + +fclose($client3); +fclose($server3); +?> +--EXPECT-- +Server SO_KEEPALIVE: enabled +Server TCP_KEEPIDLE: 60 +Server TCP_KEEPINTVL: 10 +Server TCP_KEEPCNT: 5 +Client SO_KEEPALIVE: enabled +Client TCP_KEEPIDLE: 30 +Client TCP_KEEPINTVL: 5 +Client TCP_KEEPCNT: 3 +Server disabled SO_KEEPALIVE: disabled +Client disabled SO_KEEPALIVE: disabled diff --git a/main/network.c b/main/network.c index 96953531c3761..58f688a4fea67 100644 --- a/main/network.c +++ b/main/network.c @@ -452,9 +452,9 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd, /* Bind to a local IP address. * Returns the bound socket, or -1 on failure. * */ -/* {{{ php_network_bind_socket_to_local_addr */ -php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned port, - int socktype, long sockopts, zend_string **error_string, int *error_code +php_socket_t php_network_bind_socket_to_local_addr_ex(const char *host, unsigned port, + int socktype, long sockopts, php_sockvals *sockvals, zend_string **error_string, + int *error_code ) { int num_addrs, n, err = 0; @@ -533,6 +533,35 @@ php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned po setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&sockoptval, sizeof(sockoptval)); } #endif +#ifdef SO_KEEPALIVE + if (sockopts & STREAM_SOCKOP_SO_KEEPALIVE) { + setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (char*)&sockoptval, sizeof(sockoptval)); + } +#endif + + /* Set socket values if provided */ + if (sockvals != NULL) { +#if defined(TCP_KEEPIDLE) + if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) { + setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); + } +#elif defined(TCP_KEEPALIVE) + /* macOS uses TCP_KEEPALIVE instead of TCP_KEEPIDLE */ + if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) { + setsockopt(sock, IPPROTO_TCP, TCP_KEEPALIVE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); + } +#endif +#ifdef TCP_KEEPINTVL + if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPINTVL) { + setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, (char*)&sockvals->keepalive.keepintvl, sizeof(sockvals->keepalive.keepintvl)); + } +#endif +#ifdef TCP_KEEPCNT + if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPCNT) { + setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt)); + } +#endif + } n = bind(sock, sa, socklen); @@ -560,7 +589,13 @@ php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned po return sock; } -/* }}} */ + +php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned port, + int socktype, long sockopts, zend_string **error_string, int *error_code + ) +{ + return php_network_bind_socket_to_local_addr_ex(host, port, socktype, sockopts, NULL, error_string, error_code); +} PHPAPI zend_result php_network_parse_network_address_with_port(const char *addr, size_t addrlen, struct sockaddr *sa, socklen_t *sl) { @@ -824,11 +859,9 @@ PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, * enable non-blocking mode on the socket. * Returns the connected (or connecting) socket, or -1 on failure. * */ - -/* {{{ php_network_connect_socket_to_host */ -php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short port, +php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned short port, int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string, - int *error_code, const char *bindto, unsigned short bindport, long sockopts + int *error_code, const char *bindto, unsigned short bindport, long sockopts, php_sockvals *sockvals ) { int num_addrs, n, fatal = 0; @@ -952,6 +985,40 @@ php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short } } #endif + +#ifdef SO_KEEPALIVE + { + int val = 1; + if (sockopts & STREAM_SOCKOP_SO_KEEPALIVE) { + setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (char*)&val, sizeof(val)); + } + } +#endif + + /* Set socket values if provided */ + if (sockvals != NULL) { +#if defined(TCP_KEEPIDLE) + if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) { + setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); + } +#elif defined(TCP_KEEPALIVE) + /* macOS uses TCP_KEEPALIVE instead of TCP_KEEPIDLE */ + if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) { + setsockopt(sock, IPPROTO_TCP, TCP_KEEPALIVE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); + } +#endif +#ifdef TCP_KEEPINTVL + if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPINTVL) { + setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, (char*)&sockvals->keepalive.keepintvl, sizeof(sockvals->keepalive.keepintvl)); + } +#endif +#ifdef TCP_KEEPCNT + if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPCNT) { + setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt)); + } +#endif + } + n = php_network_connect_socket(sock, sa, socklen, asynchronous, timeout ? &working_timeout : NULL, error_string, error_code); @@ -998,7 +1065,15 @@ php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short return sock; } -/* }}} */ + +php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short port, + int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string, + int *error_code, const char *bindto, unsigned short bindport, long sockopts + ) +{ + return php_network_connect_socket_to_host_ex(host, port, socktype, asynchronous, timeout, + error_string, error_code, bindto, bindport, sockopts, NULL); +} /* {{{ php_any_addr * Fills any (wildcard) address into php_sockaddr_storage diff --git a/main/php_network.h b/main/php_network.h index 45e1e1902631d..08d6bbc140ce9 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -124,6 +124,7 @@ typedef int php_socket_t; #define STREAM_SOCKOP_IPV6_V6ONLY_ENABLED (1 << 4) #define STREAM_SOCKOP_TCP_NODELAY (1 << 5) #define STREAM_SOCKOP_SO_REUSEADDR (1 << 6) +#define STREAM_SOCKOP_SO_KEEPALIVE (1 << 7) /* uncomment this to debug poll(2) emulation on systems that have poll(2) */ /* #define PHP_USE_POLL_2_EMULATION 1 */ @@ -266,10 +267,28 @@ typedef struct { } php_sockaddr_storage; #endif +#define PHP_SOCKVAL_TCP_KEEPIDLE (1 << 0) +#define PHP_SOCKVAL_TCP_KEEPCNT (1 << 1) +#define PHP_SOCKVAL_TCP_KEEPINTVL (1 << 2) + +typedef struct { + unsigned int mask; + struct { + int keepidle; + int keepcnt; + int keepintvl; + } keepalive; +} php_sockvals; + BEGIN_EXTERN_C() PHPAPI int php_network_getaddresses(const char *host, int socktype, struct sockaddr ***sal, zend_string **error_string); PHPAPI void php_network_freeaddresses(struct sockaddr **sal); +PHPAPI php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned short port, + int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string, + int *error_code, const char *bindto, unsigned short bindport, long sockopts, php_sockvals *sockvals + ); + PHPAPI php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short port, int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string, int *error_code, const char *bindto, unsigned short bindport, long sockopts @@ -286,6 +305,10 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd, #define php_connect_nonb(sock, addr, addrlen, timeout) \ php_network_connect_socket((sock), (addr), (addrlen), 0, (timeout), NULL, NULL) +PHPAPI php_socket_t php_network_bind_socket_to_local_addr_ex(const char *host, unsigned port, + int socktype, long sockopts, php_sockvals *sockvals, zend_string **error_string, int *error_code + ); + PHPAPI php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned port, int socktype, long sockopts, zend_string **error_string, int *error_code ); diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index db35a9b7952c8..969d364ffe1c2 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -16,7 +16,7 @@ #include "php.h" #include "ext/standard/file.h" -#include "streams/php_streams_int.h" +#include "php_streams.h" #include "php_network.h" #if defined(PHP_WIN32) || defined(__riscos__) @@ -48,8 +48,18 @@ static const php_stream_ops php_stream_udp_socket_ops; #ifdef AF_UNIX static const php_stream_ops php_stream_unix_socket_ops; static const php_stream_ops php_stream_unixdg_socket_ops; -#endif +#define PHP_STREAM_XPORT_IS_UNIX_DG(stream) php_stream_is(stream, &php_stream_unixdg_socket_ops) +#define PHP_STREAM_XPORT_IS_UNIX_ST(stream) php_stream_is(stream, &php_stream_unix_socket_ops) +#define PHP_STREAM_XPORT_IS_UNIX(stream) \ + (PHP_STREAM_XPORT_IS_UNIX_DG(stream) || PHP_STREAM_XPORT_IS_UNIX_ST(stream)) +#else +#define PHP_STREAM_XPORT_IS_UNIX_DG(stream) false +#define PHP_STREAM_XPORT_IS_UNIX_STD(stream) false +#define PHP_STREAM_XPORT_IS_UNIX(stream) false +#endif +#define PHP_STREAM_XPORT_IS_UDP(stream) (php_stream_is(stream, &php_stream_udp_socket_ops)) +#define PHP_STREAM_XPORT_IS_TCP(stream) (!PHP_STREAM_XPORT_IS_UNIX(stream) && !PHP_STREAM_XPORT_IS_UDP(stream)) static int php_tcp_sockop_set_option(php_stream *stream, int option, int value, void *ptrparam); @@ -669,18 +679,19 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t * int portno, err; long sockopts = STREAM_SOCKOP_NONE; zval *tmpzval = NULL; + php_sockvals sockvals = {0}; #ifdef AF_UNIX - if (stream->ops == &php_stream_unix_socket_ops || stream->ops == &php_stream_unixdg_socket_ops) { + if (PHP_STREAM_XPORT_IS_UNIX(stream)) { struct sockaddr_un unix_addr; - sock->socket = socket(PF_UNIX, stream->ops == &php_stream_unix_socket_ops ? SOCK_STREAM : SOCK_DGRAM, 0); + sock->socket = socket(PF_UNIX, PHP_STREAM_XPORT_IS_UNIX_ST(stream) ? SOCK_STREAM : SOCK_DGRAM, 0); if (sock->socket == SOCK_ERR) { if (xparam->want_errortext) { char errstr[256]; xparam->outputs.error_text = strpprintf(0, "Failed to create unix%s socket %s", - stream->ops == &php_stream_unix_socket_ops ? "" : "datagram", + PHP_STREAM_XPORT_IS_UNIX_ST(stream) ? "" : " datagram", php_socket_strerror_s(errno, errstr, sizeof(errstr))); } return -1; @@ -729,7 +740,7 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t * #endif #ifdef SO_BROADCAST - if (stream->ops == &php_stream_udp_socket_ops /* SO_BROADCAST is only applicable for UDP */ + if (PHP_STREAM_XPORT_IS_UDP(stream) /* SO_BROADCAST is only applicable for UDP */ && PHP_STREAM_CONTEXT(stream) && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_broadcast")) != NULL && zend_is_true(tmpzval) @@ -738,9 +749,53 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t * } #endif - sock->socket = php_network_bind_socket_to_local_addr(host, portno, - stream->ops == &php_stream_udp_socket_ops ? SOCK_DGRAM : SOCK_STREAM, +#ifdef SO_KEEPALIVE + if (PHP_STREAM_XPORT_IS_TCP(stream) /* SO_KEEPALIVE is only applicable for TCP */ + && PHP_STREAM_CONTEXT(stream) + && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_keepalive")) != NULL + && zend_is_true(tmpzval) + ) { + sockopts |= STREAM_SOCKOP_SO_KEEPALIVE; + } +#endif + + /* Parse TCP keepalive parameters - only for TCP streams */ + if (PHP_STREAM_XPORT_IS_TCP(stream)) { +#if defined(TCP_KEEPIDLE) || defined(TCP_KEEPALIVE) + if (PHP_STREAM_CONTEXT(stream) + && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_keepidle")) != NULL + && Z_TYPE_P(tmpzval) == IS_LONG + ) { + sockvals.mask |= PHP_SOCKVAL_TCP_KEEPIDLE; + sockvals.keepalive.keepidle = (int)Z_LVAL_P(tmpzval); + } +#endif + +#ifdef TCP_KEEPINTVL + if (PHP_STREAM_CONTEXT(stream) + && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_keepintvl")) != NULL + && Z_TYPE_P(tmpzval) == IS_LONG + ) { + sockvals.mask |= PHP_SOCKVAL_TCP_KEEPINTVL; + sockvals.keepalive.keepintvl = (int)Z_LVAL_P(tmpzval); + } +#endif + +#ifdef TCP_KEEPCNT + if (PHP_STREAM_CONTEXT(stream) + && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_keepcnt")) != NULL + && Z_TYPE_P(tmpzval) == IS_LONG + ) { + sockvals.mask |= PHP_SOCKVAL_TCP_KEEPCNT; + sockvals.keepalive.keepcnt = (int)Z_LVAL_P(tmpzval); + } +#endif + } + + sock->socket = php_network_bind_socket_to_local_addr_ex(host, portno, + PHP_STREAM_XPORT_IS_UDP(stream) ? SOCK_DGRAM : SOCK_STREAM, sockopts, + sockvals.mask ? &sockvals : NULL, xparam->want_errortext ? &xparam->outputs.error_text : NULL, &err ); @@ -761,12 +816,13 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ int ret; zval *tmpzval = NULL; long sockopts = STREAM_SOCKOP_NONE; + php_sockvals sockvals = {0}; #ifdef AF_UNIX - if (stream->ops == &php_stream_unix_socket_ops || stream->ops == &php_stream_unixdg_socket_ops) { + if (PHP_STREAM_XPORT_IS_UNIX(stream)) { struct sockaddr_un unix_addr; - sock->socket = socket(PF_UNIX, stream->ops == &php_stream_unix_socket_ops ? SOCK_STREAM : SOCK_DGRAM, 0); + sock->socket = socket(PF_UNIX, PHP_STREAM_XPORT_IS_UNIX_ST(stream) ? SOCK_STREAM : SOCK_DGRAM, 0); if (sock->socket == SOCK_ERR) { if (xparam->want_errortext) { @@ -807,7 +863,7 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ } #ifdef SO_BROADCAST - if (stream->ops == &php_stream_udp_socket_ops /* SO_BROADCAST is only applicable for UDP */ + if (PHP_STREAM_XPORT_IS_UDP(stream) /* SO_BROADCAST is only applicable for UDP */ && PHP_STREAM_CONTEXT(stream) && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_broadcast")) != NULL && zend_is_true(tmpzval) @@ -816,11 +872,7 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ } #endif - if (stream->ops != &php_stream_udp_socket_ops /* TCP_NODELAY is only applicable for TCP */ -#ifdef AF_UNIX - && stream->ops != &php_stream_unix_socket_ops - && stream->ops != &php_stream_unixdg_socket_ops -#endif + if (PHP_STREAM_XPORT_IS_TCP(stream) /* TCP_NODELAY is only applicable for TCP */ && PHP_STREAM_CONTEXT(stream) && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_nodelay")) != NULL && zend_is_true(tmpzval) @@ -828,19 +880,63 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ sockopts |= STREAM_SOCKOP_TCP_NODELAY; } +#ifdef SO_KEEPALIVE + if (PHP_STREAM_XPORT_IS_TCP(stream) /* SO_KEEPALIVE is only applicable for TCP */ + && PHP_STREAM_CONTEXT(stream) + && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_keepalive")) != NULL + && zend_is_true(tmpzval) + ) { + sockopts |= STREAM_SOCKOP_SO_KEEPALIVE; + } +#endif + + /* Parse TCP keepalive parameters - only for TCP streams */ + if (PHP_STREAM_XPORT_IS_TCP(stream)) { +#if defined(TCP_KEEPIDLE) || defined(TCP_KEEPALIVE) + if (PHP_STREAM_CONTEXT(stream) + && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_keepidle")) != NULL + && Z_TYPE_P(tmpzval) == IS_LONG + ) { + sockvals.mask |= PHP_SOCKVAL_TCP_KEEPIDLE; + sockvals.keepalive.keepidle = (int)Z_LVAL_P(tmpzval); + } +#endif + +#ifdef TCP_KEEPINTVL + if (PHP_STREAM_CONTEXT(stream) + && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_keepintvl")) != NULL + && Z_TYPE_P(tmpzval) == IS_LONG + ) { + sockvals.mask |= PHP_SOCKVAL_TCP_KEEPINTVL; + sockvals.keepalive.keepintvl = (int)Z_LVAL_P(tmpzval); + } +#endif + +#ifdef TCP_KEEPCNT + if (PHP_STREAM_CONTEXT(stream) + && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_keepcnt")) != NULL + && Z_TYPE_P(tmpzval) == IS_LONG + ) { + sockvals.mask |= PHP_SOCKVAL_TCP_KEEPCNT; + sockvals.keepalive.keepcnt = (int)Z_LVAL_P(tmpzval); + } +#endif + } + /* Note: the test here for php_stream_udp_socket_ops is important, because we * want the default to be TCP sockets so that the openssl extension can * re-use this code. */ - sock->socket = php_network_connect_socket_to_host(host, portno, - stream->ops == &php_stream_udp_socket_ops ? SOCK_DGRAM : SOCK_STREAM, + sock->socket = php_network_connect_socket_to_host_ex(host, portno, + PHP_STREAM_XPORT_IS_UDP(stream) ? SOCK_DGRAM : SOCK_STREAM, xparam->op == STREAM_XPORT_OP_CONNECT_ASYNC, xparam->inputs.timeout, xparam->want_errortext ? &xparam->outputs.error_text : NULL, &err, bindto, bindport, - sockopts + sockopts, + sockvals.mask ? &sockvals : NULL ); ret = sock->socket == -1 ? -1 : 0; From 0fd8aae6e851b94123288ea67726ea68622c0c17 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 30 Dec 2025 16:53:22 +0100 Subject: [PATCH 2/2] Fix TCP_KEEPALIVE no inheriting for accepted sockets on MacOS --- ext/openssl/xp_ssl.c | 21 ++++++++++++++------- main/network.c | 40 +++++++++++++++++++++++++++++++++++----- main/php_network.h | 20 +++++++++++++++++--- main/streams/xp_socket.c | 21 ++++++++++++++------- 4 files changed, 80 insertions(+), 22 deletions(-) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 62929246a07f0..482715c745554 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -2219,25 +2219,32 @@ static int php_openssl_sockop_stat(php_stream *stream, php_stream_statbuf *ssb) static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_netstream_data_t *sock, php_stream_xport_param *xparam STREAMS_DC) /* {{{ */ { - bool nodelay = false; + php_sockvals sockvals = {0}; zval *tmpzval = NULL; xparam->outputs.client = NULL; - if (PHP_STREAM_CONTEXT(stream) && - (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_nodelay")) != NULL && - zend_is_true(tmpzval)) { - nodelay = true; + if (PHP_STREAM_CONTEXT(stream)) { + tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_nodelay"); + if (tmpzval != NULL && zend_is_true(tmpzval)) { + sockvals.mask |= PHP_SOCKVAL_TCP_NODELAY; + sockvals.tcp_nodelay = 1; + } + tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_keepidle"); + if (tmpzval != NULL && Z_TYPE_P(tmpzval) == IS_LONG) { + sockvals.mask |= PHP_SOCKVAL_TCP_KEEPIDLE; + sockvals.keepalive.keepidle = Z_LVAL_P(tmpzval); + } } - php_socket_t clisock = php_network_accept_incoming(sock->s.socket, + php_socket_t clisock = php_network_accept_incoming_ex(sock->s.socket, xparam->want_textaddr ? &xparam->outputs.textaddr : NULL, xparam->want_addr ? &xparam->outputs.addr : NULL, xparam->want_addr ? &xparam->outputs.addrlen : NULL, xparam->inputs.timeout, xparam->want_errortext ? &xparam->outputs.error_text : NULL, &xparam->outputs.error_code, - nodelay); + &sockvals); if (clisock != SOCK_ERR) { php_openssl_netstream_data_t *clisockdata = (php_openssl_netstream_data_t*) emalloc(sizeof(*clisockdata)); diff --git a/main/network.c b/main/network.c index 58f688a4fea67..c4e2ab2e67daa 100644 --- a/main/network.c +++ b/main/network.c @@ -801,15 +801,22 @@ PHPAPI int php_network_get_sock_name(php_socket_t sock, * version of the address will be emalloc'd and returned. * */ -/* {{{ php_network_accept_incoming */ -PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, + /* Accept a client connection from a server socket, + * using an optional timeout. + * Returns the peer address in addr/addrlen (it will emalloc + * these, so be sure to efree the result). + * If you specify textaddr, a text-printable + * version of the address will be emalloc'd and returned. + * */ + +PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock, zend_string **textaddr, struct sockaddr **addr, socklen_t *addrlen, struct timeval *timeout, zend_string **error_string, int *error_code, - int tcp_nodelay + php_sockvals *sockvals ) { php_socket_t clisock = -1; @@ -833,11 +840,19 @@ PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, textaddr, addr, addrlen ); - if (tcp_nodelay) { #ifdef TCP_NODELAY + if (PHP_SOCKVAL_IS_SET(sockvals, PHP_SOCKVAL_TCP_NODELAY)) { + int tcp_nodelay = 1; setsockopt(clisock, IPPROTO_TCP, TCP_NODELAY, (char*)&tcp_nodelay, sizeof(tcp_nodelay)); + } #endif +#ifdef TCP_KEEPALIVE + /* MacOS does not inherit TCP_KEEPALIVE so it needs to be set */ + if (PHP_SOCKVAL_IS_SET(sockvals, PHP_SOCKVAL_TCP_KEEPIDLE)) { + setsockopt(clisock, IPPROTO_TCP, TCP_KEEPALIVE, + (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); } +#endif } else { error = php_socket_errno(); } @@ -852,7 +867,22 @@ PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, return clisock; } -/* }}} */ + +PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, + zend_string **textaddr, + struct sockaddr **addr, + socklen_t *addrlen, + struct timeval *timeout, + zend_string **error_string, + int *error_code, + int tcp_nodelay + ) +{ + php_sockvals sockvals = { .mask = tcp_nodelay ? PHP_SOCKVAL_TCP_NODELAY : 0 }; + + return php_network_accept_incoming_ex(srvsock, textaddr, addr, addrlen, timeout, error_string, + error_code, &sockvals); +} /* Connect to a remote host using an interruptible connect with optional timeout. * Optionally, the connect can be made asynchronously, which will implicitly diff --git a/main/php_network.h b/main/php_network.h index 08d6bbc140ce9..db449ae35ffec 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -267,12 +267,16 @@ typedef struct { } php_sockaddr_storage; #endif -#define PHP_SOCKVAL_TCP_KEEPIDLE (1 << 0) -#define PHP_SOCKVAL_TCP_KEEPCNT (1 << 1) -#define PHP_SOCKVAL_TCP_KEEPINTVL (1 << 2) +#define PHP_SOCKVAL_TCP_NODELAY (1 << 0) +#define PHP_SOCKVAL_TCP_KEEPIDLE (1 << 1) +#define PHP_SOCKVAL_TCP_KEEPCNT (1 << 2) +#define PHP_SOCKVAL_TCP_KEEPINTVL (1 << 3) + +#define PHP_SOCKVAL_IS_SET(sockvals, opt) (sockvals->mask & opt) typedef struct { unsigned int mask; + int tcp_nodelay; struct { int keepidle; int keepcnt; @@ -313,6 +317,16 @@ PHPAPI php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsi int socktype, long sockopts, zend_string **error_string, int *error_code ); +PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock, + zend_string **textaddr, + struct sockaddr **addr, + socklen_t *addrlen, + struct timeval *timeout, + zend_string **error_string, + int *error_code, + php_sockvals *sockvals + ); + PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, zend_string **textaddr, struct sockaddr **addr, diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index 969d364ffe1c2..7a7f007f9183d 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -964,25 +964,32 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ static inline int php_tcp_sockop_accept(php_stream *stream, php_netstream_data_t *sock, php_stream_xport_param *xparam STREAMS_DC) { - bool nodelay = 0; + php_sockvals sockvals = {0}; zval *tmpzval = NULL; xparam->outputs.client = NULL; - if ((NULL != PHP_STREAM_CONTEXT(stream)) && - (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_nodelay")) != NULL && - zend_is_true(tmpzval)) { - nodelay = 1; + if (PHP_STREAM_CONTEXT(stream)) { + tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_nodelay"); + if (tmpzval != NULL && zend_is_true(tmpzval)) { + sockvals.mask |= PHP_SOCKVAL_TCP_NODELAY; + sockvals.tcp_nodelay = 1; + } + tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_keepidle"); + if (tmpzval != NULL && Z_TYPE_P(tmpzval) == IS_LONG) { + sockvals.mask |= PHP_SOCKVAL_TCP_KEEPIDLE; + sockvals.keepalive.keepidle = Z_LVAL_P(tmpzval); + } } - php_socket_t clisock = php_network_accept_incoming(sock->socket, + php_socket_t clisock = php_network_accept_incoming_ex(sock->socket, xparam->want_textaddr ? &xparam->outputs.textaddr : NULL, xparam->want_addr ? &xparam->outputs.addr : NULL, xparam->want_addr ? &xparam->outputs.addrlen : NULL, xparam->inputs.timeout, xparam->want_errortext ? &xparam->outputs.error_text : NULL, &xparam->outputs.error_code, - nodelay); + &sockvals); if (clisock != SOCK_ERR) { php_netstream_data_t *clisockdata = (php_netstream_data_t*) emalloc(sizeof(*clisockdata));