Skip to content

Commit f425767

Browse files
authored
Merge pull request #60 from clue-labs/connector-source
Server now passes client source address to Connector
2 parents 4442bcf + 2960c45 commit f425767

File tree

3 files changed

+96
-17
lines changed

3 files changed

+96
-17
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ of the actual application level protocol, such as HTTP, SMTP, IMAP, Telnet etc.
2121
* [Proxy chaining](#proxy-chaining)
2222
* [Connection timeout](#connection-timeout)
2323
* [Server](#server)
24+
* [Server connector](#server-connector)
2425
* [Protocol version](#server-protocol-version)
2526
* [Authentication](#server-authentication)
2627
* [Proxy chaining](#server-proxy-chaining)
@@ -562,6 +563,11 @@ $socket = new Socket($port, $loop);
562563
$server = new Server($loop, $socket);
563564
```
564565

566+
#### Server connector
567+
568+
The `Server` uses an instance of the [`ConnectorInterface`](#connectorinterface)
569+
to establish outgoing connections for each incoming connection request.
570+
565571
If you need custom connector settings (DNS resolution, timeouts etc.), you can explicitly pass a
566572
custom instance of the [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface):
567573

@@ -579,6 +585,18 @@ $connector = new DnsConnector(
579585
$server = new Server($loop, $socket, $connector);
580586
```
581587

588+
If you want to forward the outgoing connection through another SOCKS proxy, you
589+
may also pass a [`Client`](#client) instance as a connector, see also
590+
[server proxy chaining](#server-proxy-chaining) for more details.
591+
592+
Internally, the `Server` uses the normal [`connect()`](#connect) method, but
593+
it also passes the original client IP as the `?source={remote}` parameter.
594+
The `source` parameter contains the full remote URI, including the protocol
595+
and any authentication details, for example `socks5://user:pass@1.2.3.4:5678`.
596+
You can use this parameter for logging purposes or to restrict connection
597+
requests for certain clients by providing a custom implementation of the
598+
[`ConnectorInterface`](#connectorinterface).
599+
582600
#### Server protocol version
583601

584602
The `Server` supports all protocol versions (SOCKS4, SOCKS4a and SOCKS5) by default.

src/Server.php

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,23 @@ public function handleSocks4(ConnectionInterface $stream, $protocolVersion, Stre
172172
// suppliying hostnames is only allowed for SOCKS4a (or automatically detected version)
173173
$supportsHostname = ($protocolVersion === null || $protocolVersion === '4a');
174174

175+
$remote = $stream->getRemoteAddress();
176+
if ($remote !== null) {
177+
// remove transport scheme and prefix socks4:// instead
178+
if (($pos = strpos($remote, '://')) !== false) {
179+
$remote = substr($remote, $pos + 3);
180+
}
181+
$remote = 'socks4://' . $remote;
182+
}
183+
175184
$that = $this;
176185
return $reader->readByteAssert(0x01)->then(function () use ($reader) {
177186
return $reader->readBinary(array(
178187
'port' => 'n',
179188
'ipLong' => 'N',
180189
'null' => 'C'
181190
));
182-
})->then(function ($data) use ($reader, $supportsHostname) {
191+
})->then(function ($data) use ($reader, $supportsHostname, $remote) {
183192
if ($data['null'] !== 0x00) {
184193
throw new Exception('Not a null byte');
185194
}
@@ -191,12 +200,12 @@ public function handleSocks4(ConnectionInterface $stream, $protocolVersion, Stre
191200
}
192201
if ($data['ipLong'] < 256 && $supportsHostname) {
193202
// invalid IP => probably a SOCKS4a request which appends the hostname
194-
return $reader->readStringNull()->then(function ($string) use ($data){
195-
return array($string, $data['port']);
203+
return $reader->readStringNull()->then(function ($string) use ($data, $remote){
204+
return array($string, $data['port'], $remote);
196205
});
197206
} else {
198207
$ip = long2ip($data['ipLong']);
199-
return array($ip, $data['port']);
208+
return array($ip, $data['port'], $remote);
200209
}
201210
})->then(function ($target) use ($stream, $that) {
202211
return $that->connectTarget($stream, $target)->then(function (ConnectionInterface $remote) use ($stream){
@@ -215,14 +224,24 @@ public function handleSocks4(ConnectionInterface $stream, $protocolVersion, Stre
215224

216225
public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamReader $reader)
217226
{
227+
$remote = $stream->getRemoteAddress();
228+
if ($remote !== null) {
229+
// remove transport scheme and prefix socks5:// instead
230+
if (($pos = strpos($remote, '://')) !== false) {
231+
$remote = substr($remote, $pos + 3);
232+
}
233+
$remote = 'socks5://' . $remote;
234+
}
235+
218236
$that = $this;
219237
return $reader->readByte()->then(function ($num) use ($reader) {
220238
// $num different authentication mechanisms offered
221239
return $reader->readLength($num);
222-
})->then(function ($methods) use ($reader, $stream, $auth) {
240+
})->then(function ($methods) use ($reader, $stream, $auth, &$remote) {
223241
if ($auth === null && strpos($methods,"\x00") !== false) {
224242
// accept "no authentication"
225243
$stream->write(pack('C2', 0x05, 0x00));
244+
226245
return 0x00;
227246
} else if ($auth !== null && strpos($methods,"\x02") !== false) {
228247
// username/password authentication (RFC 1929) sub negotiation
@@ -231,18 +250,15 @@ public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamRead
231250
return $reader->readByte();
232251
})->then(function ($length) use ($reader) {
233252
return $reader->readLength($length);
234-
})->then(function ($username) use ($reader, $auth, $stream) {
253+
})->then(function ($username) use ($reader, $auth, $stream, &$remote) {
235254
return $reader->readByte()->then(function ($length) use ($reader) {
236255
return $reader->readLength($length);
237-
})->then(function ($password) use ($username, $auth, $stream) {
256+
})->then(function ($password) use ($username, $auth, $stream, &$remote) {
238257
// username and password given => authenticate
239-
$remote = $stream->getRemoteAddress();
258+
259+
// prefix username/password to remote URI
240260
if ($remote !== null) {
241-
// remove transport scheme and prefix socks5:// instead
242-
if (($pos = strpos($remote, '://')) !== false) {
243-
$remote = substr($remote, $pos + 3);
244-
}
245-
$remote = 'socks5://' . rawurlencode($username) . ':' . rawurlencode($password) . '@' . $remote;
261+
$remote = str_replace('://', '://' . rawurlencode($username) . ':' . rawurlencode($password) . '@', $remote);
246262
}
247263

248264
return $auth($username, $password, $remote)->then(function () use ($stream, $username) {
@@ -296,9 +312,9 @@ public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamRead
296312
} else {
297313
throw new UnexpectedValueException('Invalid target type');
298314
}
299-
})->then(function ($host) use ($reader) {
300-
return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host) {
301-
return array($host, $data['port']);
315+
})->then(function ($host) use ($reader, &$remote) {
316+
return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host, &$remote) {
317+
return array($host, $data['port'], $remote);
302318
});
303319
})->then(function ($target) use ($that, $stream) {
304320
return $that->connectTarget($stream, $target);
@@ -322,14 +338,18 @@ public function connectTarget(ConnectionInterface $stream, array $target)
322338
if (strpos($uri, ':') !== false) {
323339
$uri = '[' . $uri . ']';
324340
}
325-
$uri = $uri . ':' . $target[1];
341+
$uri .= ':' . $target[1];
326342

327343
// validate URI so a string hostname can not pass excessive URI parts
328344
$parts = parse_url('tcp://' . $uri);
329345
if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || count($parts) !== 3) {
330346
return Promise\reject(new InvalidArgumentException('Invalid target URI given'));
331347
}
332348

349+
if (isset($target[2])) {
350+
$uri .= '?source=' . rawurlencode($target[2]);
351+
}
352+
333353
$stream->emit('target', $target);
334354
$that = $this;
335355
$connecting = $this->connector->connect($uri);

tests/ServerTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,19 @@ public function testConnectWillCreateConnection()
9393
$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
9494
}
9595

96+
public function testConnectWillCreateConnectionWithSourceUri()
97+
{
98+
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
99+
100+
$promise = new Promise(function () { });
101+
102+
$this->connector->expects($this->once())->method('connect')->with('google.com:80?source=socks5%3A%2F%2F10.20.30.40%3A5060')->willReturn($promise);
103+
104+
$promise = $this->server->connectTarget($stream, array('google.com', 80, 'socks5://10.20.30.40:5060'));
105+
106+
$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
107+
}
108+
96109
public function testConnectWillRejectIfConnectionFails()
97110
{
98111
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
@@ -178,6 +191,20 @@ public function testHandleSocks4aConnectionWithHostnameWillEstablishOutgoingConn
178191
$connection->emit('data', array("\x04\x01" . "\x00\x50" . "\x00\x00\x00\x01" . "\x00" . "example.com" . "\x00"));
179192
}
180193

194+
public function testHandleSocks4aConnectionWithHostnameAndSourceAddressWillEstablishOutgoingConnection()
195+
{
196+
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'getRemoteAddress'))->getMock();
197+
$connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://10.20.30.40:5060');
198+
199+
$promise = new Promise(function () { });
200+
201+
$this->connector->expects($this->once())->method('connect')->with('example.com:80?source=socks4%3A%2F%2F10.20.30.40%3A5060')->willReturn($promise);
202+
203+
$this->server->onConnection($connection);
204+
205+
$connection->emit('data', array("\x04\x01" . "\x00\x50" . "\x00\x00\x00\x01" . "\x00" . "example.com" . "\x00"));
206+
}
207+
181208
public function testHandleSocks4aConnectionWithInvalidHostnameWillNotEstablishOutgoingConnection()
182209
{
183210
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end'))->getMock();
@@ -202,6 +229,20 @@ public function testHandleSocks5ConnectionWithIpv4WillEstablishOutgoingConnectio
202229
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x01" . pack('N', ip2long('127.0.0.1')) . "\x00\x50"));
203230
}
204231

232+
public function testHandleSocks5ConnectionWithIpv4AndSourceAddressWillEstablishOutgoingConnection()
233+
{
234+
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write', 'getRemoteAddress'))->getMock();
235+
$connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://10.20.30.40:5060');
236+
237+
$promise = new Promise(function () { });
238+
239+
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:80?source=socks5%3A%2F%2F10.20.30.40%3A5060')->willReturn($promise);
240+
241+
$this->server->onConnection($connection);
242+
243+
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x01" . pack('N', ip2long('127.0.0.1')) . "\x00\x50"));
244+
}
245+
205246
public function testHandleSocks5ConnectionWithIpv6WillEstablishOutgoingConnection()
206247
{
207248
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write'))->getMock();

0 commit comments

Comments
 (0)