Compare commits
No commits in common. "master" and "1.0.0" have entirely different histories.
10 changed files with 204 additions and 247 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,8 +1,6 @@
|
|||
# Custom
|
||||
testing/
|
||||
build/
|
||||
config/client-config.json
|
||||
config/server-config.json
|
||||
|
||||
# ---> Node
|
||||
# Logs
|
||||
|
|
12
README.md
12
README.md
|
@ -1,13 +1,3 @@
|
|||
# tcp-ws-proxy
|
||||
|
||||
A TCP -> WS -> TCP proxy
|
||||
|
||||
<hr>
|
||||
|
||||
This was originally made for accessing MariaDB databases over a websocket on a site proxied by Cloudflare.
|
||||
It seems to work pretty well in my testing so i've made the repo for it public.
|
||||
|
||||
# Known issues
|
||||
- When using it over Cloudflare it *is* possible to create too many websocket connections. See [#2](https://git.eusv.net/tgpholly/tcp-ws-proxy/issues/2).
|
||||
- The console spam can get pretty bad on the client with multiple connections. This should be resolved once all local client connections share the same pooled websockets.
|
||||
- There is no handling for if the websocket drops but the sockets on both sides do not. It will not re-establish a connection it will just terminate the TCP connections on both sides. This should be improved.
|
||||
A TCP -> WS -> TCP proxy
|
203
index.ts
203
index.ts
|
@ -1,14 +1,197 @@
|
|||
import Client from "./src/Client";
|
||||
import Server from "./src/Server";
|
||||
import { WebSocket, WebSocketServer } from "ws";
|
||||
import { Socket, createServer } from "net";
|
||||
import { Packet } from "./src/enum/Packet";
|
||||
import { Endian, createReader, createWriter } from "bufferstuff";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import IServerConfig from "./src/interface/IServerConfig";
|
||||
import { AuthState } from "./src/enum/AuthState";
|
||||
import IClientConfig from "./src/interface/IClientConfig";
|
||||
|
||||
const keepAliveMessage = createWriter(Endian.LE, 1).writeUByte(Packet.KeepAlive).toBuffer();
|
||||
|
||||
if (process.argv[2] === "server") {
|
||||
new Server();
|
||||
} else if (process.argv[2] === "client") {
|
||||
new Client();
|
||||
} else {
|
||||
if (process.argv[2] == null || process.argv[2].trim() == "") {
|
||||
console.log(`You must pick an option. Valid options:\n - client\n - server`);
|
||||
} else {
|
||||
console.log(`${process.argv[2]} is not a valid option. Valid options:\n\t - client\n\t - server`);
|
||||
if (!existsSync("./config/server-config.json")) {
|
||||
console.error("server-config.json is missing!");
|
||||
process.exit(1);
|
||||
}
|
||||
const config:IServerConfig = JSON.parse(readFileSync("./config/server-config.json").toString());
|
||||
const server = new WebSocketServer({ port: config.port }, () => console.log(`Server started at ${config.port}`));
|
||||
|
||||
server.on("connection", (socket) => {
|
||||
console.log("Connection");
|
||||
let queuedMessages = new Array<Buffer>();
|
||||
let connectedToServer = false;
|
||||
let connectingToServer = false;
|
||||
let authed = false;
|
||||
let client:Socket | null = null;
|
||||
const clientKeepAlive = setInterval(() => {
|
||||
socket.send(keepAliveMessage);
|
||||
}, 5000);
|
||||
|
||||
socket.on("message", (data, isBinary) => {
|
||||
if (!isBinary) {
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: The types declarations for ws are really messed up >:(
|
||||
// @ts-ignore
|
||||
const packetData = createReader(Endian.LE, data);
|
||||
const packetId = packetData.readUByte();
|
||||
let tempReadLength = 0;
|
||||
|
||||
switch (packetId) {
|
||||
case Packet.KeepAlive: break; // We don't really care, it's just so CF doesn't drop the connection.
|
||||
case Packet.Auth:
|
||||
try {
|
||||
if (packetData.readShortString() !== config.authKey) {
|
||||
socket.send(createWriter(Endian.LE, 2).writeUByte(Packet.Auth).writeUByte(AuthState.Bad).toBuffer());
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
socket.send(createWriter(Endian.LE, 2).writeUByte(Packet.Auth).writeUByte(AuthState.Good).toBuffer());
|
||||
authed = true;
|
||||
|
||||
client = new Socket();
|
||||
|
||||
client.on("connect", () => {
|
||||
connectedToServer = true;
|
||||
if (queuedMessages.length > 0) {
|
||||
for (const message of queuedMessages) {
|
||||
console.log("Sent", message);
|
||||
client?.write(message);
|
||||
}
|
||||
queuedMessages.length = 0;
|
||||
}
|
||||
});
|
||||
|
||||
client.on("data", (chunk) => {
|
||||
socket.send(createWriter(Endian.LE, 5)
|
||||
.writeUByte(Packet.Data)
|
||||
.writeUInt(chunk.length)
|
||||
.writeBuffer(chunk)
|
||||
.toBuffer());
|
||||
});
|
||||
|
||||
function clientCloseOrError() {
|
||||
socket.close();
|
||||
}
|
||||
client.on("close", clientCloseOrError);
|
||||
client.on("error", clientCloseOrError);
|
||||
|
||||
connectingToServer = true;
|
||||
client.connect({
|
||||
host: config.localHost,
|
||||
port: config.localPort
|
||||
});
|
||||
} catch (e) {
|
||||
client?.end();
|
||||
client = null;
|
||||
socket.close();
|
||||
}
|
||||
break;
|
||||
case Packet.Data:
|
||||
if (!authed) {
|
||||
return;
|
||||
}
|
||||
|
||||
tempReadLength = packetData.readUInt();
|
||||
if (connectedToServer) {
|
||||
client?.write(packetData.readBuffer(tempReadLength));
|
||||
} else {
|
||||
queuedMessages.push(packetData.readBuffer(tempReadLength));
|
||||
}
|
||||
//console.log("[SERVER] Data:", data, " Length:", tempReadLength);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function closeOrError() {
|
||||
clearInterval(clientKeepAlive);
|
||||
client?.end();
|
||||
}
|
||||
socket.on("close", closeOrError);
|
||||
socket.on("error", closeOrError);
|
||||
});
|
||||
} else if (process.argv[2] === "client") {
|
||||
if (!existsSync("./config/client-config.json")) {
|
||||
console.error("client-config.json is missing!");
|
||||
process.exit(1);
|
||||
}
|
||||
const config:IClientConfig = JSON.parse(readFileSync("./config/client-config.json").toString());
|
||||
const server = createServer((socket) => {
|
||||
let authed = false;
|
||||
let queuedMessages = new Array<Buffer>();
|
||||
let txBytes = 0;
|
||||
let rxBytes = 0;
|
||||
const txrxInterval = setInterval(() => {
|
||||
console.log(`TX: ${(txBytes / 1024).toFixed(2)}KB/s | RX: ${(rxBytes / 1024).toFixed(2)}KB/s`);
|
||||
txBytes = rxBytes = 0;
|
||||
}, 1000);
|
||||
|
||||
const client = new WebSocket(config.remoteAddress);
|
||||
client.on("open", () => {
|
||||
// Send off auth as soon as we connect
|
||||
client.send(createWriter(Endian.LE, 2 + config.authKey.length).writeUByte(Packet.Auth).writeShortString(config.authKey).toBuffer());
|
||||
});
|
||||
|
||||
client.on("message", (data) => {
|
||||
// @ts-ignore
|
||||
const packetData = createReader(Endian.LE, data);
|
||||
const packetId = packetData.readUByte();
|
||||
let tempReadLength = 0;
|
||||
let bufferData:Buffer;
|
||||
|
||||
switch (packetId) {
|
||||
case Packet.KeepAlive:
|
||||
client.send(keepAliveMessage);
|
||||
break;
|
||||
|
||||
case Packet.Auth:
|
||||
if (authed) {
|
||||
return;
|
||||
}
|
||||
|
||||
authed = packetData.readUByte() === AuthState.Good;
|
||||
if (authed && queuedMessages.length > 0) {
|
||||
for (const message of queuedMessages) {
|
||||
client.send(message);
|
||||
}
|
||||
queuedMessages.length = 0;
|
||||
}
|
||||
break;
|
||||
case Packet.Data:
|
||||
tempReadLength = packetData.readUInt();
|
||||
bufferData = packetData.readBuffer(tempReadLength);
|
||||
rxBytes += bufferData.length;
|
||||
socket.write(bufferData);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function clientCloseOrError() {
|
||||
socket.end();
|
||||
clearInterval(txrxInterval);
|
||||
}
|
||||
client.on("close", clientCloseOrError);
|
||||
client.on("error", clientCloseOrError);
|
||||
|
||||
socket.on("data", (chunk) => {
|
||||
const bufferData = createWriter(Endian.LE, 5).writeUByte(Packet.Data).writeUInt(chunk.length).writeBuffer(chunk).toBuffer();
|
||||
txBytes += chunk.length;
|
||||
if (authed) {
|
||||
client.send(bufferData);
|
||||
} else {
|
||||
queuedMessages.push(bufferData);
|
||||
}
|
||||
});
|
||||
|
||||
function serverCloseOrError() {
|
||||
client.close();
|
||||
clearInterval(txrxInterval);
|
||||
}
|
||||
socket.on("close", serverCloseOrError);
|
||||
socket.on("error", serverCloseOrError);
|
||||
});
|
||||
|
||||
server.listen(config.localPort, () => console.log(`Local server listening at ${config.localPort}`));
|
||||
}
|
10
package-lock.json
generated
10
package-lock.json
generated
|
@ -1,15 +1,16 @@
|
|||
{
|
||||
"name": "tcp-ws-proxy",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tcp-ws-proxy",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bufferstuff": "^1.5.0",
|
||||
"dyetty": "^1.0.1",
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -385,6 +386,11 @@
|
|||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dyetty": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dyetty/-/dyetty-1.0.1.tgz",
|
||||
"integrity": "sha512-MQEccirDXkAQf5U1gIwcIz46+vMMEEyAl33nCqOJ7TeCRKgcHTZdG013gmWRWw3Q9wivnJqcJ04ohZnyF8nRew=="
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tcp-ws-proxy",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.0",
|
||||
"description": "A TCP -> WS -> TCP proxy",
|
||||
"main": "build/index.js",
|
||||
"scripts": {
|
||||
|
@ -31,6 +31,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"bufferstuff": "^1.5.0",
|
||||
"dyetty": "^1.0.1",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
import { AuthState } from "./enum/AuthState";
|
||||
import { Endian, createReader, createWriter } from "bufferstuff";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { createServer } from "net";
|
||||
import { Packet } from "./enum/Packet";
|
||||
import { WebSocket } from "ws";
|
||||
import IClientConfig from "./interface/IClientConfig";
|
||||
import Constants from "./Constants";
|
||||
|
||||
export default class Client {
|
||||
constructor() {
|
||||
if (!existsSync("./config/client-config.json")) {
|
||||
console.error("client-config.json is missing!");
|
||||
process.exit(1);
|
||||
}
|
||||
const config:IClientConfig = JSON.parse(readFileSync("./config/client-config.json").toString());
|
||||
let CLIENT_ID = 0;
|
||||
const server = createServer((socket) => {
|
||||
let authed = false;
|
||||
let queuedMessages = new Array<Buffer>();
|
||||
let txBytes = 0;
|
||||
let rxBytes = 0;
|
||||
const thisClientId = CLIENT_ID++;
|
||||
console.log(`[LOCAL CLIENT ${thisClientId}] New Connection`);
|
||||
const txrxInterval = setInterval(() => {
|
||||
console.log(`[LOCAL CLIENT ${thisClientId}] TX: ${(txBytes / 1024).toFixed(2)}KB/s | RX: ${(rxBytes / 1024).toFixed(2)}KB/s`);
|
||||
txBytes = rxBytes = 0;
|
||||
}, 1000);
|
||||
|
||||
const client = new WebSocket(config.remoteAddress);
|
||||
client.on("open", () => {
|
||||
// Send off auth as soon as we connect
|
||||
client.send(createWriter(Endian.LE, 2 + config.authKey.length).writeUByte(Packet.Auth).writeShortString(config.authKey).toBuffer());
|
||||
});
|
||||
|
||||
client.on("message", (data) => {
|
||||
// @ts-ignore
|
||||
const packetData = createReader(Endian.LE, data);
|
||||
const packetId = packetData.readUByte();
|
||||
let tempReadLength = 0;
|
||||
let bufferData:Buffer;
|
||||
|
||||
switch (packetId) {
|
||||
case Packet.KeepAlive:
|
||||
client.send(Constants.KEEPALIVE_PACKET);
|
||||
break;
|
||||
|
||||
case Packet.Auth:
|
||||
if (authed) {
|
||||
return;
|
||||
}
|
||||
|
||||
authed = packetData.readUByte() === AuthState.Good;
|
||||
if (authed && queuedMessages.length > 0) {
|
||||
for (const message of queuedMessages) {
|
||||
client.send(message);
|
||||
}
|
||||
queuedMessages.length = 0;
|
||||
}
|
||||
break;
|
||||
case Packet.Data:
|
||||
tempReadLength = packetData.readUInt();
|
||||
bufferData = packetData.readBuffer(tempReadLength);
|
||||
rxBytes += bufferData.length;
|
||||
socket.write(bufferData);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function clientCloseOrError() {
|
||||
socket.end();
|
||||
clearInterval(txrxInterval);
|
||||
console.log(`[LOCAL CLIENT ${thisClientId}] Remote server terminated the connection.`);
|
||||
}
|
||||
client.on("close", clientCloseOrError);
|
||||
client.on("error", clientCloseOrError);
|
||||
|
||||
socket.on("data", (chunk) => {
|
||||
const bufferData = createWriter(Endian.LE, 5).writeUByte(Packet.Data).writeUInt(chunk.length).writeBuffer(chunk).toBuffer();
|
||||
txBytes += chunk.length;
|
||||
if (authed) {
|
||||
client.send(bufferData);
|
||||
} else {
|
||||
queuedMessages.push(bufferData);
|
||||
}
|
||||
});
|
||||
|
||||
function serverCloseOrError() {
|
||||
client.close();
|
||||
clearInterval(txrxInterval);
|
||||
console.log(`[LOCAL CLIENT ${thisClientId}] Disconnected`);
|
||||
}
|
||||
socket.on("close", serverCloseOrError);
|
||||
socket.on("error", serverCloseOrError);
|
||||
});
|
||||
|
||||
server.listen(config.localPort, () => console.log(`Local server listening at ${config.localPort}`));
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { Endian, createWriter } from "bufferstuff";
|
||||
import { Packet } from "./enum/Packet";
|
||||
|
||||
export default abstract class Constants {
|
||||
public static DEBUG_MODE = true;
|
||||
public static KEEPALIVE_PACKET = createWriter(Endian.LE, 1).writeUByte(Packet.KeepAlive).toBuffer();
|
||||
}
|
115
src/Server.ts
115
src/Server.ts
|
@ -1,115 +0,0 @@
|
|||
import { AuthState } from "./enum/AuthState";
|
||||
import { Endian, createReader, createWriter } from "bufferstuff";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { Socket } from "net";
|
||||
import { Packet } from "./enum/Packet";
|
||||
import { WebSocketServer } from "ws";
|
||||
import IServerConfig from "./interface/IServerConfig";
|
||||
import Constants from "./Constants";
|
||||
|
||||
export default class Server {
|
||||
constructor() {
|
||||
if (!existsSync("./config/server-config.json")) {
|
||||
console.error("server-config.json is missing!");
|
||||
process.exit(1);
|
||||
}
|
||||
const config:IServerConfig = JSON.parse(readFileSync("./config/server-config.json").toString());
|
||||
const server = new WebSocketServer({ port: config.port }, () => console.log(`Server started at ${config.port}`));
|
||||
|
||||
server.on("connection", (socket) => {
|
||||
console.log("Connection");
|
||||
let queuedMessages = new Array<Buffer>();
|
||||
let connectedToServer = false;
|
||||
let connectingToServer = false;
|
||||
let authed = false;
|
||||
let client:Socket | null = null;
|
||||
const clientKeepAlive = setInterval(() => {
|
||||
socket.send(Constants.KEEPALIVE_PACKET);
|
||||
}, 5000);
|
||||
|
||||
socket.on("message", (data, isBinary) => {
|
||||
if (!isBinary) {
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: The types declarations for ws are really messed up >:(
|
||||
// @ts-ignore
|
||||
const packetData = createReader(Endian.LE, data);
|
||||
const packetId = packetData.readUByte();
|
||||
let tempReadLength = 0;
|
||||
|
||||
switch (packetId) {
|
||||
case Packet.KeepAlive: break; // We don't really care, it's just so CF doesn't drop the connection.
|
||||
case Packet.Auth:
|
||||
try {
|
||||
if (packetData.readShortString() !== config.authKey) {
|
||||
socket.send(createWriter(Endian.LE, 2).writeUByte(Packet.Auth).writeUByte(AuthState.Bad).toBuffer());
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
socket.send(createWriter(Endian.LE, 2).writeUByte(Packet.Auth).writeUByte(AuthState.Good).toBuffer());
|
||||
authed = true;
|
||||
|
||||
client = new Socket();
|
||||
|
||||
client.on("connect", () => {
|
||||
connectedToServer = true;
|
||||
if (queuedMessages.length > 0) {
|
||||
for (const message of queuedMessages) {
|
||||
console.log("Sent", message);
|
||||
client?.write(message);
|
||||
}
|
||||
queuedMessages.length = 0;
|
||||
}
|
||||
});
|
||||
|
||||
client.on("data", (chunk) => {
|
||||
socket.send(createWriter(Endian.LE, 5)
|
||||
.writeUByte(Packet.Data)
|
||||
.writeUInt(chunk.length)
|
||||
.writeBuffer(chunk)
|
||||
.toBuffer());
|
||||
});
|
||||
|
||||
function clientCloseOrError() {
|
||||
socket.close();
|
||||
}
|
||||
client.on("close", clientCloseOrError);
|
||||
client.on("error", clientCloseOrError);
|
||||
|
||||
connectingToServer = true;
|
||||
client.connect({
|
||||
host: config.localHost,
|
||||
port: config.localPort
|
||||
});
|
||||
} catch (e) {
|
||||
client?.end();
|
||||
client = null;
|
||||
socket.close();
|
||||
}
|
||||
break;
|
||||
case Packet.Data:
|
||||
if (!authed) {
|
||||
return;
|
||||
}
|
||||
|
||||
tempReadLength = packetData.readUInt();
|
||||
if (connectedToServer) {
|
||||
client?.write(packetData.readBuffer(tempReadLength));
|
||||
} else {
|
||||
queuedMessages.push(packetData.readBuffer(tempReadLength));
|
||||
}
|
||||
//console.log("[SERVER] Data:", data, " Length:", tempReadLength);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function closeOrError() {
|
||||
clearInterval(clientKeepAlive);
|
||||
client?.end();
|
||||
}
|
||||
socket.on("close", closeOrError);
|
||||
socket.on("error", closeOrError);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue