Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UDP support #50

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions bin/wstunnel.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ module.exports = (Server, Client) => {
.string('s')
.string('t')
.string('p')
.boolean('u')
.alias('u', 'udp')
.alias('p', 'proxy')
.alias('t', 'tunnel')
.boolean('c')
Expand All @@ -52,11 +54,12 @@ module.exports = (Server, Client) => {
.describe('c', 'accept any certificates')
.describe('http', 'force to use http tunnel').argv;

const proto = argv.u ? 'udp' : 'tcp';
if (argv.s) {
let server;
if (argv.t) {
let [host, port] = argv.t.split(':');
server = new Server(host, port);
server = new Server({ host, port, proto });
} else {
server = new Server();
}
Expand Down Expand Up @@ -123,11 +126,7 @@ module.exports = (Server, Client) => {
remoteAddr = `${toks[2]}:${toks[3]}`;
} else if (toks.length === 3) {
remoteAddr = `${toks[1]}:${toks[2]}`;
if (toks[0] === 'stdio') {
localHost = toks[0];
} else {
localPort = toks[0];
}
localPort = toks[0];
} else if (toks.length === 1) {
remoteAddr = '';
localPort = toks[0];
Expand All @@ -136,11 +135,10 @@ module.exports = (Server, Client) => {
console.log(optimist.help());
process.exit(1);
}
localPort = parseInt(localPort);
if (localHost === 'stdio') {
client.startStdio(wsHostUrl, remoteAddr, { 'x-wstclient': machineId });
if (localPort === 'stdio') {
client.startStdio({ wsHostUrl, remoteAddr, proto }, { 'x-wstclient': machineId });
} else {
client.start(localHost, localPort, wsHostUrl, remoteAddr, {
client.start({ localHost, localPort: parseInt(localPort), wsHostUrl, remoteAddr, proto }, {
'x-wstclient': machineId,
});
}
Expand Down
94 changes: 73 additions & 21 deletions lib/WstClient.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const net = require('net');
const dgram = require('dgram');
const WsStream = require('./WsStream');
const url = require('url');
const log = require('lawg');
const ClientConn = require('./httptunnel/ClientConn');
const bindStream = require('./bindStream');
const bindUdpStream = require('./bindUdpStream');
const createWsClient = () => new (require('websocket').client)();

module.exports = wst_client = class wst_client extends require('events')
Expand All @@ -18,6 +20,7 @@ module.exports = wst_client = class wst_client extends require('events')
constructor() {
super();
this.tcpServer = net.createServer();
this.udpServer = dgram.createSocket('udp4');
}

verbose() {
Expand All @@ -40,32 +43,26 @@ module.exports = wst_client = class wst_client extends require('events')
setHttpOnly(httpOnly) {
this.httpOnly = httpOnly;
}
// example: start("localhost", 8081, "wss://ws.domain.com:454", "dst.domain.com:22")
// example: start({
// localHost: "localhost",
// localPort: 8081,
// wsHostUrl: "wss://ws.domain.com:454",
// remoteAddr: "dst.domain.com:22",
// proto: "tcp"
// });
// meaning: tunnel localhost:8081 to remoteAddr by using websocket connection to wsHost
// @wsHostUrl: ws:// denotes standard socket, wss:// denotes ssl socket
// may be changed at any time to change websocket server info
start(localHost, localPort, wsHostUrl, remoteAddr, optionalHeaders, cb) {
start({ localHost, localPort, wsHostUrl, remoteAddr, proto }, optionalHeaders, cb) {
this.wsHostUrl = wsHostUrl;

this.tcpServer.listen(localPort, localHost, cb);
this.tcpServer.on('connection', (tcpConn) => {
const bind = (tcp, s) => {
bindStream(tcp, s);
this.emit('tunnel', tcp, s);
};
this._connect(
this.wsHostUrl,
remoteAddr,
optionalHeaders,
(err, stream) => {
if (err) this.emit('connectFailed', err);
else bind(tcpConn, stream);
}
);
});
if (proto === 'udp') {
this.listenUdp(localPort, localHost, remoteAddr, optionalHeaders, cb);
} else {
this.listenTcp(localPort, localHost, remoteAddr, optionalHeaders, cb);
}
}

startStdio(wsHostUrl, remoteAddr, optionalHeaders, cb) {
startStdio({ wsHostUrl, remoteAddr, proto }, optionalHeaders, cb) {
this.wsHostUrl = wsHostUrl;
const bind = (s) => {
process.stdin.pipe(s);
Expand All @@ -76,6 +73,7 @@ module.exports = wst_client = class wst_client extends require('events')
this._connect(
this.wsHostUrl,
remoteAddr,
proto,
optionalHeaders,
(err, stream) => {
if (err) this.emit('connectFailed', err);
Expand All @@ -85,7 +83,61 @@ module.exports = wst_client = class wst_client extends require('events')
);
}

_connect(wsHostUrl, remoteAddr, optionalHeaders, cb) {
listenUdp(localPort, localHost, remoteAddr, optionalHeaders, cb) {
const udpServer = dgram.createSocket('udp4');
this.connections = new Set();
udpServer.bind(localPort, localHost, cb);
udpServer.on('message', (data, rinfo) => {
const id = `${rinfo.address}:${rinfo.port}`;
if (!this.connections.has(id)) {
this.connections.add(id);
this._connect(
this.wsHostUrl,
remoteAddr,
'udp',
optionalHeaders,
(err, stream) => {
if (err) {
this.emit('connectFailed', err);
this.connections.delete(id);
} else {
bindUdpStream(stream, udpServer, rinfo.address, rinfo.port, () => {
this.connections.delete(id);
});
stream.write(data);
this.emit('tunnel', udpServer, stream);
}
}
);
}
});
}

listenTcp(localPort, localHost, remoteAddr, optionalHeaders, cb) {
const tcpServer = net.createServer();
tcpServer.listen(localPort, localHost, cb);
tcpServer.on('connection', (tcpConn) => {
this._connect(
this.wsHostUrl,
remoteAddr,
'tcp',
optionalHeaders,
(err, stream) => {
if (err) {
this.emit('connectFailed', err);
} else {
bindStream(tcpConn, stream);
this.emit('tunnel', tcpConn, stream);
}
}
);
});
}

_connect(wsHostUrl, remoteAddr, proto, optionalHeaders, cb) {
if (remoteAddr && proto) {
remoteAddr = `${remoteAddr}:${proto}`;
}
if (this.httpOnly) {
return this._httpConnect(wsHostUrl, remoteAddr, optionalHeaders, cb);
} else {
Expand Down
128 changes: 77 additions & 51 deletions lib/WstServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ const WebSocketServer = require('websocket').server;
const http = require('http');
const url = require('url');
const net = require('net');
const dgram = require('dgram');
const WsStream = require('./WsStream');
const log = require('lawg');
const HttpTunnelServer = require('./httptunnel/Server');
const HttpTunnelReq = require('./httptunnel/ConnRequest');
const ChainedWebApps = require('./ChainedWebApps');

const bindStream = require('./bindStream');
const bindUdpStream = require('./bindUdpStream');
const httpReqRemoteIp = require('./httpReqRemoteIp');
module.exports = wst_server = class wst_server {
// if dstHost, dstPort are specified here, then all tunnel end points are at dstHost:dstPort, regardless what
// client requests, for security option
// webapp: customize webapp if any, you may use express app
constructor(dstHost, dstPort, webapp) {
this.dstHost = dstHost;
this.dstPort = dstPort;
constructor({ host, port, proto, webapp } = {}) {
this.dstHost = host;
this.dstPort = port;
this.dstProto = proto;
this.httpServer = http.createServer();
this.wsServer = new WebSocketServer({
httpServer: this.httpServer,
Expand All @@ -30,9 +34,61 @@ module.exports = wst_server = class wst_server {
apps.bindToHttpServer(this.httpServer);
}

accept(request, remote, connWrapperCb) {
let wsConn;
const ip = httpReqRemoteIp(request.httpRequest);
try {
wsConn = request.accept('tunnel-protocol', request.origin);
log(
`Client ${ip} established ${
request instanceof HttpTunnelReq ? 'http' : 'ws'
} tunnel to ${remote}`
);
} catch (e) {
log(`Client ${ip} rejected due to ${e.toString()}`);
return;
}
if (connWrapperCb) {
wsConn = connWrapperCb(wsConn);
}
return wsConn;
}

connectUdp(request, connWrapperCb, host, port) {
const socket = dgram.createSocket('udp4');
socket.bind(() => {
socket.removeAllListeners('error');
const wsConn = this.accept(request, `${host}:${port}:udp`, connWrapperCb);
if (wsConn) {
bindUdpStream(wsConn, socket, host, port, () => {
socket.close();
});
}
});
socket.on('error', (err) =>
request.reject(500, JSON.stringify(`Tunnel connect error to ${host}:${port}:udp: ` + err))
);
}

connectTcp(request, connWrapperCb, host, port) {
const tcpConn = net.connect(
{ host, port, allowHalfOpen: false },
() => {
tcpConn.removeAllListeners('error');
const wsConn = this.accept(request, `${host}:${port}`, connWrapperCb);
if (wsConn) {
bindStream(wsConn, tcpConn);
}
}
);
tcpConn.on('error', (err) =>
request.reject(500, JSON.stringify(`Tunnel connect error to ${host}:${port}:tcp: ` + err))
);
}

// localAddr: [addr:]port, the local address to listen at, i.e. localhost:8888, 8888, 0.0.0.0:8888
start(localAddr, cb) {
const [localHost, localPort] = Array.from(this._parseAddr(localAddr));
const [localHost, localPort] = this._parseAddr(localAddr);
return this.httpServer.listen(localPort, localHost, (err) => {
if (cb) {
cb(err);
Expand All @@ -42,47 +98,16 @@ module.exports = wst_server = class wst_server {
const { httpRequest } = request;
return this.authenticate(
httpRequest,
(rejectReason, target, monitor) => {
(rejectReason, target) => {
if (rejectReason) {
return request.reject(500, JSON.stringify(rejectReason));
}
const { host, port } = target;
var tcpConn = net.connect(
{ host, port, allowHalfOpen: false },
() => {
tcpConn.removeAllListeners('error');
const ip = require('./httpReqRemoteIp')(httpRequest);
let wsConn = null;
try {
wsConn = request.accept('tunnel-protocol', request.origin);
log(
`Client ${ip} established ${
request instanceof HttpTunnelReq ? 'http' : 'ws'
} tunnel to ${host}:${port}`
);
} catch (e) {
log(`Client ${ip} rejected due to ${e.toString()}`);
tcpConn.end();
return;
}
if (connWrapperCb) {
wsConn = connWrapperCb(wsConn);
}
require('./bindStream')(wsConn, tcpConn);
if (monitor) {
return monitor.bind(wsConn, tcpConn);
}
}
);

tcpConn.on('error', (err) =>
request.reject(
500,
JSON.stringify(
`Tunnel connect error to ${host}:${port}: ` + err
)
)
);
const { host, port, proto } = target;
if (proto === 'udp') {
this.connectUdp(request, connWrapperCb, host, port);
} else {
this.connectTcp(request, connWrapperCb, host, port);
}
}
);
};
Expand All @@ -101,20 +126,21 @@ module.exports = wst_server = class wst_server {
});
}

// authCb(rejectReason, {host, port}, monitor)
// authCb(rejectReason, {host, port})
authenticate(httpRequest, authCb) {
let host, port;
let host, port, proto;
if (this.dstHost && this.dstPort) {
[host, port] = Array.from([this.dstHost, this.dstPort]);
[host, port, proto] = [this.dstHost, this.dstPort, this.dstProto];
} else {
const dst = this.parseUrlDst(httpRequest.url);
if (!dst) {
return authCb('Unable to determine tunnel target');
} else {
({ host, port } = dst);
({ host, port, proto } = dst);
}
}
return authCb(null, { host, port }); // allow by default
port = parseInt(port);
return authCb(null, { host, port, proto }); // allow by default
}

// returns {host, port} or undefined
Expand All @@ -123,8 +149,8 @@ module.exports = wst_server = class wst_server {
if (!uri.query.dst) {
return undefined;
} else {
const [host, port] = Array.from(uri.query.dst.split(':'));
return { host, port };
const [host, port, proto] = uri.query.dst.split(':');
return { host, port, proto };
}
}

Expand All @@ -134,7 +160,7 @@ module.exports = wst_server = class wst_server {
if (typeof localAddr === 'number') {
localPort = localAddr;
} else {
[localHost, localPort] = Array.from(localAddr.split(':'));
[localHost, localPort] = localAddr.split(':');
if (/^\d+$/.test(localHost)) {
localPort = localHost;
localHost = null;
Expand Down
Loading