Skip to content

Commit

Permalink
Merge pull request #8 from clue-labs/socks
Browse files Browse the repository at this point in the history
Add `SshSocksConnector` to support local SSH SOCKS proxy server and support secure TLS connections
  • Loading branch information
clue authored Dec 14, 2018
2 parents 72d70dc + 4726de4 commit 2902999
Show file tree
Hide file tree
Showing 7 changed files with 1,024 additions and 41 deletions.
214 changes: 179 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ existing higher-level protocol implementation.
**Table of contents**

* [Quickstart example](#quickstart-example)
* [API](#api)
* [SshProcessConnector](#sshprocessconnector)
* [SshSocksConnector](#sshsocksconnector)
* [Usage](#usage)
* [SshProcessConnector](#sshprocessconnector)
* [Plain TCP connections](#plain-tcp-connections)
* [Secure TLS connections](#secure-tls-connections)
* [HTTP requests](#http-requests)
* [Connection timeout](#connection-timeout)
* [DNS resolution](#dns-resolution)
Expand Down Expand Up @@ -80,7 +83,7 @@ $loop->run();

See also the [examples](examples).

## Usage
## API

### SshProcessConnector

Expand All @@ -92,8 +95,12 @@ any destination by using an intermediary SSH server as a proxy server.
```

This class is implemented as a lightweight process wrapper around the `ssh`
client binary, so you'll have to make sure that you have a suitable SSH client
installed. On Debian/Ubuntu-based systems, you may simply install it like this:
client binary, so it will spawn one `ssh` process for each connection. For
example, if you [open a connection](#plain-tcp-connections) to
`tcp://reactphp.org:80`, it will run the equivalent of `ssh -W reactphp.org:80 [email protected]`
and forward data from its standard I/O streams. For this to work, you'll have to
make sure that you have a suitable SSH client installed. On Debian/Ubuntu-based
systems, you may simply install it like this:

```bash
$ sudo apt install openssh-client
Expand All @@ -110,7 +117,18 @@ The proxy URL may or may not contain a scheme and port definition. The default
port will be `22` for SSH, but you may have to use a custom port depending on
your SSH server setup.

This is the main class in this package.
Keep in mind that this class is implemented as a lightweight process wrapper
around the `ssh` client binary and that it will spawn one `ssh` process for each
connection. If you open more connections, it will spawn one `ssh` process for
each connection. Each process will take some time to create a new SSH connection
and then keep running until the connection is closed, so you're recommended to
limit the total number of concurrent connections. If you plan to only use a
single or few connections (such as a single database connection), using this
class is the recommended approach. If you plan to create multiple connections or
have a larger number of connections (such as an HTTP client), you're recommended
to use the [`SshSocksConnector`](#sshsocksconnector) instead.

This is one of the two main classes in this package.
Because it implements ReactPHP's standard
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface),
it can simply be used in place of a normal connector.
Expand All @@ -131,16 +149,105 @@ higher-level component:
+ $client = new SomeClient($proxy);
```

#### Plain TCP connections
### SshSocksConnector

The `SshSocksConnector` is responsible for creating plain TCP/IP connections to
any destination by using an intermediary SSH server as a proxy server.

```
[you] -> [proxy] -> [destination]
```

This class is implemented as a lightweight process wrapper around the `ssh`
client binary and it will spawn one `ssh` process on demand for multiple
connections. For example, once you [open a connection](#plain-tcp-connections)
to `tcp://reactphp.org:80` for the first time, it will run the equivalent of
`ssh -D 1080 [email protected]` to run the SSH client in local SOCKS proxy server
mode and will then create a SOCKS client connection to this server process. You
can create any number of connections over this one process and it will keep this
process running while there are any open connections and will automatically
close if when it is idle. For this to work, you'll have to make sure that you
have a suitable SSH client installed. On Debian/Ubuntu-based systems, you may
simply install it like this:

```bash
$ sudo apt install openssh-client
```

Its constructor simply accepts an SSH proxy server URL and a loop to bind to:

```php
$loop = React\EventLoop\Factory::create();
$proxy = new Clue\React\SshProxy\SshSocksConnector('[email protected]', $loop);
```

The proxy URL may or may not contain a scheme and port definition. The default
port will be `22` for SSH, but you may have to use a custom port depending on
your SSH server setup.

Keep in mind that this class is implemented as a lightweight process wrapper
around the `ssh` client binary and that it will spawn one `ssh` process for
multiple connections. This process will take some time to create a new SSH
connection and then keep running until the last connection is closed. If you
plan to create multiple connections or have a larger number of concurrent
connections (such as an HTTP client), using this class is the recommended
approach. If you plan to only use a single or few connections (such as a single
database connection), you're recommended to use the [`SshProcessConnector`](#sshprocessconnector)
instead.

This class defaults to spawning the SSH client process in SOCKS proxy server
mode listening on `127.0.0.1:1080`. If this port is already in use or if you want
to use multiple instances of this class to connect to different SSH proxy
servers, you may optionally pass a unique bind address like this:

```php
$proxy = new Clue\React\SshProxy\SshSocksConnector('[email protected]?bind=127.1.1.1:1081', $loop);
```

> *Security note for multi-user systems*: This class will spawn the SSH client
process in local SOCKS server mode and will accept connections only on the
localhost 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 and
create connections over it. If this applies to your deployment, you're
recommended to use the [`SshProcessConnector](#sshprocessconnector) instead
or 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 standard
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface),
it can simply be used in place of a normal connector.
Accordingly, it provides only a single public method, the
[`connect()`](https://github.com/reactphp/socket#connect) method.
The `connect(string $uri): PromiseInterface<ConnectionInterface, Exception>`
method can be used to establish a streaming connection.
It returns a [Promise](https://github.com/reactphp/promise) which either
fulfills with a [ConnectionInterface](https://github.com/reactphp/socket#connectioninterface)
on success or rejects with an `Exception` on error.

This makes it fairly simple to add SSH proxy support to pretty much any
higher-level component:

```diff
- $client = new SomeClient($connector);
+ $proxy = new SshSocksConnector('[email protected]', $loop);
+ $client = new SomeClient($proxy);
```

## Usage

### Plain TCP connections

SSH proxy servers are commonly used to issue HTTPS requests to your destination.
However, this is actually performed on a higher protocol layer and this
connector is actually inherently a general-purpose plain TCP/IP connector.
As documented above, you can simply invoke its `connect()` method to establish
a streaming plain TCP/IP connection and use any higher level protocol like so:
project is actually inherently a general-purpose plain TCP/IP connector.
As documented above, you can simply invoke the `connect()` method to establish
a streaming plain TCP/IP connection on the `SshProcessConnector` or `SshSocksConnector`
and use any higher level protocol like so:

```php
$proxy = new SshProcessConnector('[email protected]', $connector);
$proxy = new SshProcessConnector('[email protected]', $loop);
// or
$proxy = new SshSocksConnector('[email protected]', $loop);

$proxy->connect('tcp://smtp.googlemail.com:587')->then(function (ConnectionInterface $stream) {
$stream->write("EHLO local\r\n");
Expand All @@ -150,8 +257,8 @@ $proxy->connect('tcp://smtp.googlemail.com:587')->then(function (ConnectionInter
});
```

You can either use the `SshProcessConnector` directly or you may want to wrap this connector
in ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector):
You can either use the `SshProcessConnector` or `SshSocksConnector` directly or you
may want to wrap this connector in ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector):

```php
$connector = new Connector($loop, array(
Expand All @@ -167,25 +274,60 @@ $connector->connect('tcp://smtp.googlemail.com:587')->then(function (ConnectionI
});
```

Keep in mind that this class is implemented as a lightweight process wrapper
around the `ssh` client binary, so it will spawn one `ssh` process for each
connection. Each process will keep running until the connection is closed, so
you're recommended to limit the total number of concurrent connections.
For this example, you can use either the `SshProcessConnector` or `SshSocksConnector`.
Keep in mind that this project is implemented as a lightweight process wrapper
around the `ssh` client binary. While the `SshProcessConnector` will spawn one
`ssh` process for each connection, the `SshSocksConnector` will spawn one `ssh`
process that will be shared for multiple connections, see also above for more
details.

### Secure TLS connections

The `SshSocksConnector` 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 using
secure HTTPS to your destination site. You can simply wrap this connector in
ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector) or the
low-level [`SecureConnector`](https://github.com/reactphp/socket#secureconnector):

```php
$proxy = new SshSocksConnector('[email protected]', $loop);

$connector = new Connector($loop, array(
'tcp' => $proxy,
'dns' => false
));

$connector->connect('tls://smtp.googlemail.com:465')->then(function (ConnectionInterface $stream) {
$stream->write("EHLO local\r\n");
$stream->on('data', function ($chunk) use ($stream) {
echo $chunk;
});
});
```

> Note how secure TLS connections are in fact entirely handled outside of
this SSH proxy client implementation.
The `SshProcessConnector` does not currently support secure TLS connections
because PHP's underlying crypto functions require a socket resource and do not
work for virtual connections. As an alternative, you're recommended to use the
`SshSocksConnector` as given in the above example.

#### HTTP requests
### HTTP requests

HTTP operates on a higher layer than this low-level SSH proxy implementation.
If you want to issue HTTP requests, you can add a dependency for
[clue/reactphp-buzz](https://github.com/clue/reactphp-buzz).
It can interact with this library by issuing all HTTP requests through your SSH
proxy server, similar to how it can issue
[HTTP requests through an HTTP CONNECT proxy server](https://github.com/clue/reactphp-buzz#http-proxy).
At the moment, this only works for plaintext HTTP requests.
When using the `SshSocksConnector` (recommended), this works for both plain HTTP
and TLS-encrypted HTTPS requests. When using the `SshProcessConnector`, this only
works for plaintext HTTP requests.

#### Connection timeout
### Connection timeout

By default, the `SshProcessConnector` does not implement any timeouts for establishing remote
connections.
By default, neither the `SshProcessConnector` nor the `SshSocksConnector` implement
any timeouts for establishing remote connections.
Your underlying operating system may impose limits on pending and/or idle TCP/IP
connections, anywhere in a range of a few minutes to several hours.

Expand Down Expand Up @@ -216,26 +358,26 @@ See also any of the [examples](examples).
> Note how the connection timeout is in fact entirely handled outside of this
SSH proxy client implementation.

#### DNS resolution
### DNS resolution

By default, the `SshProcessConnector` does not perform any DNS resolution at all and simply
forwards any hostname you're trying to connect to the remote proxy server.
The remote proxy server is thus responsible for looking up any hostnames via DNS
(this default mode is thus called *remote DNS resolution*).
By default, neither the `SshProcessConnector` nor the `SshSocksConnector` perform
any DNS resolution at all and simply forwards any hostname you're trying to
connect to the remote proxy server. The remote proxy server is thus responsible
for looking up any hostnames via DNS (this default mode is thus called *remote DNS resolution*).

As an alternative, you can also send the destination IP to the remote proxy
server.
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 called *local DNS resolution*).

The default *remote DNS resolution* is useful if your local `SshProcessConnector` either can
not resolve target hostnames because it has no direct access to the internet or
if it should not resolve target hostnames because its outgoing DNS traffic might
be intercepted.
The default *remote DNS resolution* is useful if your local `SshProcessConnector`
or `SshSocksConnector` either can not resolve target hostnames because it has no
direct access to the internet or if it should not resolve target hostnames
because its outgoing DNS traffic might be intercepted.

As noted above, the `SshProcessConnector` defaults to using remote DNS resolution.
However, wrapping the `SshProcessConnector` in ReactPHP's
As noted above, the `SshProcessConnector` and `SshSocksConnector` default to using
remote DNS resolution. However, wrapping them in ReactPHP's
[`Connector`](https://github.com/reactphp/socket#connector) actually
performs local DNS resolution unless explicitly defined otherwise.
Given that remote DNS resolution is assumed to be the preferred mode, all
Expand All @@ -261,7 +403,7 @@ $connector = new Connector($loop, array(
> Note how local DNS resolution is in fact entirely handled outside of this
SSH proxy client implementation.

#### Password authentication
### Password authentication

Note that this class is implemented as a lightweight process wrapper around the
`ssh` client binary. It works under the assumption that you have verified you
Expand All @@ -286,7 +428,9 @@ If your SSH proxy server requires password authentication, you may pass the
username and password as part of the SSH proxy server URL like this:

```php
$proxy = new SshProcessConnector('user:[email protected]', $connector);
$proxy = new SshProcessConnector('user:[email protected]', $loop);
// or
$proxy = new SshSocksConnector('user:[email protected]', $loop);
```

For this to work, you will have to have the `sshpass` binary installed. On
Expand All @@ -305,7 +449,7 @@ $pass = 'p@ss';

$proxy = new SshProcessConnector(
rawurlencode($user) . ':' . rawurlencode($pass) . '@example.com:2222',
$connector
$loop
);
```

Expand Down
9 changes: 5 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
},
"require": {
"php": ">=5.3",
"clue/socks-react": "^1.0",
"react/child-process": "^0.5",
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
"react/event-loop": "^1.0 || ^0.5",
"react/promise": "^2.1 || ^1.2.1",
"react/socket": "^1.0 || ^0.8.0",
"react/socket": "^1.1",
"react/stream": "^1.0 || ^0.7.2"
},
"require-dev": {
"phpunit/phpunit": "^7.4 || ^6.4 || ^5.0 || ^4.8.36",
"clue/block-react": "^1.3"
"clue/block-react": "^1.3",
"phpunit/phpunit": "^7.4 || ^6.4 || ^5.0 || ^4.8.36"
}
}
39 changes: 39 additions & 0 deletions examples/11-proxy-https.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

// A simple example which requests https://google.com/ through an SSH proxy server.
// The proxy can be given through the SSH_PROXY env and defaults to localhost otherwise.
//
// You can assign the SSH_PROXY environment and prefix this with a space to make
// sure your login credentials are not stored in your bash history like this:
//
// $ export SSH_PROXY=user:[email protected]
// $ php examples/11-proxy-https.php
//
// For illustration purposes only. If you want to send HTTP requests in a real
// world project, take a look at https://github.com/clue/reactphp-buzz#http-proxy

use Clue\React\SshProxy\SshSocksConnector;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;

require __DIR__ . '/../vendor/autoload.php';

$url = getenv('SSH_PROXY') !== false ? getenv('SSH_PROXY') : 'ssh://localhost:22';

$loop = React\EventLoop\Factory::create();

$proxy = new SshSocksConnector($url, $loop);
$connector = new Connector($loop, array(
'tcp' => $proxy,
'timeout' => 3.0,
'dns' => false
));

$connector->connect('tls://google.com:443')->then(function (ConnectionInterface $stream) {
$stream->write("GET / HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n");
$stream->on('data', function ($chunk) {
echo $chunk;
});
}, 'printf');

$loop->run();
4 changes: 2 additions & 2 deletions src/SshProcessConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ public function __construct($uri, LoopInterface $loop)
}
$this->cmd .= 'ssh -vv ';

// disable interactive password prompt if no password was given (see sshpass below)
if (!isset($parts['pass'])) {
// disable interactive password prompt if no password was given (see sshpass above)
if ($pass === null) {
$this->cmd .= '-o BatchMode=yes ';
}

Expand Down
Loading

0 comments on commit 2902999

Please sign in to comment.