diff --git a/README.md b/README.md index f70b6ba..b7b9899 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,14 @@ of the actual application level protocol, such as HTTP, SMTP, IMAP, Telnet etc. * [Authentication](#authentication) * [Proxy chaining](#proxy-chaining) * [Connection timeout](#connection-timeout) + * [SOCKS over TLS](#socks-over-tls) * [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) * [Unix domain sockets](#server-unix-domain-sockets) * [Servers](#servers) * [Using a PHP SOCKS server](#using-a-php-socks-server) @@ -555,6 +557,60 @@ as usual. > Also note how connection timeout is in fact entirely handled outside of this SOCKS client implementation. +#### SOCKS over TLS + +All [SOCKS protocol versions](#protocol-version) 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 +your data. + +However, the initial SOCKS communication between the client and the proxy is +usually via an unencrypted, plain TCP/IP connection. +This means that an eavesdropper may be able to see *where* you connect to and +may also be able to see your [SOCKS authentication](#authentication) details +in cleartext. + +As an alternative, you may establish a secure TLS connection to your SOCKS +proxy before starting the initial SOCKS communication. +This means that no eavesdroppper will be able to see the destination address +you want to connect to or your [SOCKS authentication](#authentication) details. + +You can use the `sockss://` URI scheme or use an explicit +[SOCKS protocol version](#protocol-version) like this: + +```php +$client = new Client('sockss://127.0.0.1:1080', new Connector($loop)); + +$client = new Client('socks5s://127.0.0.1:1080', new Connector($loop)); +``` + +See also [example 32](examples). + +Simiarly, you can also combine this with [authentication](#authentication) +like this: + +```php +$client = new Client('sockss://user:pass@127.0.0.1:1080', new Connector($loop)); +``` + +> Note that for most use cases, [secure TLS connections](#secure-tls-connections) + should be used instead. SOCKS over TLS is considered advanced usage and is + used very rarely in practice. + In particular, the SOCKS server has to accept secure TLS connections, see + also [Server SOCKS over TLS](#server-socks-over-tls) for more details. + Also, PHP does not support "double encryption" over a single connection. + This means that enabling [secure TLS connections](#secure-tls-connections) + over a communication channel that has been opened with SOCKS over TLS + may not be supported. + +> Note that the SOCKS protocol does not support the notion of TLS. The above + works reasonably well because TLS is only used for the connection between + client and proxy server and the SOCKS protocol data is otherwise identical. + This implies that this may also have only limited support for + [proxy chaining](#proxy-chaining) over multiple TLS paths. + #### Unix domain sockets All [SOCKS protocol versions](#protocol-version) support forwarding TCP/IP @@ -756,6 +812,54 @@ 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 SOCKS over TLS + +All [SOCKS protocol versions](#server-protocol-version) 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 +your data. + +However, the initial SOCKS communication between the client and the proxy is +usually via an unencrypted, plain TCP/IP connection. +This means that an eavesdropper may be able to see *where* the client connects +to and may also be able to see the [SOCKS authentication](#authentication) +details in cleartext. + +As an alternative, you may listen for SOCKS over TLS connections so +that the client has to establish a secure TLS connection to your SOCKS +proxy before starting the initial SOCKS communication. +This means that no eavesdroppper will be able to see the destination address +the client wants to connect to or their [SOCKS authentication](#authentication) +details. + +You can simply start your listening socket on the `tls://` URI scheme like this: + +```php +$loop = \React\EventLoop\Factory::create(); + +// listen on tls://127.0.0.1:1080 with the given server certificate +$socket = new React\Socket\Server('tls://127.0.0.1:1080', $loop, array( + 'tls' => array( + 'local_cert' => __DIR__ . '/localhost.pem', + ) +)); +$server = new Server($loop, $socket); +``` + +See also [example 31](examples). + +> Note that for most use cases, [secure TLS connections](#secure-tls-connections) + should be used instead. SOCKS over TLS is considered advanced usage and is + used very rarely in practice. + +> Note that the SOCKS protocol does not support the notion of TLS. The above + works reasonably well because TLS is only used for the connection between + client and proxy server and the SOCKS protocol data is otherwise identical. + This implies that this does also not support [proxy chaining](#server-proxy-chaining) + over multiple TLS paths. + #### Server Unix domain sockets All [SOCKS protocol versions](#server-protocol-version) support forwarding TCP/IP diff --git a/examples/31-server-secure.php b/examples/31-server-secure.php new file mode 100644 index 0000000..b4b2109 --- /dev/null +++ b/examples/31-server-secure.php @@ -0,0 +1,21 @@ + array( + 'local_cert' => __DIR__ . '/localhost.pem', +))); + +// start a new server listening for incoming connection on the given socket +$server = new Server($loop, $socket); + +echo 'SOCKS over TLS server listening on ' . str_replace('tls:', 'sockss:', $socket->getAddress()) . PHP_EOL; + +$loop->run(); diff --git a/examples/32-http-secure.php b/examples/32-http-secure.php new file mode 100644 index 0000000..d304bd4 --- /dev/null +++ b/examples/32-http-secure.php @@ -0,0 +1,33 @@ + array( + 'verify_peer' => false, + 'verify_peer_name' => false +)))); +$connector = new Connector($loop, array( + 'tcp' => $client, + 'timeout' => 3.0, + 'dns' => false +)); + +echo 'Demo SOCKS over TLS client connecting to secure SOCKS server ' . $proxy . PHP_EOL; + +$connector->connect('tcp://www.google.com:80')->then(function (ConnectionInterface $stream) { + echo 'connected' . PHP_EOL; + $stream->write("GET / HTTP/1.0\r\n\r\n"); + $stream->on('data', function ($data) { + echo $data; + }); +}, 'printf'); + +$loop->run(); diff --git a/examples/localhost.pem b/examples/localhost.pem new file mode 100644 index 0000000..be69279 --- /dev/null +++ b/examples/localhost.pem @@ -0,0 +1,49 @@ +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBZMRIwEAYDVQQDDAkxMjcu +MC4wLjExCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK +DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMwMTQ1OTA2WhcNMjYx +MjI4MTQ1OTA2WjBZMRIwEAYDVQQDDAkxMjcuMC4wLjExCzAJBgNVBAYTAkFVMRMw +EQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0 +eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8SZWNS+Ktg0Py +W8dx5uXZ+ZUawd3wnzLMHW7EhoUpIrIdp3kDU9NezF68dOhPMJY/Kh+6btRCxWXN +2OVTqS5Xi826j3TSE07iF83JRLeveW0PcodjUBd+RzdwCWWo2pfMJz4v7x1wu1c9 +zNi6JxxpDAXTFSB4GiWsI4tFu2XmMRhfm6LRK4WPfsZIJKokdiG5fKSPDn7nrVj0 +UUXr2eBsEAzdwL14U9+mwbLdaAkz3qK3fqi8sEC09lEWm95gKMOhkQf5qvXODtT4 +wdVrrKDTyehLv0xaItnUDnXzrkMBU5QS9TQzzqSW6ZaBsSxtONEFUiXiN9dtyXsY +YCUE54G/AgMBAAGjUDBOMB0GA1UdDgQWBBQ2GRz3QsQzdXaTMnPVCKfpigA10DAf +BgNVHSMEGDAWgBQ2GRz3QsQzdXaTMnPVCKfpigA10DAMBgNVHRMEBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQA77iZ4KrpPY18Ezjt0mngYAuAxunKddXYdLZ2khywN +0uI/VzYnkFVtrsC7y2jLHSxlmE2/viPPGZDUplENV2acN6JNW+tlt7/bsrQHDQw3 +7VCF27EWiDxHsaghhLkqC+kcop5YR5c0oDQTdEWEKSbow2zayUXDYbRRs76SClTe +824Yul+Ts8Mka+AX2PXDg47iZ84fJRN/nKavcJUTJ2iS1uYw0GNnFMge/uwsfMR3 +V47qN0X5emky8fcq99FlMCbcy0gHAeSWAjClgr2dd2i0LDatUbj7YmdmFcskOgII +IwGfvuWR2yPevYGAE0QgFeLHniN3RW8zmpnX/XtrJ4a7 +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8SZWNS+Ktg0Py +W8dx5uXZ+ZUawd3wnzLMHW7EhoUpIrIdp3kDU9NezF68dOhPMJY/Kh+6btRCxWXN +2OVTqS5Xi826j3TSE07iF83JRLeveW0PcodjUBd+RzdwCWWo2pfMJz4v7x1wu1c9 +zNi6JxxpDAXTFSB4GiWsI4tFu2XmMRhfm6LRK4WPfsZIJKokdiG5fKSPDn7nrVj0 +UUXr2eBsEAzdwL14U9+mwbLdaAkz3qK3fqi8sEC09lEWm95gKMOhkQf5qvXODtT4 +wdVrrKDTyehLv0xaItnUDnXzrkMBU5QS9TQzzqSW6ZaBsSxtONEFUiXiN9dtyXsY +YCUE54G/AgMBAAECggEBAKiO/3FE1CMddkCLZVtUp8ShqJgRokx9WI5ecwFApAkV +ZHsjqDQQYRNmxhDUX/w0tOzLGyhde2xjJyZG29YviKsbHwu6zYwbeOzy/mkGOaK/ +g6DmmMmRs9Z6juifoQCu4GIFZ6il2adIL2vF7OeJh+eKudQj/7NFRSB7mXzNrQWK +tZY3eux5zXWmio7pgZrx1HFZQiiL9NVLwT9J7oBnaoO3fREiu5J2xBpljG9Cr0j1 +LLiVLhukWJYRlHDtGt1CzI9w8iKo44PCRzpKyxpbsOrQxeSyEWUYQRv9VHA59LC7 +tVAJTbnTX1BNHkGZkOkoOpoZLwBaM2XbbDtcOGCAZMECgYEA+mTURFQ85/pxawvk +9ndqZ+5He1u/bMLYIJDp0hdB/vgD+vw3gb2UyRwp0I6Wc6Si4FEEnbY7L0pzWsiR +43CpLs+cyLfnD9NycuIasxs5fKb/1s1nGTkRAp7x9x/ZTtEf8v4YTmmMXFHzdo7V +pv+czO89ppEDkxEtMf/b5SifhO8CgYEAwIDIUvXLduGhL+RPDwjc2SKdydXGV6om +OEdt/V8oS801Z7k8l3gHXFm7zL/MpHmh9cag+F9dHK42kw2RSjDGsBlXXiAO1Z0I +2A34OdPw/kow8fmIKWTMu3+28Kca+3RmUqeyaq0vazQ/bWMO9px+Ud3YfLo1Tn5I +li0MecAx8DECgYEAvsLceKYYtL83c09fg2oc1ctSCCgw4WJcGAtvJ9DyRZacKbXH +b/+H/+OF8879zmKqd+0hcCnqUzAMTCisBLPLIM+o6b45ufPkqKObpcJi/JWaKgLY +vf2c+Psw6o4IF6T5Cz4MNIjzF06UBknxecYZpoPJ20F1kLCwVvxPgfl99l8CgYAb +XfOcv67WTstgiJ+oroTfJamy+P5ClkDqvVTosW+EHz9ZaJ8xlXHOcj9do2LPey9I +Rp250azmF+pQS5x9JKQKgv/FtN8HBVUtigbhCb14GUoODICMCfWFLmnumoMefnTR +iV+3BLn6Dqp5vZxx+NuIffZ5/Or5JsDhALSGVomC8QKBgAi3Z/dNQrDHfkXMNn/L ++EAoLuAbFgLs76r9VGgNaRQ/q5gex2bZEGoBj4Sxvs95NUIcfD9wKT7FF8HdxARv +y3o6Bfc8Xp9So9SlFXrje+gkdEJ0rQR67d+XBuJZh86bXJHVrMwpoNL+ahLGdVSe +81oh1uCH1YPLM29hPyaohxL8 +-----END PRIVATE KEY----- diff --git a/src/Client.php b/src/Client.php index 4589ee1..ee643b6 100644 --- a/src/Client.php +++ b/src/Client.php @@ -28,11 +28,15 @@ class Client implements ConnectorInterface 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)?)\+?unix:\/\/(.*?@)?(.+?$)/', $socksUri, $match)) { - $socksUri = ($match[1] !== '' ? ($match[1] . '://') : '') . $match[2] . 'localhost'; + if (preg_match('/^(socks(?:5|4|4a)?)(s|\+unix):\/\/(.*?@)?(.+?)$/', $socksUri, $match)) { + // rewrite URI to parse SOCKS scheme, authentication and dummy host + $socksUri = $match[1] . '://' . $match[3] . 'localhost'; + + // connector uses appropriate transport scheme and explicit host given $connector = new FixedUriConnector( - 'unix://' . $match[3], + ($match[2] === 's' ? 'tls://' : 'unix://') . $match[4], $connector ); } @@ -82,7 +86,7 @@ private function setProtocolVersionFromScheme($scheme) } elseif ($scheme === 'socks4') { $this->protocolVersion = '4'; } else { - throw new InvalidArgumentException('Invalid protocol version given'); + throw new InvalidArgumentException('Invalid protocol version given "' . $scheme . '://"'); } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index a8eea1f..e304901 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -41,6 +41,20 @@ public function testCtorAcceptsUriWithHostOnlyAssumesDefaultPort() $this->assertTrue(true); } + public function testCtorAcceptsUriWithSecureScheme() + { + $client = new Client('sockss://127.0.0.1:9050', $this->connector); + + $this->assertTrue(true); + } + + public function testCtorAcceptsUriWithSecureVersionScheme() + { + $client = new Client('socks5s://127.0.0.1:9050', $this->connector); + + $this->assertTrue(true); + } + public function testCtorAcceptsUriWithSocksUnixScheme() { $client = new Client('socks+unix:///tmp/socks.socket', $this->connector); diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 6c890d8..2f1190e 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -95,6 +95,51 @@ public function testConnectionSocks5() $this->assertResolveStream($this->client->connect('www.google.com:80')); } + /** @group internet */ + public function testConnectionSocksOverTls() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Required function does not exist in your environment (HHVM?)'); + } + + $socket = new \React\Socket\Server('tls://127.0.0.1:0', $this->loop, array('tls' => array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem', + ))); + $this->server = new Server($this->loop, $socket); + + $this->connector = new Connector($this->loop, array('tls' => array( + 'verify_peer' => false, + 'verify_peer_name' => false + ))); + $this->client = new Client(str_replace('tls:', 'sockss:', $socket->getAddress()), $this->connector); + + $this->assertResolveStream($this->client->connect('www.google.com:80')); + } + + /** + * @group internet + * @requires PHP 5.6 + */ + public function testConnectionSocksOverTlsUsesPeerNameFromSocksUri() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Required function does not exist in your environment (HHVM?)'); + } + + $socket = new \React\Socket\Server('tls://127.0.0.1:0', $this->loop, array('tls' => array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem', + ))); + $this->server = new Server($this->loop, $socket); + + $this->connector = new Connector($this->loop, array('tls' => array( + 'verify_peer' => false, + 'verify_peer_name' => true + ))); + $this->client = new Client(str_replace('tls:', 'sockss:', $socket->getAddress()), $this->connector); + + $this->assertResolveStream($this->client->connect('www.google.com:80')); + } + /** @group internet */ public function testConnectionSocksOverUnix() {