From b6a5203694e7368608b8eb7d2a5998e54abcb94d Mon Sep 17 00:00:00 2001 From: Ben Schwartz Date: Fri, 16 Dec 2016 14:33:29 -0500 Subject: [PATCH 1/3] Add SOCKS4 message parsing and generation to the SOCKS library. --- src/lib/socks/headers.spec.ts | 83 +++++++++++++++ src/lib/socks/headers.ts | 192 +++++++++++++++++++++++++++++++++- 2 files changed, 274 insertions(+), 1 deletion(-) diff --git a/src/lib/socks/headers.spec.ts b/src/lib/socks/headers.spec.ts index e7d6fd2dc2..ee0a474a04 100644 --- a/src/lib/socks/headers.spec.ts +++ b/src/lib/socks/headers.spec.ts @@ -234,4 +234,87 @@ describe('socks', function() { udpMessageArray[2] = 7; expect(() => { Socks.interpretUdpMessage(udpMessageArray); }).toThrow(); }); + + it('roundtrip SOCKS 4 request', () => { + let request : Socks.Request = { + command: Socks.Command.TCP_CONNECT, + endpoint: { + address: '192.0.2.4', + port: 54321 + } + }; + let requestBuffer = Socks.composeRequestBufferV4(request); + let requestArray = new Uint8Array(requestBuffer); + expect(requestArray[0]).toEqual(Socks.Version.VERSION4); + expect(requestArray[1]).toEqual(Socks.Command.TCP_CONNECT); + expect(requestArray[2] << 8 | requestArray[3]).toEqual(request.endpoint.port); + expect(requestArray[8]).toEqual(0); + expect(requestArray.length).toEqual(9); + let requestAgain = Socks.interpretRequestBufferV4(requestBuffer); + expect(requestAgain).toEqual(request); + }); + + it('roundtrip SOCKS 4a request', () => { + let request : Socks.Request = { + command: Socks.Command.TCP_CONNECT, + endpoint: { + address: 'www.example.com', + port: 1200 + } + }; + let requestBuffer = Socks.composeRequestBufferV4(request); + let requestArray = new Uint8Array(requestBuffer); + expect(requestArray[0]).toEqual(Socks.Version.VERSION4); + expect(requestArray[1]).toEqual(Socks.Command.TCP_CONNECT); + expect(requestArray[2] << 8 | requestArray[3]).toEqual(request.endpoint.port); + expect(requestArray[4] | requestArray[5] | requestArray[6]).toEqual(0); + expect(requestArray[7]).toEqual(request.endpoint.address.length); + expect(requestArray[8]).toEqual(0); + expect(requestArray.length).toEqual(10 + request.endpoint.address.length); + let requestAgain = Socks.interpretRequestBufferV4(requestBuffer); + expect(requestAgain).toEqual(request); + }); + + it('check SOCKS 4 version', () => { + let request : Socks.Request = { + command: Socks.Command.TCP_CONNECT, + endpoint: { + address: '192.0.2.4', + port: 54321 + } + }; + let requestBuffer = Socks.composeRequestBufferV4(request); + let version = Socks.checkVersion(requestBuffer); + expect(version).toEqual(Socks.Version.VERSION4); + }); + + it('check SOCKS 5 version', () => { + let request : Socks.Request = { + command: Socks.Command.TCP_CONNECT, + endpoint: { + address: '192.0.2.4', + port: 54321 + } + }; + let requestBuffer = Socks.composeRequestBuffer(request); + let version = Socks.checkVersion(requestBuffer); + expect(version).toEqual(Socks.Version.VERSION5); + }); + + it('roundtrip IPv4 response', () => { + let response :Socks.Response = { + reply: Socks.Reply.SUCCEEDED, + endpoint: { + address: '255.0.1.77', + port: 65535 + } + }; + let responseBuffer = Socks.composeResponseBufferV4(response); + let responseArray = new Uint8Array(responseBuffer); + expect(responseArray[0]).toEqual(Socks.Version.VERSION0); + expect(responseArray[1]).toEqual(Socks.ResultCode4.REQUEST_GRANTED); + expect(responseArray[2] << 8 | responseArray[3]).toEqual(response.endpoint.port); + let responseAgain = Socks.interpretResponseBufferV4(responseBuffer); + expect(responseAgain).toEqual(response); + }); }); diff --git a/src/lib/socks/headers.ts b/src/lib/socks/headers.ts index 605ce7a3d9..3ef5745fd3 100644 --- a/src/lib/socks/headers.ts +++ b/src/lib/socks/headers.ts @@ -10,7 +10,9 @@ import * as net from '../net/net.types'; // VERSION - Socks protocol and subprotocol version headers export enum Version { + VERSION0 = 0x00, VERSION1 = 0x01, + VERSION4 = 0x04, VERSION5 = 0x05 } @@ -52,6 +54,12 @@ export enum Reply { RESERVED = 0x09 // 0x09 - 0xFF unassigned } +// Result Code (CD) in SOCKS 4. +export enum ResultCode4 { + REQUEST_GRANTED = 90, + REQUEST_FAILED = 91 +} + // Represents the destination portion of a SOCKS request. // @see interpretDestination export interface Destination { @@ -143,7 +151,14 @@ export interface UdpMessage { data :Uint8Array; } - +export function checkVersion(buffer:ArrayBuffer) : Version { + let handshakeBytes = new Uint8Array(buffer); + let socksVersion = handshakeBytes[0]; + if (socksVersion != Version.VERSION5 && socksVersion != Version.VERSION4) { + throw new Error('unsupported SOCKS version: ' + socksVersion); + } + return socksVersion; +} // Client to Server (Step 1) // Authentication method negotiation @@ -578,3 +593,178 @@ export function interpretResponseBuffer(buffer:ArrayBuffer) : Response { return response; } + +// SOCKS 4 and 4a support. +// These protocols consist of a single message: +// +----+----+----+----+----+----+----+----+----+----+....+----+ +// | VN | CD | DSTPORT | DSTIP | USERID |NULL| +// +----+----+----+----+----+----+----+----+----+----+....+----+ +// # of bytes: 1 1 2 4 variable 1 +// Where VN is 4 and CD is 1 for CONNECT (2 for BIND). +// In SOCKS 4, a DSTIP of 0.0.0.x means that the command is followed by the +// domain, which is x bytes long, followed by another NULL byte. +// +// The server responds +// +----+----+----+----+----+----+----+----+ +// | VN | CD | DSTPORT | DSTIP | +// +----+----+----+----+----+----+----+----+ +// # of bytes: 1 1 2 4 +// To confirm a successful (CD = 90) or failed (CD = 91) connection. +// See https://www.openssh.com/txt/socks4.protocol and +// https://www.openssh.com/txt/socks4a.protocol. + +interface HeaderV4 { + version:Version; + code:Command|ResultCode4; + port:number; + ipv4:ipaddr.IPv4Address; +} + +export function composeRequestBufferV4(request:Request) : ArrayBuffer { + let destination = makeDestinationFromEndpoint(request.endpoint); + + // SOCKS 4 + let address :string = request.endpoint.address; + let extraBytes = 0; + if (destination.addressType === AddressType.DNS) { + // SOCKS 4a + address = '0.0.0.' + request.endpoint.address.length; + extraBytes = request.endpoint.address.length + 1; + } + + let ipv4 = ipaddr.IPv4.parse(address); + let header = composeHeaderBufferV4({ + version: Version.VERSION4, + code: Command.TCP_CONNECT, + ipv4, + port: request.endpoint.port + }); + + let userId = ''; // This implementation does not support userId. + + // The shortest possible request packet is 9 bytes + let byteArray = new Uint8Array(9 + userId.length + extraBytes); + byteArray.set(header, 0); + byteArray.set(new Buffer(userId, 'ascii'), 8); + byteArray[8 + userId.length] = 0; // NULL + if (extraBytes > 0) { + // SOCKS 4a + byteArray.set(new Buffer(request.endpoint.address, 'ascii'), + 9 + userId.length); + byteArray[9 + userId.length + request.endpoint.address.length] = 0; + } + + return byteArray.buffer; +} + +export function interpretRequestBufferV4(buffer:ArrayBuffer) : Request { + let byteArray = new Uint8Array(buffer); + + // Fail if the request is too short to be valid. + if (byteArray.length < 9) { + throw new Error('SOCKS4 request too short'); + } + + let header: HeaderV4 = interpretHeaderBufferV4(buffer); + + // Fail if client is not talking Socks version 4. + if (header.version !== Version.VERSION4) { + throw new Error('expecting SOCKS4'); + } + + // Fail unless we got a CONNECT command. + if (header.code !== Command.TCP_CONNECT) { + throw new Error('unsupported SOCKS4 command: ' + header.code); + } + + let userIdChars : string[] = []; + let i : number; + for (i = 8; byteArray[i] !== 0; ++i) { + userIdChars.push(String.fromCharCode(byteArray[i])); + } + let userId = userIdChars.join(''); + let address :string; + if (header.ipv4.range() === 'unspecified') { + // SOCKS 4a + let addressChars : string[] = []; + for (++i; byteArray[i] !== 0; ++i) { + addressChars.push(String.fromCharCode(byteArray[i])); + } + address = addressChars.join(''); + if (address.length !== header.ipv4.octets[3]) { + throw new Error('Address "' + address + + '" doesn\'t have expected length ' + header.ipv4.octets[3]); + } + } else { + // SOCKS 4 + address = header.ipv4.toString(); + } + let request :Request = { + command: header.code, + endpoint: {address, port: header.port} + }; + + if (!isValidRequest(request)) { + throw new Error('Constructed invalid request object: ' + + JSON.stringify(request)); + } + + return request; +} + +export function composeResponseBufferV4(response:Response) : ArrayBuffer { + let code :ResultCode4 = response.reply === Reply.SUCCEEDED ? + ResultCode4.REQUEST_GRANTED : ResultCode4.REQUEST_FAILED; + return composeHeaderBufferV4({ + version: Version.VERSION0, + code, + ipv4: ipaddr.IPv4.parse(response.endpoint.address), + port: response.endpoint.port + }); +} + +export function interpretResponseBufferV4(buffer:ArrayBuffer) : Response { + let header = interpretHeaderBufferV4(buffer); + if (header.version !== Version.VERSION0) { + throw new Error('Unexpected version ' + header.version); + } + + if (!(header.code in ResultCode4)) { + throw new Error('Unexpected code ' + header.code); + } + + let reply = header.code === ResultCode4.REQUEST_GRANTED ? + Reply.SUCCEEDED : Reply.FAILURE; + let endpoint :net.Endpoint = { + address: header.ipv4.toString(), + port: header.port + }; + return {reply, endpoint}; +} + +function composeHeaderBufferV4(header:HeaderV4) : Uint8Array { + let byteArray = new Uint8Array(8); + byteArray[0] = header.version; + byteArray[1] = header.code; + byteArray[2] = header.port >> 8; + byteArray[3] = header.port & 0xFF; + byteArray.set(header.ipv4.octets, 4); + return byteArray; +} + +function interpretHeaderBufferV4(buffer:ArrayBuffer) : HeaderV4 { + let byteArray = new Uint8Array(buffer); + + // Fail if the request is too short to be valid. + if (byteArray.length < 8) { + throw new Error('SOCKS4 header is too short'); + } + + // Fail if client is not talking Socks version 4. + let version :number = byteArray[0]; + let code :number = byteArray[1]; + let port = byteArray[2] << 8 | byteArray[3]; + let ipv4Bytes :number[] = Array.prototype.slice.call(byteArray.subarray(4, 8)); + let ipv4 = new ipaddr.IPv4(ipv4Bytes); + return {version, code, ipv4, port}; +} From 4e4f195cdad13b409f4993b57653524ebdbf0813 Mon Sep 17 00:00:00 2001 From: Ben Schwartz Date: Fri, 16 Dec 2016 17:58:04 -0500 Subject: [PATCH 2/3] Fix return type of composeResponseBufferV4 --- src/lib/socks/headers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/socks/headers.ts b/src/lib/socks/headers.ts index 3ef5745fd3..c5763d6d54 100644 --- a/src/lib/socks/headers.ts +++ b/src/lib/socks/headers.ts @@ -720,7 +720,7 @@ export function composeResponseBufferV4(response:Response) : ArrayBuffer { code, ipv4: ipaddr.IPv4.parse(response.endpoint.address), port: response.endpoint.port - }); + }).buffer; } export function interpretResponseBufferV4(buffer:ArrayBuffer) : Response { From b84b3c9e24d9ffc4e6eca839eff5dfc85c7b8209 Mon Sep 17 00:00:00 2001 From: Ben Schwartz Date: Fri, 16 Dec 2016 18:01:10 -0500 Subject: [PATCH 3/3] Add SOCKS4 support to SocksToRtc --- .../socks-echo/base-spec.core-env.ts | 14 +++++ .../socks-echo/freedom-module.json | 2 +- .../socks-echo/proxy-integration-test.ts | 36 ++++++++++- .../proxy-integration-test.types.ts | 2 +- src/lib/socks-to-rtc/socks-to-rtc.ts | 60 ++++++++++++------- 5 files changed, 90 insertions(+), 24 deletions(-) diff --git a/src/lib/integration-tests/socks-echo/base-spec.core-env.ts b/src/lib/integration-tests/socks-echo/base-spec.core-env.ts index dbe1b742dc..52484589d3 100644 --- a/src/lib/integration-tests/socks-echo/base-spec.core-env.ts +++ b/src/lib/integration-tests/socks-echo/base-spec.core-env.ts @@ -409,4 +409,18 @@ export function socksEchoTestDescription(useChurn:boolean) { expect(e.reply).toEqual(socks_headers.Reply.HOST_UNREACHABLE); }).then(done); }); + + it('run a simple echo test using SOCKS 4', (done) => { + var input = arraybuffers.stringToArrayBuffer('arbitrary test string'); + var testModule = createTestModule(); + testModule.startEchoServer().then((port:number) => { + return testModule.connect(port, '127.0.0.1', /* useV4 */ true); + }).then((connectionId:string) => { + return testModule.echo(connectionId, input); + }).then((output:ArrayBuffer) => { + expect(arraybuffers.byteEquality(input, output)).toBe(true); + }).catch((e:any) => { + expect(e).toBeUndefined(); + }).then(done); + }); }; diff --git a/src/lib/integration-tests/socks-echo/freedom-module.json b/src/lib/integration-tests/socks-echo/freedom-module.json index 8647e4457e..8e75ef225f 100644 --- a/src/lib/integration-tests/socks-echo/freedom-module.json +++ b/src/lib/integration-tests/socks-echo/freedom-module.json @@ -39,7 +39,7 @@ }, "connect": { "type": "method", - "value": ["number", "string"], + "value": ["number", "string", "boolean"], "ret": "string" }, "setRepeat": { diff --git a/src/lib/integration-tests/socks-echo/proxy-integration-test.ts b/src/lib/integration-tests/socks-echo/proxy-integration-test.ts index d6943aef45..e057a79dde 100644 --- a/src/lib/integration-tests/socks-echo/proxy-integration-test.ts +++ b/src/lib/integration-tests/socks-echo/proxy-integration-test.ts @@ -201,6 +201,37 @@ export default class AbstractProxyIntegrationTest implements ProxyIntegrationTes }); } + private connectThroughSocksV4_ = (socksEndpoint:net.Endpoint, webEndpoint:net.Endpoint) : Promise => { + var connection = new tcp.Connection({endpoint: socksEndpoint}); + connection.onceClosed.then(() => { + console.log('Socket ' + connection.connectionId + ' has closed'); + this.dispatchEvent_('sockClosed', connection.connectionId); + }); + + let request :socks_headers.Request = { + command: socks_headers.Command.TCP_CONNECT, + endpoint: webEndpoint, + }; + connection.send(socks_headers.composeRequestBufferV4(request)); + var connected = new Promise((F, R) => { + connection.onceConnected.then(F); + connection.onceClosed.then(R); + }); + var firstBufferPromise :Promise = connection.receiveNext(); + return connected.then((i:tcp.ConnectionInfo) => { + return firstBufferPromise; + }).then((buffer:ArrayBuffer) : Promise => { + let response = socks_headers.interpretResponseBufferV4(buffer); + log.debug('Received request response: %1', [response]); + if (response.reply !== socks_headers.Reply.SUCCEEDED) { + // TODO: Fix bad style: reject should only and always be an error. + // We should be resolving with result status. + return Promise.reject(response); + } + return Promise.resolve(connection); + }); + } + private connectThroughSocks_ = (socksEndpoint:net.Endpoint, webEndpoint:net.Endpoint) : Promise => { var connection = new tcp.Connection({endpoint: socksEndpoint}); connection.onceClosed.then(() => { @@ -246,13 +277,16 @@ export default class AbstractProxyIntegrationTest implements ProxyIntegrationTes }); } - public connect = (port:number, address?:string) : Promise => { + public connect = (port:number, address?:string, useV4?:boolean) : Promise => { try { return this.socksEndpoint_.then((socksEndpoint:net.Endpoint) : Promise => { var echoEndpoint :net.Endpoint = { address: address || this.localhost_, port: port }; + if (useV4) { + return this.connectThroughSocksV4_(socksEndpoint, echoEndpoint) + } return this.connectThroughSocks_(socksEndpoint, echoEndpoint); }).then((connection:tcp.Connection) => { this.connections_[connection.connectionId] = connection; diff --git a/src/lib/integration-tests/socks-echo/proxy-integration-test.types.ts b/src/lib/integration-tests/socks-echo/proxy-integration-test.types.ts index adc2b0211e..a9ad3e4d11 100644 --- a/src/lib/integration-tests/socks-echo/proxy-integration-test.types.ts +++ b/src/lib/integration-tests/socks-echo/proxy-integration-test.types.ts @@ -6,7 +6,7 @@ export interface ReceivedDataEvent { export interface ProxyIntegrationTester { startEchoServer() :Promise; // Returns a unique identifier for the connection (the connectionId). - connect(port:number, address?:string) :Promise; + connect(port:number, address?:string, useV4?:boolean) :Promise; // Sets the number of concatenated copies of the input to echo. (default: 1) setRepeat(repeat:number) :Promise; echo(connectionId:string, content:ArrayBuffer) :Promise; diff --git a/src/lib/socks-to-rtc/socks-to-rtc.ts b/src/lib/socks-to-rtc/socks-to-rtc.ts index a170df2639..130621cbf0 100644 --- a/src/lib/socks-to-rtc/socks-to-rtc.ts +++ b/src/lib/socks-to-rtc/socks-to-rtc.ts @@ -224,8 +224,7 @@ export class Session { // The session is ready once we've completed both // auth and request handshakes. - this.onceReady = this.doAuthHandshake_().then( - this.doRequestHandshake_).then((response:socks_headers.Response) => { + this.onceReady = this.doHandshake_().then((response:socks_headers.Response) => { if (response.reply !== socks_headers.Reply.SUCCEEDED) { throw new Error('handshake failed with reply code ' + socks_headers.Reply[response.reply]); @@ -300,20 +299,29 @@ export class Session { }); } + private doHandshake_ = () : Promise => { + return this.tcpConnection_.receiveNext().then((firstBuffer:ArrayBuffer) => { + let version = socks_headers.checkVersion(firstBuffer); + if (version === socks_headers.Version.VERSION5) { + return this.doAuthHandshake_(firstBuffer).then(this.doRequestHandshake_); + } else if (version === socks_headers.Version.VERSION4) { + return this.doV4Handshake_(firstBuffer); + } else { + throw new Error('Invalid SOCKS version ' + version); + } + }); + } + // Receive a socks connection and send the initial Auth messages. // Assumes: no packet fragmentation. // TODO: send failure to client if auth fails // TODO: handle packet fragmentation: // https://github.com/uProxy/uproxy/issues/323 // TODO: Needs unit tests badly since it's mocked by several other tests. - private doAuthHandshake_ = () - : Promise => { - return this.tcpConnection_.receiveNext() - .then(socks_headers.interpretAuthHandshakeBuffer) - .then((auths:socks_headers.Auth[]) => { - this.tcpConnection_.send( + private doAuthHandshake_ = (authRequestBuffer:ArrayBuffer) : Promise => { + let auths:socks_headers.Auth[] = socks_headers.interpretAuthHandshakeBuffer(authRequestBuffer); + return this.tcpConnection_.send( socks_headers.composeAuthResponse(socks_headers.Auth.NOAUTH)); - }); } // Handles the SOCKS handshake, fulfilling with the socks.Response instance @@ -332,14 +340,20 @@ export class Session { private doRequestHandshake_ = () : Promise => { return this.tcpConnection_.receiveNext() .then(socks_headers.interpretRequestBuffer) - .then((request:socks_headers.Request) => { - // The domain name is very sensitive, so we keep it out of the - // info-level logs, which may be uploaded. - log.debug('%1: received endpoint from SOCKS client: %2', [ - this.longId(), JSON.stringify(request.endpoint)]); - this.tcpConnection_.pause(); - return this.dataChannel_.send({ str: JSON.stringify(request) }); - }) + .then(this.handshakeWithPeer_) + .then((response:socks_headers.Response) => { + return this.tcpConnection_.send(socks_headers.composeResponseBuffer( + response)).then((discard:any) => { return response; }); + }); + } + + private handshakeWithPeer_ = (request:socks_headers.Request) : Promise => { + // The domain name is very sensitive, so we keep it out of the + // info-level logs, which may be uploaded. + log.debug('%1: received endpoint from SOCKS client: %2', [ + this.longId(), JSON.stringify(request.endpoint)]); + this.tcpConnection_.pause(); + return this.dataChannel_.send({ str: JSON.stringify(request) }) .then(() => { // Equivalent to channel.receiveNext(), if it existed. return new Promise((F, R) => { @@ -370,13 +384,17 @@ export class Session { return { reply: socks_headers.Reply.FAILURE }; - }) - .then((response:socks_headers.Response) => { - return this.tcpConnection_.send(socks_headers.composeResponseBuffer( - response)).then((discard:any) => { return response; }); }); } + private doV4Handshake_ = (buffer:ArrayBuffer) : Promise => { + let request = socks_headers.interpretRequestBufferV4(buffer); + return this.handshakeWithPeer_(request).then((response:socks_headers.Response) => { + return this.tcpConnection_.send(socks_headers.composeResponseBufferV4( + response)).then((discard:any) => { return response; }); + }); + } + // Sends a packet over the data channel. // Invoked when a packet is received over the TCP socket. private sendOnChannel_ = (data:ArrayBuffer) : void => {