Skip to content

Add keepalive support#95

Merged
rlerdorf merged 4 commits intomasterfrom
feature/keepalive-support
Apr 4, 2026
Merged

Add keepalive support#95
rlerdorf merged 4 commits intomasterfrom
feature/keepalive-support

Conversation

@rlerdorf
Copy link
Copy Markdown
Member

@rlerdorf rlerdorf commented Apr 4, 2026

Add SSH keepalive support via libssh2

Closes #79 (replaces the approach, not the code)

Problem

Long-lived SSH connections can be silently dropped by firewalls or NAT
devices when idle. There was no way to configure keepalives from PHP.

PR #79 proposed a global php.ini setting using raw SO_KEEPALIVE
socket options. That approach is Linux-only, and applies to all connections.

Fix

Expose the existing libssh2 keepalive API as two new per-connection
functions:

ssh2_keepalive_config(resource $session, bool $want_reply, int $interval)

Configures how often keepalive messages are sent. $interval is the
number of seconds that can pass without any I/O before a keepalive is
sent. Use 0 (the default) to disable. $want_reply controls whether
the server is expected to respond.

ssh2_keepalive_send(resource $session): int|false

Sends a keepalive if needed. Returns the number of seconds you can wait
before calling it again, or false on error.

Example

$ssh = ssh2_connect('example.com', 22);
ssh2_auth_password($ssh, 'user', 'pass');

// Send keepalive every 30 seconds of inactivity
ssh2_keepalive_config($ssh, true, 30);

// In a long-running loop
$seconds = ssh2_keepalive_send($ssh);
sleep($seconds);

Why this approach

  • Uses libssh2_keepalive_config() and libssh2_keepalive_send(),
    which work cross-platform (Linux, macOS, Windows)
  • Per-connection, not a global setting
  • Follows the same pattern as ssh2_set_timeout()
  • Feature-detected in config.m4 via PHP_CHECK_LIBRARY

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds per-connection SSH keepalive support by exposing libssh2’s keepalive API to PHP, preventing idle SSH sessions from being silently dropped.

Changes:

  • Introduces ssh2_keepalive_config() and ssh2_keepalive_send() in the extension.
  • Adds a PHPT covering basic keepalive configuration/send/disable behavior.
  • Feature-detects libssh2 keepalive support at configure time (PHP_SSH2_KEEPALIVE).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
ssh2.c Implements and registers the new keepalive functions and their arginfo.
config.m4 Adds a configure-time check for libssh2 keepalive symbols and defines PHP_SSH2_KEEPALIVE.
tests/ssh2_keepalive.phpt Adds a basic functional test for keepalive config/send behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@rlerdorf rlerdorf merged commit 77685aa into master Apr 4, 2026
20 checks passed
@ormus112
Copy link
Copy Markdown

This is not a very good solution to the problem, since it requires fixing the existing PHP code to enable SO_KEEPALIVE in ssh2 connections.

It seems to me that both options are necessary - both enabling SO_KEEPALIVE for a specific connection via PHP code, and global enabling for all ssh2 connections via the config on the DevOps/SRE side.

@rlerdorf
Copy link
Copy Markdown
Member Author

This is not a very good solution to the problem, since it requires fixing the existing PHP code to enable SO_KEEPALIVE in ssh2 connections.

It seems to me that both options are necessary - both enabling SO_KEEPALIVE for a specific connection via PHP code, and global enabling for all ssh2 connections via the config on the DevOps/SRE side.

You don't need SO_KEEPALIVE at the TCP level with SSH keepalive enabled since SSH keepalive generates traffic on the socket. Architecturally you want this at the SSH level because it gives you a stronger guarantee that the ssh session is still alive which in turn implies the TCP socket is still alive. With SO_KEEPALIVE you only know the socket is alive. Plus doing it at the application protocol layer makes it portable across all platforms.

@rlerdorf
Copy link
Copy Markdown
Member Author

This is not a very good solution to the problem, since it requires fixing the existing PHP code to enable SO_KEEPALIVE in ssh2 connections.
It seems to me that both options are necessary - both enabling SO_KEEPALIVE for a specific connection via PHP code, and global enabling for all ssh2 connections via the config on the DevOps/SRE side.

