Compare commits

..

No commits in common. "b8c423edca834d8787a5ccd4a7fb02be59118967" and "175286e791005c96a94cb05ff4163d9c0be02694" have entirely different histories.

9 changed files with 54 additions and 230 deletions

139
README.md
View File

@ -2,129 +2,32 @@
| Sponsored by [ppl](https://ppl.family) |
"The M-PROXY Protocol" for node.js
A strategy for packing and unpacking tunneled network messages (or any stream) in node.js
A strategy for packing and unpacking multiplexed streams.
<small>Where you have distinct clients on one side trying to reach distinct servers on the other.</small>
```
Browser <--\ /--> Device
Browser <---- M-PROXY Service ----> 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.
The M-PROXY Protocol
===================
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).
It also has a backchannel for communicating with the proxy itself.
Each message has a header with a socket identifier (family, addr, port), and may have additional information.
```
<version><headerlen><family>,<address>,<port>,<datalen>,<service>,<port>,<name>
```
```
<254><45>IPv4,127.0.1.1,4321,199,https,443,example.com
```
```
version (8 bits) 254 is version 1
header length (8 bits) the remaining length of the header before data begins
These values are used to identify a specific client among many
socket family (string) the IPv4 or IPv6 connection from a client
socket address (string) the x.x.x.x remote address of the client
socket port (string) the 1-65536 source port of the remote client
data length (string) the number of bytes in the wrapped packet, in case the network re-chunks the packet
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'
as listed in /etc/services, otherwise 'tls', 'tcp', or 'udp' for generics
Also used for messages with the proxy (i.e. authentication)
* 'control' for authentication, etc
* '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.
host or server name (string) Useful for services that can be routed by name, such as http, https, smtp, and dns.
```
v1 is text-based. Future versions may be binary.
API
===
Examples
```js
var Packer = require('proxy-packer');
```
```js
unpacker = Packer.create(handlers); // Create a state machine for unpacking
Packer.create({
onmessage: function (msg) {
// msg = { family, address, port, service, data };
}
, onend: function (msg) {
// msg = { family, address, port };
}
, onerror: function (err) {
// err = { message, family, address, port };
}
});
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" ]'
var chunk = Packer.pack(address, data, service);
var addr = Packer.socketToAddr(socket);
var id = Packer.addrToId(address);
var id = Packer.socketToId(socket);
handlers.onmessage = function (tun) { } // a client has sent a message
// tun = { family, address, port, data
// , service, serviceport, name };
var myDuplex = Packer.Stream.create(socketOrStream);
handlers.onpause = function (tun) { } // proxy requests to pause upload to a client
// tun = { family, address, port };
handlers.onresume = function (tun) { } // proxy requests to resume upload to a client
// tun = { 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 };
```
<!--
TODO
handlers.onconnect = function (tun) { } // a new client has connected
-->
```js
var chunk = Packer.pack(tun, data); // Add M-PROXY header to data
// tun = { family, address, port
// , service, serviceport, name }
var addr = Packer.socketToAddr(socket); // Probe raw, raw socket for address info
var id = Packer.addrToId(address); // Turn M-PROXY address info into a deterministic id
var id = Packer.socketToId(socket); // Turn raw, raw socket info into a deterministic id
```
## API Helpers
```js
var socket = Packer.Stream.wrapSocket(socketOrStream); // workaround for https://github.com/nodejs/node/issues/8854
// which was just closed recently, but probably still needs
// something more like this (below) to work as intended
// https://github.com/findhit/proxywrap/blob/master/lib/proxywrap.js
```
```js
var myTransform = Packer.Transform.create({
address: {
family: '...'
@ -142,7 +45,7 @@ If you want to write a compatible packer, just make sure that for any given inpu
you get the same output as the packer does.
```bash
node test/pack.js input.json output.bin
node test-pack.js input.json output.bin
hexdump output.bin
```
@ -154,10 +57,8 @@ Where `input.json` looks something like this:
, "address": {
"family": "IPv4"
, "address": "127.0.1.1"
, "port": 4321
, "port": 443
, "service": "foo"
, "serviceport": 443
, "name": 'example.com'
}
, "filepath": "./sni.tcp.bin"
}

View File

@ -120,8 +120,6 @@ Packer.create = function (opts) {
machine.port = machine._headers[2];
machine.bodyLen = parseInt(machine._headers[3], 10) || 0;
machine.service = machine._headers[4];
machine.serviceport = machine._headers[5];
machine.name = machine._headers[6];
//console.log('machine.service', machine.service);
return true;
@ -150,13 +148,11 @@ Packer.create = function (opts) {
}
}
msg.family = machine.family;
msg.address = machine.address;
msg.port = machine.port;
msg.service = machine.service;
msg.serviceport = machine.serviceport;
msg.name = machine.name;
msg.data = data;
msg.family = machine.family;
msg.address = machine.address;
msg.port = machine.port;
msg.service = machine.service;
msg.data = data;
if (machine.emit) {
machine.emit(serviceEvents[msg.service] || serviceEvents.default);
@ -186,7 +182,7 @@ Packer.create = function (opts) {
return machine;
};
Packer.pack = function (meta, data, service) {
Packer.pack = function (address, data, service) {
data = data || Buffer.from(' ');
if (!Buffer.isBuffer(data)) {
data = new Buffer(JSON.stringify(data));
@ -196,7 +192,7 @@ Packer.pack = function (meta, data, service) {
}
if (service && service !== 'control') {
meta.service = service;
address.service = service;
}
var version = 1;
@ -206,62 +202,19 @@ Packer.pack = function (meta, data, service) {
}
else {
header = Buffer.from([
meta.family, meta.address, meta.port, data.byteLength,
(meta.service || ''), (meta.serviceport || ''), (meta.name || '')
address.family, address.address, address.port, data.byteLength, (address.service || '')
].join(','));
}
var metaBuf = Buffer.from([ 255 - version, header.length ]);
var buf = Buffer.alloc(metaBuf.byteLength + header.byteLength + data.byteLength);
var meta = Buffer.from([ 255 - version, header.length ]);
var buf = Buffer.alloc(meta.byteLength + header.byteLength + data.byteLength);
metaBuf.copy(buf, 0);
meta.copy(buf, 0);
header.copy(buf, 2);
data.copy(buf, 2 + header.byteLength);
return buf;
};
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
&& 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
&& 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) {
// remoteAddress, remotePort... ugh... https://github.com/nodejs/node/issues/8854
var value = socket[propName] || socket['_' + propName];
@ -282,12 +235,10 @@ Packer.socketToAddr = function (socket) {
// tlsSocket.remoteAddress = remoteAddress; // causes core dump
// console.log(tlsSocket.remoteAddress);
var props = extractSocketProps(socket, [ 'remoteFamily', 'remoteAddress', 'remotePort', 'localPort' ]);
return {
family: props.remoteFamily
, address: props.remoteAddress
, port: props.remotePort
, serviceport: props.localPort
family: extractSocketProp(socket, 'remoteFamily')
, address: extractSocketProp(socket, 'remoteAddress')
, port: extractSocketProp(socket, 'remotePort')
};
};
@ -317,22 +268,9 @@ var sockFuncs = [
, 'setNoDelay'
, '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.
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();
addressNames.forEach(function (name) {
myDuplex[name] = extractSocketProp(socket, name);
@ -374,7 +312,6 @@ Packer.wrapSocket = function (socket) {
});
return myDuplex;
*/
};
var Transform = require('stream').Transform;
@ -386,8 +323,6 @@ function MyTransform(options) {
}
this.__my_addr = options.address;
this.__my_service = options.service;
this.__my_serviceport = options.serviceport;
this.__my_name = options.name;
Transform.call(this, options);
}
util.inherits(MyTransform, Transform);
@ -396,8 +331,6 @@ MyTransform.prototype._transform = function (data, encoding, callback) {
var address = this.__my_addr;
address.service = address.service || this.__my_service;
address.serviceport = address.serviceport || this.__my_serviceport;
address.name = address.name || this.__my_name;
this.push(Packer.pack(address, data));
callback();
};

View File

@ -2,9 +2,8 @@
, "address": {
"family": "IPv4"
, "address": "127.0.1.1"
, "port": 4321
, "service": "https"
, "serviceport": 443
, "port": 443
, "service": "foo"
}
, "filepath": "./sni.hello.bin"
}

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "proxy-packer",
"version": "1.4.3",
"version": "1.4.2",
"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",
"scripts": {

View File

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

View File

@ -1,18 +1,16 @@
'use strict';
var sni = require('sni');
var hello = require('fs').readFileSync(__dirname + '/sni.hello.bin');
var hello = require('fs').readFileSync('./sni.hello.bin');
var version = 1;
var address = {
family: 'IPv4'
, address: '127.0.1.1'
, port: 4321
, service: 'foo-https'
, serviceport: 443
, name: 'foo-pokemap.hellabit.com'
, port: 443
, service: 'foo'
};
var header = address.family + ',' + address.address + ',' + address.port + ',' + hello.byteLength
+ ',' + (address.service || '') + ',' + (address.serviceport || '') + ',' + (address.name || '')
+ ',' + (address.service || '')
;
var buf = Buffer.concat([
Buffer.from([ 255 - version, header.length ])
@ -22,21 +20,21 @@ var buf = Buffer.concat([
var services = { 'ssh': 22, 'http': 4080, 'https': 8443 };
var clients = {};
var count = 0;
var packer = require('../');
var packer = require('./');
var machine = packer.create({
onmessage: function (tun) {
var id = tun.family + ',' + tun.address + ',' + tun.port;
onmessage: function (opts) {
var id = opts.family + ',' + opts.address + ',' + opts.port;
var service = 'https';
var port = services[service];
var servername = sni(tun.data);
var servername = sni(opts.data);
console.log('');
console.log('[onMessage]');
if (!tun.data.equals(hello)) {
if (!opts.data.equals(hello)) {
throw new Error("'data' packet is not equal to original 'hello' packet");
}
console.log('all', tun.data.byteLength, 'bytes are equal');
console.log('src:', tun.family, tun.address + ':' + tun.port + ':' + tun.serviceport);
console.log('all', opts.data.byteLength, 'bytes are equal');
console.log('src:', opts.family, opts.address + ':' + opts.port);
console.log('dst:', 'IPv4 127.0.0.1:' + port);
if (!clients[id]) {
@ -44,7 +42,7 @@ var machine = packer.create({
if (!servername) {
throw new Error("no servername found for '" + id + "'");
}
console.log("servername: '" + servername + "'", tun.name);
console.log("servername: '" + servername + "'");
}
count += 1;