Skip to content

Remove explicit SOCKS4a handling and merge into SOCKS4 protocol handling and remove explicit SOCKS4a handling and merge into SOCKS4 protocol handling #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 16, 2018
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
111 changes: 45 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# clue/reactphp-socks [![Build Status](https://travis-ci.org/clue/reactphp-socks.svg?branch=master)](https://travis-ci.org/clue/reactphp-socks)

Async SOCKS4, SOCKS4a and SOCKS5 proxy client and server implementation, built on top of [ReactPHP](http://reactphp.org).
Async SOCKS proxy connector client and server implementation, use any TCP/IP-based
protocol through a SOCKS5 or SOCKS4(a) proxy server, built on top of
[ReactPHP](https://reactphp.org).

The SOCKS protocol family can be used to easily tunnel TCP connections independent
of the actual application level protocol, such as HTTP, SMTP, IMAP, Telnet etc.
Expand All @@ -22,7 +24,6 @@ of the actual application level protocol, such as HTTP, SMTP, IMAP, Telnet etc.
* [Unix domain sockets](#unix-domain-sockets)
* [Server](#server)
* [Server connector](#server-connector)
* [Protocol version](#server-protocol-version)
* [Authentication](#server-authentication)
* [Proxy chaining](#server-proxy-chaining)
* [SOCKS over TLS](#server-socks-over-tls)
Expand Down Expand Up @@ -241,91 +242,91 @@ This works for both plain HTTP and SSL encrypted HTTPS requests.

#### Protocol version

This library supports the SOCKS4, SOCKS4a and SOCKS5 protocol versions.

While SOCKS4 already had (a somewhat limited) support for `SOCKS BIND` requests
and SOCKS5 added generic UDP support (`SOCKS UDPASSOCIATE`), this library
focuses on the most commonly used core feature of `SOCKS CONNECT`.
In this mode, a SOCKS server acts as a generic proxy allowing higher level
application protocols to work through it.
This library supports the SOCKS5 and SOCKS4(a) protocol versions.
It focuses on the most commonly used core feature of connecting to a destination
host through the SOCKS proxy server. In this mode, a SOCKS proxy server acts as
a generic proxy allowing higher level application protocols to work through it.

<table>
<tr>
<th></th>
<th>SOCKS4</th>
<th>SOCKS4a</th>
<th>SOCKS5</th>
<th>SOCKS4(a)</th>
</tr>
<tr>
<th>Protocol specification</th>
<td><a href="http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol">SOCKS4.protocol</a></td>
<td><a href="http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4A.protocol">SOCKS4A.protocol</a></td>
<td><a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a></td>
<td>
<a href="http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol">SOCKS4.protocol</a> /
<a href="http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4A.protocol">SOCKS4A.protocol</a>
</td>
</tr>
<tr>
<th>Tunnel outgoing TCP connections</th>
<td>✓</td>
<th>Tunnel outgoing TCP/IP connections</th>
<td>✓</td>
<td>✓</td>
</tr>
<tr>
<th><a href="#dns-resolution">Remote DNS resolving</a></th>
<td>✗</td>
<td>✓</td>
<th><a href="#dns-resolution">Remote DNS resolution</a></th>
<td>✓</td>
<td>✗ / ✓</td>
</tr>
<tr>
<th>IPv6 addresses</th>
<td>✗</td>
<td>✗</td>
<td>✓</td>
<td>✗</td>
</tr>
<tr>
<th><a href="#authentication">Username/Password authentication</a></th>
<td>✗</td>
<td>✗</td>
<td>✓ (as per <a href="http://tools.ietf.org/html/rfc1929">RFC 1929</a>)</td>
<td>✗</td>
</tr>
<tr>
<th>Handshake # roundtrips</th>
<td>1</td>
<td>1</td>
<td>2 (3 with authentication)</td>
<td>1</td>
</tr>
<tr>
<th>Handshake traffic<br />+ remote DNS</th>
<td>17 bytes<br />✗</td>
<td>17 bytes<br />+ hostname + 1</td>
<td><em>variable</em> (+ auth + IPv6)<br />+ hostname - 3</td>
<td>17 bytes<br />+ hostname + 1</td>
</tr>
<tr>
<th>Incoming BIND requests</th>
<td><em>not implemented</em></td>
<td><em>not implemented</em></td>
</tr>
<tr>
<th>UDP datagrams</th>
<td><em>not implemented</em></td>
<td>✗</td>
</tr>
<tr>
<th>GSSAPI authentication</th>
<td><em>not implemented</em></td>
<td>✗</td>
</tr>
</table>

Note, this is __not__ a full SOCKS5 implementation due to missing GSSAPI
authentication (but it's unlikely you're going to miss it anyway).

By default, the `Client` communicates via SOCKS5 with the SOCKS server.
This is done because SOCKS5 is the latest version from the SOCKS protocol family
and generally has best support across other vendors.

If want to explicitly set the protocol version, use the supported values URI
schemes `socks4://` or `socks4a://`as part of the SOCKS URI:
If want to explicitly set the protocol version to SOCKS4(a), you can use the URI
scheme `socks4://` as part of the SOCKS URI:

```php
$client = new Client('socks4a://127.0.0.1', $connector);
$client = new Client('socks4://127.0.0.1', $connector);
```

As seen above, both SOCKS5 and SOCKS4a support remote and local DNS resolution.
If you've explicitly set this to SOCKS4, then you may want to check the following
chapter about local DNS resolution or you may only connect to IPv4 addresses.

#### DNS resolution

By default, the `Client` does not perform any DNS resolution at all and simply
forwards any hostname you're trying to connect to to the SOCKS server.
The remote SOCKS server is thus responsible for looking up any hostnames via DNS
(this default mode is thus called *remote DNS resolution*).
As seen above, this mode is supported by the SOCKS5 and SOCKS4a protocols, but
not the SOCKS4 protocol, as the protocol lacks a way to communicate hostnames.
not the original SOCKS4 protocol, as the protocol lacks a way to communicate hostnames.

On the other hand, all SOCKS protocol versions support sending destination IP
addresses to the SOCKS server.
Expand Down Expand Up @@ -372,12 +373,6 @@ as usual.
> Note how local DNS resolution is in fact entirely handled outside of this
SOCKS client implementation.

If you've explicitly set the client to SOCKS4 and stick to the default
*remote DNS resolution*, then you may only connect to IPv4 addresses because
the protocol lacks a way to communicate hostnames.
If you try to connect to a hostname despite, the resulting promise will be
rejected right away.

#### Authentication

This library supports username/password authentication for SOCKS5 servers as
Expand Down Expand Up @@ -617,6 +612,7 @@ $client = new Client('socks+unix://user:pass@/tmp/proxy.sock', new Connector($lo

The `Server` is responsible for accepting incoming communication from SOCKS clients
and forwarding the requested connection to the target host.
It supports the SOCKS5 and SOCKS4(a) protocol versions by default.
It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage)
and an underlying TCP/IP socket server like this:

Expand Down Expand Up @@ -666,23 +662,6 @@ You can use this parameter for logging purposes or to restrict connection
requests for certain clients by providing a custom implementation of the
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface).

#### Server protocol version

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

If want to explicitly set the protocol version, use the supported values `4`, `4a` or `5`:

```PHP
$server->setProtocolVersion(5);
```

In order to reset the protocol version to its default (i.e. automatic detection),
use `null` as protocol version.

```PHP
$server->setProtocolVersion(null);
```

#### Server authentication

By default, the `Server` does not require any authentication from the clients.
Expand Down Expand Up @@ -789,8 +768,8 @@ Proxy chaining can happen on the server side and/or the client side:

#### Server SOCKS over TLS

All [SOCKS protocol versions](#server-protocol-version) support forwarding TCP/IP
based connections and higher level protocols.
Both SOCKS5 and SOCKS4(a) protocol versions support forwarding TCP/IP based
connections and higher level protocols.
This implies that you can also use [secure TLS connections](#secure-tls-connections)
to transfer sensitive data across SOCKS proxy servers.
This means that no eavesdropper nor the proxy server will be able to decrypt
Expand Down Expand Up @@ -841,8 +820,8 @@ See also [example 31](examples).

#### Server Unix domain sockets

All [SOCKS protocol versions](#server-protocol-version) support forwarding TCP/IP
based connections and higher level protocols.
Both SOCKS5 and SOCKS4(a) protocol versions support forwarding TCP/IP based
connections and higher level protocols.
In some advanced cases, it may be useful to let your SOCKS server listen on a
Unix domain socket (UDS) path instead of a IP:port combination.
For example, this allows you to rely on file system permissions instead of
Expand Down Expand Up @@ -925,7 +904,7 @@ $client = new Client('socks+unix:///tmp/proxy.sock', $connector);

The [Tor anonymity network](http://www.torproject.org) client software is designed
to encrypt your traffic and route it over a network of several nodes to conceal its origin.
It presents a SOCKS4 and SOCKS5 interface on TCP port 9050 by default
It presents a SOCKS5 and SOCKS4(a) interface on TCP port 9050 by default
which allows you to tunnel any traffic through the anonymity network.
In most scenarios you probably don't want your client to resolve the target hostnames,
because you would leak DNS information to anybody observing your local traffic.
Expand Down Expand Up @@ -984,7 +963,7 @@ MIT, see LICENSE
* If you want to learn more about processing streams of data, refer to the
documentation of the underlying
[react/stream](https://github.com/reactphp/stream) component.
* As an alternative to a SOCKS (SOCKS4/SOCKS5) proxy, you may also want to look into
* As an alternative to a SOCKS5 / SOCKS4(a) proxy, you may also want to look into
using an HTTP CONNECT proxy instead.
You may want to use [clue/reactphp-http-proxy](https://github.com/clue/reactphp-http-proxy)
which also provides an implementation of the same
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "clue/socks-react",
"description": "Async SOCKS4, SOCKS4a and SOCKS5 proxy client and server implementation, built on top of ReactPHP",
"keywords": ["socks client", "socks server", "proxy", "tcp tunnel", "socks protocol", "async", "ReactPHP"],
"description": "Async SOCKS proxy connector client and server implementation, use any TCP/IP-based protocol through a SOCKS5 or SOCKS4(a) proxy server, built on top of ReactPHP.",
"keywords": ["socks client", "socks server", "socks5", "socks4a", "proxy server", "tcp tunnel", "async", "ReactPHP"],
"homepage": "https://github.com/clue/reactphp-socks",
"license": "MIT",
"authors": [
Expand Down
16 changes: 5 additions & 11 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ final class Client implements ConnectorInterface

private $socksUri;

private $protocolVersion = '5';
private $protocolVersion = 5;

private $auth = null;

public function __construct($socksUri, ConnectorInterface $connector)
{
// support `sockss://` scheme for SOCKS over TLS
// support `socks+unix://` scheme for Unix domain socket (UDS) paths
if (preg_match('/^(socks(?:5|4|4a)?)(s|\+unix):\/\/(.*?@)?(.+?)$/', $socksUri, $match)) {
if (preg_match('/^(socks(?:5|4)?)(s|\+unix):\/\/(.*?@)?(.+?)$/', $socksUri, $match)) {
// rewrite URI to parse SOCKS scheme, authentication and dummy host
$socksUri = $match[1] . '://' . $match[3] . 'localhost';

Expand Down Expand Up @@ -77,11 +77,9 @@ public function __construct($socksUri, ConnectorInterface $connector)
private function setProtocolVersionFromScheme($scheme)
{
if ($scheme === 'socks' || $scheme === 'socks5') {
$this->protocolVersion = '5';
} elseif ($scheme === 'socks4a') {
$this->protocolVersion = '4a';
$this->protocolVersion = 5;
} elseif ($scheme === 'socks4') {
$this->protocolVersion = '4';
$this->protocolVersion = 4;
} else {
throw new InvalidArgumentException('Invalid protocol version given "' . $scheme . '://"');
}
Expand Down Expand Up @@ -128,10 +126,6 @@ public function connect($uri)
$host = trim($parts['host'], '[]');
$port = $parts['port'];

if ($this->protocolVersion === '4' && false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return Promise\reject(new InvalidArgumentException('Requires an IPv4 address for SOCKS4'));
}

if (strlen($host) > 255 || $port > 65535 || $port < 0 || (string)$port !== (string)(int)$port) {
return Promise\reject(new InvalidArgumentException('Invalid target specified'));
}
Expand Down Expand Up @@ -204,7 +198,7 @@ public function handleConnectedSocks(ConnectionInterface $stream, $host, $port)
$deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response (ECONNRESET)', defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104));
});

if ($this->protocolVersion === '5') {
if ($this->protocolVersion === 5) {
$promise = $this->handleSocks5($stream, $host, $port, $reader);
} else {
$promise = $this->handleSocks4($stream, $host, $port, $reader);
Expand Down
48 changes: 8 additions & 40 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ final class Server

private $auth = null;

private $protocolVersion = null;

public function __construct(LoopInterface $loop, ConnectorInterface $connector = null)
{
if ($connector === null) {
Expand All @@ -65,28 +63,12 @@ public function listen(ServerInterface $socket)
});
}

public function setProtocolVersion($version)
{
if ($version !== null) {
$version = (string)$version;
if (!in_array($version, array('4', '4a', '5'), true)) {
throw new InvalidArgumentException('Invalid protocol version given');
}
if ($version !== '5' && $this->auth !== null){
throw new UnexpectedValueException('Unable to change protocol version to anything but SOCKS5 while authentication is used. Consider removing authentication info or sticking to SOCKS5');
}
}
$this->protocolVersion = $version;
}

public function setAuth($auth)
{
if (!is_callable($auth)) {
throw new InvalidArgumentException('Given authenticator is not a valid callable');
}
if ($this->protocolVersion !== null && $this->protocolVersion !== '5') {
throw new UnexpectedValueException('Authentication requires SOCKS5. Consider using protocol version 5 or waive authentication');
}

// wrap authentication callback in order to cast its return value to a promise
$this->auth = function($username, $password, $remote) use ($auth) {
$ret = call_user_func($auth, $username, $password, $remote);
Expand Down Expand Up @@ -163,38 +145,24 @@ private function handleSocks(ConnectionInterface $stream)
$stream->on('data', array($reader, 'write'));

$that = $this;
$that = $this;

$auth = $this->auth;
$protocolVersion = $this->protocolVersion;

// authentication requires SOCKS5
if ($auth !== null) {
$protocolVersion = '5';
}

return $reader->readByte()->then(function ($version) use ($stream, $that, $protocolVersion, $auth, $reader){
return $reader->readByte()->then(function ($version) use ($stream, $that, $auth, $reader){
if ($version === 0x04) {
if ($protocolVersion === '5') {
throw new UnexpectedValueException('SOCKS4 not allowed due to configuration');
if ($auth !== null) {
throw new UnexpectedValueException('SOCKS4 not allowed because authentication is required');
}
return $that->handleSocks4($stream, $protocolVersion, $reader);
return $that->handleSocks4($stream, $reader);
} else if ($version === 0x05) {
if ($protocolVersion !== null && $protocolVersion !== '5') {
throw new UnexpectedValueException('SOCKS5 not allowed due to configuration');
}
return $that->handleSocks5($stream, $auth, $reader);
}
throw new UnexpectedValueException('Unexpected/unknown version number');
});
}

/** @internal */
public function handleSocks4(ConnectionInterface $stream, $protocolVersion, StreamReader $reader)
public function handleSocks4(ConnectionInterface $stream, StreamReader $reader)
{
// 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
Expand All @@ -212,7 +180,7 @@ public function handleSocks4(ConnectionInterface $stream, $protocolVersion, Stre
'ipLong' => 'N',
'null' => 'C'
));
})->then(function ($data) use ($reader, $supportsHostname, $remote) {
})->then(function ($data) use ($reader, $remote) {
if ($data['null'] !== 0x00) {
throw new Exception('Not a null byte');
}
Expand All @@ -222,7 +190,7 @@ public function handleSocks4(ConnectionInterface $stream, $protocolVersion, Stre
if ($data['port'] === 0) {
throw new Exception('Invalid port');
}
if ($data['ipLong'] < 256 && $supportsHostname) {
if ($data['ipLong'] < 256) {
// invalid IP => probably a SOCKS4a request which appends the hostname
return $reader->readStringNull()->then(function ($string) use ($data, $remote){
return array($string, $data['port'], $remote);
Expand Down
Loading