From 22699e4c9a175276971a82b3a5726416ad9d6f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 13 Dec 2017 10:08:11 +0100 Subject: [PATCH] Support communication over Unix domain sockets (UDS) --- README.md | 80 ++++++++++++++++++++++++++++++++++++++++ composer.json | 2 +- src/Client.php | 10 +++++ tests/ClientTest.php | 14 +++++++ tests/FunctionalTest.php | 61 ++++++++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e24633..f70b6ba 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,13 @@ of the actual application level protocol, such as HTTP, SMTP, IMAP, Telnet etc. * [Authentication](#authentication) * [Proxy chaining](#proxy-chaining) * [Connection timeout](#connection-timeout) + * [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) + * [Unix domain sockets](#server-unix-domain-sockets) * [Servers](#servers) * [Using a PHP SOCKS server](#using-a-php-socks-server) * [Using SSH as a SOCKS server](#using-ssh-as-a-socks-server) @@ -553,6 +555,42 @@ as usual. > Also note how connection timeout is in fact entirely handled outside of this SOCKS client implementation. +#### Unix domain sockets + +All [SOCKS protocol versions](#protocol-version) 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 +having to rely on explicit [authentication](#authentication). + +You can use the `socks+unix://` URI scheme or use an explicit +[SOCKS protocol version](#protocol-version) like this: + +```php +$client = new Client('socks+unix:///tmp/proxy.sock', new Connector($loop)); + +$client = new Client('socks5+unix:///tmp/proxy.sock', new Connector($loop)); +``` + +Simiarly, you can also combine this with [authentication](#authentication) +like this: + +```php +$client = new Client('socks+unix://user:pass@/tmp/proxy.sock', new Connector($loop)); +``` + +> Note that Unix domain sockets (UDS) are considered advanced usage and PHP only + has limited support for this. + In particular, enabling [secure TLS](#secure-tls-connections) may not be + supported. + +> Note that SOCKS protocol does not support the notion of UDS paths. The above + works reasonably well because UDS is only used for the connection between + client and proxy server and the path will not actually passed over the protocol. + This implies that this does also not support [proxy chaining](#proxy-chaining) + over multiple UDS paths. + ### Server The `Server` is responsible for accepting incoming communication from SOCKS clients @@ -718,6 +756,32 @@ Proxy chaining can happen on the server side and/or the client side: not really know anything about chaining at all. This means that this is a server-only property and can be implemented as above. +#### Server Unix domain sockets + +All [SOCKS protocol versions](#server-protocol-version) 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 +having to rely on explicit [authentication](#server-authentication). + +You can simply start your listening socket on the `unix://` URI scheme like this: + +```php +$loop = \React\EventLoop\Factory::create(); + +// listen on /tmp/proxy.sock +$socket = new React\Socket\Server('unix:///tmp/proxy.sock', $loop); +$server = new Server($loop, $socket); +``` + +> Note that Unix domain sockets (UDS) are considered advanced usage and that + the SOCKS protocol does not support the notion of UDS paths. The above + works reasonably well because UDS is only used for the connection between + client and proxy server and the path will not actually passed over the protocol. + This implies that this does also not support [proxy chaining](#server-proxy-chaining) + over multiple UDS paths. + ## Servers ### Using a PHP SOCKS server @@ -754,6 +818,22 @@ Now you can simply use this SSH SOCKS server like this: $client = new Client('127.0.0.1:1080', $connector); ``` +Note that the above will allow all users on the local system to connect over +your SOCKS server without authentication which may or may not be what you need. +As an alternative, recent OpenSSH client versions also support +[Unix domain sockets](#unix-domain-sockets) (UDS) paths so that you can rely +on Unix file system permissions instead: + +```bash +$ ssh -D/tmp/proxy.sock example.com +``` + +Now you can simply use this SSH SOCKS server like this: + +```PHP +$client = new Client('socks+unix:///tmp/proxy.sock', $connector); +``` + ### Using the Tor (anonymity network) to tunnel SOCKS connections The [Tor anonymity network](http://www.torproject.org) client software is designed diff --git a/composer.json b/composer.json index 6762a08..50ef83a 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ }, "require": { "php": ">=5.3", - "react/socket": "^1.0 || ^0.8 || ^0.7", + "react/socket": "^1.0 || ^0.8.6", "react/promise": "^2.1 || ^1.2", "evenement/evenement": "~3.0|~1.0|~2.0" }, diff --git a/src/Client.php b/src/Client.php index a681fe1..4589ee1 100644 --- a/src/Client.php +++ b/src/Client.php @@ -7,6 +7,7 @@ use React\Promise\Deferred; use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; +use React\Socket\FixedUriConnector; use \Exception; use \InvalidArgumentException; use RuntimeException; @@ -27,6 +28,15 @@ class Client implements ConnectorInterface public function __construct($socksUri, ConnectorInterface $connector) { + // support `socks+unix://` scheme for Unix domain socket (UDS) paths + if (preg_match('/^(socks(?:5|4|4a)?)\+?unix:\/\/(.*?@)?(.+?$)/', $socksUri, $match)) { + $socksUri = ($match[1] !== '' ? ($match[1] . '://') : '') . $match[2] . 'localhost'; + $connector = new FixedUriConnector( + 'unix://' . $match[3], + $connector + ); + } + // assume default scheme if none is given if (strpos($socksUri, '://') === false) { $socksUri = 'socks://' . $socksUri; diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 450f186..a8eea1f 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -41,6 +41,20 @@ public function testCtorAcceptsUriWithHostOnlyAssumesDefaultPort() $this->assertTrue(true); } + public function testCtorAcceptsUriWithSocksUnixScheme() + { + $client = new Client('socks+unix:///tmp/socks.socket', $this->connector); + + $this->assertTrue(true); + } + + public function testCtorAcceptsUriWithSocks5UnixScheme() + { + $client = new Client('socks5+unix:///tmp/socks.socket', $this->connector); + + $this->assertTrue(true); + } + /** * @expectedException InvalidArgumentException */ diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 01de80e..6c890d8 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -6,6 +6,8 @@ use React\Socket\TimeoutConnector; use React\Socket\SecureConnector; use React\Socket\TcpConnector; +use React\Socket\UnixServer; +use React\Socket\Connector; class FunctionalTest extends TestCase { @@ -93,6 +95,65 @@ public function testConnectionSocks5() $this->assertResolveStream($this->client->connect('www.google.com:80')); } + /** @group internet */ + public function testConnectionSocksOverUnix() + { + if (!in_array('unix', stream_get_transports())) { + $this->markTestSkipped('System does not support unix:// scheme'); + } + + $path = sys_get_temp_dir() . '/test' . mt_rand(1000, 9999) . '.sock'; + $socket = new UnixServer($path, $this->loop); + $this->server = new Server($this->loop, $socket); + + $this->connector = new Connector($this->loop); + $this->client = new Client('socks+unix://' . $path, $this->connector); + + $this->assertResolveStream($this->client->connect('www.google.com:80')); + + unlink($path); + } + + /** @group internet */ + public function testConnectionSocks5OverUnix() + { + if (!in_array('unix', stream_get_transports())) { + $this->markTestSkipped('System does not support unix:// scheme'); + } + + $path = sys_get_temp_dir() . '/test' . mt_rand(1000, 9999) . '.sock'; + $socket = new UnixServer($path, $this->loop); + $this->server = new Server($this->loop, $socket); + $this->server->setProtocolVersion(5); + + $this->connector = new Connector($this->loop); + $this->client = new Client('socks5+unix://' . $path, $this->connector); + + $this->assertResolveStream($this->client->connect('www.google.com:80')); + + unlink($path); + } + + /** @group internet */ + public function testConnectionSocksWithAuthenticationOverUnix() + { + if (!in_array('unix', stream_get_transports())) { + $this->markTestSkipped('System does not support unix:// scheme'); + } + + $path = sys_get_temp_dir() . '/test' . mt_rand(1000, 9999) . '.sock'; + $socket = new UnixServer($path, $this->loop); + $this->server = new Server($this->loop, $socket); + $this->server->setAuthArray(array('name' => 'pass')); + + $this->connector = new Connector($this->loop); + $this->client = new Client('socks+unix://name:pass@' . $path, $this->connector); + + $this->assertResolveStream($this->client->connect('www.google.com:80')); + + unlink($path); + } + /** @group internet */ public function testConnectionAuthenticationFromUri() {