Skip to content

Commit c17fa1a

Browse files
committed
Add SshSocksConnector to support local SSH SOCKS proxy server
1 parent 72d70dc commit c17fa1a

File tree

7 files changed

+961
-41
lines changed

7 files changed

+961
-41
lines changed

README.md

Lines changed: 169 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,95 @@ 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+
> *Security note for multi-user systems*: This class will spawn the SSH client
199+
process in local SOCKS server mode and will accept connections on the
200+
localhost interface only. If you're running on a multi-user system, other
201+
users on the same system may be able to connect to this proxy server and
202+
create connections over it. If this applies to your deployment, you're
203+
recommended to use the [`SshProcessConnector](#sshprocessconnector) instead.
204+
205+
This is one of the two main classes in this package.
206+
Because it implements ReactPHP's standard
207+
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface),
208+
it can simply be used in place of a normal connector.
209+
Accordingly, it provides only a single public method, the
210+
[`connect()`](https://github.com/reactphp/socket#connect) method.
211+
The `connect(string $uri): PromiseInterface<ConnectionInterface, Exception>`
212+
method can be used to establish a streaming connection.
213+
It returns a [Promise](https://github.com/reactphp/promise) which either
214+
fulfills with a [ConnectionInterface](https://github.com/reactphp/socket#connectioninterface)
215+
on success or rejects with an `Exception` on error.
216+
217+
This makes it fairly simple to add SSH proxy support to pretty much any
218+
higher-level component:
219+
220+
```diff
221+
- $client = new SomeClient($connector);
222+
+ $proxy = new SshSocksConnector('user@example.com', $loop);
223+
+ $client = new SomeClient($proxy);
224+
```
225+
226+
## Usage
227+
228+
### Plain TCP connections
135229

136230
SSH proxy servers are commonly used to issue HTTPS requests to your destination.
137231
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:
232+
project is actually inherently a general-purpose plain TCP/IP connector.
233+
As documented above, you can simply invoke the `connect()` method to establish
234+
a streaming plain TCP/IP connection on the `SshProcessConnector` or `SshSocksConnector`
235+
and use any higher level protocol like so:
141236

142237
```php
143-
$proxy = new SshProcessConnector('user@example.com', $connector);
238+
$proxy = new SshProcessConnector('user@example.com', $loop);
239+
// or
240+
$proxy = new SshSocksConnector('user@example.com', $loop);
144241

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

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):
250+
You can either use the `SshProcessConnector` or `SshSocksConnector` directly or you
251+
may want to wrap this connector in ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector):
155252

156253
```php
157254
$connector = new Connector($loop, array(
@@ -167,25 +264,60 @@ $connector->connect('tcp://smtp.googlemail.com:587')->then(function (ConnectionI
167264
});
168265
```
169266

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

175-
#### HTTP requests
305+
### HTTP requests
176306

177307
HTTP operates on a higher layer than this low-level SSH proxy implementation.
178308
If you want to issue HTTP requests, you can add a dependency for
179309
[clue/reactphp-buzz](https://github.com/clue/reactphp-buzz).
180310
It can interact with this library by issuing all HTTP requests through your SSH
181311
proxy server, similar to how it can issue
182312
[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.
313+
When using the `SshSocksConnector` (recommended), this works for both plain HTTP
314+
and TLS-encrypted HTTPS requests. When using the `SshProcessConnector`, this only
315+
works for plaintext HTTP requests.
184316

185-
#### Connection timeout
317+
### Connection timeout
186318

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

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

219-
#### DNS resolution
351+
### DNS resolution
220352

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*).
353+
By default, neither the `SshProcessConnector` nor the `SshSocksConnector` perform
354+
any DNS resolution at all and simply forwards any hostname you're trying to
355+
connect to the remote proxy server. The remote proxy server is thus responsible
356+
for looking up any hostnames via DNS (this default mode is thus called *remote DNS resolution*).
225357

226358
As an alternative, you can also send the destination IP to the remote proxy
227359
server.
228360
In this mode you either have to stick to using IPs only (which is ofen unfeasable)
229361
or perform any DNS lookups locally and only transmit the resolved destination IPs
230362
(this mode is thus called *local DNS resolution*).
231363

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.
364+
The default *remote DNS resolution* is useful if your local `SshProcessConnector`
365+
or `SshSocksConnector` either can not resolve target hostnames because it has no
366+
direct access to the internet or if it should not resolve target hostnames
367+
because its outgoing DNS traffic might be intercepted.
236368

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

264-
#### Password authentication
396+
### Password authentication
265397

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

288420
```php
289-
$proxy = new SshProcessConnector('user:pass@example.com', $connector);
421+
$proxy = new SshProcessConnector('user:pass@example.com', $loop);
422+
// or
423+
$proxy = new SshSocksConnector('user:pass@example.com', $loop);
290424
```
291425

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

306440
$proxy = new SshProcessConnector(
307441
rawurlencode($user) . ':' . rawurlencode($pass) . '@example.com:2222',
308-
$connector
442+
$loop
309443
);
310444
```
311445

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)