Files
LocalNetMsg/docs/remote-terminal-forward-usb.md
T

623 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 远程终端、端口转发、USB 透传 规划文档
> 目标:在 LocalNetMsg 中实现三类“远程使用对方设备”的能力——**对方机器的本地终端(shell)**、**TCP 端口转发**、**USB 设备透传**。本文先定整体架构、协议、库选型,再分阶段落地。
>
> 适配范围:Windows / macOS / Linux 三端。被控端的 shell 按平台决定:Windows → PowerShell(默认 `pwsh.exe`,回退 `powershell.exe`),macOS → zsh(回退 bash),Linux → bash(按 `SHELL` 环境变量)。
---
## 1. 设计目标与边界
| 项目 | MVPv0.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.1Rust 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 interfaceclient 端发 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`,剩余 TTLin/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+ typestandard/class/vendor+ recipientdevice/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 收到 SIGHUPwin 下 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`