Skip to content

Commit 9a7c21d

Browse files
committed
Report matching SOCKS5 error codes for server side connection errors
1 parent 7a6eea8 commit 9a7c21d

2 files changed

Lines changed: 136 additions & 8 deletions

File tree

src/Server.php

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,28 @@
1414
use \UnexpectedValueException;
1515
use \InvalidArgumentException;
1616
use \Exception;
17+
use React\Promise\Timer\TimeoutException;
1718

1819
class Server extends EventEmitter
1920
{
21+
// the following error codes are only used for SOCKS5 only
22+
/** @internal */
23+
const ERROR_GENERAL = 0x01;
24+
/** @internal */
25+
const ERROR_NOT_ALLOWED_BY_RULESET = 0x02;
26+
/** @internal */
27+
const ERROR_NETWORK_UNREACHABLE = 0x03;
28+
/** @internal */
29+
const ERROR_HOST_UNREACHABLE = 0x04;
30+
/** @internal */
31+
const ERROR_CONNECTION_REFUSED = 0x05;
32+
/** @internal */
33+
const ERROR_TTL = 0x06;
34+
/** @internal */
35+
const ERROR_COMMAND_UNSUPPORTED = 0x07;
36+
/** @internal */
37+
const ERROR_ADDRESS_UNSUPPORTED = 0x08;
38+
2039
protected $loop;
2140

2241
private $connector;
@@ -274,7 +293,7 @@ public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamRead
274293
});
275294
} else {
276295
// reject all offered authentication methods
277-
$stream->end(pack('C2', 0x05, 0xFF));
296+
$stream->write(pack('C2', 0x05, 0xFF));
278297
throw new UnexpectedValueException('No acceptable authentication mechanism found');
279298
}
280299
})->then(function ($method) use ($reader, $stream) {
@@ -289,7 +308,7 @@ public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamRead
289308
throw new UnexpectedValueException('Invalid SOCKS version');
290309
}
291310
if ($data['command'] !== 0x01) {
292-
throw new UnexpectedValueException('Only CONNECT requests supported');
311+
throw new UnexpectedValueException('Only CONNECT requests supported', Server::ERROR_COMMAND_UNSUPPORTED);
293312
}
294313
// if ($data['null'] !== 0x00) {
295314
// throw new UnexpectedValueException('Reserved byte has to be NULL');
@@ -310,7 +329,7 @@ public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamRead
310329
return inet_ntop($addr);
311330
});
312331
} else {
313-
throw new UnexpectedValueException('Invalid target type');
332+
throw new UnexpectedValueException('Invalid address type', Server::ERROR_ADDRESS_UNSUPPORTED);
314333
}
315334
})->then(function ($host) use ($reader, &$remote) {
316335
return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host, &$remote) {
@@ -319,14 +338,13 @@ public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamRead
319338
})->then(function ($target) use ($that, $stream) {
320339
return $that->connectTarget($stream, $target);
321340
}, function($error) use ($stream) {
322-
throw new UnexpectedValueException('SOCKS5 protocol error',0,$error);
341+
throw new UnexpectedValueException('SOCKS5 protocol error', $error->getCode(), $error);
323342
})->then(function (ConnectionInterface $remote) use ($stream) {
324343
$stream->write(pack('C4Nn', 0x05, 0x00, 0x00, 0x01, 0, 0));
325344

326345
return $remote;
327346
}, function(Exception $error) use ($stream){
328-
$code = 0x01;
329-
$stream->end(pack('C4Nn', 0x05, $code, 0x00, 0x01, 0, 0));
347+
$stream->write(pack('C4Nn', 0x05, $error->getCode() === 0 ? Server::ERROR_GENERAL : $error->getCode(), 0x00, 0x01, 0, 0));
330348

331349
throw $error;
332350
});
@@ -378,7 +396,25 @@ public function connectTarget(ConnectionInterface $stream, array $target)
378396

379397
return $remote;
380398
}, function(Exception $error) {
381-
throw new UnexpectedValueException('Unable to connect to remote target', 0, $error);
399+
// default to general/unknown error
400+
$code = Server::ERROR_GENERAL;
401+
402+
// map common socket error conditions to limited list of SOCKS error codes
403+
if ((defined('SOCKET_EACCES') && $error->getCode() === SOCKET_EACCES) || $error->getCode() === 13) {
404+
$code = Server::ERROR_NOT_ALLOWED_BY_RULESET;
405+
} elseif ((defined('SOCKET_EHOSTUNREACH') && $error->getCode() === SOCKET_EHOSTUNREACH) || $error->getCode() === 113) {
406+
$code = Server::ERROR_HOST_UNREACHABLE;
407+
} elseif ((defined('SOCKET_ENETUNREACH') && $error->getCode() === SOCKET_ENETUNREACH) || $error->getCode() === 101) {
408+
$code = Server::ERROR_NETWORK_UNREACHABLE;
409+
} elseif ((defined('SOCKET_ECONNREFUSED') && $error->getCode() === SOCKET_ECONNREFUSED) || $error->getCode() === 111 || $error->getMessage() === 'Connection refused') {
410+
// Socket component does not currently assign an error code for this, so we have to resort to checking the exception message
411+
$code = Server::ERROR_CONNECTION_REFUSED;
412+
} elseif ((defined('SOCKET_ETIMEDOUT') && $error->getCode() === SOCKET_ETIMEDOUT) || $error->getCode() === 110 || $error instanceof TimeoutException) {
413+
// Socket component does not currently assign an error code for this, but we can rely on the TimeoutException
414+
$code = Server::ERROR_TTL;
415+
}
416+
417+
throw new UnexpectedValueException('Unable to connect to remote target', $code, $error);
382418
});
383419
}
384420
}

