feat(remote): serial/USB/USBIP remote forwarding with virtual serial port
This commit is contained in:
@@ -0,0 +1,623 @@
|
||||
# 远程终端、端口转发、USB 透传 规划文档
|
||||
|
||||
> 目标:在 LocalNetMsg 中实现三类“远程使用对方设备”的能力——**对方机器的本地终端(shell)**、**TCP 端口转发**、**USB 设备透传**。本文先定整体架构、协议、库选型,再分阶段落地。
|
||||
>
|
||||
> 适配范围:Windows / macOS / Linux 三端。被控端的 shell 按平台决定:Windows → PowerShell(默认 `pwsh.exe`,回退 `powershell.exe`),macOS → zsh(回退 bash),Linux → bash(按 `SHELL` 环境变量)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计目标与边界
|
||||
|
||||
| 项目 | MVP(v0.2.0) | v0.3.0 | 不做(明确放弃) |
|
||||
|---|---|---|---|
|
||||
| 远程终端 | 单 PTY、单 session、只读/可写 | 多 session、窗口大小同步、UTF-8 CJK、ANSI 颜色、剪贴板同步 | 远程录制/回放(交给 asciinema) |
|
||||
| 端口转发 | TCP `127.0.0.1` 监听、字节计数、TTL | 多并发、IPv6、UDP | SOCKS / HTTP CONNECT / 公网映射 |
|
||||
| USB 透传 | HID 设备名单 + “绑定本机、远程附加” | 串口、复合设备过滤 | Isochronous、复杂描述符自定义 |
|
||||
|
||||
不做 = 让用户走更合适的工具(AnyDesk/RustDesk 远控、Serveo/Cloudflared Tunnel、VirtualHere)。本地调试范围以**单局域网、单设备对、单设备**为限。
|
||||
|
||||
---
|
||||
|
||||
## 2. 选型与依赖
|
||||
|
||||
### 2.1 库与版本(2026-07 通过 `registry.npmmirror.com` 核实)
|
||||
|
||||
| 用途 | 包 | 版本 | 备注 |
|
||||
|---|---|---|---|
|
||||
| PTY 后端(被控端) | `@lydell/node-pty` | `1.2.0-beta.12`(`beta=1.2.0-beta.14`,可选升级) | 自带六平台预编译:win32-x64/arm64、darwin-x64/arm64、linux-x64/arm64;`latest` 是 `1.2.0-beta.12`。**优先 `latest`**,只在 win11 ARM 用户出问题时再切 beta |
|
||||
| 终端前端(控制端) | `@xterm/xterm` | `6.0.0` | xterm.js |
|
||||
| 终端自适应 | `@xterm/addon-fit` | `0.11.0` | 让终端随容器尺寸变化 |
|
||||
| 终端链接 | `@xterm/addon-web-links` | `0.12.0` | 终端内 URL 自动可点击 |
|
||||
| 端口转发 / TCP 隧道 | Node 内置 `net` 模块 | — | 不引入额外依赖;与现有 `ws`、`file-server` 同风格 |
|
||||
| USB 后端(被控端) | `usb`(node-usb v3.0.1,Rust N-API 重写版) | `3.0.1` | 同时提供 `usb`(自由访问)和 `webusb`(需授权)两种 API |
|
||||
| HID 快速通道 | `node-hid`(v3.3.0) | `3.3.0` | 只针对 HID 类(键盘/鼠标/游戏手柄/自定义 HID)速度更快、API 更友好;非 HID 设备走 `usb` |
|
||||
| 串口 / USB-Serial | `serialport`(v13.0.0) | `13.0.0` | 需要 Node ≥20 |
|
||||
| 公网隧道(可选 v0.3+) | `localtunnel` | `2.0.2` | 公共中继;不推荐默认开,作为开发期选项 |
|
||||
|
||||
> 镜像访问:`https://registry.npmmirror.com/<pkg>/latest` 可直接读 JSON,元数据字段含 `dist-tags.latest`、`engines`、`os`、`cpu`、`napi.targets`,比 GitHub 页面稳定。GitHub raw/README 多数代理 403/Cloudflare 拦截,npm 元数据是兜底。
|
||||
|
||||
### 2.2 不选 / 避坑
|
||||
|
||||
- **`node-pty`** 上游 `microsoft/node-pty` `latest=1.1.0`,仅 `beta=1.2.0-beta.14`。`@lydell/node-pty` 是社区 fork,包更小、预编译更全。直接用社区版,等 `microsoft/node-pty` 进入 stable 再切。
|
||||
- **`robotjs`**:维护停滞,对 Electron 32 ABI 兼容性差。
|
||||
- **`usbipd-win`**:Windows 官方 USB/IP,原理正确,但与我们 Node 进程隔离;MVP 不集成,后续可作为被控端可选依赖以获得 Isochronous 能力。
|
||||
- **FreeRDP / libvncserver / RustDesk**:都是完整远控项目,体积与许可(AGPL)不适合直接嵌入。
|
||||
- **`@lydell/node-pty-*-*`** 这些平台包随主包自动拉,无需单独声明。
|
||||
|
||||
---
|
||||
|
||||
## 3. 整体架构
|
||||
|
||||
### 3.1 分层
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Renderer (Vue 3) │ UI: TerminalPanel / PortForwardPanel / UsbPanel
|
||||
│ xterm.js + addon-fit + addon-web-links │ Pinia stores: terminal / forward / usb
|
||||
└───────────────▲──────────────────────────┘
|
||||
│ IPC (contextBridge)
|
||||
┌───────────────┴──────────────────────────┐
|
||||
│ Preload (typed window.api) │
|
||||
└───────────────▲──────────────────────────┘
|
||||
│ ipcMain.handle / broadcastToRenderer
|
||||
┌───────────────┴────────────────────────────────────────────────────────┐
|
||||
│ Main (Node) │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ RemoteTerminal │ │ PortForward │ │ UsbForward │ │
|
||||
│ │ (manager.ts) │ │ (manager.ts) │ │ (manager.ts) │ │
|
||||
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ┌────────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐ │
|
||||
│ │ @lydell/node-pty│ │ net (TCP) │ │ usb / node-hid │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ └──────── WS 信令(复用 chat-client.ts:91)──┘ │
|
||||
│ src/main/protocol.ts WsFrame 新增 terminal / forward / usb 控制帧 │
|
||||
└───────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 与现有代码的衔接
|
||||
|
||||
| 现有 | 复用方式 |
|
||||
|---|---|
|
||||
| `src/main/protocol.ts:30` `WsFrame` union | 扩展三条新控制帧:`terminal`、`forward`、`usb` |
|
||||
| `src/main/chat-client.ts:91` `ws://${peer.address}:${peer.chatPort}` | 复用同一 outgoing WS;多路复用消息类型 |
|
||||
| `src/main/ipc.ts:59` `bindNetworkContext` | 在末尾给三个新 manager 各建一组 `c.chatServer.on('xxx', ...)` 与 IPC handler |
|
||||
| `src/main/discovery.ts` UDP 广播 | 不动;新功能依然走 LAN 局域网 |
|
||||
| `src/main/db.ts` | 新增 `audit_log` 表 + 写入 helper,记录 terminal/forward/usb 事件 |
|
||||
| `src/renderer/src/stores/*` | 新增 `useTerminalStore`、`useForwardStore`、`useUsbStore` |
|
||||
| `src/renderer/src/App.vue` | 在 sidebar 增加“工具”下拉,包含三个子入口 |
|
||||
| `src/renderer/src/components/SettingsView.vue` | 增加“远程”开关 + 默认 TTL/最大带宽/允许的设备名单 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 协议扩展(WsFrame 新增)
|
||||
|
||||
```ts
|
||||
// src/main/protocol.ts(追加,不改 PROTOCOL_VERSION)
|
||||
export type TerminalFrame =
|
||||
| { type: 'terminal.open'; sessionId: string; rows: number; cols: number; shell?: string }
|
||||
| { type: 'terminal.input'; sessionId: string; data: string /* base64 of bytes */ }
|
||||
| { type: 'terminal.output'; sessionId: string; data: string /* base64 of bytes */ }
|
||||
| { type: 'terminal.resize'; sessionId: string; rows: number; cols: number }
|
||||
| { type: 'terminal.close'; sessionId: string; reason?: string }
|
||||
| { type: 'terminal.ack'; sessionId: string; ok: boolean; reason?: string }
|
||||
|
||||
export type ForwardFrame =
|
||||
| { type: 'forward.open'; sessionId: string; listenPort: number; targetHost: string; targetPort: number; ttlSec: number }
|
||||
| { type: 'forward.data'; sessionId: string; dir: 'c2s' | 's2c'; data: string /* base64 */; fin?: boolean }
|
||||
| { type: 'forward.close'; sessionId: string; reason?: string; bytesIn: number; bytesOut: number }
|
||||
| { type: 'forward.ack'; sessionId: string; ok: boolean; reason?: string }
|
||||
|
||||
export type UsbFrame =
|
||||
| { type: 'usb.list'; reqId: string }
|
||||
| { type: 'usb.devices'; reqId: string; devices: Array<{ busId: string; vid: number; pid: number; class: number; subclass: number; product?: string; manufacturer?: string; serial?: string }> }
|
||||
| { type: 'usb.attach'; sessionId: string; busId: string }
|
||||
| { type: 'usb.detach'; sessionId: string }
|
||||
| { type: 'usb.transfer'; sessionId: string; dir: 'host->dev' | 'dev->host'; data?: string; fin?: boolean; status?: string }
|
||||
| { type: 'usb.ack'; reqId?: string; sessionId?: string; ok: boolean; reason?: string }
|
||||
```
|
||||
|
||||
`WsFrame` 联合类型追加上面三个分支。**信令用 JSON**,**数据帧也用 JSON 但 payload 用 base64**——避免把 WS 当二进制流用,简化现有 codec。
|
||||
|
||||
### 4.1 控制权与角色
|
||||
|
||||
每条 `*.open` 帧必须包含以下 meta,由发送方填:
|
||||
|
||||
- `fromDeviceId`(自动从握手记录填)
|
||||
- `appVersion`、`nonce`(每次 `open` 重新生成)
|
||||
|
||||
被控端收到 `open` 后:
|
||||
1. 校验对端是否在 `usb/terminal/forward.allowPeers` 中(设置项)。
|
||||
2. 弹窗确认(被控端的 window),10 秒倒计时。
|
||||
3. 通过/拒绝,发送 ack。
|
||||
|
||||
---
|
||||
|
||||
## 5. 模块设计
|
||||
|
||||
### 5.1 Remote Terminal(`src/main/remote/terminal.ts`)
|
||||
|
||||
```
|
||||
class TerminalManager {
|
||||
// 被控端: 接受 open 时新建 pty
|
||||
handleOpen(env: MessageEnvelope): void
|
||||
// 控制端: 写 stdin / 收 stdout
|
||||
sendInput(sessionId: string, bytes: Buffer): void
|
||||
onOutput(cb: (sessionId: string, bytes: Buffer) => void): void
|
||||
resize(sessionId: number, rows: number, cols: number): void
|
||||
close(sessionId: string, reason?: string): void
|
||||
}
|
||||
```
|
||||
|
||||
平台默认 shell:
|
||||
|
||||
```ts
|
||||
// src/main/remote/shell.ts
|
||||
import { platform } from 'node:process'
|
||||
import { execSync } from 'node:child_process'
|
||||
|
||||
export function defaultShell(): { file: string; args: string[] } {
|
||||
switch (platform) {
|
||||
case 'win32': {
|
||||
const file = execSync('where pwsh.exe', { stdio: ['ignore', 'pipe', 'ignore'] })
|
||||
.toString().trim() ? 'pwsh.exe' : 'powershell.exe'
|
||||
return { file, args: [] }
|
||||
}
|
||||
case 'darwin':
|
||||
case 'linux': {
|
||||
return { file: process.env.SHELL || (platform === 'darwin' ? '/bin/zsh' : '/bin/bash'), args: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
被控端启动逻辑:
|
||||
|
||||
```ts
|
||||
import * as pty from '@lydell/node-pty'
|
||||
|
||||
const env = { ...process.env, TERM: 'xterm-256color', LANG: process.env.LANG || 'en_US.UTF-8' }
|
||||
const proc = pty.spawn(shell.file, shell.args, {
|
||||
name: 'xterm-256color',
|
||||
cols: frame.cols, rows: frame.rows,
|
||||
cwd: os.homedir(), env, encoding: null /* bytes */,
|
||||
})
|
||||
proc.onData((data: string) => send('terminal.output', { sessionId, data: Buffer.from(data, 'utf8').toString('base64') }))
|
||||
proc.onExit(({ exitCode }) => send('terminal.close', { sessionId, reason: `exit ${exitCode}` }))
|
||||
```
|
||||
|
||||
控制端写入:
|
||||
|
||||
```ts
|
||||
proc.write(Buffer.from(frame.data, 'base64').toString('utf8'))
|
||||
```
|
||||
|
||||
> ConPTY 是 Windows 10+ 默认;xp/7 用 winpty(`@lydell/node-pty` 自动选)。`encoding: null` 让数据以 string 但保留字节流语义;解码统一在两端做。
|
||||
|
||||
### 5.2 Port Forward(`src/main/remote/forward.ts`)
|
||||
|
||||
```
|
||||
class ForwardManager {
|
||||
// 申请方(控制端)
|
||||
async request(peerDeviceId, { listenPort, targetHost, targetPort, ttlSec }): Promise<SessionId>
|
||||
// 数据流
|
||||
onData(sessionId, dir, bytes)
|
||||
sendData(sessionId, dir, bytes, fin?)
|
||||
close(sessionId, reason?)
|
||||
}
|
||||
```
|
||||
|
||||
申请方作为 TCP server(`net.createServer`,`host: '127.0.0.1'`),被控端作为 TCP client:
|
||||
|
||||
```
|
||||
控制端浏览器/应用 -> 127.0.0.1:listenPort
|
||||
↓
|
||||
ForwardManager (net.Server)
|
||||
↓ (base64 frame)
|
||||
chat-client.ts:91 (现有 WS)
|
||||
↓
|
||||
对方 ForwardManager (net.Socket)
|
||||
↓
|
||||
对方 127.0.0.1:targetPort
|
||||
```
|
||||
|
||||
要点:
|
||||
- 单 forward session 一个 TCP 连接,复用现有 WS 子协议;帧不超过 16 KB/帧,超出分片。
|
||||
- 字节计数:双向分别累计;TTL 到期(默认 1 小时)自动关闭。
|
||||
- 端口黑名单:禁止 `22, 23, 53, 80, 135, 139, 443, 445, 3389, 5900, 5985/5986`,被控端在 `forward.ack` 里拒。
|
||||
- 不开 `0.0.0.0`;v0.3+ 再加 LAN 提示。
|
||||
- 关闭时若仍有未消费字节,发送 `fin:true` 让对端 socket 半关闭,不丢数据。
|
||||
|
||||
### 5.3 USB / 串口 共享(`src/main/remote/usb.ts`)
|
||||
|
||||
**老实说在前**:纯 Node.js 在 Windows 上做不到"USB 设备完全透明地变成本机 USB"——那是商业软件
|
||||
(VirtualHere / USB Network Gate / FlexiHub)或 Linux usbip + WSL 才有的能力。我们提供三种模式,按场景取用:
|
||||
|
||||
| 模式 | 能力 | 平台 | 透明度 |
|
||||
|---|---|---|---|
|
||||
| **`serial`** | 真正的双向字节流转发:server 端 `SerialPort.open`,client 端发 hex 字符串 | 全平台 | ✓ 完整(OS 看来就是普通串口) |
|
||||
| **`usb`** | libusb 字节桥:server 端 `node-usb` open + claim interface,client 端发 controlTransfer/bulkTransfer 请求 | 全平台 | ✗ 不是透明 USB——是远程调设备 |
|
||||
| **`usbip`** | 调系统 `usbip` CLI,让 Linux 内核接管(vhci_hcd 模块) | Linux only | ✓ 真透明(设备出现在本机 lsusb) |
|
||||
|
||||
#### 5.3.1 `serial` 模式(最实用)
|
||||
|
||||
覆盖所有 USB-串口适配器(CH340 / CP210x / FTDI / PL2303)和原生 COM/tty。**对端不用装任何驱动**——`serialport` 走 OS 标准串口 API,自动枚举。
|
||||
对硬件开发者来说这就是"远程调试 Arduino / STM32 / ESP32 / 串口打印机 / 串口屏"。
|
||||
|
||||
```
|
||||
控制端 被控端
|
||||
usb.attach(busId, { kind: 'serial', baudRate: 115200 })
|
||||
└→ chatClient.send(usb.attach)
|
||||
└→ 弹窗确认
|
||||
└→ new SerialPort({ path, baudRate: ... }).open()
|
||||
└→ sp.on('data', buf) → usb.data { dir: 'dev->host' }
|
||||
usb.serialSend(sid, bytes)
|
||||
└→ chatClient.send(usb.data { dir: 'host->dev' })
|
||||
└→ sp.write(buf)
|
||||
```
|
||||
|
||||
#### 5.3.2 `usb` 模式(libusb 字节桥)
|
||||
|
||||
适合访问自定义 USB 设备(单片机编程器、调试器、JTAG、自定义硬件)。
|
||||
**不是透明 USB**——是 `node-usb` 的 controlTransfer / bulkTransfer 调用搬到对端机器上。
|
||||
要"像本机 USB 一样"必须走 usbip 或商业软件。
|
||||
|
||||
```
|
||||
控制端 被控端
|
||||
usb.attach(busId, { kind: 'usb', interfaceNumber: 0 })
|
||||
└→ chatClient.send(usb.attach)
|
||||
└→ node-usb.open(vid,pid)
|
||||
└→ claimInterface(0)
|
||||
└→ 返回 endpoints 列表
|
||||
usb.ctrlIn(sid, { requestType, request, value, index }, length)
|
||||
└→ chatClient.send(usb.ctrlIn { setup, length })
|
||||
└→ dev.nativeControlTransferIn(setup, 5000, length)
|
||||
└→ usb.ctrlResult { ok, data, status }
|
||||
usb.bulkIn(sid, endpoint, length) 类似
|
||||
usb.bulkOut(sid, endpoint, bytes) 类似
|
||||
```
|
||||
|
||||
#### 5.3.3 `usbip` 模式(Linux only)
|
||||
|
||||
真透明 USB 透传。`usbip attach -r <peer> -b <busid>` 让本机内核接管,设备出现在 `lsusb`。
|
||||
**前置条件**:用户机器装了 `usbip` 包 + 加载 `vhci_hcd` 内核模块。我们的应用首次用时自动检测 + 提示安装(`permissions.ts` 里的 `diagnoseAndFixUsbAttach`)。
|
||||
**macOS/Windows 默认不支持**——UI 会显示提示但禁用该选项。
|
||||
|
||||
#### 5.3.4 协议帧
|
||||
|
||||
```ts
|
||||
type UsbFrame =
|
||||
| { type: 'list'; reqId: string }
|
||||
| { type: 'devices'; reqId: string; devices: UsbDeviceInfo[] }
|
||||
| { type: 'attach'; sessionId: string; direction: UsbDirection; busId: string; config: UsbAttachConfig }
|
||||
| { type: 'attached'; sessionId: string; ok: boolean; reason?: string; info?: UsbAttachedInfo }
|
||||
| { type: 'detach'; sessionId: string; reason?: string }
|
||||
| { type: 'detached'; sessionId: string; ok: boolean; reason?: string }
|
||||
// 字节流 (serial 模式)
|
||||
| { type: 'data'; sessionId: string; dir: 'host->dev' | 'dev->host'; data: string; fin?: boolean }
|
||||
// USB 控制传输 (request-response, 用 reqId 配对)
|
||||
| { type: 'ctrlOut'; sessionId: string; reqId: string; setup: UsbControlSetup; data?: string }
|
||||
| { type: 'ctrlIn'; sessionId: string; reqId: string; setup: UsbControlSetup; length: number }
|
||||
| { type: 'ctrlResult'; sessionId: string; reqId: string; ok: boolean; data?: string; status?: number }
|
||||
// USB 批量/中断传输 (request-response, 用 reqId 配对)
|
||||
| { type: 'bulkOut'; sessionId: string; reqId: string; endpoint: number; data: string }
|
||||
| { type: 'bulkIn'; sessionId: string; reqId: string; endpoint: number; length: number; timeoutMs?: number }
|
||||
| { type: 'bulkResult'; sessionId: string; reqId: string; ok: boolean; data?: string; status?: number }
|
||||
| { type: 'ack'; ok: boolean; reason?: string }
|
||||
| { type: 'error'; sessionId: string; reason: string }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据流与生命周期
|
||||
|
||||
### 6.1 远程终端
|
||||
|
||||
```text
|
||||
控制端 被控端
|
||||
ui-click "打开终端"
|
||||
└→ TerminalPanel.onOpen()
|
||||
└→ window.api.terminal.open(peerId, { rows, cols })
|
||||
└→ ipcMain: terminal.open
|
||||
└→ chatClient.send(terminal.open frame)
|
||||
└→ ChatServer → TerminalManager.handleOpen
|
||||
├→ 检查 peer 在 allowPeers
|
||||
├→ 弹窗 (被控 UI)
|
||||
├→ pty.spawn(defaultShell())
|
||||
└→ send(terminal.ack { ok: true })
|
||||
ui 显示 ack + xterm 开始渲染
|
||||
ui-keypress
|
||||
└→ window.api.terminal.sendInput(sid, bytes)
|
||||
└→ chatClient.send(terminal.input frame)
|
||||
└→ TerminalManager → pty.write(bytes)
|
||||
pty.onData(bytes)
|
||||
└→ chatClient.send(terminal.output frame)
|
||||
ui: xterm.write(bytes)
|
||||
```
|
||||
|
||||
### 6.2 端口转发
|
||||
|
||||
```text
|
||||
控制端 被控端
|
||||
window.api.forward.open(peerId, { listenPort, targetHost, targetPort, ttlSec })
|
||||
└→ chatClient.send(forward.open frame)
|
||||
└→ ack { ok }(先校验端口黑名单、TTL)
|
||||
|
||||
ui → curl 127.0.0.1:listenPort
|
||||
└→ net.Server connection
|
||||
└→ ForwardManager 维护 sessionId, socket map
|
||||
└→ chatClient.send(forward.data { dir: 'c2s', data: base64, fin })
|
||||
└→ net.Socket.write(data)
|
||||
└→ target process
|
||||
response bytes → chatClient.send(forward.data { dir: 's2c', data })
|
||||
ui ← net.Server socket.write(data)
|
||||
```
|
||||
|
||||
### 6.3 USB / 串口 共享
|
||||
|
||||
#### 6.3.1 serial 模式 (字节流)
|
||||
|
||||
```text
|
||||
控制端 被控端
|
||||
window.api.usb.list(peerId)
|
||||
└→ chatClient.send(usb.list)
|
||||
└→ SerialPort.list() + (libusb if any) + (usbip if linux)
|
||||
└→ usb.devices { kind: 'serial' | 'usb' | 'usbip', ... }
|
||||
|
||||
ui 选 serial 设备 + 波特率
|
||||
└→ window.api.usb.attach(peerId, busId, { config: { kind: 'serial', baudRate: 115200 } })
|
||||
└→ chatClient.send(usb.attach { config })
|
||||
└→ 弹窗确认
|
||||
└→ new SerialPort(...).open()
|
||||
└→ sp.on('data', buf) → usb.data { dir: 'dev->host', data: base64 }
|
||||
└→ usb.attached { ok, info: { kind: 'serial', serialPath } }
|
||||
|
||||
ui 写字节
|
||||
└→ window.api.usbSerialSend(sid, bytes)
|
||||
└→ chatClient.send(usb.data { dir: 'host->dev', data })
|
||||
└→ sp.write(buf)
|
||||
|
||||
ui 关闭
|
||||
└→ window.api.usb.detach(sid) → chatClient.send(usb.detach)
|
||||
└→ sp.close() → cleanupServerSession
|
||||
```
|
||||
|
||||
#### 6.3.2 usb 模式 (libusb 字节桥)
|
||||
|
||||
```text
|
||||
控制端 被控端
|
||||
ui 选 usb 设备
|
||||
└→ window.api.usb.attach(peerId, busId, { config: { kind: 'usb', interfaceNumber: 0 } })
|
||||
└→ chatClient.send(usb.attach { config })
|
||||
└→ 弹窗确认
|
||||
└→ node-usb.findDeviceByIds(vid, pid)
|
||||
└→ dev.open() → selectConfiguration(1) → claimInterface(0)
|
||||
└→ 枚举 endpoints
|
||||
└→ usb.attached { ok, info: { kind: 'usb', endpoints: [...] } }
|
||||
|
||||
ui 发 control IN (例: GET_DESCRIPTOR)
|
||||
└→ window.api.usbCtrlIn(sid, { requestType: 0x80, request: 0x06, value: 0x0100, index: 0 }, 18)
|
||||
└→ chatClient.send(usb.ctrlIn { setup, length: 18 })
|
||||
└→ dev.nativeControlTransferIn(setup, 5000, 18)
|
||||
└→ usb.ctrlResult { ok, data, status: 18 }
|
||||
|
||||
ui 发 bulk IN
|
||||
└→ window.api.usbBulkIn(sid, 0x81, 64) → 类似 ctrlIn
|
||||
|
||||
ui 发 bulk OUT
|
||||
└→ window.api.usbBulkOut(sid, 0x01, hexData) → 走 ctrlOut-style request-response
|
||||
```
|
||||
|
||||
#### 6.3.3 usbip 模式 (Linux only)
|
||||
|
||||
```text
|
||||
控制端 (Linux 客户端) 被控端 (Linux 设备持有方)
|
||||
ui 选 usbip 设备
|
||||
└→ window.api.usb.attach(peerId, busId, { config: { kind: 'usbip' } })
|
||||
└→ chatClient.send(usb.attach { config: { kind: 'usbip' } })
|
||||
└→ 弹窗确认 → 仅记录 (不直接 open)
|
||||
└→ usb.attached { ok }
|
||||
└→ permissions.diagnoseAndFixUsbAttach() — 检查 usbip + vhci_hcd
|
||||
└→ `usbip attach -r <peerLanIp> -b <busId>` (spawn sudo-prompt)
|
||||
└─ → 内核 vhci_hcd 接收 → 设备出现在 `lsusb`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. UI 设计
|
||||
|
||||
### 7.1 Sidebar 增加“工具”tab
|
||||
|
||||
```
|
||||
设备 文件 工具 设置
|
||||
└─ 终端
|
||||
└─ 端口转发
|
||||
└─ USB 设备
|
||||
```
|
||||
|
||||
### 7.2 TerminalPanel.vue
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="term-panel">
|
||||
<div class="term-status">
|
||||
<span :class="state">{{ stateLabel }}</span>
|
||||
<span>{{ peer.name }} · {{ shellLabel }}</span>
|
||||
<el-button size="small" @click="disconnect">断开</el-button>
|
||||
</div>
|
||||
<div ref="hostEl" class="term-host" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
// ... 见 src/renderer/src/components/remote/TerminalPanel.vue
|
||||
</script>
|
||||
```
|
||||
|
||||
### 7.3 ForwardPanel.vue
|
||||
|
||||
- “+ 新建转发”按钮 → 弹表单(peer / 监听端口 / 目标主机 / 目标端口 / TTL)
|
||||
- 列表展示:`peerName, listen:127.0.0.1:PORT0 → peer:HOST:PORT1`,剩余 TTL,in/out 字节数
|
||||
- 行内操作:停用、复制链接
|
||||
|
||||
### 7.4 UsbPanel.vue
|
||||
|
||||
顶部方向切换 (我用对方的 / 对方用我的) + 模式切换 (串口 / USB / USB/IP) + 设备扫描按钮。
|
||||
|
||||
**串口模式**(最实用):
|
||||
- 设备表 + 波特率下拉 + Attach 按钮
|
||||
- 附加后显示 hex 流(serial 字节流可视化)
|
||||
- "发送文本"按钮快速发 ASCII/UTF-8 字节
|
||||
|
||||
**USB 模式**(libusb 字节桥):
|
||||
- 设备表 + Interface # 输入 + "Linux detach 内核驱动" 复选
|
||||
- 附加后显示:
|
||||
- **Endpoints 列表**(从 attach 帧的 `info.endpoints` 拿到,提示用户 EP 地址和方向)
|
||||
- **控制传输表单**:方向(IN/OUT)+ type(standard/class/vendor)+ recipient(device/interface/endpoint/other)+ request + wValue + wIndex + data(hex) / length
|
||||
- **批量/中断传输表单**:方向(IN/OUT)+ EP 地址 + length + timeout + data(hex)
|
||||
- **调用日志**:每次 ctrl/bulk 调用的时间、tag、status、hex dump
|
||||
|
||||
**USB/IP 模式**(Linux):
|
||||
- 顶部显示提示:"USB/IP 是 Linux 内核自带的真透明 USB 透传...需要装 `usbip` + 加载 `vhci_hcd` 模块"
|
||||
- 设备表 + Attach 按钮
|
||||
- 附加后由内核接管,本应用不再做数据转发
|
||||
|
||||
底部始终有"对方正在使用我的设备"横条(当 server-side session 存在时),可一键 stop。
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全模型
|
||||
|
||||
> 本项目原本 LAN 信任域 = 同一子网任意主机;启用终端/转发/USB 后需要更明确的边界。
|
||||
|
||||
### 8.1 信任与确认
|
||||
|
||||
- 每条 `*.open` 必须在**被控端**弹窗(默认聚焦窗口、10 秒倒计时、不可超时自动通过)。
|
||||
- 设置项 `remote.allowPeers: Record<DeviceId, { terminal: bool; forward: bool; usb: bool }>`,默认全 false。
|
||||
- 同一对端 24 小时内只能弹一次允许(除非重新启动应用)。
|
||||
- “仅只读终端”开关:禁用 `terminal.input`(被控端拒绝写入帧)。
|
||||
- `forward.open` 必须显式选择目标端口范围 + TTL;被控端按白名单校验 host。
|
||||
|
||||
### 8.2 端口安全
|
||||
|
||||
- 监听 host 永远 `127.0.0.1`,不开 `0.0.0.0`。
|
||||
- 只挡自家端口 `SELF_PORTS = {47800, 47900, 47901}`(防把消息/文件端口转发造成死循环/桥接冲突),**其他端口一律不挡**(用户的 SSH/RDP/MySQL 等服务我们不去判断)。
|
||||
- 默认 TTL 1 小时,最大 24 小时。
|
||||
- 默认带宽 10 MB/s,最大 100 MB/s,按 session 计数。
|
||||
|
||||
### 8.3 USB / 串口 安全
|
||||
|
||||
- 每条 `usb.list` / `usb.attach` 必须**被控端**显式授权(弹窗)。
|
||||
- `serial` 模式:被控端打开串口前弹窗告知"对方将通过此串口发送/接收字节",用户勾"记住"后 24h 内不再弹。
|
||||
- `usb` 模式:被控端 claim interface 前弹窗,告知"对方将通过本机的 USB 设备 X 发送控制/批量传输"。
|
||||
- `usbip` 模式:弹窗告知"对方将 attach 你的 USB 设备 X 到他自己的机器上"(设备会从你机器上消失)。
|
||||
- 设备列表本身(vendor/product/serial)属于"信息泄露"敏感字段,要求接收端在 `usb.list` 时显式授权一次。
|
||||
- 被控端顶部横幅"USB/串口 设备已被 X 远程使用中"(chat header 同时显示)。
|
||||
- session 30 秒无活动自动 detach。
|
||||
|
||||
### 8.4 审计
|
||||
|
||||
新增表(`db.ts` 内 `initAudit()`):
|
||||
|
||||
```sql
|
||||
CREATE TABLE audit_log (
|
||||
id INTEGER PRIMARY KEY,
|
||||
ts INTEGER NOT NULL,
|
||||
action TEXT NOT NULL, -- terminal.open / forward.open / usb.attach ...
|
||||
source_device_id TEXT,
|
||||
target_device_id TEXT,
|
||||
session_id TEXT,
|
||||
payload_json TEXT,
|
||||
result TEXT, -- ok / denied / error / closed
|
||||
bytes_in INTEGER DEFAULT 0,
|
||||
bytes_out INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX idx_audit_ts ON audit_log(ts DESC);
|
||||
```
|
||||
|
||||
保留 30 天(设置项可改),手动“导出 audit.zip”。
|
||||
|
||||
---
|
||||
|
||||
## 9. 与现有 pitfalls 的对齐
|
||||
|
||||
| Pitfall | 对策 |
|
||||
|---|---|
|
||||
| #1 `chat-client` 必带 `'error'` 监听 | 新增帧引入不影响;已有 on('error') 已稳 |
|
||||
| #2 drag-drop file path | 不涉及 |
|
||||
| #3 native rebuild | `@lydell/node-pty`、`usb`、`node-hid`、`serialport` 都是 native,必须走现有 `postinstall` 的 `electron-builder install-app-deps`;CI 加 `npm rebuild --runtime=electron` 兜底 |
|
||||
| #4 dist-Electron 路径 | 不动 |
|
||||
| #5 nsis oneClick false | 不动 |
|
||||
| #6 dragenter/dragleave 深度计数 | 不涉及 |
|
||||
| #7 渲染层只暴露 在线/离线 两种状态 | 新增的三个 UI 独立成 tab,不污染 ChatView/Sidebar 的现有状态机 |
|
||||
| #8 文件冲突命名 | 新增 `downloadDir` 复用 `file-server.ts` 既有目录,不改 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 落地节奏
|
||||
|
||||
### v0.2.0 — 单平台、单 session、只读终端 + 本机转发
|
||||
|
||||
1. 加 `@lydell/node-pty`、`@xterm/xterm`、`@xterm/addon-fit`、`@xterm/addon-web-links`、`usb`、`node-hid`、`serialport`。
|
||||
2. `src/main/protocol.ts` 加 `WsFrame` 三类控制帧。
|
||||
3. `src/main/remote/{terminal,forward,usb}.ts` 三个 manager。
|
||||
4. `src/main/db.ts` 加 `audit_log` 表。
|
||||
5. `src/main/ipc.ts` 增 `terminal:*`、`forward:*`、`usb:*` 共约 10 个 handler。
|
||||
6. `src/renderer/src/components/remote/{TerminalPanel,ForwardPanel,UsbPanel}.vue`。
|
||||
7. `src/renderer/src/stores/{terminal,forward,usb}.ts`。
|
||||
8. `SettingsView.vue` 增加“远程”开关与 allowPeers 列表。
|
||||
9. `npm run typecheck` 通过;手动两机对测。
|
||||
|
||||
### v0.3.0 — 多 session、带宽/TTL、串口透传
|
||||
|
||||
1. `forward.maxBandwidth`、`forward.defaultTtl` 设置项。
|
||||
2. `serialport` 路径完善(Linux udev 提示)。
|
||||
3. xterm.js 同步主题、深色模式。
|
||||
|
||||
### v0.4.0 — 公网可选
|
||||
|
||||
1. `localtunnel` 接入(仅转发;终端和 USB 不走公网)。
|
||||
2. 文案与免责声明。
|
||||
|
||||
### 不做
|
||||
|
||||
- 屏幕/桌面、鼠标键盘输入、剪贴板双向同步、音频、文件双向同步——都留给 RustDesk / AnyDesk。
|
||||
- 公网中继自建(成本过高,Y 不如直接用现成 SaaS)。
|
||||
- 远程重启、远程安装驱动、远程执行任意脚本。
|
||||
|
||||
---
|
||||
|
||||
## 11. 验证清单(手动两机)
|
||||
|
||||
| # | 场景 | 期望 |
|
||||
|---|---|---|
|
||||
| T1 | 控制端请求被控端终端 → 被控端允许 | xterm 渲染 PowerShell/zsh/bash 提示符 |
|
||||
| T2 | 控制端输入 `ls -la` | 双端能正确显示(中文文件名 OK) |
|
||||
| T3 | 控制端把窗口拉大 | 终端按比例刷新(fit + resize 帧) |
|
||||
| T4 | 控制端断开 | 被控端 PTY 收到 SIGHUP(win 下 ConPTY 退出) |
|
||||
| F1 | 控制端 127.0.0.1:5180 → 被控 127.0.0.1:22 | ack 被控拒(黑名单) |
|
||||
| F2 | 控制端 127.0.0.1:5180 → 被控 127.0.0.1:3000 | 浏览器开 5180 看到 3000 内容 |
|
||||
| F3 | TTL 到期 | listener 自动释放;`netstat` 看不到端口 |
|
||||
| U1 | 列出被控端 USB 设备 | 控制端看到 product/serial |
|
||||
| U2 | attach 串口 /dev/ttyUSB0 | 被控端 z串口被占;控制端能 write |
|
||||
| U3 | 被控端关闭应用 | 控制端收到 `usb.close { reason: 'peer-down' }` |
|
||||
| S1 | 控制端不在 allowPeers | 被控端收到 `open` 直接 ack false,写 audit |
|
||||
| S2 | 控制端伪造 `fromDeviceId` | 被控端按 WS 握手记录覆盖,伪造失败 |
|
||||
| S3 | 同时 5 个并发 forward | 全部独立,UI 列表正确 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 参考
|
||||
|
||||
- `@lydell/node-pty` README & 平台包列表:`https://github.com/lydell/node-pty`
|
||||
- `@xterm/xterm` v6.0.0:`https://xtermjs.org/`
|
||||
- `usb@3.0.1`(Rust 重写版):`https://github.com/node-usb/node-usb-rs`
|
||||
- `node-hid@3.3.0`:`https://github.com/node-hid/node-hid`
|
||||
- `serialport@13.0.0`:`https://serialport.io/`
|
||||
- `localtunnel@2.0.2`:`https://github.com/localtunnel/localtunnel`
|
||||
- 现有项目入口:`AGENTS.md`、`src/main/protocol.ts`、`src/main/chat-client.ts:91`、`src/main/ipc.ts:59`
|
||||
Reference in New Issue
Block a user