You don't need SO_KEEPALIVE at the TCP level with SSH keepalive enabled since SSH keepalive generates traffic on the socket. Architecturally you want this at the SSH level because it gives you a stronger guarantee that the ssh session is still alive which in turn implies the TCP socket is still alive. With SO_KEEPALIVE you only know the socket is alive. Plus doing it at the application protocol layer makes it portable across all platforms.

Although, having said that, it might not be a bad idea to allow a stream context. It still wouldn't really make sense to set SO_KEEPALIVE in the stream context for the same reason, but I could see needing to set bindto, for example.

@ormus112
Copy link
Copy Markdown

ormus112 commented Apr 11, 2026

  1. In my case, no PHP code-level solutions could handle "stuck SSH connection" errors. The PHP process simply "freezes" until a forced reboot. This isn't the first time I've encountered this, apparently somewhere within the SSH2 module or PHP, this error isn't handled correctly. Most likely, keepalive at the SSH level will also work incorrectly. This needs to be tested.
    SO_KEEPALIVE work on kernel level and work correctly in this situation.

  2. Unsupported legacy PHP code that's difficult to fix a major problem in the industry. A way to force enable ssh2 keepalive this at the sysadmin level (in php.ini) is highly needed.

It's also worth mentioning that kernel sysctl parameters provide more flexible packet management capabilities, which is relevant for a large number of SSH connections... But this is not as critical as the first two points.

I just wanted to point out that the problem is much deeper than it seems at first glance.
Anyway, thank you for your time.

@rlerdorf
Copy link
Copy Markdown
Member Author

  1. In my case, no PHP code-level solutions could handle "stuck SSH connection" errors. The PHP process simply "freezes" until a forced reboot. This isn't the first time I've encountered this, apparently somewhere within the SSH2 module or PHP, this error isn't handled correctly. Most likely, keepalive at the SSH level will also work incorrectly. This needs to be tested.
    SO_KEEPALIVE work on kernel level and work correctly in this situation.
  2. Unsupported legacy PHP code that's difficult to fix a major problem in the industry. A way to force enable ssh2 keepalive this at the admin level (in php.ini) is highly needed.

It's also worth mentioning that kernel sysctl parameters provide more flexible packet management capabilities, which is relevant for a large number of SSH connections... But this is not as critical as the first two points.

I just wanted to point out that the problem is much deeper than it seems at first glance. Anyway, thank you for your time.

That sounds like a different issue though. If ssh connections are getting stuck, keepalive isn't going to solve that. Do you have a series of steps to reproduce that state?

Also, I've had firewalls drop empty ACK packets on me which is what SO_KEEPALIVE uses, so again in that case you want the keepalive at the application layer.

@ormus112
Copy link
Copy Markdown

ormus112 commented Apr 11, 2026

That sounds like a different issue though. If ssh connections are getting stuck, keepalive isn't going to solve that. Do you have a series of steps to reproduce that state?

There's no way to limit the response time when executing a command on a remote server over the ssh. Even max_execution_time and set_time_limit is ignored.

<?php
ini_set('max_execution_time', 15);
set_time_limit(15);

$connection = ssh2_connect('1.2.3.4', 22);

if (ssh2_auth_pubkey_file($connection, 'root','/var/www/keys/id_rsa.pub','/var/www/keys/id_rsa'))  {
    echo "Public Key Authentication Successful\n";
    }
    else {
    die('Public Key Authentication Failed');
    }

ssh2_exec($connection, '/usr/bin/sleep 30000');
echo "all ok";
?>

This code will run forever (30000 seconds) and should fail after 15 seconds.

Accordingly, if the connection to the remote server is lost while waiting for a command response via ssh, PHP freezes completely until the socket is forcibly closed (for example, by the kernel, if using SO_KEEPALIVE).

And this is only the top level of the problem - we need a way to set the maximum response time separately from the overall execution time of the PHP code.

@rlerdorf
Copy link
Copy Markdown
Member Author

That sounds like a different issue though. If ssh connections are getting stuck, keepalive isn't going to solve that. Do you have a series of steps to reproduce that state?

There's no way to limit the response time when executing a command on a remote server over the ssh. Even max_execution_time and set_time_limit is ignored.

<?php
ini_set('max_execution_time', 15);
set_time_limit(15);

$connection = ssh2_connect('1.2.3.4', 22);

