Compare commits

..

No commits in common. "master" and "v1.4.3" have entirely different histories.

6 changed files with 466 additions and 780 deletions

151
README.md
View File

@ -1,4 +1,6 @@
# proxy-packer | a [Root](https://rootprojects.org) project # proxy-packer
| Sponsored by [ppl](https://ppl.family) |
"The M-PROXY Protocol" for node.js "The M-PROXY Protocol" for node.js
@ -11,11 +13,10 @@ Browser <---- M-PROXY Service ----> Device
Browser <--/ \--> Device Browser <--/ \--> Device
``` ```
<small>Many clients may connect to a single device. A single client may connect to many devices.</small>
It's the kind of thing you'd use to build a poor man's VPN, or port-forward router. It's the kind of thing you'd use to build a poor man's VPN, or port-forward router.
# The M-PROXY Protocol The M-PROXY Protocol
===================
This is similar to "The PROXY Protocol" (a la HAProxy), but desgined for multiplexed tls, http, tcp, and udp This is similar to "The PROXY Protocol" (a la HAProxy), but desgined for multiplexed tls, http, tcp, and udp
tunneled over arbitrary streams (such as WebSockets). tunneled over arbitrary streams (such as WebSockets).
@ -47,107 +48,46 @@ data length (string) the number of bytes in the wrapped packet, in
These optional values can be very useful at the start of a new connection These optional values can be very useful at the start of a new connection
service name (string) Either a standard service name (port + protocol), such as 'https' service name (string) Either a standard service name (port + protocol), such as 'https'
as listed in /etc/services, otherwise 'tls', 'tcp', or 'udp' for generics as listed in /etc/services, otherwise 'tls', 'tcp', or 'udp' for generics
Also used for messages with the proxy (i.e. authentication) Also 'control' is used for messages to the proxy (such as 'pause' events)
* 'control' for proxy<->server messages, including authentication, health, etc
* 'connection' for a specific client
* 'error' for a specific client
* 'pause' to pause upload to a specific client (not the whole tunnel)
* 'resume' to resume upload to a specific client (not the whole tunnel)
service port (string) The listening port, such as 443. Useful for non-standard or dynamic services. service port (string) The listening port, such as 443. Useful for non-standard or dynamic services.
host or server name (string) Useful for services that can be routed by name, such as http, https, smtp, and dns. host or server name (string) Useful for services that can be routed by name, such as http, https, smtp, and dns.
``` ```
## Tunneled TCP SNI Packet v1 is text-based. Future versions may be binary.
You should see that the result is simply all of the original packet with a leading header. API
===
Note that `16 03 01 00` starts at the 29th byte (at index 28 or 0x1C) instead of at index 0:
```
0 1 2 3 4 5 6 7 8 9 A B C D D F
0000000 fe 1a 49 50 76 34 2c 31 32 37 2e 30 2e 31 2e 31 <-- 0xfe = v1, 0x1a = 26 more bytes for header
0000010 2c 34 34 33 2c 31 39 39 2c 66 6f 6f
16 03 01 00 <-- first 4 bytes of tcp packet
0000020 c2 01 00 00 be 03 03 57 e3 76 50 66 03 df 99 76
0000030 24 c8 31 e6 e8 08 34 6b b4 7b bb 2c f3 17 aa 5c
0000040 ec 09 da da 83 5a b2 00 00 56 00 ff c0 24 c0 23
0000050 c0 0a c0 09 c0 08 c0 28 c0 27 c0 14 c0 13 c0 12
0000060 c0 26 c0 25 c0 05 c0 04 c0 03 c0 2a c0 29 c0 0f
0000070 c0 0e c0 0d 00 6b 00 67 00 39 00 33 00 16 00 3d
0000080 00 3c 00 35 00 2f 00 0a c0 07 c0 11 c0 02 c0 0c
0000090 00 05 00 04 00 af 00 ae 00 8d 00 8c 00 8a 00 8b
00000a0 01 00 00 3f 00 00 00 19 00 17 00 00 14 70 6f 6b
00000b0 65 6d 61 70 2e 68 65 6c 6c 61 62 69 74 2e 63 6f
00000c0 6d 00 0a 00 08 00 06 00 17 00 18 00 19 00 0b 00
00000d0 02 01 00 00 0d 00 0c 00 0a 05 01 04 01 02 01 04
00000e0 03 02 03
00000e3
```
The v1 header uses strings for address and service descriptor information,
but future versions may be binary.
# API
```js ```js
var Packer = require('proxy-packer'); var Packer = require('proxy-packer');
``` ```
## Unpacker / Parser State Machine
The unpacker creates a state machine.
Each data chunk going in must be in sequence (tcp guarantees this),
composing a full message with header and data (unless data length is 0).
The state machine progresses through these states:
- version
- headerLength
- header
- data
At the end of the data event (which may or may not contain a buffer of data)
one of the appropriate handlers will be called.
- control
- connection
- message
- pause
- resume
- end
- error
```js ```js
unpacker = Packer.create(handlers); // Create a state machine for unpacking unpacker = Packer.create(handlers); // Create a state machine for unpacking
unpacker.fns.addData(chunk); // process a chunk of data handlers.oncontrol = function (tun) { } // for communicating with the proxy
// tun.data is an array
// '[ -1, "[Error] bad hello" ]'
// '[ 0, "[Error] out-of-band error message" ]'
// '[ 1, "hello", 254, [ "add_token", "delete_token" ] ]'
// '[ 1, "add_token" ]'
// '[ 1, "delete_token" ]'
handlers.oncontrol = function(tun) {}; // for communicating with the proxy handlers.onmessage = function (tun) { } // a client has sent a message
// tun.data is an array // tun = { family, address, port, data
// '[ -1, "[Error] bad hello" ]' // , service, serviceport, name };
// '[ 0, "[Error] out-of-band error message" ]'
// '[ 1, "hello", 254, [ "add_token", "delete_token" ] ]'
// '[ 1, "add_token" ]'
// '[ 1, "delete_token" ]'
handlers.onconnection = function(tun) {}; // a client has established a connection handlers.onpause = function (tun) { } // proxy requests to pause upload to a client
// tun = { family, address, port };
handlers.onmessage = function(tun) {}; // a client has sent a message handlers.onresume = function (tun) { } // proxy requests to resume upload to a client
// tun = { family, address, port, data // tun = { family, address, port };
// , service, serviceport, name };
handlers.onpause = function(tun) {}; // proxy requests to pause upload to a client handlers.onend = function (tun) { } // proxy requests to close a client's socket
// tun = { family, address, port }; // tun = { family, address, port };
handlers.onresume = function(tun) {}; // proxy requests to resume upload to a client handlers.onerror = function (err) { } // proxy is relaying a client's error
// tun = { family, address, port }; // err = { message, family, address, port };
handlers.onend = function(tun) {}; // proxy requests to close a client's socket
// tun = { family, address, port };
handlers.onerror = function(err) {}; // proxy is relaying a client's error
// err = { message, family, address, port };
``` ```
<!-- <!--
@ -157,17 +97,10 @@ handlers.onconnect = function (tun) { } // a new client has co
--> -->
## Packer & Extras
Packs header metadata about connection into a buffer (potentially with original data), ready to send.
```js ```js
var headerAndBody = Packer.pack(tun, data); // Add M-PROXY header to data var chunk = Packer.pack(tun, data); // Add M-PROXY header to data
// tun = { family, address, port // tun = { family, address, port
// , service, serviceport, name } // , service, serviceport, name }
var headerBuf = Packer.packHeader(tun, data); // Same as above, but creates a buffer for header only
// (data can be converted to a buffer or sent as-is)
var addr = Packer.socketToAddr(socket); // Probe raw, raw socket for address info var addr = Packer.socketToAddr(socket); // Probe raw, raw socket for address info
@ -180,20 +113,20 @@ var id = Packer.socketToId(socket); // Turn raw, raw socket info into a determin
```js ```js
var socket = Packer.Stream.wrapSocket(socketOrStream); // workaround for https://github.com/nodejs/node/issues/8854 var socket = Packer.Stream.wrapSocket(socketOrStream); // workaround for https://github.com/nodejs/node/issues/8854
// which was just closed recently, but probably still needs // which was just closed recently, but probably still needs
// something more like this (below) to work as intended // something more like this (below) to work as intended
// https://github.com/findhit/proxywrap/blob/master/lib/proxywrap.js // https://github.com/findhit/proxywrap/blob/master/lib/proxywrap.js
``` ```
```js ```js
var myTransform = Packer.Transform.create({ var myTransform = Packer.Transform.create({
address: { address: {
family: '...', family: '...'
address: '...', , address: '...'
port: '...' , port: '...'
}, }
// hint at the service to be used // hint at the service to be used
service: 'https' , service: 'https'
}); });
``` ```
@ -210,7 +143,6 @@ hexdump output.bin
Where `input.json` looks something like this: Where `input.json` looks something like this:
`input.json`: `input.json`:
``` ```
{ "version": 1 { "version": 1
, "address": { , "address": {
@ -225,12 +157,12 @@ Where `input.json` looks something like this:
} }
``` ```
## Raw TCP SNI Packet Raw TCP SNI Packet
------------------
and `sni.tcp.bin` is any captured tcp packet, such as this one with a tls hello: and `sni.tcp.bin` is any captured tcp packet, such as this one with a tls hello:
`sni.tcp.bin`: `sni.tcp.bin`:
``` ```
0 1 2 3 4 5 6 7 8 9 A B C D D F 0 1 2 3 4 5 6 7 8 9 A B C D D F
0000000 16 03 01 00 c2 01 00 00 be 03 03 57 e3 76 50 66 0000000 16 03 01 00 c2 01 00 00 be 03 03 57 e3 76 50 66
@ -249,7 +181,8 @@ and `sni.tcp.bin` is any captured tcp packet, such as this one with a tls hello:
00000c7 00000c7
``` ```
## Tunneled TCP SNI Packet Tunneled TCP SNI Packet
-----------------------
You should see that the result is simply all of the original packet with a leading header. You should see that the result is simply all of the original packet with a leading header.

259
index.js
View File

@ -3,37 +3,33 @@
var Packer = module.exports; var Packer = module.exports;
var serviceEvents = { var serviceEvents = {
default: 'tunnelData', default: 'tunnelData'
connection: 'tunnelConnection', , control: 'tunnelControl'
control: 'tunnelControl', , error: 'tunnelError'
error: 'tunnelError', , end: 'tunnelEnd'
end: 'tunnelEnd', , pause: 'tunnelPause'
pause: 'tunnelPause', , resume: 'tunnelResume'
resume: 'tunnelResume'
}; };
var serviceFuncs = { var serviceFuncs = {
default: 'onmessage', default: 'onmessage'
connection: 'onconnection', , control: 'oncontrol'
control: 'oncontrol', , error: 'onerror'
error: 'onerror', , end: 'onend'
end: 'onend', , pause: 'onpause'
pause: 'onpause', , resume: 'onresume'
resume: 'onresume'
}; };
Packer.create = function(opts) { Packer.create = function (opts) {
var machine; var machine;
if (!opts.onMessage && !opts.onmessage) { if (!opts.onMessage && !opts.onmessage) {
machine = new (require('events')).EventEmitter(); machine = new (require('events').EventEmitter)();
} else { } else {
machine = {}; machine = {};
} }
machine.onmessage = opts.onmessage || opts.onMessage; machine.onmessage = opts.onmessage || opts.onMessage;
machine.oncontrol = opts.oncontrol || opts.onControl; machine.oncontrol = opts.oncontrol || opts.onControl;
machine.onconnection =
opts.onconnection || opts.onConnection || function() {};
machine.onerror = opts.onerror || opts.onError; machine.onerror = opts.onerror || opts.onError;
machine.onend = opts.onend || opts.onEnd; machine.onend = opts.onend || opts.onEnd;
machine.onpause = opts.onpause || opts.onPause; machine.onpause = opts.onpause || opts.onPause;
@ -45,11 +41,10 @@ Packer.create = function(opts) {
machine.chunkIndex = 0; machine.chunkIndex = 0;
machine.buf = null; machine.buf = null;
machine.bufIndex = 0; machine.bufIndex = 0;
machine.fns.collectData = function(chunk, size) { machine.fns.collectData = function (chunk, size) {
var chunkLeft = chunk.length - machine.chunkIndex; var chunkLeft = chunk.length - machine.chunkIndex;
var hasLen = size > 0;
if (!hasLen) { if (size <= 0) {
return Buffer.alloc(0); return Buffer.alloc(0);
} }
@ -68,10 +63,7 @@ Packer.create = function(opts) {
// Read and mark as read however much data we need from the chunk to complete our buffer. // Read and mark as read however much data we need from the chunk to complete our buffer.
var partLen = size - machine.bufIndex; var partLen = size - machine.bufIndex;
var part = chunk.slice( var part = chunk.slice(machine.chunkIndex, machine.chunkIndex+partLen);
machine.chunkIndex,
machine.chunkIndex + partLen
);
machine.chunkIndex += partLen; machine.chunkIndex += partLen;
// If we had nothing buffered than the part of the chunk we just read is all we need. // If we had nothing buffered than the part of the chunk we just read is all we need.
@ -88,11 +80,11 @@ Packer.create = function(opts) {
return buf; return buf;
}; };
machine.fns.version = function(chunk) { machine.fns.version = function (chunk) {
//console.log(''); //console.log('');
//console.log('[version]'); //console.log('[version]');
if (255 - machine._version !== chunk[machine.chunkIndex]) { if ((255 - machine._version) !== chunk[machine.chunkIndex]) {
console.error('not v' + machine._version + ' (or data is corrupt)'); console.error("not v" + machine._version + " (or data is corrupt)");
// no idea how to fix this yet // no idea how to fix this yet
} }
machine.chunkIndex += 1; machine.chunkIndex += 1;
@ -100,8 +92,9 @@ Packer.create = function(opts) {
return true; return true;
}; };
machine.headerLen = 0; machine.headerLen = 0;
machine.fns.headerLength = function(chunk) { machine.fns.headerLength = function (chunk) {
//console.log(''); //console.log('');
//console.log('[headerLength]'); //console.log('[headerLength]');
machine.headerLen = chunk[machine.chunkIndex]; machine.headerLen = chunk[machine.chunkIndex];
@ -110,7 +103,7 @@ Packer.create = function(opts) {
return true; return true;
}; };
machine.fns.header = function(chunk) { machine.fns.header = function (chunk) {
//console.log(''); //console.log('');
//console.log('[header]'); //console.log('[header]');
var header = machine.fns.collectData(chunk, machine.headerLen); var header = machine.fns.collectData(chunk, machine.headerLen);
@ -129,25 +122,20 @@ Packer.create = function(opts) {
machine.service = machine._headers[4]; machine.service = machine._headers[4];
machine.serviceport = machine._headers[5]; machine.serviceport = machine._headers[5];
machine.name = machine._headers[6]; machine.name = machine._headers[6];
machine.servicename = machine._headers[7];
//console.log('machine.service', machine.service); //console.log('machine.service', machine.service);
return true; return true;
}; };
machine.fns.data = function(chunk) { machine.fns.data = function (chunk) {
//console.log(''); //console.log('');
//console.log('[data]'); //console.log('[data]');
var data; var data = machine.fns.collectData(chunk, machine.bodyLen);
// The 'connection' event may not have a body
// Other events may not have a body either
if (machine.bodyLen) {
data = machine.fns.collectData(chunk, machine.bodyLen);
// We don't have the entire body yet so return false. // We don't have the entire body yet so return false.
if (!data) { if (!data) {
return false; return false;
} }
}
// //
// data, end, error // data, end, error
@ -156,8 +144,8 @@ Packer.create = function(opts) {
if ('error' === machine.service) { if ('error' === machine.service) {
try { try {
msg = JSON.parse(data.toString()); msg = JSON.parse(data.toString());
} catch (e) { } catch(e) {
msg.message = 'e:' + JSON.stringify(data); msg.message = data.toString();
msg.code = 'E_UNKNOWN_ERR'; msg.code = 'E_UNKNOWN_ERR';
} }
} }
@ -170,21 +158,10 @@ Packer.create = function(opts) {
msg.name = machine.name; msg.name = machine.name;
msg.data = data; msg.data = data;
if ('connection' === machine.service) {
msg.service = machine.servicename;
}
//console.log('msn', machine.service);
if (machine.emit) { if (machine.emit) {
machine.emit( machine.emit(serviceEvents[msg.service] || serviceEvents.default);
serviceEvents[machine.service] ||
serviceEvents[msg.service] ||
serviceEvents.default
);
} else { } else {
(machine[serviceFuncs[machine.service]] || (machine[serviceFuncs[msg.service]] || machine[serviceFuncs.default])(msg);
machine[serviceFuncs[msg.service]] ||
machine[serviceFuncs.default])(msg);
} }
return true; return true;
@ -192,7 +169,7 @@ Packer.create = function(opts) {
machine.state = 0; machine.state = 0;
machine.states = ['version', 'headerLength', 'header', 'data']; machine.states = ['version', 'headerLength', 'header', 'data'];
machine.fns.addChunk = function(chunk) { machine.fns.addChunk = function (chunk) {
//console.log(''); //console.log('');
//console.log('[addChunk]'); //console.log('[addChunk]');
machine.chunkIndex = 0; machine.chunkIndex = 0;
@ -204,124 +181,45 @@ Packer.create = function(opts) {
machine.state %= machine.states.length; machine.state %= machine.states.length;
} }
} }
if ('data' === machine.states[machine.state] && 0 === machine.bodyLen) {
machine.fns[machine.states[machine.state]](chunk);
machine.state += 1;
machine.state %= machine.states.length;
}
}; };
return machine; return machine;
}; };
Packer.packHeader = function(meta, data, service, andBody, oldways) { Packer.pack = function (meta, data, service) {
if (oldways && !data) { data = data || Buffer.from(' ');
data = Buffer.from(' '); if (!Buffer.isBuffer(data)) {
data = new Buffer(JSON.stringify(data));
} }
if (data && !Buffer.isBuffer(data)) { if (!data.byteLength) {
data = Buffer.from(JSON.stringify(data));
}
if (oldways && !data.byteLength) {
data = Buffer.from(' '); data = Buffer.from(' ');
} }
if (service && -1 === ['control', 'connection'].indexOf(service)) { if (service && service !== 'control') {
//console.log('end?', service);
meta.service = service; meta.service = service;
} }
var size = (data && data.byteLength) || 0;
var sizeReserve = andBody ? size : 0;
var version = 1; var version = 1;
var header; var header;
if (service === 'control') { if (service === 'control') {
header = Buffer.from(['', '', '', size, service].join(',')); header = Buffer.from(['', '', '', data.byteLength, service].join(','));
} else if (service === 'connection') {
header = Buffer.from(
[
meta.family,
meta.address,
meta.port,
size,
'connection',
meta.serviceport || '',
meta.name || '',
meta.service || ''
].join(',')
);
} else {
header = Buffer.from(
[
meta.family,
meta.address,
meta.port,
size,
meta.service || '',
meta.serviceport || '',
meta.name || ''
].join(',')
);
} }
var metaBuf = Buffer.from([255 - version, header.length]); else {
var buf = Buffer.alloc( header = Buffer.from([
metaBuf.byteLength + header.byteLength + sizeReserve meta.family, meta.address, meta.port, data.byteLength,
); (meta.service || ''), (meta.serviceport || ''), (meta.name || '')
].join(','));
}
var metaBuf = Buffer.from([ 255 - version, header.length ]);
var buf = Buffer.alloc(metaBuf.byteLength + header.byteLength + data.byteLength);
metaBuf.copy(buf, 0); metaBuf.copy(buf, 0);
header.copy(buf, 2); header.copy(buf, 2);
if (sizeReserve) {
data.copy(buf, 2 + header.byteLength); data.copy(buf, 2 + header.byteLength);
}
return buf; return buf;
}; };
Packer.pack = function(meta, data, service) {
return Packer.packHeader(meta, data, service, true, true);
};
function extractSocketProps(socket, propNames) {
var props = {};
if (socket.remotePort) {
propNames.forEach(function(propName) {
props[propName] = socket[propName];
});
} else if (socket._remotePort) {
propNames.forEach(function(propName) {
props[propName] = socket['_' + propName];
});
} else if (socket._handle) {
if (
socket._handle._parent &&
socket._handle._parent.owner &&
socket._handle._parent.owner.stream &&
socket._handle._parent.owner.stream.remotePort
) {
propNames.forEach(function(propName) {
props[propName] = socket._handle._parent.owner.stream[propName];
});
} else if (
socket._handle._parentWrap &&
socket._handle._parentWrap.remotePort
) {
propNames.forEach(function(propName) {
props[propName] = socket._handle._parentWrap[propName];
});
} else if (
socket._handle._parentWrap &&
socket._handle._parentWrap._handle &&
socket._handle._parentWrap._handle.owner &&
socket._handle._parentWrap._handle.owner.stream &&
socket._handle._parentWrap._handle.owner.stream.remotePort
) {
propNames.forEach(function(propName) {
props[propName] =
socket._handle._parentWrap._handle.owner.stream[propName];
});
}
}
return props;
}
function extractSocketProp(socket, propName) { function extractSocketProp(socket, propName) {
// remoteAddress, remotePort... ugh... https://github.com/nodejs/node/issues/8854 // remoteAddress, remotePort... ugh... https://github.com/nodejs/node/issues/8854
var value = socket[propName] || socket['_' + propName]; var value = socket[propName] || socket['_' + propName];
@ -331,48 +229,40 @@ function extractSocketProp(socket, propName) {
try { try {
value = value || socket._handle._parentWrap[propName]; value = value || socket._handle._parentWrap[propName];
value = value = value || socket._handle._parentWrap._handle.owner.stream[propName];
value || socket._handle._parentWrap._handle.owner.stream[propName];
} catch (e) {} } catch (e) {}
return value || ''; return value || '';
} }
Packer.socketToAddr = function(socket) { Packer.socketToAddr = function (socket) {
// TODO BUG XXX // TODO BUG XXX
// https://github.com/nodejs/node/issues/8854 // https://github.com/nodejs/node/issues/8854
// tlsSocket.remoteAddress = remoteAddress; // causes core dump // tlsSocket.remoteAddress = remoteAddress; // causes core dump
// console.log(tlsSocket.remoteAddress); // console.log(tlsSocket.remoteAddress);
var props = extractSocketProps(socket, [
'remoteFamily',
'remoteAddress',
'remotePort',
'localPort'
]);
return { return {
family: props.remoteFamily, family: extractSocketProp(socket, 'remoteFamily')
address: props.remoteAddress, , address: extractSocketProp(socket, 'remoteAddress')
port: props.remotePort, , port: extractSocketProp(socket, 'remotePort')
serviceport: props.localPort
}; };
}; };
Packer.addrToId = function(address) { Packer.addrToId = function (address) {
return address.family + ',' + address.address + ',' + address.port; return address.family + ',' + address.address + ',' + address.port;
}; };
Packer.socketToId = function(socket) { Packer.socketToId = function (socket) {
return Packer.addrToId(Packer.socketToAddr(socket)); return Packer.addrToId(Packer.socketToAddr(socket));
}; };
var addressNames = [ var addressNames = [
'remoteAddress', 'remoteAddress'
'remotePort', , 'remotePort'
'remoteFamily', , 'remoteFamily'
'localAddress', , 'localAddress'
'localPort' , 'localPort'
]; ];
/*
var sockFuncs = [ var sockFuncs = [
'address' 'address'
, 'destroy' , 'destroy'
@ -383,23 +273,9 @@ var sockFuncs = [
, 'setNoDelay' , 'setNoDelay'
, 'setTimeout' , 'setTimeout'
]; ];
*/ // Improved workaround for https://github.com/nodejs/node/issues/8854
// Unlike Packer.Stream.create this should handle all of the events needed to make everything work. // Unlike Packer.Stream.create this should handle all of the events needed to make everything work.
Packer.wrapSocket = function(socket) { Packer.wrapSocket = function (socket) {
// node v10.2+ doesn't need a workaround for https://github.com/nodejs/node/issues/8854
addressNames.forEach(function(name) {
Object.defineProperty(socket, name, {
enumerable: false,
configurable: true,
get: function() {
return extractSocketProp(socket, name);
}
});
});
return socket;
// Improved workaround for https://github.com/nodejs/node/issues/8854
/*
// TODO use defineProperty to override remotePort, etc
var myDuplex = new require('stream').Duplex(); var myDuplex = new require('stream').Duplex();
addressNames.forEach(function (name) { addressNames.forEach(function (name) {
myDuplex[name] = extractSocketProp(socket, name); myDuplex[name] = extractSocketProp(socket, name);
@ -441,7 +317,6 @@ Packer.wrapSocket = function(socket) {
}); });
return myDuplex; return myDuplex;
*/
}; };
var Transform = require('stream').Transform; var Transform = require('stream').Transform;
@ -459,7 +334,7 @@ function MyTransform(options) {
} }
util.inherits(MyTransform, Transform); util.inherits(MyTransform, Transform);
MyTransform.prototype._transform = function(data, encoding, callback) { MyTransform.prototype._transform = function (data, encoding, callback) {
var address = this.__my_addr; var address = this.__my_addr;
address.service = address.service || this.__my_service; address.service = address.service || this.__my_service;
@ -471,11 +346,11 @@ MyTransform.prototype._transform = function(data, encoding, callback) {
Packer.Stream = {}; Packer.Stream = {};
var Dup = { var Dup = {
write: function(chunk, encoding, cb) { write: function (chunk, encoding, cb) {
//console.log('_write', chunk.byteLength); //console.log('_write', chunk.byteLength);
this.__my_socket.write(chunk, encoding, cb); this.__my_socket.write(chunk, encoding, cb);
}, }
read: function(size) { , read: function (size) {
//console.log('_read'); //console.log('_read');
var x = this.__my_socket.read(size); var x = this.__my_socket.read(size);
if (x) { if (x) {
@ -484,7 +359,7 @@ var Dup = {
} }
} }
}; };
Packer.Stream.create = function(socket) { Packer.Stream.create = function (socket) {
if (!Packer.Stream.warned) { if (!Packer.Stream.warned) {
console.warn('`Stream.create` deprecated, use `wrapSocket` instead'); console.warn('`Stream.create` deprecated, use `wrapSocket` instead');
Packer.Stream.warned = true; Packer.Stream.warned = true;
@ -518,7 +393,7 @@ Packer.Stream.create = function(socket) {
}; };
Packer.Transform = {}; Packer.Transform = {};
Packer.Transform.create = function(opts) { Packer.Transform.create = function (opts) {
// Note: service refers to the port that the incoming request was from, // Note: service refers to the port that the incoming request was from,
// if known (smtps, smtp, https, http, etc) // if known (smtps, smtp, https, http, etc)
// { address: '127.0.0.1', service: 'https' } // { address: '127.0.0.1', service: 'https' }

View File

@ -1,6 +1,6 @@
{ {
"name": "proxy-packer", "name": "proxy-packer",
"version": "2.0.4", "version": "1.4.3",
"description": "A strategy for packing and unpacking a proxy stream (i.e. packets through a tunnel). Handles multiplexed and tls connections. Used by telebit and telebitd.", "description": "A strategy for packing and unpacking a proxy stream (i.e. packets through a tunnel). Handles multiplexed and tls connections. Used by telebit and telebitd.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -1,11 +1,10 @@
{ { "version": 1
"version": 1, , "address": {
"address": { "family": "IPv4"
"family": "IPv4", , "address": "127.0.1.1"
"address": "127.0.1.1", , "port": 4321
"port": 4321, , "service": "https"
"service": "https", , "serviceport": 443
"serviceport": 443 }
}, , "filepath": "./sni.hello.bin"
"filepath": "./sni.hello.bin"
} }

View File

@ -1,31 +1,28 @@
(function() { ;(function () {
'use strict'; 'use strict';
var fs = require('fs'); var fs = require('fs');
var infile = process.argv[2]; var infile = process.argv[2];
var outfile = process.argv[3]; var outfile = process.argv[3];
var sni = require('sni'); var sni = require('sni');
if (!infile || !outfile) { if (!infile || !outfile) {
console.error('Usage:'); console.error("Usage:");
console.error('node test/pack.js test/input.json test/output.bin'); console.error("node test/pack.js test/input.json test/output.bin");
process.exit(1); process.exit(1);
return; return;
} }
var path = require('path'); var path = require('path');
var json = JSON.parse(fs.readFileSync(infile, 'utf8')); var json = JSON.parse(fs.readFileSync(infile, 'utf8'));
var data = require('fs').readFileSync( var data = require('fs').readFileSync(path.resolve(path.dirname(infile), json.filepath), null);
path.resolve(path.dirname(infile), json.filepath), var Packer = require('../index.js');
null
);
var Packer = require('../index.js');
var servername = sni(data); var servername = sni(data);
var m = data.toString().match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); var m = data.toString().match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
var hostname = ((m && m[1].toLowerCase()) || '').split(':')[0]; var hostname = (m && m[1].toLowerCase() || '').split(':')[0];
/* /*
function pack() { function pack() {
var version = json.version; var version = json.version;
var address = json.address; var address = json.address;
@ -40,16 +37,9 @@ function pack() {
} }
*/ */
json.address.name = servername || hostname; json.address.name = servername || hostname;
var buf = Packer.pack(json.address, data); var buf = Packer.pack(json.address, data);
fs.writeFileSync(outfile, buf, null); fs.writeFileSync(outfile, buf, null);
console.log( console.log("wrote " + buf.byteLength + " bytes to '" + outfile + "' ('hexdump " + outfile + "' to inspect)");
'wrote ' +
buf.byteLength + }());
" bytes to '" +
outfile +
"' ('hexdump " +
outfile +
"' to inspect)"
);
})();

View File

@ -3,197 +3,87 @@
var sni = require('sni'); var sni = require('sni');
var hello = require('fs').readFileSync(__dirname + '/sni.hello.bin'); var hello = require('fs').readFileSync(__dirname + '/sni.hello.bin');
var version = 1; var version = 1;
function getAddress() { var address = {
return { family: 'IPv4'
family: 'IPv4', , address: '127.0.1.1'
address: '127.0.1.1', , port: 4321
port: 4321, , service: 'foo-https'
service: 'foo-https', , serviceport: 443
serviceport: 443, , name: 'foo-pokemap.hellabit.com'
name: 'foo-pokemap.hellabit.com' };
}; var header = address.family + ',' + address.address + ',' + address.port + ',' + hello.byteLength
} + ',' + (address.service || '') + ',' + (address.serviceport || '') + ',' + (address.name || '')
var addr = getAddress(); ;
var connectionHeader =
addr.family +
',' +
addr.address +
',' +
addr.port +
',0,connection,' +
(addr.serviceport || '') +
',' +
(addr.name || '') +
',' +
(addr.service || '');
var header =
addr.family +
',' +
addr.address +
',' +
addr.port +
',' +
hello.byteLength +
',' +
(addr.service || '') +
',' +
(addr.serviceport || '') +
',' +
(addr.name || '');
var endHeader =
addr.family +
',' +
addr.address +
',' +
addr.port +
',0,end,' +
(addr.serviceport || '') +
',' +
(addr.name || '');
var buf = Buffer.concat([ var buf = Buffer.concat([
Buffer.from([255 - version, connectionHeader.length]), Buffer.from([ 255 - version, header.length ])
Buffer.from(connectionHeader), , Buffer.from(header)
Buffer.from([255 - version, header.length]), , hello
Buffer.from(header),
hello,
Buffer.from([255 - version, endHeader.length]),
Buffer.from(endHeader)
]); ]);
var services = { ssh: 22, http: 4080, https: 8443 }; var services = { 'ssh': 22, 'http': 4080, 'https': 8443 };
var clients = {}; var clients = {};
var count = 0; var count = 0;
var packer = require('../'); var packer = require('../');
var machine = packer.create({ var machine = packer.create({
onconnection: function(tun) { onmessage: function (tun) {
console.info('');
if (!tun.service || 'connection' === tun.service) {
throw new Error('missing service: ' + JSON.stringify(tun));
}
console.info('[onConnection]');
count += 1;
},
onmessage: function(tun) {
//console.log('onmessage', tun);
var id = tun.family + ',' + tun.address + ',' + tun.port; var id = tun.family + ',' + tun.address + ',' + tun.port;
var service = 'https'; var service = 'https';
var port = services[service]; var port = services[service];
var servername = sni(tun.data); var servername = sni(tun.data);
console.info( console.log('');
'[onMessage]', console.log('[onMessage]');
service,
port,
servername,
tun.data.byteLength
);
if (!tun.data.equals(hello)) { if (!tun.data.equals(hello)) {
throw new Error( throw new Error("'data' packet is not equal to original 'hello' packet");
"'data' packet is not equal to original 'hello' packet"
);
} }
//console.log('all', tun.data.byteLength, 'bytes are equal'); console.log('all', tun.data.byteLength, 'bytes are equal');
//console.log('src:', tun.family, tun.address + ':' + tun.port + ':' + tun.serviceport); console.log('src:', tun.family, tun.address + ':' + tun.port + ':' + tun.serviceport);
//console.log('dst:', 'IPv4 127.0.0.1:' + port); console.log('dst:', 'IPv4 127.0.0.1:' + port);
if (!clients[id]) { if (!clients[id]) {
clients[id] = true; clients[id] = true;
if (!servername) { if (!servername) {
throw new Error("no servername found for '" + id + "'"); throw new Error("no servername found for '" + id + "'");
} }
//console.log("servername: '" + servername + "'", tun.name); console.log("servername: '" + servername + "'", tun.name);
} }
count += 1; count += 1;
}, }
onerror: function() { , onerror: function () {
throw new Error('Did not expect onerror'); throw new Error("Did not expect onerror");
}, }
onend: function() { , onend: function () {
console.info('[onEnd]'); throw new Error("Did not expect onend");
count += 1;
} }
}); });
var packed = packer.pack(address, hello);
var packts, packed;
packts = [];
packts.push(packer.packHeader(getAddress(), null, 'connection'));
//packts.push(packer.pack(address, hello));
packts.push(packer.packHeader(getAddress(), hello));
packts.push(hello);
packts.push(packer.packHeader(getAddress(), null, 'end'));
packed = Buffer.concat(packts);
if (!packed.equals(buf)) { if (!packed.equals(buf)) {
console.error('');
console.error(buf.toString('hex') === packed.toString('hex')); console.error(buf.toString('hex') === packed.toString('hex'));
console.error('');
console.error('auto-packed:');
console.error(packed.toString('hex'), packed.byteLength); console.error(packed.toString('hex'), packed.byteLength);
console.error('');
console.error('hand-packed:');
console.error(buf.toString('hex'), buf.byteLength); console.error(buf.toString('hex'), buf.byteLength);
console.error(''); throw new Error("packer did not pack as expected");
throw new Error('packer (new) did not pack as expected');
} }
packts = [];
packts.push(packer.pack(getAddress(), null, 'connection'));
packts.push(packer.pack(getAddress(), hello));
//packts.push(packer.packHeader(getAddress(), hello));
//packts.push(hello);
packts.push(packer.pack(getAddress(), null, 'end'));
packed = Buffer.concat(packts);
// XXX TODO REMOVE console.log('');
//
// Nasty fix for short-term backwards-compat
//
// In the old way of doing things we always have at least one byte
// of data (due to a parser bug which has now been fixed) and so
// there are two strings padded with a space which gives the
// data a length of 1 rather than 0
//
// Here all four of those instances are replaced, but it requires
// maching a few things on either side.
//
// Only 6 bytes are changed - two 1 => 0, four ' ' => ''
var hex = packed
.toString('hex')
//.replace(/2c313939/, '2c30')
.replace(/32312c312c636f/, '32312c302c636f')
.replace(/3332312c312c656e64/, '3332312c302c656e64')
.replace(/7320/, '73')
.replace(/20$/, '');
if (hex !== buf.toString('hex')) {
console.error('');
console.error(buf.toString('hex') === hex);
console.error('');
console.error('auto-packed:');
console.error(hex, packed.byteLength);
console.error('');
console.error('hand-packed:');
console.error(buf.toString('hex'), buf.byteLength);
console.error('');
throw new Error('packer (old) did not pack as expected');
}
console.info('');
// full message in one go // full message in one go
// 223 = 2 + 22 + 199 // 223 = 2 + 22 + 199
console.info('[WHOLE BUFFER]', 2, header.length, hello.length, buf.byteLength); console.log('[WHOLE BUFFER]', 2, header.length, hello.length, buf.byteLength);
clients = {}; clients = {};
machine.fns.addChunk(buf); machine.fns.addChunk(buf);
console.info(''); console.log('');
// messages one byte at a time // messages one byte at a time
console.info('[BYTE-BY-BYTE BUFFER]', 1); console.log('[BYTE-BY-BYTE BUFFER]', 1);
clients = {}; clients = {};
buf.forEach(function(byte) { buf.forEach(function (byte) {
machine.fns.addChunk(Buffer.from([byte])); machine.fns.addChunk(Buffer.from([ byte ]));
}); });
console.info(''); console.log('');
// split messages in overlapping thirds // split messages in overlapping thirds
// 0-2 (2) // 0-2 (2)
@ -202,27 +92,26 @@ console.info('');
// 223-225 (2) // 223-225 (2)
// 225-247 (22) // 225-247 (22)
// 247-446 (199) // 247-446 (199)
buf = Buffer.concat([buf, buf]); buf = Buffer.concat([ buf, buf ]);
console.info('[OVERLAPPING BUFFERS]', buf.length); console.log('[OVERLAPPING BUFFERS]', buf.length);
clients = {}; clients = {};
[ [ buf.slice(0, 7) // version + header
buf.slice(0, 7), // version + header , buf.slice(7, 14) // header
buf.slice(7, 14), // header , buf.slice(14, 21) // header
buf.slice(14, 21), // header , buf.slice(21, 28) // header + body
buf.slice(21, 28), // header + body , buf.slice(28, 217) // body
buf.slice(28, 217), // body , buf.slice(217, 224) // body + version
buf.slice(217, 224), // body + version , buf.slice(224, 238) // version + header
buf.slice(224, 238), // version + header , buf.slice(238, buf.byteLength) // header + body
buf.slice(238, buf.byteLength) // header + body ].forEach(function (buf) {
].forEach(function(buf) {
machine.fns.addChunk(Buffer.from(buf)); machine.fns.addChunk(Buffer.from(buf));
}); });
console.info(''); console.log('');
process.on('exit', function() { process.on('exit', function () {
if (count !== 12) { if (count !== 4) {
throw new Error('should have delivered 12 messages, not ' + count); throw new Error("should have delivered 4 messages, not", count);
} }
console.info('TESTS PASS'); console.log('TESTS PASS');
console.info(''); console.log('');
}); });