tests/ServerTest.php

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use Clue\React\Socks\Server;
44
use React\Promise\Promise;
5+
use React\Promise\Timer\TimeoutException;
56

67
class ServerTest extends TestCase
78
{
@@ -154,6 +155,67 @@ public function testConnectWillAbortIfPromiseIsCancelled()
154155
$promise->then(null, $this->expectCallableOnce());
155156
}
156157

158+
public function provideConnectionErrors()
159+
{
160+
return array(
161+
array(
162+
new RuntimeException('', SOCKET_EACCES),
163+
Server::ERROR_NOT_ALLOWED_BY_RULESET
164+
),
165+
array(
166+
new RuntimeException('', SOCKET_ENETUNREACH),
167+
Server::ERROR_NETWORK_UNREACHABLE
168+
),
169+
array(
170+
new RuntimeException('', SOCKET_EHOSTUNREACH),
171+
Server::ERROR_HOST_UNREACHABLE,
172+
),
173+
array(
174+
new RuntimeException('', SOCKET_ECONNREFUSED),
175+
Server::ERROR_CONNECTION_REFUSED
176+
),
177+
array(
178+
new RuntimeException('Connection refused'),
179+
Server::ERROR_CONNECTION_REFUSED
180+
),
181+
array(
182+
new RuntimeException('', SOCKET_ETIMEDOUT),
183+
Server::ERROR_TTL
184+
),
185+
array(
186+
new TimeoutException(1.0),
187+
Server::ERROR_TTL
188+
),
189+
array(
190+
new RuntimeException(),
191+
Server::ERROR_GENERAL
192+
)
193+
);
194+
}
195+
196+
/**
197+
* @dataProvider provideConnectionErrors
198+
* @param Exception $error
199+
* @param int $expectedCode
200+
*/
201+
public function testConnectWillReturnMappedSocks5ErrorCodeFromConnector($error, $expectedCode)
202+
{
203+
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
204+
205+
$promise = \React\Promise\reject($error);
206+
207+
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
208+
209+
$promise = $this->server->connectTarget($stream, array('google.com', 80));
210+
211+
$code = null;
212+
$promise->then(null, function ($error) use (&$code) {
213+
$code = $error->getCode();
214+
});
215+
216+
$this->assertEquals($expectedCode, $code);
217+
}
218+
157219
public function testHandleSocksConnectionWillEndOnInvalidData()
158220
{
159221
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end'))->getMock();
@@ -269,14 +331,44 @@ public function testHandleSocks5ConnectionWithHostnameWillEstablishOutgoingConne
269331
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x03\x0B" . "example.com" . "\x00\x50"));
270332
}
271333

272-
public function testHandleSocks5ConnectionWithInvalidHostnameWillNotEstablishOutgoingConnection()
334+
public function testHandleSocks5ConnectionWithConnectorRefusedWillReturnReturnRefusedError()
335+
{
336+
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write'))->getMock();
337+
338+
$promise = \React\Promise\reject(new RuntimeException('Connection refused'));
339+
340+
$this->connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($promise);
341+
342+
$this->server->onConnection($connection);
343+
344+
$connection->expects($this->exactly(2))->method('write')->withConsecutive(array("\x05\x00"), array("\x05\x05" . "\x00\x01\x00\x00\x00\x00\x00\x00"));
345+
346+
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x03\x0B" . "example.com" . "\x00\x50"));
347+
}
348+
349+
public function testHandleSocks5UdpCommandWillNotEstablishOutgoingConnectionAndReturnCommandError()
273350
{
274351
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write'))->getMock();
275352

276353
$this->connector->expects($this->never())->method('connect');
277354

278355
$this->server->onConnection($connection);
279356

357+
$connection->expects($this->exactly(2))->method('write')->withConsecutive(array("\x05\x00"), array("\x05\x07" . "\x00\x01\x00\x00\x00\x00\x00\x00"));
358+
359+
$connection->emit('data', array("\x05\x01\x00" . "\x05\x03\x00\x03\x0B" . "example.com" . "\x00\x50"));
360+
}
361+
362+
public function testHandleSocks5ConnectionWithInvalidHostnameWillNotEstablishOutgoingConnectionAndReturnGeneralError()
363+
{
364+
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write'))->getMock();
365+
366+
$this->connector->expects($this->never())->method('connect');
367+
368+
$this->server->onConnection($connection);
369+
370+
$connection->expects($this->exactly(2))->method('write')->withConsecutive(array("\x05\x00"), array("\x05\x01" . "\x00\x01\x00\x00\x00\x00\x00\x00"));
371+
280372
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x03\x15" . "tls://example.com:80?" . "\x00\x50"));
281373
}
282374

0 commit comments

Comments
 (0)