Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ of the actual application level protocol, such as HTTP, SMTP, IMAP, Telnet etc.
* [Proxy chaining](#proxy-chaining)
* [Connection timeout](#connection-timeout)
* [Server](#server)
* [Server connector](#server-connector)
* [Protocol version](#server-protocol-version)
* [Authentication](#server-authentication)
* [Proxy chaining](#server-proxy-chaining)
Expand Down Expand Up @@ -562,6 +563,11 @@ $socket = new Socket($port, $loop);
$server = new Server($loop, $socket);
```

#### Server connector

The `Server` uses an instance of the [`ConnectorInterface`](#connectorinterface)
to establish outgoing connections for each incoming connection request.

If you need custom connector settings (DNS resolution, timeouts etc.), you can explicitly pass a
custom instance of the [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface):

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

If you want to forward the outgoing connection through another SOCKS proxy, you
may also pass a [`Client`](#client) instance as a connector, see also
[server proxy chaining](#server-proxy-chaining) for more details.

Internally, the `Server` uses the normal [`connect()`](#connect) method, but
it also passes the original client IP as the `?source={remote}` parameter.
The `source` parameter contains the full remote URI, including the protocol
and any authentication details, for example `socks5://user:pass@1.2.3.4:5678`.
You can use this parameter for logging purposes or to restrict connection
requests for certain clients by providing a custom implementation of the
[`ConnectorInterface`](#connectorinterface).

#### Server protocol version

The `Server` supports all protocol versions (SOCKS4, SOCKS4a and SOCKS5) by default.
Expand Down
54 changes: 37 additions & 17 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,23 @@ public function handleSocks4(ConnectionInterface $stream, $protocolVersion, Stre
// suppliying hostnames is only allowed for SOCKS4a (or automatically detected version)
$supportsHostname = ($protocolVersion === null || $protocolVersion === '4a');

$remote = $stream->getRemoteAddress();
if ($remote !== null) {
// remove transport scheme and prefix socks4:// instead
if (($pos = strpos($remote, '://')) !== false) {
$remote = substr($remote, $pos + 3);
}
$remote = 'socks4://' . $remote;
}

$that = $this;
return $reader->readByteAssert(0x01)->then(function () use ($reader) {
return $reader->readBinary(array(
'port' => 'n',
'ipLong' => 'N',
'null' => 'C'
));
})->then(function ($data) use ($reader, $supportsHostname) {
})->then(function ($data) use ($reader, $supportsHostname, $remote) {
if ($data['null'] !== 0x00) {
throw new Exception('Not a null byte');
}
Expand All @@ -191,12 +200,12 @@ public function handleSocks4(ConnectionInterface $stream, $protocolVersion, Stre
}
if ($data['ipLong'] < 256 && $supportsHostname) {
// invalid IP => probably a SOCKS4a request which appends the hostname
return $reader->readStringNull()->then(function ($string) use ($data){
return array($string, $data['port']);
return $reader->readStringNull()->then(function ($string) use ($data, $remote){
return array($string, $data['port'], $remote);
});
} else {
$ip = long2ip($data['ipLong']);
return array($ip, $data['port']);
return array($ip, $data['port'], $remote);
}
})->then(function ($target) use ($stream, $that) {
return $that->connectTarget($stream, $target)->then(function (ConnectionInterface $remote) use ($stream){
Expand All @@ -215,14 +224,24 @@ public function handleSocks4(ConnectionInterface $stream, $protocolVersion, Stre

public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamReader $reader)
{
$remote = $stream->getRemoteAddress();
if ($remote !== null) {
// remove transport scheme and prefix socks5:// instead
if (($pos = strpos($remote, '://')) !== false) {
$remote = substr($remote, $pos + 3);
}
$remote = 'socks5://' . $remote;
}

$that = $this;
return $reader->readByte()->then(function ($num) use ($reader) {
// $num different authentication mechanisms offered
return $reader->readLength($num);
})->then(function ($methods) use ($reader, $stream, $auth) {
})->then(function ($methods) use ($reader, $stream, $auth, &$remote) {
if ($auth === null && strpos($methods,"\x00") !== false) {
// accept "no authentication"
$stream->write(pack('C2', 0x05, 0x00));

return 0x00;
} else if ($auth !== null && strpos($methods,"\x02") !== false) {
// username/password authentication (RFC 1929) sub negotiation
Expand All @@ -231,18 +250,15 @@ public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamRead
return $reader->readByte();
})->then(function ($length) use ($reader) {
return $reader->readLength($length);
})->then(function ($username) use ($reader, $auth, $stream) {
})->then(function ($username) use ($reader, $auth, $stream, &$remote) {
return $reader->readByte()->then(function ($length) use ($reader) {
return $reader->readLength($length);
})->then(function ($password) use ($username, $auth, $stream) {
})->then(function ($password) use ($username, $auth, $stream, &$remote) {
// username and password given => authenticate
$remote = $stream->getRemoteAddress();

// prefix username/password to remote URI
if ($remote !== null) {
// remove transport scheme and prefix socks5:// instead
if (($pos = strpos($remote, '://')) !== false) {
$remote = substr($remote, $pos + 3);
}
$remote = 'socks5://' . rawurlencode($username) . ':' . rawurlencode($password) . '@' . $remote;
$remote = str_replace('://', '://' . rawurlencode($username) . ':' . rawurlencode($password) . '@', $remote);
}

return $auth($username, $password, $remote)->then(function () use ($stream, $username) {
Expand Down Expand Up @@ -296,9 +312,9 @@ public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamRead
} else {
throw new UnexpectedValueException('Invalid target type');
}
})->then(function ($host) use ($reader) {
return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host) {
return array($host, $data['port']);
})->then(function ($host) use ($reader, &$remote) {
return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host, &$remote) {
return array($host, $data['port'], $remote);
});
})->then(function ($target) use ($that, $stream) {
return $that->connectTarget($stream, $target);
Expand All @@ -322,14 +338,18 @@ public function connectTarget(ConnectionInterface $stream, array $target)
if (strpos($uri, ':') !== false) {
$uri = '[' . $uri . ']';
}
$uri = $uri . ':' . $target[1];
$uri .= ':' . $target[1];

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

if (isset($target[2])) {
$uri .= '?source=' . rawurlencode($target[2]);
}

$stream->emit('target', $target);
$that = $this;
$connecting = $this->connector->connect($uri);
Expand Down
41 changes: 41 additions & 0 deletions tests/ServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ public function testConnectWillCreateConnection()
$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
}

public function testConnectWillCreateConnectionWithSourceUri()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

$promise = new Promise(function () { });

$this->connector->expects($this->once())->method('connect')->with('google.com:80?source=socks5%3A%2F%2F10.20.30.40%3A5060')->willReturn($promise);

$promise = $this->server->connectTarget($stream, array('google.com', 80, 'socks5://10.20.30.40:5060'));

$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
}

public function testConnectWillRejectIfConnectionFails()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
Expand Down Expand Up @@ -178,6 +191,20 @@ public function testHandleSocks4aConnectionWithHostnameWillEstablishOutgoingConn
$connection->emit('data', array("\x04\x01" . "\x00\x50" . "\x00\x00\x00\x01" . "\x00" . "example.com" . "\x00"));
}

public function testHandleSocks4aConnectionWithHostnameAndSourceAddressWillEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'getRemoteAddress'))->getMock();
$connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://10.20.30.40:5060');

$promise = new Promise(function () { });

$this->connector->expects($this->once())->method('connect')->with('example.com:80?source=socks4%3A%2F%2F10.20.30.40%3A5060')->willReturn($promise);

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

$connection->emit('data', array("\x04\x01" . "\x00\x50" . "\x00\x00\x00\x01" . "\x00" . "example.com" . "\x00"));
}

public function testHandleSocks4aConnectionWithInvalidHostnameWillNotEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end'))->getMock();
Expand All @@ -202,6 +229,20 @@ public function testHandleSocks5ConnectionWithIpv4WillEstablishOutgoingConnectio
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x01" . pack('N', ip2long('127.0.0.1')) . "\x00\x50"));
}

public function testHandleSocks5ConnectionWithIpv4AndSourceAddressWillEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write', 'getRemoteAddress'))->getMock();
$connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://10.20.30.40:5060');

$promise = new Promise(function () { });

$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:80?source=socks5%3A%2F%2F10.20.30.40%3A5060')->willReturn($promise);

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

$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x01" . pack('N', ip2long('127.0.0.1')) . "\x00\x50"));
}

public function testHandleSocks5ConnectionWithIpv6WillEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write'))->getMock();
Expand Down