1.0.0 • Published 1 year ago

@stream-toolbox/tunnel v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

@stream-toolbox/tunnel

English 中文文档


↔️ Forward data bidirectionally between two duplexes, like a.pipe(b).pipe(a), but with better handling for various cases.

Installation

npm i @stream-toolbox/tunnel

Quick Start

const { createServer, createConnection } = require("node:net");
const tunnel = require("@stream-toolbox/tunnel");

// Map the serverPort of serverAddress to port 8001 of local host
const serverAddress = "xxx.xxx.xxx.xxx"; // IP or hostname
const serverPort = 22;

const localAddress = "0.0.0.0";
const localPort = 8001;

const proxyServer = createServer((clientSocket) => {
  const clientAddress = clientSocket.remoteAddress;

  console.log(`TCP connection established successfully: ${clientAddress} <===> ${localAddress}`);

  const serverSocket = createConnection({
    host: serverAddress,
    port: serverPort,
    timeout: 3000,
  })
    .once("timeout", () => {
      console.log(`TCP connection establishment timed out: ${localAddress} <===> ${serverAddress}`);
      serverSocket.destroy();
      clientSocket.destroy();
    })
    .once("error", (err) => {
      console.log(`TCP connection establishment failed: ${localAddress} <===> ${serverAddress}`, err);
      serverSocket.destroy();
      clientSocket.destroy();
    })
    .once("connect", () => {
      console.log(`TCP connection established successfully: ${localAddress} <===> ${serverAddress}`);

      // after connection established, remove the connection timeout and failure event listeners
      serverSocket.removeAllListeners("error");
      serverSocket.removeAllListeners("timeout");
      serverSocket.setTimeout(0);

      // Bidirectional forwarding data of clientSocket and serverSocket
      tunnel(clientSocket, serverSocket, (err, time) => {
        console.log(`Tunnel closed, tunnel alive for ${time} milliseconds`);
        console.log(`${clientAddress} sended ${serverSocket.bytesWritten} bytes data to ${serverAddress}`);
        console.log(`${serverAddress} sended ${clientSocket.bytesWritten} bytes data to ${clientAddress}`);

        if (err) {
          console.log(`The tunnel is broken because of ${err.causedBy === serverSocket ? "serverSocket" : "clientSocket"} throws an error:`, err);
        }
      });
    });
}).listen(localPort, localAddress, () => {
  console.log("proxy server start", proxyServer.address());
});

If you can use the ssh -i ~/.ssh/id_rsa -p 22 root@xxx.xxx.xxx.xxx command to remotely log in to the xxx.xxx.xxx.xxx host, after starting the above proxy server locally, now you should also be able to use ssh -i ~/.ssh/id_rsa -p 8001 root@127.0.0.1 to log in to the host.

Diagram when the client is directly connected to the remote ssh server:

Diagram when connecting through the proxy server:

API

type DuplexLike = {
  rs: Readable; // readable stream
  ws: Writable; // writable stream
  allowHalfOpen?: boolean; // default false,which means when rs ended,call ws.end() automatically
  allowPipeHalfOpen?: boolean; // default false,which means when rs ended,end the writable stream of the other side of tunnel automatically
};

function tunnel(a: Duplex | DuplexLike, b: Duplex | DuplexLike): Promise<number>;
function tunnel(a: Duplex | DuplexLike, b: Duplex | DuplexLike, condition: number): Promise<number>;
function tunnel(a: Duplex | DuplexLike, b: Duplex | DuplexLike, callback: callback): void;
function tunnel(a: Duplex | DuplexLike, b: Duplex | DuplexLike, condition: number, callback: callback): void;

For the explanation of allowHalfOpen, allowPipeHalfOpen and condition, refer to the figure below:


↔️ 在两个 duplex 间双向转发数据,类似 a.pipe(b).pipe(a),但是对各种情况有更完善的处理。

安装

npm i @stream-toolbox/tunnel

快速开始

const { createServer, createConnection } = require("node:net");
const tunnel = require("@stream-toolbox/tunnel");

// 将 serverAddress 的 serverPort 端口映射到本机的 8001 端口
const serverAddress = "xxx.xxx.xxx.xxx"; // IP 地址或域名
const serverPort = 22;

const localAddress = "0.0.0.0";
const localPort = 8001;

const proxyServer = createServer((clientSocket) => {
  const clientAddress = clientSocket.remoteAddress;

  console.log(`TCP 连接建立成功: ${clientAddress} <===> ${localAddress}`);

  const serverSocket = createConnection({
    host: serverAddress,
    port: serverPort,
    timeout: 3000,
  })
    .once("timeout", () => {
      console.log(`TCP 连接建立超时: ${localAddress} <===> ${serverAddress}`);
      serverSocket.destroy();
      clientSocket.destroy();
    })
    .once("error", (err) => {
      console.log(`TCP 连接建立失败: ${localAddress} <===> ${serverAddress}`, err);
      serverSocket.destroy();
      clientSocket.destroy();
    })
    .once("connect", () => {
      console.log(`TCP 连接建立成功: ${localAddress} <===> ${serverAddress}`);

      // TCP 连接建立成功后,移除连接超时和连接失败的事件监听
      serverSocket.removeAllListeners("error");
      serverSocket.removeAllListeners("timeout");
      serverSocket.setTimeout(0);

      // 对 clientSocket 和 serverSocket 的数据进行双向转发
      tunnel(clientSocket, serverSocket, (err, time) => {
        console.log(`隧道已断开,隧道存活了 ${time} 毫秒`);
        console.log(`${clientAddress} 累积向 ${serverAddress} 发送了 ${serverSocket.bytesWritten} 字节的数据`);
        console.log(`${serverAddress} 累积向 ${clientAddress} 发送了 ${clientSocket.bytesWritten} 字节的数据`);

        if (err) {
          console.log(`隧道断开是因为 ${err.causedBy === serverSocket ? "serverSocket" : "clientSocket"} 抛出了错误:`, err);
        }
      });
    });
}).listen(localPort, localAddress, () => {
  console.log("proxy server start", proxyServer.address());
});

如果你可以使用 ssh -i ~/.ssh/id_rsa -p 22 root@xxx.xxx.xxx.xxx 命令远程登录到 xxx.xxx.xxx.xxx 主机,在本地启动上面的代理服务后,你现在应该也可以使用 ssh -i ~/.ssh/id_rsa -p 8001 root@127.0.0.1 登录进去该主机了。

客户端与 SSH 服务端直连时的示意图:

通过代理服务连接时的示意图:

API

type DuplexLike = {
  rs: Readable; // readable stream
  ws: Writable; // writable stream
  allowHalfOpen?: boolean; // 默认 false,即当 rs 结束时,自动终止 ws
  allowPipeHalfOpen?: boolean; // 默认 false,即当 rs 结束时,自动终止 tunnel 另一端的 ws
};

function tunnel(a: Duplex | DuplexLike, b: Duplex | DuplexLike): Promise<number>;
function tunnel(a: Duplex | DuplexLike, b: Duplex | DuplexLike, condition: number): Promise<number>;
function tunnel(a: Duplex | DuplexLike, b: Duplex | DuplexLike, callback: callback): void;
function tunnel(a: Duplex | DuplexLike, b: Duplex | DuplexLike, condition: number, callback: callback): void;

关于 allowHalfOpen, allowPipeHalfOpen 以及 condition 参数的解释参考下图: