Commit 9582d8e6d74 for php.net

commit 9582d8e6d746befac8b3aa84a2cb503d64030716
Author: Jakub Zelenka <bukka@php.net>
Date:   Mon Nov 3 22:22:01 2025 +0100

    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

diff --git a/NEWS b/NEWS
index 12740c65c3c..3b20cbb026c 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 d52827bf961..f9e50623519 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 00000000000..a437d16694b
--- /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--
+<?php
+if (!defined('TCP_KEEPIDLE') && !defined('TCP_KEEPALIVE')) {
+    die('skip TCP_KEEPIDLE/TCP_KEEPALIVE not available');
+}
+if (!defined('TCP_KEEPINTVL')) {
+    die('skip TCP_KEEPINTVL not available');
+}
+if (!defined('TCP_KEEPCNT')) {
+    die('skip TCP_KEEPCNT not available');
+}
+?>
+--FILE--
+<?php
+// Test server with SO_KEEPALIVE enabled
+$server_context = stream_context_create([
+    'socket' => [
+        '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 96953531c37..58f688a4fea 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 45e1e190263..08d6bbc140c 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 db35a9b7952..969d364ffe1 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;