update docs and cleanup files

This commit is contained in:
AJ ONeal 2018-05-30 22:48:32 -06:00
parent 175286e791
commit 6b2b9607ec
9 changed files with 162 additions and 50 deletions

131
README.md
View File

@ -2,32 +2,123 @@
| Sponsored by [ppl](https://ppl.family) | | Sponsored by [ppl](https://ppl.family) |
A strategy for packing and unpacking tunneled network messages (or any stream) in node.js "The M-PROXY Protocol" for node.js
Examples 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
```
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 'control' is used for messages to the proxy (such as 'pause' events)
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
===
```js ```js
var Packer = require('proxy-packer'); var Packer = require('proxy-packer');
```
Packer.create({ ```js
onmessage: function (msg) { unpacker = Packer.create(handlers); // Create a state machine for unpacking
// msg = { family, address, port, service, data };
} handlers.oncontrol = function (tun) { } // for communicating with the proxy
, onend: function (msg) { // tun.data is an array
// msg = { family, address, port }; // '[ -1, "[Error] bad hello" ]'
} // '[ 0, "[Error] out-of-band error message" ]'
, onerror: function (err) { // '[ 1, "hello", 254, [ "add_token", "delete_token" ] ]'
// '[ 1, "add_token" ]'
// '[ 1, "delete_token" ]'
handlers.onmessage = function (tun) { } // a client has sent a message
// tun = { family, address, port, data
// , service, serviceport, name };
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 }; // err = { message, family, address, port };
} ```
});
var chunk = Packer.pack(address, data, service); <!--
var addr = Packer.socketToAddr(socket); TODO
var id = Packer.addrToId(address);
var id = Packer.socketToId(socket);
var myDuplex = Packer.Stream.create(socketOrStream); 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({ var myTransform = Packer.Transform.create({
address: { address: {
family: '...' family: '...'
@ -45,7 +136,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. you get the same output as the packer does.
```bash ```bash
node test-pack.js input.json output.bin node test/pack.js input.json output.bin
hexdump output.bin hexdump output.bin
``` ```
@ -57,8 +148,10 @@ Where `input.json` looks something like this:
, "address": { , "address": {
"family": "IPv4" "family": "IPv4"
, "address": "127.0.1.1" , "address": "127.0.1.1"
, "port": 443 , "port": 4321
, "service": "foo" , "service": "foo"
, "serviceport": 443
, "name": 'example.com'
} }
, "filepath": "./sni.tcp.bin" , "filepath": "./sni.tcp.bin"
} }

View File

@ -120,6 +120,8 @@ Packer.create = function (opts) {
machine.port = machine._headers[2]; machine.port = machine._headers[2];
machine.bodyLen = parseInt(machine._headers[3], 10) || 0; machine.bodyLen = parseInt(machine._headers[3], 10) || 0;
machine.service = machine._headers[4]; machine.service = machine._headers[4];
machine.serviceport = machine._headers[5];
machine.name = machine._headers[6];
//console.log('machine.service', machine.service); //console.log('machine.service', machine.service);
return true; return true;
@ -152,6 +154,8 @@ Packer.create = function (opts) {
msg.address = machine.address; msg.address = machine.address;
msg.port = machine.port; msg.port = machine.port;
msg.service = machine.service; msg.service = machine.service;
msg.serviceport = machine.serviceport;
msg.name = machine.name;
msg.data = data; msg.data = data;
if (machine.emit) { if (machine.emit) {
@ -182,7 +186,7 @@ Packer.create = function (opts) {
return machine; return machine;
}; };
Packer.pack = function (address, data, service) { Packer.pack = function (meta, data, service) {
data = data || Buffer.from(' '); data = data || Buffer.from(' ');
if (!Buffer.isBuffer(data)) { if (!Buffer.isBuffer(data)) {
data = new Buffer(JSON.stringify(data)); data = new Buffer(JSON.stringify(data));
@ -192,7 +196,7 @@ Packer.pack = function (address, data, service) {
} }
if (service && service !== 'control') { if (service && service !== 'control') {
address.service = service; meta.service = service;
} }
var version = 1; var version = 1;
@ -202,13 +206,14 @@ Packer.pack = function (address, data, service) {
} }
else { else {
header = Buffer.from([ header = Buffer.from([
address.family, address.address, address.port, data.byteLength, (address.service || '') meta.family, meta.address, meta.port, data.byteLength,
(meta.service || ''), (meta.serviceport || ''), (meta.name || '')
].join(',')); ].join(','));
} }
var meta = Buffer.from([ 255 - version, header.length ]); var metaBuf = Buffer.from([ 255 - version, header.length ]);
var buf = Buffer.alloc(meta.byteLength + header.byteLength + data.byteLength); var buf = Buffer.alloc(metaBuf.byteLength + header.byteLength + data.byteLength);
meta.copy(buf, 0); metaBuf.copy(buf, 0);
header.copy(buf, 2); header.copy(buf, 2);
data.copy(buf, 2 + header.byteLength); data.copy(buf, 2 + header.byteLength);
@ -323,6 +328,8 @@ function MyTransform(options) {
} }
this.__my_addr = options.address; this.__my_addr = options.address;
this.__my_service = options.service; this.__my_service = options.service;
this.__my_serviceport = options.serviceport;
this.__my_name = options.name;
Transform.call(this, options); Transform.call(this, options);
} }
util.inherits(MyTransform, Transform); util.inherits(MyTransform, Transform);
@ -331,6 +338,8 @@ 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;
address.serviceport = address.serviceport || this.__my_serviceport;
address.name = address.name || this.__my_name;
this.push(Packer.pack(address, data)); this.push(Packer.pack(address, data));
callback(); callback();
}; };

View File

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

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

Binary file not shown.

View File

@ -4,24 +4,30 @@
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');
if (!infile || !outfile) { if (!infile || !outfile) {
console.error("Usage:"); console.error("Usage:");
console.error("node test-pack.js input.json 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 json = JSON.parse(fs.readFileSync(infile, 'utf8')); var json = JSON.parse(fs.readFileSync(infile, 'utf8'));
var data = require('fs').readFileSync(json.filepath, null); var data = require('fs').readFileSync(path.resolve(path.dirname(infile), json.filepath), null);
var Packer = require('./index.js'); 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];
/* /*
function pack() { function pack() {
var version = json.version; var version = json.version;
var address = json.address; var address = json.address;
var header = address.family + ',' + address.address + ',' + address.port + ',' + data.byteLength var header = address.family + ',' + address.address + ',' + address.port + ',' + data.byteLength
+ ',' + (address.service || '') + ',' + (address.service || '') + ',' + (address.serviceport || '') + ',' + (servername || hostname || '')
; ;
var buf = Buffer.concat([ var buf = Buffer.concat([
Buffer.from([ 255 - version, header.length ]) Buffer.from([ 255 - version, header.length ])
@ -31,6 +37,7 @@ function pack() {
} }
*/ */
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("wrote " + buf.byteLength + " bytes to '" + outfile + "' ('hexdump " + outfile + "' to inspect)"); console.log("wrote " + buf.byteLength + " bytes to '" + outfile + "' ('hexdump " + outfile + "' to inspect)");

View File

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