if (ssh2_auth_pubkey_file($connection, 'root','/var/www/keys/id_rsa.pub','/var/www/keys/id_rsa'))  {
    echo "Public Key Authentication Successful\n";
    }
    else {
    die('Public Key Authentication Failed');
    }

ssh2_exec($connection, '/usr/bin/sleep 30000');
echo "all ok";
?>

This code will run forever (30000 seconds) and should fail after 15 seconds.

Accordingly, if the connection to the remote server is lost while waiting for a command response via ssh, PHP freezes completely until the socket is forcibly closed (for example, by the kernel, if using SO_KEEPALIVE).

And this is only the top level of the problem - we need a way to set the maximum response time separately from the overall execution time of the PHP code.

Ah, that is definitely a different problem. And no level of keepalive would fix this since the connection is still very much alive.
I think what is happening is that libssh2_channel_close() blocks waiting for the remote end to send SSH_MSG_CHANNEL_CLOSE
which won't be sent until the remote command finishes. ssh2_set_timeout is only applied around explicit stream reads/writes,
not during shutdown. There is probably a way to add a timeout on the libssh2_channel_close() or perhaps even lower level at
the socket level with a SO_RCVTIMEO to timeout that recv we are blocked on.

@ormus112
Copy link
Copy Markdown

ormus112 commented Apr 11, 2026

And no level of keepalive would fix this since the connection is still very much alive.

If the connection to the remote server is lost while waiting for a command response via ssh, PHP freezes completely until the socket is forcibly closed (for example, by the kernel, if using SO_KEEPALIVE).

TСP does not check that the other side alive if the send buffers on both sides are empty (except for the SO_KEEPALIVE, of course).

Perhaps a check at the ssh keepalive level will determine this, but perhaps not - testing is necessary.

@rlerdorf
Copy link
Copy Markdown
Member Author

And no level of keepalive would fix this since the connection is still very much alive.

If the connection to the remote server is lost while waiting for a command response via ssh, PHP freezes completely until the socket is forcibly closed (for example, by the kernel, if using SO_KEEPALIVE). TСP does not check that the other side alive if the send buffers on both sides are empty (except for the SO_KEEPALIVE, of course).

Perhaps a check at the ssh keepalive level will determine this, but perhaps not - testing is necessary.

ssh keepalive should help a bit because libssh2's internal blocking loop calls libssh2_keepalive_send() and keepalive messages are still sent while blocked on libssh2_channel_close().
TCP retransmissions will eventually fail and the socket should error out because now there is actually traffic on the socket. There is nothing to detect a dead socket when neither side is
sending anything.

But, I don't think that is the right way to fix this problem even though it would help. Applying the session timeout to the close code path is a much cleaner and more direct way to handle it than waiting on retransmissions to fail on keepalives.

@rlerdorf
Copy link
Copy Markdown
Member Author

@ormus112 try pr #97
It works for me using your reproducer

@ormus112
Copy link
Copy Markdown

ormus112 commented Apr 11, 2026

I don't see any don't see any reason why keepalive at the ssh level is better than SO_KEEPALIVE at the tcp level:

  1. SO_KEEPALIVE - more unixway solution than ssh keepalive. Our task is to determine the availability of the remote server; going up to the application level from network level for this is unnecessary.
  2. SO_KEEPALIVE is a more flexible solution that allows for more flexible settings (net.ipv4.tcp_keepalive_time, net.ipv4.tcp_keepalive_intvl, net.ipv4.tcp_keepalive_probes), which is important in high traffic situations. Ssh keepalive requires redefining the global TCP retransmission settings for fine-tuning. This is not always possible.
  3. SO_KEEPALIVE is implemented at the kernel level long time ago and is more productive, debugged and understandable than libssh2, php and other user software.

Portability issues don't seem that important to me: they're critical and important for highload. On Windows and MacOS, this isn't really necessary. You just need to build if (Linux || freebsd ) { in the configure.

The only thing my solution would need is the ability to enable SO_KEEPALIVE on a specific connection, rather than globally on all php.ssh2 connections... I might do that someday, although it's not required in my current case.

I'll probably stick with so_keepalive - is better in my case.

Also, global activation of keepalive via config is critical for me, and your solution requires php code editing.

Anyway, thank you for your time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants