Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork7
Async SSH proxy connector and forwarder, tunnel any TCP/IP-based protocol through an SSH server, built on top of ReactPHP.
License
clue/reactphp-ssh-proxy
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Async SSH proxy connector and forwarder, tunnel any TCP/IP-based protocol through an SSH server,built on top ofReactPHP.
Secure Shell (SSH) is a securenetwork protocol that is most commonly used to access a login shell on a remoteserver. Its architecture allows it to use multiple secure channels over a singleconnection. Among others, this can also be used to create an "SSH tunnel", whichis commonly used to tunnel HTTP(S) traffic through an intermediary ("proxy"), toconceal the origin address (anonymity) or to circumvent address blocking(geoblocking). This can be used to tunnel any TCP/IP-based protocol (HTTP, SMTP,IMAP etc.) and as such also allows you to access local services that are otherwisenot accessible from the outside (database behind firewall).This library is implemented as a lightweight process wrapper around thessh clientbinary and provides a simple API to create these tunneled connections for you.Because it implements ReactPHP's standardConnectorInterface,it can simply be used in place of a normal connector.This makes it fairly simple to add SSH proxy support to pretty much anyexisting higher-level protocol implementation.
- Async execution of connections -Send any number of SSH proxy requests in parallel and process theirresponses as soon as results come in.The Promise-based design provides asane interface to working with out oforder responses and possible connection errors.
- Standard interfaces -Allows easy integration with existing higher-level components by implementingReactPHP's standard
ConnectorInterface. - Lightweight, SOLID design -Provides a thin abstraction that isjust good enoughand does not get in your way.Builds on top of well-tested components and well-established concepts instead of reinventing the wheel.
- Good test coverage -Comes with an automated tests suite and is regularly tested against actual SSH servers in the wild.
Table of contents
We invest a lot of time developing, maintaining and updating our awesomeopen-source projects. You can help us sustain this high-quality of our work bybecoming a sponsor on GitHub. Sponsors getnumerous benefits in return, see oursponsoring pagefor details.
Let's take these projects to the next level together! 🚀
The following example code demonstrates how this library can be used to send asecure HTTPS request to google.com through a remote SSH server:
<?phprequire__DIR__ .'/vendor/autoload.php';$proxy =newClue\React\SshProxy\SshProcessConnector('alice@example.com');$connector =newReact\Socket\Connector(array('tcp' =>$proxy,'dns' =>false));$browser =newReact\Http\Browser($connector);$browser->get('https://google.com/')->then(function (Psr\Http\Message\ResponseInterface$response) {var_dump($response->getHeaders(), (string)$response->getBody());},function (Exception$e) {echo'Error:' .$e->getMessage() .PHP_EOL;});
See also theexamples.
TheSshProcessConnector is responsible for creating plain TCP/IP connections toany destination by using an intermediary SSH server as a proxy server.
[you] -> [proxy] -> [destination]This class is implemented as a lightweight process wrapper around thesshclient binary, so it will spawn onessh process for each connection. Forexample, if youopen a connection totcp://reactphp.org:80, it will run the equivalent ofssh -W reactphp.org:80 alice@example.comand forward data from its standard I/O streams. For this to work, you'll have tomake sure that you have a suitable SSH client installed. On Debian/Ubuntu-basedsystems, you may simply install it like this:
sudo apt install openssh-client
Its constructor simply accepts an SSH proxy server URL:
$proxy =newClue\React\SshProxy\SshProcessConnector('alice@example.com');
The proxy URL may or may not contain a scheme and port definition. The defaultport will be22 for SSH, but you may have to use a custom port depending onyour SSH server setup.
This class takes an optionalLoopInterface|null $loop parameter that can be used topass the event loop instance to use for this object. You can use anull valuehere in order to use thedefault loop.This value SHOULD NOT be given unless you're sure you want to explicitly use agiven event loop instance.
Keep in mind that this class is implemented as a lightweight process wrapperaround thessh client binary and that it will spawn onessh process for eachconnection. If you open more connections, it will spawn onessh process foreach connection. Each process will take some time to create a new SSH connectionand then keep running until the connection is closed, so you're recommended tolimit the total number of concurrent connections. If you plan to only use asingle or few connections (such as a single database connection), using thisclass is the recommended approach. If you plan to create multiple connections orhave a larger number of connections (such as an HTTP client), you're recommendedto use theSshSocksConnector instead.
This is one of the two main classes in this package.Because it implements ReactPHP's standardConnectorInterface,it can simply be used in place of a normal connector.Accordingly, it provides only a single public method, theconnect() method.Theconnect(string $uri): PromiseInterface<ConnectionInterface, Exception>method can be used to establish a streaming connection.It returns aPromise which eitherfulfills with aConnectionInterfaceon success or rejects with anException on error.
This makes it fairly simple to add SSH proxy support to pretty much anyhigher-level component:
- $acme = new AcmeApi($connector);+ $proxy = new Clue\React\SshProxy\SshProcessConnector('alice@example.com');+ $acme = new AcmeApi($proxy);
TheSshSocksConnector is responsible for creating plain TCP/IP connections toany destination by using an intermediary SSH server as a proxy server.
[you] -> [proxy] -> [destination]This class is implemented as a lightweight process wrapper around thesshclient binary and it will spawn onessh process on demand for multipleconnections. For example, once youopen a connectiontotcp://reactphp.org:80 for the first time, it will run the equivalent ofssh -D 1080 alice@example.com to run the SSH client in local SOCKS proxy servermode and will then create a SOCKS client connection to this server process. Youcan create any number of connections over this one process and it will keep thisprocess running while there are any open connections and will automaticallyclose it when it is idle. For this to work, you'll have to make sure that youhave a suitable SSH client installed. On Debian/Ubuntu-based systems, you maysimply install it like this:
sudo apt install openssh-client
Its constructor simply accepts an SSH proxy server URL:
$proxy =newClue\React\SshProxy\SshSocksConnector('alice@example.com');
The proxy URL may or may not contain a scheme and port definition. The defaultport will be22 for SSH, but you may have to use a custom port depending onyour SSH server setup.
This class takes an optionalLoopInterface|null $loop parameter that can be used topass the event loop instance to use for this object. You can use anull valuehere in order to use thedefault loop.This value SHOULD NOT be given unless you're sure you want to explicitly use agiven event loop instance.
Keep in mind that this class is implemented as a lightweight process wrapperaround thessh client binary and that it will spawn onessh process formultiple connections. This process will take some time to create a new SSHconnection and then keep running until the last connection is closed. If youplan to create multiple connections or have a larger number of concurrentconnections (such as an HTTP client), using this class is the recommendedapproach. If you plan to only use a single or few connections (such as a singledatabase connection), you're recommended to use theSshProcessConnectorinstead.
This class defaults to spawning the SSH client process in SOCKS proxy servermode listening on127.0.0.1:1080. If this port is already in use or if you wantto use multiple instances of this class to connect to different SSH proxyservers, you may optionally pass a unique bind address like this:
$proxy =newClue\React\SshProxy\SshSocksConnector('alice@example.com?bind=127.1.1.1:1081');
Security note for multi-user systems: This class will spawn the SSH clientprocess in local SOCKS server mode and will accept connections only on thelocalhost interface by default. If you're running on a multi-user system,other users on the same system may be able to connect to this proxy server andcreate connections over it. If this applies to your deployment, you'rerecommended to use the`SshProcessConnector insteador set up custom firewall rules to prevent unauthorized access to this port.
This is one of the two main classes in this package.Because it implements ReactPHP's standardConnectorInterface,it can simply be used in place of a normal connector.Accordingly, it provides only a single public method, theconnect() method.Theconnect(string $uri): PromiseInterface<ConnectionInterface, Exception>method can be used to establish a streaming connection.It returns aPromise which eitherfulfills with aConnectionInterfaceon success or rejects with anException on error.
This makes it fairly simple to add SSH proxy support to pretty much anyhigher-level component:
- $acme = new AcmeApi($connector);+ $proxy = new Clue\React\SshProxy\SshSocksConnector('alice@example.com');+ $acme = new AcmeApi($proxy);
SSH proxy servers are commonly used to issue HTTPS requests to your destination.However, this is actually performed on a higher protocol layer and thisproject is actually inherently a general-purpose plain TCP/IP connector.As documented above, you can simply invoke theconnect() method to establisha streaming plain TCP/IP connection on theSshProcessConnector orSshSocksConnectorand use any higher level protocol like so:
$proxy =newClue\React\SshProxy\SshProcessConnector('alice@example.com');// or$proxy =newClue\React\SshProxy\SshSocksConnector('alice@example.com');$proxy->connect('tcp://smtp.googlemail.com:587')->then(function (React\Socket\ConnectionInterface$connection) {$connection->write("EHLO local\r\n");$connection->on('data',function ($chunk)use ($connection) {echo$chunk; });});
You can either use theSshProcessConnector orSshSocksConnector directly or youmay want to wrap this connector in ReactPHP'sConnector:
$proxy =newClue\React\SshProxy\SshProcessConnector('alice@example.com');// or$proxy =newClue\React\SshProxy\SshSocksConnector('alice@example.com');$connector =newReact\Socket\Connector(array('tcp' =>$proxy,'dns' =>false));$connector->connect('tcp://smtp.googlemail.com:587')->then(function (React\Socket\ConnectionInterface$connection) {$connection->write("EHLO local\r\n");$connection->on('data',function ($chunk)use ($connection) {echo$chunk; });});
For this example, you can use either theSshProcessConnector orSshSocksConnector.Keep in mind that this project is implemented as a lightweight process wrapperaround thessh client binary. While theSshProcessConnector will spawn onessh process for each connection, theSshSocksConnector will spawn onesshprocess that will be shared for multiple connections, see also above for moredetails.
TheSshSocksConnector can also be used if you want to establish a secure TLS connection(formerly known as SSL) between you and your destination, such as when usingsecure HTTPS to your destination site. You can simply wrap this connector inReactPHP'sConnector:
$proxy =newClue\React\SshProxy\SshSocksConnector('alice@example.com');$connector =newReact\Socket\Connector(array('tcp' =>$proxy,'dns' =>false));$connector->connect('tls://smtp.googlemail.com:465')->then(function (React\Socket\ConnectionInterface$connection) {$connection->write("EHLO local\r\n");$connection->on('data',function ($chunk)use ($connection) {echo$chunk; });});
Note how secure TLS connections are in fact entirely handled outside ofthis SSH proxy client implementation.The
SshProcessConnectordoes not currently support secure TLS connectionsbecause PHP's underlying crypto functions require a socket resource and do notwork for virtual connections. As an alternative, you're recommended to use theSshSocksConnectoras given in the above example.
This library also allows you to sendHTTP requests through an SSH proxy server.
In order to send HTTP requests, you first have to add a dependency forReactPHP's async HTTP client.This allows you to send both plain HTTP and TLS-encrypted HTTPS requests like this:
$proxy =newClue\React\SshProxy\SshSocksConnector('alice@example.com');$connector =newReact\Socket\Connector(array('tcp' =>$proxy,'dns' =>false));$browser =newReact\Http\Browser($connector);$browser->get('https://google.com/')->then(function (Psr\Http\Message\ResponseInterface$response) {var_dump($response->getHeaders(), (string)$response->getBody());},function (Exception$e) {echo'Error:' .$e->getMessage() .PHP_EOL;});
We recommend using theSshSocksConnector, this works for both plain HTTPand TLS-encrypted HTTPS requests. When using theSshProcessConnector, this onlyworks for plaintext HTTP requests.
See alsoReactPHP's HTTP clientand any of theexamples for more details.
We should now have a basic understanding of how we can tunnel any TCP/IP-basedprotocol over an SSH proxy server. Besides using this to access "external"services, this is also particularly useful because it allows you to accessnetwork services otherwise only local to this SSH server from the outside, suchas a firewalled database server.
For example, this allows us to combine anasync MySQL database client andthe above SSH proxy server setup, so we can access a firewalled MySQL databaseserver through an SSH tunnel. Here's the gist:
$proxy =newClue\React\SshProxy\SshProcessConnector('alice@example.com');$uri ='test:test@localhost/test';$factory =newReact\MySQL\Factory(null,$proxy);$connection =$factory->createLazyConnection($uri);$connection->query('SELECT * FROM book')->then(function (React\MySQL\QueryResult$command) {echocount($command->resultRows) .' row(s) in set' .PHP_EOL; },function (Exception$error) {echo'Error:' .$error->getMessage() .PHP_EOL; });$connection->quit();
See alsoexample #21 for more details.
This example will automatically launch thessh client binary to create theconnection to a database server that can not otherwise be accessed from theoutside. From the perspective of the database server, this looks just like aregular, local connection. From this code's perspective, this will create aregular, local connection which just happens to use a secure SSH tunnel totransport this to a remote server, so you can send any query like you would to alocal database server.
By default, neither theSshProcessConnector nor theSshSocksConnector implementany timeouts for establishing remote connections.Your underlying operating system may impose limits on pending and/or idle TCP/IPconnections, anywhere in a range of a few minutes to several hours.
Many use cases require more control over the timeout and likely values muchsmaller, usually in the range of a few seconds only.
You can use ReactPHP'sConnectorto decorate any givenConnectorInterface instance.It provides the sameconnect() method, but will automatically reject theunderlying connection attempt if it takes too long:
$proxy =newClue\React\SshProxy\SshProcessConnector('alice@example.com');// or$proxy =newClue\React\SshProxy\SshSocksConnector('alice@example.com');$connector =newReact\Socket\Connector(array('tcp' =>$proxy,'dns' =>false,'timeout' =>3.0));$connector->connect('tcp://google.com:80')->then(function (React\Socket\ConnectionInterface$connection) {// connection succeeded within 3.0 seconds});
See also any of theexamples.
Note how the connection timeout is in fact entirely handled outside of thisSSH proxy client implementation.
By default, neither theSshProcessConnector nor theSshSocksConnector performany DNS resolution at all and simply forward any hostname you're trying toconnect to the remote proxy server. The remote proxy server is thus responsiblefor looking up any hostnames via DNS (this default mode is thus calledremote DNS resolution).
As an alternative, you can also send the destination IP to the remote proxyserver.In this mode you either have to stick to using IPs only (which is ofen unfeasable)or perform any DNS lookups locally and only transmit the resolved destination IPs(this mode is thus calledlocal DNS resolution).
The defaultremote DNS resolution is useful if your localSshProcessConnectororSshSocksConnector either can not resolve target hostnames because it has nodirect access to the internet or if it should not resolve target hostnamesbecause its outgoing DNS traffic might be intercepted.
As noted above, theSshProcessConnector andSshSocksConnector default to usingremote DNS resolution. However, wrapping them in ReactPHP'sConnector actuallyperforms local DNS resolution unless explicitly defined otherwise.Given that remote DNS resolution is assumed to be the preferred mode, allother examples explicitly disable DNS resolution like this:
$proxy =newClue\React\SshProxy\SshProcessConnector('alice@example.com');// or$proxy =newClue\React\SshProxy\SshSocksConnector('alice@example.com');$connector =newReact\Socket\Connector(array('tcp' =>$proxy,'dns' =>false));
If you want to explicitly uselocal DNS resolution, you can use the following code:
$proxy =newClue\React\SshProxy\SshProcessConnector('alice@example.com');// or$proxy =newClue\React\SshProxy\SshSocksConnector('alice@example.com');// set up Connector which uses Google's public DNS (8.8.8.8)$connector =newReact\Socket\Connector(array('tcp' =>$proxy,'dns' =>'8.8.8.8'));
Note how local DNS resolution is in fact entirely handled outside of thisSSH proxy client implementation.
Note that this class is implemented as a lightweight process wrapper around thessh client binary. It works under the assumption that you have verified youcan access your SSH proxy server on the command line like this:
# test SSH accessssh alice@example.comecho hello
Because this class is designed to be used to create any number of connections,it does not provide a way to interactively ask for your password. Similarly,thessh client binary does not provide a way to "pass" in the password on thecommand line for security reasons. This means that you are highly recommended toset up pubkey-based authentication without a password for this to work best.
Additionally, this library provides a way to pass in a password in a somewhatless secure way if your use case absolutely requires this. Before proceeding,please consult your SSH documentation to find out why this may be a bad idea andwhy pubkey-based authentication is usually the better alternative.If your SSH proxy server requires password authentication, you may pass theusername and password as part of the SSH proxy server URL like this:
$proxy =newClue\React\SshProxy\SshProcessConnector('alice:password@example.com');// or$proxy =newClue\React\SshProxy\SshSocksConnector('alice:password@example.com');
For this to work, you will have to have thesshpass binary installed. OnDebian/Ubuntu-based systems, you may simply install it like this:
sudo apt install sshpass
Note that both the username and password must be percent-encoded if they containspecial characters:
$user ='he:llo';$pass ='p@ss';$url =rawurlencode($user) .':' .rawurlencode($pass) .'@example.com';$proxy =newClue\React\SshProxy\SshProcessConnector($url);
The recommended way to install this library isthrough Composer.New to Composer?
This project followsSemVer.This will install the latest supported version:
composer require clue/reactphp-ssh-proxy:^1.4
See also theCHANGELOG for details about version upgrades.
This project aims to run on any platform and thus does not require any PHPextensions and supports running on legacy PHP 5.3 through current PHP 8+ andHHVM.It'shighly recommended to use the latest supported PHP version for this project.
This project is implemented as a lightweight process wrapper around thesshclient binary, so you'll have to make sure that you have a suitable SSH clientinstalled. On Debian/Ubuntu-based systems, you may simply install it like this:
sudo apt install openssh-client
Additionally, if you usepassword authentication(not recommended), then you will have to have thesshpass binary installed. OnDebian/Ubuntu-based systems, you may simply install it like this:
sudo apt install sshpass
Running onWindows is currently not supported
To run the test suite, you first need to clone this repo and then install alldependenciesthrough Composer:
composer install
To run the test suite, go to the project root and run:
vendor/bin/phpunit
The test suite contains a number of tests that require an actual SSH proxy server.These tests will be skipped unless you configure your SSH login credentials tobe able to create some actual test connections. You can assign theSSH_PROXYenvironment and prefix this with a space to make sure your login credentials arenot stored in your bash history like this:
export SSH_PROXY=alice:password@example.comvendor/bin/phpunitThis project is released under the permissiveMIT license.
Did you know that I offer custom development services and issuing invoices forsponsorships of releases and for contributions? Contact me (@clue) for details.
- If you want to learn more about how the
ConnectorInterfaceand its usual implementations look like, refer to the documentation of the underlyingreact/socket component. - If you want to learn more about processing streams of data, refer to thedocumentation of the underlyingreact/stream component.
- As an alternative to an SSH proxy server, you may also want to look intousing a SOCKS5 or SOCKS4(a) proxy instead.You may want to useclue/reactphp-sockswhich also provides an implementation of the same
ConnectorInterfaceso that supporting either proxy protocol should be fairly trivial. - As another alternative to an SSH proxy server, you may also want to look intousing an HTTP CONNECT proxy instead.You may want to useclue/reactphp-http-proxywhich also provides an implementation of the same
ConnectorInterface
About
Async SSH proxy connector and forwarder, tunnel any TCP/IP-based protocol through an SSH server, built on top of ReactPHP.
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Contributors2
Uh oh!
There was an error while loading.Please reload this page.