port-proxy
Version:
Listens to the specified port and forwards to the specified port.(监听指定的端口,并转发到指定端口。)
389 lines (387 loc) • 14 kB
JavaScript
import dgram from "dgram";
import net from "net";
import { Readable, Writable } from "node:stream";
import { ReadableStream, TransformStream } from "node:stream/web";
var PortProxy = class PortProxy {
static #totalLimiteRate = 0;
static #totalRate = 0;
static #connections = /* @__PURE__ */ new Map();
static get #overTotalLimit() {
return PortProxy.#totalLimiteRate > 0 && PortProxy.#totalRate > PortProxy.#totalLimiteRate * 1.025;
}
static get #lowTotalLimit() {
return PortProxy.#totalLimiteRate > 0 && PortProxy.#totalRate < PortProxy.#totalLimiteRate * .975;
}
static #aveWindowSize = 0;
#source;
#sourcePort;
#target;
#targetPort;
#verbose;
#tcpServer = null;
#udpServer = null;
#protocol = "tcp";
#limiteRate = 0;
rate = 0;
windowSize = 1;
get #overlimit() {
return this.windowSize > 1 && (this.#limiteRate > 0 && this.rate > this.#limiteRate * 1.025 || PortProxy.#aveWindowSize > 1 && this.windowSize > PortProxy.#aveWindowSize * 1.1 || PortProxy.#overTotalLimit);
}
get #lowLimit() {
return this.#limiteRate > 0 && this.rate < this.#limiteRate * .975 || PortProxy.#aveWindowSize > 10 && this.windowSize < PortProxy.#aveWindowSize * .9 || PortProxy.#lowTotalLimit;
}
constructor(options) {
this.#source = options.source;
this.#sourcePort = options.sourcePort;
this.#target = options.target;
this.#targetPort = options.targetPort;
this.#verbose = options.verbose ?? false;
this.#protocol = options.protocol ?? "tcp";
this.#limiteRate = Math.floor(Math.abs(options.limiteRate ?? 0)) || 0;
}
start() {
const protocol = this.#protocol;
if (protocol === "tcp") return this.#startTcp();
else if (protocol === "udp") return this.#startUdp();
else throw new Error("未知的协议");
}
#startTcp() {
const { promise: waitReady, resolve, reject } = Promise.withResolvers();
const connections = PortProxy.#connections;
const limiteRate = this.#limiteRate;
const totalLimiteRate = PortProxy.#totalLimiteRate;
const tcpServer = this.#tcpServer = net.createServer((sourceSocket) => {
const targetSocket = net.connect({
host: this.#target,
port: this.#targetPort
});
const sourceReadStream = Readable.toWeb(sourceSocket);
const targetReadStream = Readable.toWeb(targetSocket);
const sourceWriteStream = Writable.toWeb(sourceSocket);
const targetWriteStream = Writable.toWeb(targetSocket);
connections.set(this, {
sourceSocket,
targetSocket
});
if (this.#verbose) {
const clientInfo = `${sourceSocket.remoteAddress}:${sourceSocket.remotePort}`;
const targetInfo = `${this.#target}:${this.#targetPort}`;
console.debug(`新连接: ${clientInfo} -> ${targetInfo}`);
}
if (!limiteRate && !totalLimiteRate) this.#pipeThrough({
sourceReadStream,
targetReadStream,
sourceWriteStream,
targetWriteStream
});
else this.#pipeTo({
sourceReadStream,
targetReadStream,
sourceWriteStream,
targetWriteStream
});
const cleanup = async (sourceError, targetError) => {
sourceSocket.destroy(targetError);
targetSocket.destroy(sourceError);
this.#endHandler({
sourceError,
targetError
});
await new Promise((res) => setTimeout(res, 100));
connections.delete(this);
const clientInfo = `${sourceSocket?.remoteAddress}:${sourceSocket?.remotePort} --> ${targetSocket?.remoteAddress}:${targetSocket?.remotePort}`;
console.debug(`连接 ${clientInfo} 资源已回收`);
};
sourceSocket.on("close", () => cleanup(void 0, void 0));
sourceSocket.on("error", (error) => cleanup(error, void 0));
targetSocket.on("close", () => cleanup(void 0, void 0));
targetSocket.on("error", (error) => cleanup(void 0, error));
});
tcpServer.on("error", (err) => {
console.error(`服务器错误: ${err.message}`);
reject(err);
});
tcpServer.listen(this.#sourcePort, this.#source, () => {
console.debug(`端口转发已启动: ${this.#source}:${this.#sourcePort} -> ${this.#target}:${this.#targetPort}`);
resolve();
});
return waitReady;
}
#pipeThrough({ sourceReadStream, targetReadStream, sourceWriteStream, targetWriteStream }) {
const transform = async (readableStream, writableStream) => {
const sourceToTargetTransform = new TransformStream({ transform: (chunk, controller) => {
this.#verboseHandler(readableStream === sourceReadStream ? "source" : "target", chunk);
controller.enqueue(chunk);
} });
try {
await readableStream.pipeThrough(sourceToTargetTransform).pipeTo(writableStream);
} catch (err) {
this.#endHandler({ sourceError: err });
return null;
}
};
transform(sourceReadStream, targetWriteStream);
transform(targetReadStream, sourceWriteStream);
}
#pipeTo({ sourceReadStream, targetReadStream, sourceWriteStream, targetWriteStream }) {
let currentChunk = null, lastChunkSize = 0, clearRatetimer = null, lastTime = Date.now();
const calcRate = () => {
if (clearRatetimer) clearTimeout(clearRatetimer);
if (!currentChunk) {
clearRatetimer = setTimeout(() => {
this.rate = 0;
this.windowSize = 1;
}, 1e3);
return;
}
const currentTime = Date.now();
const delta = currentTime - lastTime;
if (delta <= 0) return;
const currentChunkSize = currentChunk.byteLength;
const rate = Math.ceil(Math.abs(lastChunkSize - currentChunkSize) / (delta / 1e3));
this.rate = rate;
lastTime = currentTime;
PortProxy.#totalRate = 0;
lastChunkSize = currentChunkSize;
};
const pause = (lastTime$1) => Date.now() - lastTime$1 < Math.log(1024) / Math.log(this.windowSize + 1);
const transfStream = (stream) => {
const reader = stream.getReader();
const newReadableStream = new ReadableStream({ pull: async (controller) => {
const lastTime$1 = Date.now();
while (pause(lastTime$1)) for (let i = 0; i < 100; i++) await Promise.resolve();
currentChunk ??= await handleReadNextChunk();
if (this.#overlimit) this.windowSize = Math.max(1, Math.floor(this.windowSize * .98));
else if (this.#lowLimit) this.windowSize = Math.ceil(this.windowSize * 1.02);
if (currentChunk) {
const willsend = currentChunk.slice(0, this.windowSize);
controller.enqueue(willsend);
calcRate();
calcAveWindowSize();
this.#verboseHandler(stream === sourceReadStream ? "source" : "target", willsend);
const rest = currentChunk.slice(this.windowSize);
if (rest.byteLength) currentChunk = rest;
else {
currentChunk = await handleReadNextChunk();
if (!currentChunk) controller.close();
}
}
} });
const handleReadNextChunk = async () => {
let result;
try {
result = await reader.read();
} catch (err) {
this.#endHandler({ sourceError: err });
return null;
}
const { done, value: chunk } = result;
if (done) return null;
return chunk;
};
const calcAveWindowSize = () => {
let aveWindowSize = 0, aliveCount = 0;
if (PortProxy.#connections.size >= 2) {
PortProxy.#connections.forEach((_item, proxy) => {
PortProxy.#totalRate += proxy.rate;
if (proxy.windowSize > 10) {
aveWindowSize += proxy.windowSize;
aliveCount++;
}
});
PortProxy.#aveWindowSize = Math.floor(aveWindowSize / aliveCount);
}
};
return newReadableStream;
};
const newSourceReadableStream = transfStream(sourceReadStream);
const newTargetReadableStream = transfStream(targetReadStream);
(async () => {
const writer = targetWriteStream.getWriter();
for await (const chunk of newSourceReadableStream) try {
await writer.write(chunk);
await writer.ready;
} catch (err) {
this.#endHandler({ targetError: err });
}
})();
(async () => {
const writer = sourceWriteStream.getWriter();
for await (const chunk of newTargetReadableStream) try {
await writer.write(chunk);
await writer.ready;
} catch (err) {
this.#endHandler({ targetError: err });
}
})();
}
#startUdp() {
const { promise: waitReady, resolve, reject } = Promise.withResolvers();
const udpServer = this.#udpServer = dgram.createSocket("udp4");
const clientMap = /* @__PURE__ */ new Map();
udpServer.on("message", (msg, rinfo) => {
const clientKey = `${rinfo.address}:${rinfo.port}`;
if (this.#verbose) console.debug(`来自客户端 ${clientKey} 的数据: ${msg.length} 字节`);
if (!clientMap.has(clientKey)) {
const targetSocket = dgram.createSocket("udp4");
targetSocket.on("message", (responseMsg) => {
if (this.#verbose) console.debug(`来自服务器的数据: ${responseMsg.length} 字节, 返回给 ${clientKey}`);
udpServer.send(responseMsg, rinfo.port, rinfo.address, (err) => {
if (err && this.#verbose) console.error(`回复客户端时出错: ${err.message}`);
});
});
targetSocket.on("error", (err) => {
if (this.#verbose) console.error(`目标Socket错误: ${err.message}`);
clientMap.delete(clientKey);
targetSocket.close();
});
clientMap.set(clientKey, {
targetSocket,
lastActivity: Date.now()
});
}
const clientData = clientMap.get(clientKey);
clientData.lastActivity = Date.now();
clientData.targetSocket.send(msg, this.#targetPort, this.#target, (err) => {
if (err && this.#verbose) console.error(`转发到目标时出错: ${err.message}`);
});
});
this.#udpServer.on("error", (err) => {
console.error(`UDP服务器错误: ${err.message}`);
reject(err);
});
this.#udpServer.on("listening", () => {
const address = this.#udpServer.address();
console.debug(`UDP端口转发已启动: ${address.address}:${address.port} -> ${this.#target}:${this.#targetPort}`);
resolve();
});
setInterval(() => {
const now = Date.now();
const timeout = 6e4;
for (const [key, data] of clientMap.entries()) if (now - data.lastActivity > timeout) {
if (this.#verbose) console.debug(`清理不活动的UDP客户端: ${key}`);
data.targetSocket.close();
clientMap.delete(key);
}
}, 3e4);
this.#udpServer.bind(this.#sourcePort, this.#source);
return waitReady;
}
#verboseHandler(from, message) {
if (this.#verbose) {
console.debug(`来自 ${from} 的数据: ${message.length} 字节`);
const hex = Buffer.from(message).toString("hex");
console.debug(`${hex.slice(0, 50)}${hex.length > 100 ? "..." : ""}${hex.slice(-50, hex.length)}`);
}
}
#endHandler({ sourceError, targetError }) {
const error = sourceError ?? targetError;
if (error?.name === "AbortError") return;
if (error) console.error(error);
if (this.#verbose) {
const { sourceSocket, targetSocket } = PortProxy.#connections.get(this) ?? {};
const clientInfo = `${sourceSocket?.remoteAddress}:${sourceSocket?.remotePort} -x-> ${targetSocket?.remoteAddress}:${targetSocket?.remotePort}`;
console.debug(`连接关闭: ${clientInfo} ${sourceError?.message ?? ""} ${targetError?.message ?? ""}`);
}
}
stop() {
const tcpServer = this.#tcpServer;
const { promise: waitClose, resolve } = Promise.withResolvers();
if (tcpServer) {
PortProxy.#connections.forEach(({ sourceSocket, targetSocket }) => {
sourceSocket.destroy();
targetSocket.destroy();
});
PortProxy.#connections.clear();
tcpServer.close(() => {
console.debug(`端口转发已停止 ${this.#source}:${this.#sourcePort} -x-> ${this.#target}:${this.#targetPort}`);
resolve();
});
} else resolve();
return waitClose;
}
getStatus() {
return {
listening: this.#tcpServer ? this.#tcpServer.listening : false,
connections: this.getConnectionCount(),
config: {
source: this.#source,
sourcePort: this.#sourcePort,
target: this.#target,
targetPort: this.#targetPort,
verbose: this.#verbose
}
};
}
isRunning() {
return this.#tcpServer ? this.#tcpServer.listening : false;
}
getConnectionCount() {
return PortProxy.#connections.size;
}
};
function parseArgs(args) {
const options = {
source: "0.0.0.0",
sourcePort: 14491,
target: "127.0.0.1",
targetPort: 14490,
verbose: false,
protocol: "tcp"
};
for (let i = 0; i < args.length; i++) if (args[i] === "--source" && args[i + 1]) {
options.source = args[i + 1];
i++;
} else if (args[i] === "--source-port" && args[i + 1]) {
options.sourcePort = parseInt(args[i + 1]);
i++;
} else if (args[i] === "--target" && args[i + 1]) {
options.target = args[i + 1];
i++;
} else if (args[i] === "--target-port" && args[i + 1]) {
options.targetPort = parseInt(args[i + 1]);
i++;
} else if (args[i] === "--protocol") options.protocol = args[i + 1];
else if (args[i] === "--verbose") options.verbose = true;
else if (args[i] === "--help") {
console.debug(`
使用说明: portproxy [选项]
选项:
--source <IP> 监听地址 (默认: 0.0.0.0)
--source-port <端口> 监听端口 (默认: 14491)
--target <IP> 目标地址 (默认: 127.0.0.1)
--target-port <端口> 目标端口 (默认: 14490)
--protocol <协议> 协议 (默认: tcp)
--verbose 详细输出模式
--help 显示此帮助信息
示例:
portproxy --source 192.168.196.2 --source-port 14491 --target 127.0.0.1 --target-port 14490
`);
process.exit(0);
}
return options;
}
async function main() {
const options = parseArgs(process.argv.slice(2));
const proxy = new PortProxy(options);
const shutdown = async () => {
console.debug("\n正在停止端口转发...");
await proxy.stop();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
process.on("uncaughtException", (err) => {
console.error("uncaughtException", err);
});
try {
await proxy.start();
console.debug("按 Ctrl+C 停止端口转发");
console.debug("当前状态:", proxy.getStatus());
} catch (error) {
console.error("启动端口转发失败:", error);
process.exit(1);
}
}
if (import.meta.url === `file://${process.argv[1]}`) main().catch(console.error);
export { PortProxy, main };