Skip to content

Commit 5577fdd

Browse files
committed
Report matching socket error codes for SOCKS5 server error codes
1 parent 752d3ba commit 5577fdd

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

src/Client.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,27 @@ private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamR
324324
throw new Exception('Invalid SOCKS response');
325325
}
326326
if ($data['status'] !== 0x00) {
327-
throw new RuntimeException('Proxy refused connection with SOCKS error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
327+
// map limited list of SOCKS error codes to common socket error conditions
328+
// @link https://tools.ietf.org/html/rfc1928#section-6
329+
if ($data['status'] === Server::ERROR_GENERAL) {
330+
throw new RuntimeException('SOCKS server reported a general server failure (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
331+
} elseif ($data['status'] === Server::ERROR_NOT_ALLOWED_BY_RULESET) {
332+
throw new RuntimeException('SOCKS server reported connection is not allowed by ruleset (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
333+
} elseif ($data['status'] === Server::ERROR_NETWORK_UNREACHABLE) {
334+
throw new RuntimeException('SOCKS server reported network unreachable (ENETUNREACH)', defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101);
335+
} elseif ($data['status'] === Server::ERROR_HOST_UNREACHABLE) {
336+
throw new RuntimeException('SOCKS server reported host unreachable (EHOSTUNREACH)', defined('SOCKET_EHOSTUNREACH') ? SOCKET_EHOSTUNREACH : 113);
337+
} elseif ($data['status'] === Server::ERROR_CONNECTION_REFUSED) {
338+
throw new RuntimeException('SOCKS server reported connection refused (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
339+
} elseif ($data['status'] === Server::ERROR_TTL) {
340+
throw new RuntimeException('SOCKS server reported TTL/timeout expired (ETIMEDOUT)', defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110);
341+
} elseif ($data['status'] === Server::ERROR_COMMAND_UNSUPPORTED) {
342+
throw new RuntimeException('SOCKS server does not support the CONNECT command (EPROTO)', defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71);
343+
} elseif ($data['status'] === Server::ERROR_ADDRESS_UNSUPPORTED) {
344+
throw new RuntimeException('SOCKS server does not support this address type (EPROTO)', defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71);
345+
}
346+
347+
throw new RuntimeException('SOCKS server reported an unassigned error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
328348
}
329349
if ($data['type'] === 0x01) {
330350
// IPv4 address => skip IP and port

tests/ClientTest.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

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

67
class ClientTest extends TestCase
78
{
@@ -242,4 +243,69 @@ public function testEmitSocks5DataErrorDuringSessionWillRejectConnection()
242243

243244
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNREFUSED));
244245
}
246+
247+
public function provideConnectionErrors()
248+
{
249+
return array(
250+
array(
251+
Server::ERROR_GENERAL,
252+
SOCKET_ECONNREFUSED
253+
),
254+
array(
255+
Server::ERROR_NOT_ALLOWED_BY_RULESET,
256+
SOCKET_EACCES
257+
),
258+
array(
259+
Server::ERROR_NETWORK_UNREACHABLE,
260+
SOCKET_ENETUNREACH
261+
),
262+
array(
263+
Server::ERROR_HOST_UNREACHABLE,
264+
SOCKET_EHOSTUNREACH
265+
),
266+
array(
267+
Server::ERROR_CONNECTION_REFUSED,
268+
SOCKET_ECONNREFUSED
269+
),
270+
array(
271+
Server::ERROR_TTL,
272+
SOCKET_ETIMEDOUT
273+
),
274+
array(
275+
Server::ERROR_COMMAND_UNSUPPORTED,
276+
SOCKET_EPROTO
277+
),
278+
array(
279+
Server::ERROR_ADDRESS_UNSUPPORTED,
280+
SOCKET_EPROTO
281+
),
282+
array(
283+
200,
284+
SOCKET_ECONNREFUSED
285+
)
286+
);
287+
}
288+
289+
/**
290+
* @dataProvider provideConnectionErrors
291+
* @param int $error
292+
* @param int $expectedCode
293+
*/
294+
public function testEmitSocks5DataErrorMapsToExceptionCode($error, $expectedCode)
295+
{
296+
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
297+
$stream->expects($this->once())->method('close');
298+
299+
$promise = \React\Promise\resolve($stream);
300+
301+
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
302+
303+
$this->client = new Client('socks5://127.0.0.1:1080', $this->connector);
304+
305+
$promise = $this->client->connect('google.com:80');
306+
307+
$stream->emit('data', array("\x05\x00" . "\x05" . chr($error) . "\x00\x00"));
308+
309+
$promise->then(null, $this->expectCallableOnceWithExceptionCode($expectedCode));
310+
}
245311
}

0 commit comments

Comments
 (0)