@stream-toolbox/tunnel
Version:
Forward data bidirectionally between two duplexes, like `a.pipe(b).pipe(a)`, but with better handling for various cases.
206 lines (155 loc) • 8.93 kB
Markdown
# @stream-toolbox/tunnel
<p>
<a href="https://www.npmjs.com/package/@stream-toolbox/tunnel" target="_blank"><img src="https://img.shields.io/npm/v/@stream-toolbox/tunnel.svg?style=for-the-badge" alt="version"></a>
<a href="https://npmcharts.com/compare/@stream-toolbox/tunnel" target="_blank"><img src="https://img.shields.io/npm/dm/@stream-toolbox/tunnel.svg?style=for-the-badge" alt="downloads"></a>
<a href="https://github.com/haochuan9421/stream-toolbox/blob/master/packages/tunnel/LICENSE" target="_blank"><img src="https://img.shields.io/npm/l/@stream-toolbox/tunnel.svg?style=for-the-badge" alt="license"></a>
<a href="https://github.com/haochuan9421/stream-toolbox/tree/master/packages/tunnel" target="_blank"><img src="https://img.shields.io/node/v/@stream-toolbox/tunnel.svg?style=for-the-badge" alt="node-current"></a>
<a href="https://github.com/haochuan9421/stream-toolbox" target="_blank"><img src="https://img.shields.io/badge/stream--toolbox-%F0%9F%A7%B0-orange?style=for-the-badge"></a>
</p>
[English](#installation) [中文文档](#安装)
---
> ↔️ Forward data bidirectionally between two [duplex](https://nodejs.org/api/stream.html#duplex-and-transform-streams)es, like `a.pipe(b).pipe(a)`, but with better handling for various cases.
## Installation
```bash
npm i @stream-toolbox/tunnel
```
## Quick Start
```js
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:
<img src="https://user-images.githubusercontent.com/5093611/230406818-99666d73-d224-40e2-9a39-2b0187c0de0a.png">
Diagram when connecting through the proxy server:
<img src="https://user-images.githubusercontent.com/5093611/230704451-ed5f3734-9145-4bb9-973c-8024fc29c0a7.png">
## API
```ts
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:
<img src="https://user-images.githubusercontent.com/5093611/230703813-ddd5e69b-d8e8-4683-aba6-cbaf753e2393.png">
---
> ↔️ 在两个 [duplex](https://nodejs.org/api/stream.html#duplex-and-transform-streams) 间双向转发数据,类似 `a.pipe(b).pipe(a)`,但是对各种情况有更完善的处理。
## 安装
```bash
npm i @stream-toolbox/tunnel
```
## 快速开始
```js
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 服务端直连时的示意图:
<img src="https://user-images.githubusercontent.com/5093611/230402930-104220fa-a75f-4439-8729-f6032348e7ff.png">
通过代理服务连接时的示意图:
<img src="https://user-images.githubusercontent.com/5093611/230704470-005b3da8-670a-4644-ac64-f5ce2065a15f.png">
## API
```ts
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` 参数的解释参考下图:
<img src="https://user-images.githubusercontent.com/5093611/230703693-41e7eb1c-2bd5-4062-860a-850d0df2ba29.png">