diff --git a/README.md b/README.md index 978d2cc..a86412a 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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): @@ -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. diff --git a/src/Server.php b/src/Server.php index d464d15..bfb4e87 100644 --- a/src/Server.php +++ b/src/Server.php @@ -172,6 +172,15 @@ 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( @@ -179,7 +188,7 @@ public function handleSocks4(ConnectionInterface $stream, $protocolVersion, Stre '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'); } @@ -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){ @@ -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 @@ -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) { @@ -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); @@ -322,7 +338,7 @@ 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); @@ -330,6 +346,10 @@ public function connectTarget(ConnectionInterface $stream, array $target) 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); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index e8e7d22..612976b 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -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(); @@ -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(); @@ -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();