UNPKG

port-proxy

Version:

Listens to the specified port and forwards to the specified port.(监听指定的端口,并转发到指定端口。)

389 lines (387 loc) 14 kB
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 };