williameggers / reactphp-ssh-client
SSH client implementation for ReactPHP
Package info
github.com/williameggers/reactphp-ssh-client
pkg:composer/williameggers/reactphp-ssh-client
Requires
- php: >=8.2
- ext-mbstring: *
- ext-openssl: *
- ext-sodium: *
- evenement/evenement: ^3.0 || ^2.0 || ^1.0
- phpseclib/phpseclib: ^3.0
- psr/log: ^3.0
- react/event-loop: ^1.2
- react/promise: ^3.2
- react/promise-stream: ^1.7.0
- react/promise-timer: ^1.11.0
- react/socket: ^1.16
Requires (Dev)
- ext-pcntl: *
- friendsofphp/php-cs-fixer: ^3.75
- pestphp/pest: ^3.8.2
- pestphp/pest-plugin-type-coverage: ^3.5
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^9.6 || ^7.5 || ^11.5
- rector/rector: ^2.1
- williameggers/reactphp-ssh-server: ^1.1
This package is auto-updated.
Last update: 2026-05-11 13:33:32 UTC
README
This project is an event-driven, standalone SSH client implementation for ReactPHP developed by William Eggers. It provides a small asynchronous API for connecting to SSH servers, authenticating, opening shell sessions, executing remote commands, and forwarding TCP connections with forwardOut() and forwardIn().
Looking for an SSH server too? Check out williameggers/reactphp-ssh-server or WhispPHP/whisp.
Overview
-
Implements core SSH client functionality, including transport negotiation, host key validation, authentication, and session channel management
-
Built on a fully asynchronous, non-blocking architecture using ReactPHP for high concurrency and integration into event-driven applications
-
Supports password, public key, and keyboard-interactive authentication flows
-
Provides simple APIs for running remote commands, opening interactive shell sessions, opening outbound TCP streams with
forwardOut(), and requesting remote TCP forwarding withforwardIn() -
Exposes host key information during connection setup so applications can enforce their own trust policy
-
Supports widely-used encryption modes such as Galois/Counter Mode (GCM) and Counter Mode (CTR), along with modern host key algorithms including
ssh-ed25519 -
Designed for testing, development, automation, and internal tooling. It is not security-audited. See disclaimer.
-
Lightweight and dependency-friendly, making it suitable for embedding into existing ReactPHP applications and service tooling
Table of Contents
- Disclaimer
- Installation
- Quickstart example
- Examples
- Client usage
- Authentication
- Host key verification
- Configuration values
- Error handling
- Supported algorithms
- Unsupported features
- Contributions
- License
- Support and Credits
Disclaimer
This project is intended for educational use, testing, internal tooling, and automation scenarios. It has not been security-audited and should not be treated as a drop-in replacement for hardened SSH tooling without careful review.
In particular, applications using this library remain responsible for safe credential handling and host key verification. Examples that accept any host key are for demonstration purposes only.
If you need a mature, security-audited SSH client for general-purpose interactive system access, you should evaluate established tools such as OpenSSH.
In no event shall the authors of reactphp-ssh-client be liable for anything that happens while using this library. Please read the license for the full disclaimer.
Installation
Install via Composer:
composer require williameggers/reactphp-ssh-client
This library requires PHP 8.2 or higher and the following PHP extensions:
-
ext-sodium - for cryptographic operations such as Curve25519 and Ed25519 support
-
ext-mbstring - for multibyte string handling
-
ext-openssl - for RSA and AES encryption support
Ensure these extensions are enabled in your environment before installing the package.
Quickstart example
Here is a minimal example that connects with password authentication and runs a command:
use React\EventLoop\Loop; use WilliamEggers\React\SSHClient\Authentication\PasswordAuthentication; use WilliamEggers\React\SSHClient\Client; use WilliamEggers\React\SSHClient\Connector; use WilliamEggers\React\SSHClient\HostKey\CallbackHostKeyVerifier; use WilliamEggers\React\SSHClient\Process; $connector = new Connector(); $connector->connect( '127.0.0.1:2222', new PasswordAuthentication('test', 'abc123'), // Demo-only verifier. See the host key verification section below for a safer pattern. new CallbackHostKeyVerifier(static fn (): bool => true), )->then( function (Client $client): void { $client->exec('whoami')->then( function (Process $process) use ($client): void { $process->stdout()->on('data', static function (string $data): void { fwrite(STDOUT, $data); }); $process->stderr()->on('data', static function (string $data): void { fwrite(STDERR, $data); }); $process->join()->then(static function (int $exitCode) use ($client): void { fwrite(STDERR, sprintf("\nProcess exited with code %d\n", $exitCode)); $client->close(); Loop::stop(); }); } ); }, static function (Throwable $throwable): void { fwrite(STDERR, 'SSH connection failed: ' . $throwable->getMessage() . PHP_EOL); Loop::stop(); } ); Loop::run();
See also the examples directory.
Examples
The examples directory contains small standalone scripts for common use cases:
quickstart.php- connect with password auth and run a commandquickstart-with-logging.php- same flow with inline PSR-3 logging outputexec.php- run a command with environment variables and stream stdout/stderrshell.php- open an interactive shell sessionforward-in.php- request remote TCP forwarding and accept inboundforwarded-tcpipchannelsforward-out.php- open a direct-tcpip stream and send a raw HTTP requestforward-out-pipe.php- pipe a request stream through a direct-tcpip channelkeyboard-interactive.php- authenticate with keyboard-interactive promptspublic-key-authentication.php- authenticate with a private key from diskhost-key-verification.php- verify a server fingerprint before continuingtest-server.php- launch a local SSH server for exercising the client examples
Each example can be run directly with:
php examples/<file>.php
You can launch a matching SSH test server from this repository:
php examples/test-server.php
This example relies on the dev dependency williameggers/reactphp-ssh-server.
With the test server running, these examples work against its default configuration:
php examples/quickstart.php php examples/exec.php php examples/shell.php php examples/forward-in.php php examples/forward-out.php php examples/forward-out-pipe.php php examples/keyboard-interactive.php
To exercise forward-in.php, start the test server, run the example, and then connect to the forwarded port from another terminal:
php examples/forward-in.php curl -i http://127.0.0.1:8000
To enable public-key-authentication.php, start the server with an OpenSSH-format public key:
SSH_AUTHORIZED_KEY="$(cat /path/to/id_ed25519.pub)" php examples/test-server.php
SSH_PRIVATE_KEY=/path/to/id_ed25519 php examples/public-key-authentication.php
Client usage
Connector
The Connector is the main entry point for opening SSH connections.
It accepts an optional PSR-3 logger, an optional full connection timeout, and an optional underlying React\Socket\ConnectorInterface in its constructor, and exposes a single connect() method:
-
__construct(?LoggerInterface $logger = null, ?float $timeout = 30.0, ?React\Socket\ConnectorInterface $socketConnector = null)
-
connect(string $uri, AuthenticationInterface $authentication, HostKeyVerifierInterface $hostKeyVerifier): PromiseInterface
The timeout value applies to the full SSH connection flow, including:
-
TCP connect
-
SSH version exchange
-
key exchange
-
authentication
Pass null to disable the full connection timeout entirely. Timeout values must be greater than 0 when provided.
If provided, socketConnector is used for the raw TCP dial step before the SSH handshake begins. This allows composing the SSH client with React\Socket\ConnectorInterface-compatible decorators such as custom DNS, timeout, proxy, retry, or observability wrappers.
The optional socketConnector applies only to establishing the underlying socket connection. The timeout constructor argument still applies to the full SSH connection lifecycle.
The URI should be a host and port combination such as:
'127.0.0.1:22' 'example.com:22' '[::1]:22'
Example:
$connector = new WilliamEggers\React\SSHClient\Connector(timeout: 30.0); $connector->connect( 'example.com:22', new WilliamEggers\React\SSHClient\Authentication\PasswordAuthentication('user', 'secret'), new WilliamEggers\React\SSHClient\HostKey\CallbackHostKeyVerifier(static fn (): bool => true), );
To disable the full connection timeout:
$connector = new WilliamEggers\React\SSHClient\Connector(timeout: null);
To use a custom React socket connector for the TCP dial step:
use React\Socket\Connector as TcpConnector; use React\Socket\TimeoutConnector; $connector = new WilliamEggers\React\SSHClient\Connector( timeout: 30.0, socketConnector: new TimeoutConnector(new TcpConnector(), 10.0) );
Client
The Client represents an authenticated SSH connection and is resolved from Connector::connect().
It provides the following methods:
-
exec(string $command, ?ProcessConfig $config = null): PromiseInterface
-
shell(?ShellConfig $config = null): PromiseInterface
-
forwardIn(string $bindAddress, int $bindPort): PromiseInterface
-
forwardOut(string $srcIP, int $srcPort, string $dstIP, int $dstPort): PromiseInterface
-
on(string $event, callable $listener): void
-
close(): void
Use exec() to run a single remote command:
$client->exec('uname -a')->then(function (WilliamEggers\React\SSHClient\Process $process): void { $process->stdout()->on('data', static function (string $data): void { fwrite(STDOUT, $data); }); });
Use shell() to open an interactive session with a PTY:
use WilliamEggers\React\SSHClient\Values\ShellConfig; use WilliamEggers\React\SSHClient\Values\WinSize; $client->shell(new ShellConfig('xterm-256color', new WinSize(24, 80)))->then( function (WilliamEggers\React\SSHClient\Shell $shell): void { $shell->stdout()->on('data', static function (string $data): void { fwrite(STDOUT, $data); }); } );
Use forwardOut() to open a raw outbound TCP stream through the SSH server:
$client->forwardOut('127.0.0.1', 8000, '127.0.0.1', 80)->then( function (WilliamEggers\React\SSHClient\Channel\DirectTcpipChannel $stream): void { $stream->on('data', static function (string $data): void { fwrite(STDOUT, $data); }); $stream->end(implode("\r\n", [ 'HEAD / HTTP/1.1', 'Host: 127.0.0.1', 'Connection: close', '', '', ])); } );
Use forwardIn() to ask the SSH server to listen on a remote address and port and emit inbound TCP connections back to the client. The promise resolves with the effective listening port, which is useful when requesting port 0 for an automatically assigned port:
use WilliamEggers\React\SSHClient\Channel\ForwardedTcpipChannel; $client->on('tcp connection', function (array $info, Closure $accept, Closure $reject): void { /** @var ForwardedTcpipChannel $stream */ $stream = $accept(); $stream->end("HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"); }); $client->forwardIn('127.0.0.1', 8000)->then(static function (int $listeningPort): void { fwrite(STDOUT, 'Listening on remote port ' . $listeningPort . PHP_EOL); });
The tcp connection event receives:
-
array $infowithdestIP,destPort,srcIP, andsrcPort -
Closure $acceptto accept the forwarded connection and return aForwardedTcpipChannel -
Closure $rejectto reject the forwarded connection
Process
The Process class represents a remote exec request after it has been successfully started.
It exposes stream accessors for stdin, stdout, and stderr, plus a join() method for waiting on the remote exit status:
-
stdin(): WritableStreamInterface
-
stdout(): ReadableStreamInterface
-
stderr(): ReadableStreamInterface
-
join(): PromiseInterface
-
close(): void
join() resolves with the remote process exit code once the channel closes.
Shell
The Shell class represents an interactive remote shell session.
It provides the same stream accessors as Process, plus terminal resizing support:
-
stdin(): WritableStreamInterface
-
stdout(): ReadableStreamInterface
-
stderr(): ReadableStreamInterface
-
resize(WinSize $size): void
-
join(): PromiseInterface
-
close(): void
Use resize() to propagate terminal size changes to the remote session.
Authentication
Authentication is configured by passing an implementation of AuthenticationInterface to Connector::connect().
Password authentication
Use PasswordAuthentication when authenticating with a username and password:
use WilliamEggers\React\SSHClient\Authentication\PasswordAuthentication; $authentication = new PasswordAuthentication('user', 'secret');
Public key authentication
Use PublicKeyAuthentication when authenticating with an SSH private key.
You can either pass the private key contents directly to the constructor or use fromPath() to load from disk:
use WilliamEggers\React\SSHClient\Authentication\PublicKeyAuthentication; $authentication = PublicKeyAuthentication::fromPath('user', '/home/user/.ssh/id_ed25519');
If the key is passphrase-protected, provide the passphrase as the third argument.
Keyboard-interactive authentication
Use KeyboardInteractiveAuthentication when the server presents one or more prompts and expects corresponding responses.
The responder callback receives a KeyboardInteractiveChallenge containing:
nameinstructionlanguageTagpromptsas a list ofKeyboardInteractivePromptobjects
Example:
use WilliamEggers\React\SSHClient\Authentication\KeyboardInteractiveAuthentication; use WilliamEggers\React\SSHClient\Values\KeyboardInteractiveChallenge; $authentication = new KeyboardInteractiveAuthentication( 'user', static function (KeyboardInteractiveChallenge $challenge): array { return ['secret']; } );
Host key verification
Host key verification is required for every connection and is configured using a HostKeyVerifierInterface implementation.
This package currently provides CallbackHostKeyVerifier, which accepts a callback with the signature:
function (WilliamEggers\React\SSHClient\HostKey\HostKeyInfo $hostKey, string $address): bool
HostKeyInfo exposes:
algorithmblobopensshKeygetFingerprintSha256(): string
Example using a pinned fingerprint:
use WilliamEggers\React\SSHClient\HostKey\CallbackHostKeyVerifier; use WilliamEggers\React\SSHClient\HostKey\HostKeyInfo; $expectedFingerprint = 'SHA256:your-base64-fingerprint-here'; $verifier = new CallbackHostKeyVerifier( static function (HostKeyInfo $hostKey, string $address) use ($expectedFingerprint): bool { return hash_equals($expectedFingerprint, $hostKey->getFingerprintSha256()); } );
Important
Returning true unconditionally disables host authenticity checks and should only be used in local tests or throwaway demos. See examples/host-key-verification.php for the recommended pattern.
Configuration values
Several small value objects are used to configure process and shell sessions.
ProcessConfig
ProcessConfig currently supports:
environment- associative array of environment variables to request before starting the remote command
ShellConfig
ShellConfig currently supports:
-
term- terminal type string, defaultxterm -
size- aWinSizeinstance, default24x80 -
environment- associative array of environment variables to request before starting the shell
WinSize
WinSize accepts:
-
rows -
cols -
widthPixels -
heightPixels
Error handling
Connection and session operations may reject their promises with exceptions from the WilliamEggers\React\SSHClient\Exception namespace.
Connector::connect() may also reject with a RuntimeException if the configured full connection timeout is reached before SSH authentication completes.
The most relevant exception types are:
-
AuthenticationException- authentication failed, an unsupported auth flow was attempted, or a challenge/response exchange was invalid -
HostKeyException- the server host key was rejected by your verifier or the host key signature validation failed -
ProtocolException- the remote peer sent invalid or unexpected SSH protocol data, or the channel/session flow failed
In most applications, these should be handled by attaching rejection handlers to connect(), exec(), and shell() promises:
$connector->connect($uri, $authentication, $verifier)->then( static function (WilliamEggers\React\SSHClient\Client $client): void { // Use the client }, static function (Throwable $throwable): void { fwrite(STDERR, $throwable->getMessage() . PHP_EOL); } );
Supported algorithms
The negotiated algorithm lists are defined in KexNegotiator.
Key exchange methods
The following key exchange methods are supported:
curve25519-sha256curve25519-sha256@libssh.orgdiffie-hellman-group14-sha256
Server host key algorithms
The following server host key algorithms are supported:
ssh-ed25519rsa-sha2-256rsa-sha2-512
Encryption algorithms
The following encryption algorithms are supported:
aes256-gcm@openssh.comaes128-gcm@openssh.comaes256-ctraes192-ctraes128-ctr
MAC algorithms
The following MAC algorithms are supported:
hmac-sha2-512hmac-sha2-256hmac-sha1
Compression algorithms
The following compression algorithms are supported:
none
Unsupported features
This SSH client implementation is intentionally scoped to support core SSH connection management, authentication, exec requests, shell sessions, outbound TCP forwarding with forwardOut(), and remote TCP forwarding with forwardIn(). The following features are not currently supported:
-
SFTP and SCP - file transfer protocols and helpers are not implemented
-
Additional forwarding modes - dynamic forwarding, UNIX socket forwarding, and higher-level local forwarding helpers are not implemented
-
Agent forwarding - SSH agent forwarding is not implemented
-
Known hosts file management - this package does not parse or maintain OpenSSH
known_hostsfiles for you -
Subsystem support beyond session channels - subsystem-specific helpers are not provided
-
Proxy or jump host features - this client does not act as an SSH proxy chain or bastion helper
-
Compression negotiation beyond
none- no alternative compression modes are currently implemented
Contributions
Contributions are welcome and encouraged.
To contribute:
- Fork the repository.
- Create a new branch for your changes.
- Submit a pull request with a clear description of what you've done and why.
Please try to follow existing coding style and conventions, and include tests if applicable. Feel free to open an issue if you'd like to discuss a potential change or need guidance on where to start.
License
BSD 2-Clause License
Copyright (c) 2026, William Eggers
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
-
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
-
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Support and Credits
This project shares lineage with the related ReactPHP SSH server work and earlier SSH protocol implementations that inspired and informed its structure.
If you are using both sides of the stack, see also williameggers/reactphp-ssh-server.