Skip to content

Commit 2902999

Browse files
authored
Merge pull request clue#8 from clue-labs/socks
Add `SshSocksConnector` to support local SSH SOCKS proxy server and support secure TLS connections
2 parents 72d70dc + 4726de4 commit 2902999

File tree

7 files changed

+1024
-41
lines changed

7 files changed

+1024
-41
lines changed

README.md

Lines changed: 179 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,12 @@ existing higher-level protocol implementation.
3939
**Table of contents**
4040

4141
* [Quickstart example](#quickstart-example)
42+
* [API](#api)
43+
* [SshProcessConnector](#sshprocessconnector)
44+
* [SshSocksConnector](#sshsocksconnector)
4245
* [Usage](#usage)
43-
* [SshProcessConnector](#sshprocessconnector)
4446
* [Plain TCP connections](#plain-tcp-connections)
47+
* [Secure TLS connections](#secure-tls-connections)
4548
* [HTTP requests](#http-requests)
4649
* [Connection timeout](#connection-timeout)
4750
* [DNS resolution](#dns-resolution)
@@ -80,7 +83,7 @@ $loop->run();
8083

8184
See also the [examples](examples).
8285

83-
## Usage
86+
## API
8487

8588
### SshProcessConnector
8689

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

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

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

113-
This is the main class in this package.
120+
Keep in mind that this class is implemented as a lightweight process wrapper
121+
around the `ssh` client binary and that it will spawn one `ssh` process for each
122+
connection. If you open more connections, it will spawn one `ssh` process for
123+
each connection. Each process will take some time to create a new SSH connection
124+
and then keep running until the connection is closed, so you're recommended to
125+
limit the total number of concurrent connections. If you plan to only use a
126+
single or few connections (such as a single database connection), using this
127+
class is the recommended approach. If you plan to create multiple connections or
128+
have a larger number of connections (such as an HTTP client), you're recommended
129+
to use the [`SshSocksConnector`](#sshsocksconnector) instead.
130+
131+
This is one of the two main classes in this package.
114132
Because it implements ReactPHP's standard
115133
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface),
116134
it can simply be used in place of a normal connector.
@@ -131,16 +149,105 @@ higher-level component:
131149
+ $client = new SomeClient($proxy);
132150
```
133151

134-
#### Plain TCP connections
152+
### SshSocksConnector
153+
154+
The `SshSocksConnector` is responsible for creating plain TCP/IP connections to
155+
any destination by using an intermediary SSH server as a proxy server.
156+
157+
```
158+
[you] -> [proxy] -> [destination]
159+
```
160+
161+
This class is implemented as a lightweight process wrapper around the `ssh`
162+
client binary and it will spawn one `ssh` process on demand for multiple
163+
connections. For example, once you [open a connection](#plain-tcp-connections)
164+
to `tcp://reactphp.org:80` for the first time, it will run the equivalent of
165+
`ssh -D 1080 user@example.com` to run the SSH client in local SOCKS proxy server
166+
mode and will then create a SOCKS client connection to this server process. You
167+
can create any number of connections over this one process and it will keep this
168+
process running while there are any open connections and will automatically
169+
close if when it is idle. For this to work, you'll have to make sure that you
170+
have a suitable SSH client installed. On Debian/Ubuntu-based systems, you may
171+
simply install it like this:
172+
173+
```bash
174+
$ sudo apt install openssh-client
175+
```
176+
177+
Its constructor simply accepts an SSH proxy server URL and a loop to bind to:
178+
179+
```php
180+
$loop = React\EventLoop\Factory::create();
181+
$proxy = new Clue\React\SshProxy\SshSocksConnector('user@example.com', $loop);
182+
```
183+
184+
The proxy URL may or may not contain a scheme and port definition. The default
185+
port will be `22` for SSH, but you may have to use a custom port depending on
186+
your SSH server setup.
187+
188+
Keep in mind that this class is implemented as a lightweight process wrapper
189+
around the `ssh` client binary and that it will spawn one `ssh` process for
190+
multiple connections. This process will take some time to create a new SSH
191+
connection and then keep running until the last connection is closed. If you
192+
plan to create multiple connections or have a larger number of concurrent
193+
connections (such as an HTTP client), using this class is the recommended
194+
approach. If you plan to only use a single or few connections (such as a single
195+
database connection), you're recommended to use the [`SshProcessConnector`](#sshprocessconnector)
196+
instead.
197+
198+
This class defaults to spawning the SSH client process in SOCKS proxy server
199+
mode listening on `127.0.0.1:1080`. If this port is already in use or if you want
200+
to use multiple instances of this class to connect to different SSH proxy
201+
servers, you may optionally pass a unique bind address like this:
202+
203+
```php
204+
$proxy = new Clue\React\SshProxy\SshSocksConnector('user@example.com?bind=127.1.1.1:1081', $loop);
205+
```
206+
207+
> *Security note for multi-user systems*: This class will spawn the SSH client
208+
process in local SOCKS server mode and will accept connections only on the
209+
localhost interface by default. If you're running on a multi-user system,
210+
other users on the same system may be able to connect to this proxy server and
211+
create connections over it. If this applies to your deployment, you're
212+
recommended to use the [`SshProcessConnector](#sshprocessconnector) instead
213+
or set up custom firewall rules to prevent unauthorized access to this port.
214+
215+
This is one of the two main classes in this package.
216+
Because it implements ReactPHP's standard
217+
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface),
218+
it can simply be used in place of a normal connector.
219+
Accordingly, it provides only a single public method, the
220+
[`connect()`](https://github.com/reactphp/socket#connect) method.
221+
The `connect(string $uri): PromiseInterface<ConnectionInterface, Exception>`
222+
method can be used to establish a streaming connection.
223+
It returns a [Promise](https://github.com/reactphp/promise) which either
224+
fulfills with a [ConnectionInterface](https://github.com/reactphp/socket#connectioninterface)
225+
on success or rejects with an `Exception` on error.
226+
227+
This makes it fairly simple to add SSH proxy support to pretty much any
228+
higher-level component:
229+
230+
```diff
231+
- $client = new SomeClient($connector);
232+
+ $proxy = new SshSocksConnector('user@example.com', $loop);
233+
+ $client = new SomeClient($proxy);
234+
```
235+
236+
## Usage
237+
238+
### Plain TCP connections
135239

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

142247
```php
143-
$proxy = new SshProcessConnector('user@example.com', $connector);
248+
$proxy = new SshProcessConnector('user@example.com', $loop);
249+
// or
250+
$proxy = new SshSocksConnector('user@example.com', $loop);
144251

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

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

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

170-
Keep in mind that this class is implemented as a lightweight process wrapper
171-
around the `ssh` client binary, so it will spawn one `ssh` process for each
172-
connection. Each process will keep running until the connection is closed, so
173-
you're recommended to limit the total number of concurrent connections.
277+
For this example, you can use either the `SshProcessConnector` or `SshSocksConnector`.
278+
Keep in mind that this project is implemented as a lightweight process wrapper
279+
around the `ssh` client binary. While the `SshProcessConnector` will spawn one
280+
`ssh` process for each connection, the `SshSocksConnector` will spawn one `ssh`
281+
process that will be shared for multiple connections, see also above for more
282+
details.
283+
284+
### Secure TLS connections
285+
286+
The `SshSocksConnector` can also be used if you want to establish a secure TLS connection
287+
(formerly known as SSL) between you and your destination, such as when using
288+
secure HTTPS to your destination site. You can simply wrap this connector in
289+
ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector) or the
290+
low-level [`SecureConnector`](https://github.com/reactphp/socket#secureconnector):
291+
292+
```php
293+
$proxy = new SshSocksConnector('user@example.com', $loop);
294+
295+
$connector = new Connector($loop, array(
296+
'tcp' => $proxy,
297+
'dns' => false
298+
));
299+
300+
$connector->connect('tls://smtp.googlemail.com:465')->then(function (ConnectionInterface $stream) {
301+
$stream->write("EHLO local\r\n");
302+
$stream->on('data', function ($chunk) use ($stream) {
303+
echo $chunk;
304+
});
305+
});
306+
```
307+
308+
> Note how secure TLS connections are in fact entirely handled outside of
309+
this SSH proxy client implementation.
310+
The `SshProcessConnector` does not currently support secure TLS connections
311+
because PHP's underlying crypto functions require a socket resource and do not
312+
work for virtual connections. As an alternative, you're recommended to use the
313+
`SshSocksConnector` as given in the above example.
174314

175-
#### HTTP requests
315+
### HTTP requests
176316

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

185-
#### Connection timeout
327+
### Connection timeout
186328

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

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

219-
#### DNS resolution
361+
### DNS resolution
220362

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

226368
As an alternative, you can also send the destination IP to the remote proxy
227369
server.
228370
In this mode you either have to stick to using IPs only (which is ofen unfeasable)
229371
or perform any DNS lookups locally and only transmit the resolved destination IPs
230372
(this mode is thus called *local DNS resolution*).
231373

232-
The default *remote DNS resolution* is useful if your local `SshProcessConnector` either can
233-
not resolve target hostnames because it has no direct access to the internet or
234-
if it should not resolve target hostnames because its outgoing DNS traffic might
235-
be intercepted.
374+
The default *remote DNS resolution* is useful if your local `SshProcessConnector`
375+
or `SshSocksConnector` either can not resolve target hostnames because it has no
376+
direct access to the internet or if it should not resolve target hostnames
377+
because its outgoing DNS traffic might be intercepted.
236378

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

264-
#### Password authentication
406+
### Password authentication
265407

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

288430
```php
289-
$proxy = new SshProcessConnector('user:pass@example.com', $connector);
431+
$proxy = new SshProcessConnector('user:pass@example.com', $loop);
432+
// or
433+
$proxy = new SshSocksConnector('user:pass@example.com', $loop);
290434
```
291435

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

306450
$proxy = new SshProcessConnector(
307451
rawurlencode($user) . ':' . rawurlencode($pass) . '@example.com:2222',
308-
$connector
452+
$loop
309453
);
310454
```
311455

composer.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515
},
1616
"require": {
1717
"php": ">=5.3",
18+
"clue/socks-react": "^1.0",
1819
"react/child-process": "^0.5",
19-
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
20+
"react/event-loop": "^1.0 || ^0.5",
2021
"react/promise": "^2.1 || ^1.2.1",
21-
"react/socket": "^1.0 || ^0.8.0",
22+
"react/socket": "^1.1",
2223
"react/stream": "^1.0 || ^0.7.2"
2324
},
2425
"require-dev": {
25-
"phpunit/phpunit": "^7.4 || ^6.4 || ^5.0 || ^4.8.36",
26-
"clue/block-react": "^1.3"
26+
"clue/block-react": "^1.3",
27+
"phpunit/phpunit": "^7.4 || ^6.4 || ^5.0 || ^4.8.36"
2728
}
2829
}

examples/11-proxy-https.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
// A simple example which requests https://google.com/ through an SSH proxy server.
4+
// The proxy can be given through the SSH_PROXY env and defaults to localhost otherwise.
5+
//
6+
// You can assign the SSH_PROXY environment and prefix this with a space to make
7+
// sure your login credentials are not stored in your bash history like this:
8+
//
9+
// $ export SSH_PROXY=user:secret@example.com
10+
// $ php examples/11-proxy-https.php
11+
//
12+
// For illustration purposes only. If you want to send HTTP requests in a real
13+
// world project, take a look at https://github.com/clue/reactphp-buzz#http-proxy
14+
15+
use Clue\React\SshProxy\SshSocksConnector;
16+
use React\Socket\Connector;
17+
use React\Socket\ConnectionInterface;
18+
19+
require __DIR__ . '/../vendor/autoload.php';
20+
21+
$url = getenv('SSH_PROXY') !== false ? getenv('SSH_PROXY') : 'ssh://localhost:22';
22+
23+
$loop = React\EventLoop\Factory::create();
24+
25+
$proxy = new SshSocksConnector($url, $loop);
26+
$connector = new Connector($loop, array(
27+
'tcp' => $proxy,
28+
'timeout' => 3.0,
29+
'dns' => false
30+
));
31+
32+
$connector->connect('tls://google.com:443')->then(function (ConnectionInterface $stream) {
33+
$stream->write("GET / HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n");
34+
$stream->on('data', function ($chunk) {
35+
echo $chunk;
36+
});
37+
}, 'printf');
38+
39+
$loop->run();

src/SshProcessConnector.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ public function __construct($uri, LoopInterface $loop)
4646
}
4747
$this->cmd .= 'ssh -vv ';
4848

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

0 commit comments

Comments
 (0)