network-infra-utility 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +21 -0
  3. data/CHANGELOG.md +31 -4
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +70 -0
  6. data/Rakefile +1 -1
  7. data/bin/dns-query +834 -0
  8. data/bin/geo-doc +135 -0
  9. data/bin/geo-get +1 -1
  10. data/document/ASNum/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +242 -0
  11. data/document/DNSQuery/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +248 -0
  12. data/document/Geo/345/221/275/344/273/244/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +441 -0
  13. data/document/IP/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +297 -0
  14. data/document/MAC/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +296 -0
  15. data/document/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/344/275/277/347/224/250/346/226/271/346/263/225.md +764 -0
  16. data/network-infra-utility.gemspec +4 -2
  17. data/network.rb +3 -1
  18. data/service/geodb/GeoAPI.md +1 -0
  19. data/service/geodb/geodb.rb +278 -1
  20. data/service/ssh/README.md +942 -0
  21. data/service/ssh/bin/ssh-client +198 -0
  22. data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/345/212/237/350/203/275/351/234/200/346/261/202/346/226/207/346/241/243.md +292 -0
  23. data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/257/246/347/273/206/350/256/276/350/256/241/346/226/207/346/241/243.md +1521 -0
  24. data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/275/257/344/273/266/350/256/276/350/256/241/346/226/207/346/241/243.md +2493 -0
  25. data/service/ssh/ext/ssh_core/bin/ssh_core.cmd +28 -0
  26. data/service/ssh/ext/ssh_core/config/sys.config +0 -0
  27. data/service/ssh/ext/ssh_core/config/vm.args +0 -0
  28. data/service/ssh/ext/ssh_core/local_deps/jsx/CHECKSUM +1 -0
  29. data/service/ssh/ext/ssh_core/local_deps/jsx/LICENSE +21 -0
  30. data/service/ssh/ext/ssh_core/local_deps/jsx/README.md +696 -0
  31. data/service/ssh/ext/ssh_core/local_deps/jsx/VERSION +1 -0
  32. data/service/ssh/ext/ssh_core/local_deps/jsx/contents.tar.gz +0 -0
  33. data/service/ssh/ext/ssh_core/local_deps/jsx/metadata.config +15 -0
  34. data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.config +17 -0
  35. data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.lock +1 -0
  36. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.app.src +10 -0
  37. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.erl +506 -0
  38. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.erl +393 -0
  39. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.hrl +18 -0
  40. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_consult.erl +81 -0
  41. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_decoder.erl +1909 -0
  42. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_encoder.erl +116 -0
  43. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_parser.erl +1214 -0
  44. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_json.erl +408 -0
  45. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_term.erl +389 -0
  46. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_verify.erl +121 -0
  47. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx.erl +506 -0
  48. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.erl +393 -0
  49. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.hrl +18 -0
  50. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_consult.erl +81 -0
  51. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_decoder.erl +1909 -0
  52. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_encoder.erl +116 -0
  53. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_parser.erl +1214 -0
  54. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_json.erl +408 -0
  55. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_term.erl +389 -0
  56. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_verify.erl +121 -0
  57. data/service/ssh/ext/ssh_core/rebar.config +24 -0
  58. data/service/ssh/ext/ssh_core/rebar.lock +1 -0
  59. data/service/ssh/ext/ssh_core/src/ssh_auth_engine.erl +156 -0
  60. data/service/ssh/ext/ssh_core/src/ssh_channel_stm.erl +232 -0
  61. data/service/ssh/ext/ssh_core/src/ssh_codec.erl +83 -0
  62. data/service/ssh/ext/ssh_core/src/ssh_conn_sup.erl +48 -0
  63. data/service/ssh/ext/ssh_core/src/ssh_conn_worker.erl +535 -0
  64. data/service/ssh/ext/ssh_core/src/ssh_core.app.src +36 -0
  65. data/service/ssh/ext/ssh_core/src/ssh_core_app.erl +11 -0
  66. data/service/ssh/ext/ssh_core/src/ssh_core_sup.erl +46 -0
  67. data/service/ssh/ext/ssh_core/src/ssh_infra_sup.erl +104 -0
  68. data/service/ssh/ext/ssh_core/src/ssh_ipc.hrl +80 -0
  69. data/service/ssh/ext/ssh_core/src/ssh_ipc_coalesce.erl +94 -0
  70. data/service/ssh/ext/ssh_core/src/ssh_ipc_gateway.erl +467 -0
  71. data/service/ssh/ext/ssh_core/src/ssh_ipc_proto.erl +95 -0
  72. data/service/ssh/ext/ssh_core/src/ssh_jump_chain.erl +101 -0
  73. data/service/ssh/ext/ssh_core/src/ssh_keepalive_mgr.erl +222 -0
  74. data/service/ssh/ext/ssh_core/src/ssh_known_hosts_proxy.erl +67 -0
  75. data/service/ssh/ext/ssh_core/src/ssh_port_fwd.erl +225 -0
  76. data/service/ssh/ext/ssh_core/src/ssh_sftp_session.erl +250 -0
  77. data/service/ssh/ext/ssh_core/src/ssh_sftp_sup.erl +62 -0
  78. data/service/ssh/ext/ssh_core_rs/Cargo.lock +2345 -0
  79. data/service/ssh/ext/ssh_core_rs/Cargo.toml +30 -0
  80. data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs +34 -0
  81. data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs.cmd +40 -0
  82. data/service/ssh/ext/ssh_core_rs/src/channel.rs +296 -0
  83. data/service/ssh/ext/ssh_core_rs/src/coalesce.rs +143 -0
  84. data/service/ssh/ext/ssh_core_rs/src/codec.rs +71 -0
  85. data/service/ssh/ext/ssh_core_rs/src/conn.rs +628 -0
  86. data/service/ssh/ext/ssh_core_rs/src/gateway.rs +389 -0
  87. data/service/ssh/ext/ssh_core_rs/src/handler.rs +293 -0
  88. data/service/ssh/ext/ssh_core_rs/src/keepalive.rs +194 -0
  89. data/service/ssh/ext/ssh_core_rs/src/main.rs +351 -0
  90. data/service/ssh/ext/ssh_core_rs/src/portfwd.rs +378 -0
  91. data/service/ssh/ext/ssh_core_rs/src/proto.rs +198 -0
  92. data/service/ssh/ext/ssh_core_rs/src/sftp.rs +294 -0
  93. data/service/ssh/lib/network_infra_utility/ssh/automation/macro_engine.rb +213 -0
  94. data/service/ssh/lib/network_infra_utility/ssh/client.rb +257 -0
  95. data/service/ssh/lib/network_infra_utility/ssh/config/schema.rb +90 -0
  96. data/service/ssh/lib/network_infra_utility/ssh/config/settings.rb +103 -0
  97. data/service/ssh/lib/network_infra_utility/ssh/config/store.rb +90 -0
  98. data/service/ssh/lib/network_infra_utility/ssh/ipc/coalesce.rb +83 -0
  99. data/service/ssh/lib/network_infra_utility/ssh/ipc/errors.rb +36 -0
  100. data/service/ssh/lib/network_infra_utility/ssh/ipc/router.rb +212 -0
  101. data/service/ssh/lib/network_infra_utility/ssh/ipc/transport.rb +81 -0
  102. data/service/ssh/lib/network_infra_utility/ssh/security/host_key.rb +211 -0
  103. data/service/ssh/lib/network_infra_utility/ssh/security/vault.rb +211 -0
  104. data/service/ssh/lib/network_infra_utility/ssh/session/history.rb +56 -0
  105. data/service/ssh/lib/network_infra_utility/ssh/session/manager.rb +92 -0
  106. data/service/ssh/lib/network_infra_utility/ssh/session/session.rb +109 -0
  107. data/service/ssh/lib/network_infra_utility/ssh/session/tree.rb +95 -0
  108. data/service/ssh/lib/network_infra_utility/ssh/terminal/ansi_parser.rb +435 -0
  109. data/service/ssh/lib/network_infra_utility/ssh/terminal/buffer.rb +78 -0
  110. data/service/ssh/lib/network_infra_utility/ssh/terminal/emulator.rb +159 -0
  111. data/service/ssh/lib/network_infra_utility/ssh/terminal/logger.rb +195 -0
  112. data/service/ssh/lib/network_infra_utility/ssh/terminal/screen.rb +212 -0
  113. data/service/ssh/lib/network_infra_utility/ssh/terminal/theme.rb +127 -0
  114. data/service/ssh/lib/network_infra_utility/ssh/version.rb +7 -0
  115. data/service/ssh/lib/network_infra_utility/ssh.rb +44 -0
  116. data/support/basic/as_num.rb +221 -0
  117. data/support/basic/mac_address.rb +281 -0
  118. data/version.rb +1 -1
  119. metadata +138 -1
@@ -0,0 +1,2493 @@
1
+
2
+ # SSH 连接客户端软件设计文档
3
+
4
+ > **文档版本**:v1.0
5
+ > **编写日期**:2026-08-02
6
+ > **文档状态**:初稿
7
+ > **对应需求**:SSH连接客户端功能需求文档 v1.0
8
+
9
+ ---
10
+
11
+ ## 目录
12
+
13
+ - [1 设计概述](#1-设计概述)
14
+ - [2 系统架构](#2-系统架构)
15
+ - [3 技术选型](#3-技术选型)
16
+ - [4 Erlang SSH 核心引擎设计](#4-erlang-ssh-核心引擎设计)
17
+ - [5 Ruby 调度界面设计](#5-ruby-调度界面设计)
18
+ - [6 通信协议设计](#6-通信协议设计)
19
+ - [7 数据模型设计](#7-数据模型设计)
20
+ - [8 安全设计](#8-安全设计)
21
+ - [9 需求映射矩阵](#9-需求映射矩阵)
22
+ - [10 部署与打包](#10-部署与打包)
23
+ - [11 三期迭代设计](#11-三期迭代设计)
24
+
25
+ ---
26
+
27
+ ## 1 设计概述
28
+
29
+ ### 1.1 编写目的
30
+
31
+ 本文档基于《SSH 连接客户端功能需求文档》的全部 51 条功能需求与 20 条非功能需求,定义系统的架构分层、模块划分、接口协议、数据模型和安全机制,为开发团队提供可落地的工程蓝图。
32
+
33
+ ### 1.2 设计目标
34
+
35
+ | 目标 | 量化指标 | 对应需求 |
36
+ |------|----------|----------|
37
+ | 高并发连接 | 单实例 ≥ 50 个活跃 SSH 会话 | NFR-PERF-006 |
38
+ | 低延迟交互 | 按键到屏幕回显 ≤ 50ms | NFR-PERF-003 |
39
+ | 高可靠运行 | 连续运行 72h 无崩溃 | NFR-REL-001 |
40
+ | 跨平台一致 | Windows/macOS/Linux 功能一致度 ≥ 95% | FR-COLLAB-001 |
41
+ | 低资源占用 | 10 个活跃会话内存 ≤ 500MB | NFR-PERF-005 |
42
+
43
+ ### 1.3 架构决策摘要
44
+
45
+ | 决策点 | 选择 | 理由 |
46
+ |--------|------|------|
47
+ | SSH 协议栈语言 | Erlang/OTP | OTP 的 ssh 应用提供成熟 SSH2 协议栈,轻量进程模型天然适配高并发连接管理 |
48
+ | 调度与 UI 层语言 | Ruby | 与 network-infra-utility 项目主语言统一,gem 生态丰富,开发效率高 |
49
+ | 进程间通信 | JSON-RPC 2.0 over Unix Socket / TCP | 语言无关、调试友好、Unix Socket 零拷贝低延迟 |
50
+ | UI 渲染方案 | 分阶段:CLI → TUI(ruby-curses) → GUI(可选) | 降低首版复杂度,先交付可交互 CLI,逐步增强 |
51
+ | 配置存储格式 | YAML + 加密字段(AES-256-GCM) | YAML 可读性好,敏感字段单独加密 |
52
+ | 终端仿真 | Ruby 侧 VT100/xterm 转义解析 + Erlang 侧透传 | 转义解析近 UI 层,协议层保持透传 |
53
+
54
+ ### 1.4 设计约束
55
+
56
+ - Erlang 侧不引入第三方 SSH 库,直接基于 OTP `ssh` 应用(Erlang/OTP 26+ 内置)。
57
+ - Ruby 侧不直接处理任何 TCP/SSH 协议细节,所有网络连接由 Erlang 层代理。
58
+ - 跨平台编译:Windows 使用 Rebar3 + MSVC,macOS/Linux 使用 Rebar3 + GCC/Clang。
59
+ - 最终产物为单一守护进程(Erlang)+ Ruby gem 前端,二者通过本地 IPC 耦合。
60
+
61
+ ---
62
+
63
+ ## 2 系统架构
64
+
65
+ ### 2.1 总体架构图
66
+
67
+ ```
68
+ ┌─────────────────────────────────────────────────────────────────┐
69
+ │ Ruby 调度界面层 (Frontend) │
70
+ │ │
71
+ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐│
72
+ │ │ 会话管理 │ │ 终端仿真 │ │ 文件管理 │ │ 自动化 │ │ 安全 ││
73
+ │ │ SessMgr │ │ Terminal │ │ FileMgr │ │ Engine │ │ Vault ││
74
+ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └───┬────┘│
75
+ │ │ │ │ │ │ │
76
+ │ ┌────┴────────────┴────────────┴─────────────┴───────────┴────┐ │
77
+ │ │ IPC Client (JSON-RPC 2.0 Adapter) │ │
78
+ │ └────────────────────────┬─────────────────────────────────────┘ │
79
+ └───────────────────────────┼───────────────────────────────────────┘
80
+ │ Unix Socket / 127.0.0.1:TCP
81
+ │ JSON-RPC 2.0
82
+ ┌───────────────────────────┼───────────────────────────────────────┐
83
+ │ Erlang SSH 核心引擎层 (Backend) │
84
+ │ ┌────────────────────────┴─────────────────────────────────────┐ │
85
+ │ │ IPC Gateway (JSON-RPC Dispatcher) │ │
86
+ │ └────┬────────────┬─────────────┬────────────┬──────────────────┘ │
87
+ │ │ │ │ │ │
88
+ │ ┌────┴────┐ ┌─────┴─────┐ ┌─────┴────┐ ┌─────┴─────┐ ┌──────────┐│
89
+ │ │ 连接池 │ │ 认证引擎 │ │ 端口转发 │ │ SFTP 引擎 │ │ 跳板链 ││
90
+ │ │ConnPool │ │AuthEngine│ │PortFwd │ │SftpEngine│ │JumpChain ││
91
+ │ └────┬────┘ └──────────┘ └──────────┘ └───────────┘ └──────────┘│
92
+ │ │ │
93
+ │ ┌────┴──────────────────────────────────────────────────────────┐│
94
+ │ │ Erlang/OTP ssh 应用 (SSH2 协议栈) ││
95
+ │ │ 密钥交换 │ 对称加密 │ MAC │ 压缩 │ 通道复用 ││
96
+ │ └───────────────────────────────────────────────────────────────┘│
97
+ │ │ │
98
+ │ ┌───────────────────────────┴───────────────────────────────────┐│
99
+ │ │ TCP / 网络 I/O ││
100
+ │ └───────────────────────────────────────────────────────────────┘│
101
+ └──────────────────────────────────────────────────────────────────┘
102
+ ```
103
+
104
+ ### 2.2 分层职责
105
+
106
+ | 层 | 语言 | 核心职责 | 不负责 |
107
+ |-------------|--------|---------|--------|
108
+ | Ruby 调度层 | Ruby | 会话编排、配置管理、终端渲染、自动化脚本、凭据加解密、用户交互 | 不直接建立 TCP 连接、不处理 SSH 二进制协议 |
109
+ | IPC 通道 | 双端 | JSON-RPC 2.0 消息封装与路由 | 不包含业务逻辑 |
110
+ | Erlang 核心层 | Erlang | SSH 握手、认证、加密通道管理、端口转发、SFTP 子系统、连接池、跳板链 | 不做 UI 渲染、不做配置持久化 |
111
+ | OTP ssh 应用 | Erlang | SSH2 协议实现(RFC 4250-4256) | 由 OTP 维护 |
112
+
113
+ ### 2.3 数据流举例:用户连接一台服务器
114
+
115
+ ```
116
+ 用户点击"连接"
117
+ │
118
+ ▼
119
+ Ruby SessMgr.build_connect_spec(session)
120
+ │ 构建 JSON-RPC 请求: {"method":"conn.connect","params":{...}}
121
+ ▼
122
+ IPC Client ──Unix Socket──▶ Erlang IPC Gateway
123
+ │
124
+ ▼
125
+ Erlang ConnPool.spawn_connection(spec)
126
+ │ 调用 ssh:connect/3 建立 TCP + SSH 握手
127
+ │ 执行认证(密码/公钥/keyboard-interactive)
128
+ ▼
129
+ Erlang 返回: {"result":{"channel_id":"ch_001","fingerprint":"SHA256:..."}}
130
+ │
131
+ ▼
132
+ Ruby Terminal.create(channel_id)
133
+ │ 后续终端数据双向流:
134
+ │ Ruby → Erlang: {"method":"channel.send","params":{"id":"ch_001","data":"ls\n"}}
135
+ │ Erlang → Ruby: 推送 (push) {"method":"channel.data","params":{"id":"ch_001","data":"..."}}
136
+ ```
137
+
138
+ ### 2.4 进程模型
139
+
140
+ ```
141
+ Erlang VM (beam)
142
+ ├── ssh_core_sup (顶层 supervisor)
143
+ │ ├── ipc_gateway (gen_server, 1个, 监听 Unix Socket)
144
+ │ ├── conn_pool_sup (simple_one_for_one, N个连接)
145
+ │ │ ├── conn_worker_1 (gen_server, 1个SSH连接)
146
+ │ │ │ ├── channel_1 (gen_fsm, shell通道)
147
+ │ │ │ ├── channel_2 (gen_fsm, sftp通道)
148
+ │ │ │ └── portfwd_1 (gen_server, 端口转发)
149
+ │ │ ├── conn_worker_2
150
+ │ │ └── ...
151
+ │ ├── auth_engine (gen_server, 认证状态机)
152
+ │ ├── sftp_engine_sup (supervisor, SFTP子池)
153
+ │ └── keepalive_mgr (gen_server, 心跳巡检)
154
+ │
155
+ Ruby Process
156
+ ├── main thread
157
+ │ ├── SessMgr (会话调度)
158
+ │ ├── TerminalRouter (终端数据分发)
159
+ │ └── IPC::Client (JSON-RPC 客户端, 1个连接)
160
+ ├── event_loop thread (监听 Erlang push 消息)
161
+ └── vault_worker thread (凭据加解密)
162
+ ```
163
+
164
+ ---
165
+
166
+ ## 3 技术选型
167
+
168
+ ### 3.1 Erlang 侧
169
+
170
+ | 组件 | 选择 | 版本要求 | 说明 |
171
+ |------|------|----------|------|
172
+ | 运行时 | Erlang/OTP | 26+ | 内置 ssh 应用,支持 curve25519-sha256 |
173
+ | 构建工具 | Rebar3 | 3.20+ | OTP 标准构建工具 |
174
+ | SSH 协议栈 | OTP ssh | 随 OTP | 无第三方依赖 |
175
+ | JSON 解析 | jsx | 3.1+ | 纯 Erlang JSON 库,编译进 release |
176
+ | 日志 | logger | OTP 内置 | 结构化日志 |
177
+ | 加密 | crypto | OTP 内置 | 底层调用 OpenSSL |
178
+ | 测试框架 | Common Test | OTP 内置 | OTP 标准测试框架 |
179
+
180
+ ### 3.2 Ruby 侧
181
+
182
+ | 组件 | 选择 | 版本要求 | 说明 |
183
+ |------|------|----------|------|
184
+ | 运行时 | Ruby | 3.1+ | 项目已在用 |
185
+ | 终端渲染 | curses / reline | 标准库 | V1.0 CLI 渲染 |
186
+ | JSON | json | 标准库 | JSON-RPC 通信 |
187
+ | 加密 | openssl | 标准库 | AES-256-GCM 凭据加密 |
188
+ | 配置 | yaml | 标准库 | 会话与设置持久化 |
189
+ | 测试框架 | RSpec | 3.12+ | 与项目现有 spec 一致 |
190
+ | CLI 框架 | thor | 1.2+ | 命令行参数解析 |
191
+ | 终端 TUI | curses | 标准库 | V2.0 多面板 TUI |
192
+
193
+ ### 3.3 选型对比记录
194
+
195
+ #### SSH 协议语言对比
196
+
197
+ | 方案 | 优势 | 劣势 | 结论 |
198
+ |------|------|------|------|
199
+ | **Erlang/OTP ssh** | 协议栈成熟,进程模型天然适配高并发,容错监督树 | 生态偏后端,UI 无原生支持 | **采用**——协议核心放 Erlang |
200
+ | Ruby net/ssh | 与前端同语言,调用简单 | 单线程连接管理受限,协议实现不如官方,高并发性能差 | 否决——无法满足 50 并发 |
201
+ | Rust russh | 性能最优,内存安全 | 生态不如 OTP 成熟,开发周期长 | 否决——ROI 不够 |
202
+ | Go x/crypto/ssh | 编译单二进制,并发好 | GC 暂停影响终端低延迟,ssh 库功能覆盖中等 | 备选——V2.0 评估 |
203
+
204
+ #### 前端语言对比
205
+
206
+ | 方案 | 优势 | 劣势 | 结论 |
207
+ |------|------|------|------|
208
+ | **Ruby** | 与 network-infra-utility 统一,gem 生态丰富,开发效率高 | GUI 生态弱,性能不如编译型语言 | **采用**——CLI/TUI 先行 |
209
+ | Electron/TS | 跨平台 GUI 生态最成熟 | 引入 JS 生态,内存占用大,偏离项目技术栈 | 否决 |
210
+ | Qt/C++ | GUI 性能最优 | 开发效率低,编译复杂 | 备选——V3.0 差异化评估 |
211
+
212
+ ---
213
+
214
+ ## 4 Erlang SSH 核心引擎设计
215
+
216
+ ### 4.1 应用结构
217
+
218
+ ```
219
+ ssh_core/
220
+ ├── src/
221
+ │ ├── ssh_core.app.src % OTP application 资源文件
222
+ │ ├── ssh_core_sup.erl % 顶层 supervisor
223
+ │ ├── ssh_core_app.erl % application 回调
224
+ │ ├── ssh_ipc_gateway.erl % IPC 网关 (gen_server)
225
+ │ ├── ssh_ipc_proto.erl % JSON-RPC 协议编解码
226
+ │ ├── ssh_conn_pool_sup.erl % 连接池 supervisor
227
+ │ ├── ssh_conn_worker.erl % 单连接工作进程 (gen_server)
228
+ │ ├── ssh_channel_fsm.erl % SSH 通道状态机 (gen_fsm)
229
+ │ ├── ssh_auth_engine.erl % 认证引擎
230
+ │ ├── ssh_jump_chain.erl % 跳板链构建器
231
+ │ ├── ssh_port_fwd.erl % 端口转发管理器
232
+ │ ├── ssh_sftp_engine.erl % SFTP 引擎
233
+ │ ├── ssh_keepalive_mgr.erl % 保活巡检器
234
+ │ ├── ssh_known_hosts.erl % 已知主机管理
235
+ │ └── ssh_codec.erl % 共用编解码工具
236
+ ├── rebar.config
237
+ ├── test/
238
+ │ ├── ssh_ipc_gateway_SUITE.erl
239
+ │ ├── ssh_conn_worker_SUITE.erl
240
+ │ └── ...
241
+ └── Makefile
242
+ ```
243
+
244
+ ### 4.2 模块详细设计
245
+
246
+ #### 4.2.1 ssh_ipc_gateway — IPC 网关
247
+
248
+ **职责**:监听本地 Unix Socket(Windows 用 127.0.0.1 TCP),接受 Ruby 端连接,将 JSON-RPC 请求分发到对应模块,异步事件推送给 Ruby。
249
+
250
+ ```erlang
251
+ %% ssh_ipc_gateway.erl 核心接口
252
+ -module(ssh_ipc_gateway).
253
+ -behaviour(gen_server).
254
+
255
+ %% API
256
+ -export([start_link/1, push_event/2]).
257
+ %% gen_server callbacks
258
+ -export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
259
+
260
+ %% 状态
261
+ -record(state, {
262
+ listen_socket :: gen_tcp:socket() | undefined, % 监听 socket
263
+ clients = #{} :: #{pid() => #client{}}, % 已连接的 Ruby 客户端
264
+ rpc_routes :: #{binary() => {module(), atom()}} % 方法路由表
265
+ }).
266
+ ```
267
+
268
+ **监听策略**:
269
+
270
+ | 平台 | 传输方式 | 地址 |
271
+ |------|----------|------|
272
+ | macOS/Linux | Unix Socket | `/tmp/ssh_core_<user>.sock` |
273
+ | Windows | TCP | `127.0.0.1:随机高端口`(写入临时文件供 Ruby 读取) |
274
+
275
+ **认证机制**:Ruby 启动时从临时文件读取端口号和随机令牌,首次 JSON-RPC 请求带 `auth_token` 字段,网关校验后绑定会话。
276
+
277
+ **请求分发**:
278
+
279
+ ```erlang
280
+ %% 分发逻辑
281
+ handle_info({tcp, Socket, Data}, State) ->
282
+ case ssh_ipc_proto:decode(Data) of
283
+ {ok, #{<<"id">> := Id, <<"method">> := Method, <<"params">> := Params}} ->
284
+ {Module, Fun} = maps:get(Method, State#state.rpc_routes),
285
+ spawn(fun() ->
286
+ Result = try
287
+ {ok, Module:Fun(Params)}
288
+ catch
289
+ Class:Reason -> {error, #{class => Class, reason => Reason}}
290
+ end,
291
+ Response = ssh_ipc_proto:encode_response(Id, Result),
292
+ gen_tcp:send(Socket, Response)
293
+ end),
294
+ {noreply, State};
295
+ {ok, #{<<"method">> := Method, <<"params">> := Params}} ->
296
+ %% push notification, 不需要回复
297
+ handle_push(Method, Params),
298
+ {noreply, State}
299
+ end.
300
+ ```
301
+
302
+ #### 4.2.2 ssh_conn_worker — 连接工作进程
303
+
304
+ **职责**:管理一条完整的 SSH 连接,包括握手、认证、通道管理和连接生命周期。
305
+
306
+ **状态机**:
307
+
308
+ ```
309
+ ┌──────────┐
310
+ │ idle │
311
+ └────┬─────┘
312
+ │ connect/2
313
+ ▼
314
+ ┌──────────┐ 失败 ┌──────────┐
315
+ │connecting│───────────▶│ failed │
316
+ └────┬─────┘ └──────────┘
317
+ │ TCP connected
318
+ ▼
319
+ ┌──────────┐ 失败 ┌──────────┐
320
+ │handshaking│──────────▶│ failed │
321
+ └────┬─────┘ └──────────┘
322
+ │ SSH banner + KEX
323
+ ▼
324
+ ┌──────────┐ 失败 ┌──────────┐
325
+ │authenticating│───────▶│ failed │
326
+ └────┬─────┘ └──────────┘
327
+ │ auth success
328
+ ▼
329
+ ┌──────────┐
330
+ │ ready │◀─── 重连成功
331
+ └────┬─────┘
332
+ │ disconnect / 网络断开
333
+ ▼
334
+ ┌──────────┐ 重试 ┌──────────┐
335
+ │reconnect │───────────▶│ ready │
336
+ └────┬─────┘ 超时 └──────────┘
337
+ │ max retries
338
+ ▼
339
+ ┌──────────┐
340
+ │ closed │
341
+ └──────────┘
342
+ ```
343
+
344
+ ```erlang
345
+ %% ssh_conn_worker.erl
346
+ -module(ssh_conn_worker).
347
+ -behaviour(gen_server).
348
+
349
+ -record(conn, {
350
+ id :: binary(), % 连接唯一 ID
351
+ spec :: map(), % 连接参数
352
+ ssh_ref :: reference() | undefined, % ssh:connect 返回的句柄
353
+ channels = #{} :: #{binary() => pid()}, % 通道 Pid 映射
354
+ state = idle :: idle|connecting|handshaking|authenticating|ready|reconnect|closed|failed,
355
+ reconnect_count = 0 :: non_neg_integer(),
356
+ options :: [{atom(), term()}] % ssh:connect 选项
357
+ }).
358
+
359
+ %% 核心操作
360
+ -export([connect/1, disconnect/1, open_channel/2, open_sftp/1]).
361
+ ```
362
+
363
+ **连接建立流程**:
364
+
365
+ ```erlang
366
+ %% @doc 建立完整 SSH 连接
367
+ connect(Spec) ->
368
+ {ok, Pid} = ssh_conn_pool_sup:start_child(Spec),
369
+ ssh_conn_worker:do_connect(Pid).
370
+
371
+ %% @doc 内部连接实现
372
+ do_connect(Pid) ->
373
+ gen_server:call(Pid, do_connect, infinity).
374
+
375
+ handle_call(do_connect, _From, #conn{spec = Spec} = State) ->
376
+ Options = build_ssh_options(Spec),
377
+ Host = maps:get(<<"host">>, Spec),
378
+ Port = maps:get(<<"port">>, Spec, 22),
379
+ case ssh:connect(binary_to_list(Host), Port, Options, infinity) of
380
+ {ok, SshRef} ->
381
+ NewState = State#conn{ssh_ref = SshRef, state = ready},
382
+ %% 通知 IPC 网关推送连接就绪事件
383
+ ssh_ipc_gateway:push_event(<<"conn.ready">>, #{id => State#conn.id}),
384
+ {reply, {ok, State#conn.id}, NewState};
385
+ {error, Reason} ->
386
+ {reply, {error, Reason}, State#conn{state = failed}}
387
+ end.
388
+ ```
389
+
390
+ **跳板链处理** — FR-CONN-003:
391
+
392
+ ```erlang
393
+ %% @doc 构建多级跳板连接
394
+ %% 算法:从最后一级跳板开始反向递归,每级用 ssh:connect + ssh_connection:session_channel
395
+ build_jump_chain(TargetSpec, Jumps) ->
396
+ %% Jumps = [Jump1, Jump2, ...](从近到远)
397
+ %% 1. 连第一级跳板(直接 TCP)
398
+ {ok, Ref1} = direct_connect(hd(Jumps)),
399
+ %% 2. 逐级在上一级 SSH 通道内建立到下一级的 TCP 转发
400
+ Refs = lists:foldl(fun(Jump, Acc) ->
401
+ [ParentRef | _] = Acc,
402
+ {ok, NextRef} = tunneled_connect(ParentRef, Jump),
403
+ [NextRef | Acc]
404
+ end, [Ref1], tl(Jumps)),
405
+ %% 3. 在最后一级跳板上建立到最终目标的 SSH 连接
406
+ {ok, FinalRef} = tunneled_connect(hd(Refs), TargetSpec),
407
+ {ok, FinalRef}.
408
+
409
+ %% @doc 通过已有 SSH 通道建立到目标的 TCP 连接
410
+ tunneled_connect(SshRef, Spec) ->
411
+ Host = binary_to_list(maps:get(<<"host">>, Spec)),
412
+ Port = maps:get(<<"port">>, Spec, 22),
413
+ %% 使用 ssh_connection:direct_tcpip 建立端口转发
414
+ {ok, ChanId} = ssh_connection:direct_tcpip(SshRef, Host, Port, "127.0.0.1", 0, infinity),
415
+ %% 在这个通道上发起新的 SSH 握手
416
+ {ok, NewRef} = ssh:connect_via(ChanId, build_ssh_options(Spec)),
417
+ {ok, NewRef}.
418
+ ```
419
+
420
+ #### 4.2.3 ssh_channel_fsm — 通道状态机
421
+
422
+ **职责**:管理单个 SSH 通道(shell/exec/sftp/subsystem)的生命周期与数据流。
423
+
424
+ ```erlang
425
+ -module(ssh_channel_fsm).
426
+ -behaviour(gen_fsm).
427
+
428
+ %% 状态
429
+ %% open → 通道已打开,正在协商
430
+ %% ready → 通道就绪,可收发数据
431
+ %% flowing → 数据流动中
432
+ %% closed → 通道关闭
433
+
434
+ %% 事件
435
+ %% {data, Data} → 从 SSH 层收到数据
436
+ %% {send, Data} → Ruby 端要发送数据
437
+ %% close → 关闭通道
438
+ %% {eof, Reason} → 收到 EOF
439
+ ```
440
+
441
+ **终端通道数据流**:
442
+
443
+ ```erlang
444
+ %% ready 状态下收到 SSH 数据
445
+ ready({data, Data}, State) ->
446
+ %% 推送到 Ruby 端
447
+ ssh_ipc_gateway:push_event(<<"channel.data">>, #{
448
+ id => State#channel.id,
449
+ data => base64:encode(Data)
450
+ }),
451
+ {next_state, flowing, State};
452
+
453
+ %% flowing 状态下继续推送
454
+ flowing({data, Data}, State) ->
455
+ ssh_ipc_gateway:push_event(<<"channel.data">>, #{
456
+ id => State#channel.id,
457
+ data => base64:encode(Data)
458
+ }),
459
+ {next_state, flowing, State};
460
+
461
+ %% Ruby 端发送数据
462
+ flowing({send, Data}, State) ->
463
+ ssh_connection:send(State#channel.ssh_ref, State#channel.chan_id, Data),
464
+ {next_state, flowing, State}.
465
+ ```
466
+
467
+ #### 4.2.4 ssh_auth_engine — 认证引擎
468
+
469
+ **职责**:管理 SSH 认证流程,支持多种认证方式和回退链。对应 FR-CONN-002。
470
+
471
+ ```erlang
472
+ -module(ssh_auth_engine).
473
+
474
+ %% 认证方式类型
475
+ -type auth_method() :: password | publickey | keyboard_interactive.
476
+ -type auth_result() :: {ok, connected} | {error, term()} | {next_method, auth_method()}.
477
+
478
+ %% @doc 按认证链依次尝试
479
+ %% AuthChain = [publickey, password, keyboard_interactive]
480
+ -spec authenticate(ssh:connection_ref(), [auth_method()], map()) -> auth_result().
481
+ authenticate(Ref, [], _Creds) ->
482
+ {error, all_methods_failed};
483
+ authenticate(Ref, [Method | Rest], Creds) ->
484
+ case do_auth(Ref, Method, Creds) of
485
+ {ok, connected} ->
486
+ {ok, connected};
487
+ {next_method, _} ->
488
+ authenticate(Ref, Rest, Creds);
489
+ {error, _Reason} ->
490
+ authenticate(Ref, Rest, Creds)
491
+ end.
492
+
493
+ %% 公钥认证
494
+ do_auth(Ref, publickey, #{key_path := Path, passphrase := Pass}) ->
495
+ case ssh:load_host_key(Path, Pass) of
496
+ {ok, Key} ->
497
+ ssh:auth_user(Ref, publickey, Key);
498
+ Error ->
499
+ Error
500
+ end;
501
+
502
+ %% 密码认证
503
+ do_auth(Ref, password, #{password := Pass}) ->
504
+ ssh:auth_user(Ref, password, Pass);
505
+
506
+ %% 键盘交互(用于 2FA / OTP 场景)
507
+ do_auth(Ref, keyboard_interactive, #{handler := Handler}) ->
508
+ ssh:auth_user(Ref, keyboard_interactive, #{
509
+ prompt_fun => fun(Prompts) -> Handler(Prompts) end
510
+ }).
511
+ ```
512
+
513
+ #### 4.2.5 ssh_keepalive_mgr — 保活与重连
514
+
515
+ **对应需求**:FR-CONN-004(保活)和 FR-CONN-005(自动重连)。
516
+
517
+ ```erlang
518
+ -module(ssh_keepalive_mgr).
519
+ -behaviour(gen_server).
520
+
521
+ %% 周期巡检所有活跃连接
522
+ handle_info(tick, State) ->
523
+ lists:foreach(fun(Conn) ->
524
+ case ssh_conn_worker:get_state(Conn) of
525
+ ready ->
526
+ case send_keepalive(Conn) of
527
+ ok -> ok;
528
+ {error, _} ->
529
+ %% 连续失败计数
530
+ increment_fail_count(Conn),
531
+ case get_fail_count(Conn) >= 3 of
532
+ true ->
533
+ ssh_conn_worker:trigger_reconnect(Conn);
534
+ false ->
535
+ ok
536
+ end
537
+ end;
538
+ _ -> ok
539
+ end
540
+ end, ssh_conn_pool_sup:all_workers()),
541
+ erlang:send_after(State#state.interval, self(), tick),
542
+ {noreply, State}.
543
+ ```
544
+
545
+ **重连策略(指数退避)**:
546
+
547
+ ```
548
+ 重连间隔 = base_interval * 2 ^ min(attempt, 6), 上限 60s
549
+ attempt 0: 5s attempt 1: 10s attempt 2: 20s
550
+ attempt 3: 40s attempt 4: 60s attempt 5: 60s ...
551
+ 最大尝试 = 配置值(1-10),默认 5
552
+ ```
553
+
554
+ #### 4.2.6 ssh_port_fwd — 端口转发
555
+
556
+ **对应需求**:FR-NET-001。
557
+
558
+ ```erlang
559
+ -module(ssh_port_fwd).
560
+
561
+ %% 三种转发类型
562
+ -type fwd_type() :: local | remote | dynamic.
563
+
564
+ %% 本地转发:本地端口 → SSH隧道 → 远程目标
565
+ setup_local(SshRef, LocalPort, RemoteHost, RemotePort) ->
566
+ ssh:tcpip_tunnel(SshRef, {0,0,0,0}, LocalPort, RemoteHost, RemotePort, []).
567
+
568
+ %% 远程转发:远程端口 → SSH隧道 → 本地目标
569
+ setup_remote(SshRef, RemotePort, LocalHost, LocalPort) ->
570
+ ssh:tcpip_tunnel(SshRef, {0,0,0,0}, RemotePort, LocalHost, LocalPort, [{remote, true}]).
571
+
572
+ %% 动态转发(SOCKS5):本地端口 → SSH隧道 → 动态目标
573
+ setup_dynamic(SshRef, LocalPort) ->
574
+ %% 需要自行实现 SOCKS5 协商 + direct_tcpip
575
+ ssh_dyn_fwd:start(SshRef, LocalPort).
576
+ ```
577
+
578
+ #### 4.2.7 ssh_known_hosts — 已知主机管理
579
+
580
+ **对应需求**:FR-SEC-002。
581
+
582
+ ```erlang
583
+ -module(ssh_known_hosts).
584
+
585
+ %% 存储格式:~/.ssh/known_hosts 兼容,额外维护指纹索引
586
+ %% 文件路径可配置,V1.0 使用本地文件,V2.0+ 可选 Ruby 侧加密存储
587
+
588
+ %% @doc 首次连接时验证主机密钥
589
+ verify_host(Host, Port, Key) ->
590
+ case lookup(Host, Port) of
591
+ {ok, StoredKey} when StoredKey == Key ->
592
+ ok;
593
+ {ok, StoredKey} when StoredKey /= Key ->
594
+ {error, host_key_changed};
595
+ not_found ->
596
+ {prompt, Key} %% 通知 Ruby 弹窗确认
597
+ end.
598
+ ```
599
+
600
+ #### 4.2.8 ssh_sftp_engine — SFTP 引擎
601
+
602
+ **对应需求**:FR-FILE-001、FR-FILE-002、FR-FILE-005。
603
+
604
+ ```erlang
605
+ -module(ssh_sftp_engine).
606
+
607
+ %% 基于 OTP ssh_sftp 通道
608
+ -include_lib("kernel/include/file.hrl").
609
+
610
+ %% 可视化浏览器操作
611
+ open_dir(SshRef, Path) ->
612
+ {ok, Handle} = ssh_sftp: opendir(SshRef, Path),
613
+ {ok, list_dir_entries(SshRef, Handle)}.
614
+
615
+ read_file(SshRef, RemotePath, LocalPath, ProgressFun) ->
616
+ {ok, Handle} = ssh_sftp:open(SshRef, RemotePath, [read, binary]),
617
+ {ok, FileInfo} = ssh_sftp:read_file_info(SshRef, RemotePath),
618
+ transfer(SshRef, Handle, LocalPath, FileInfo#file_info.size, ProgressFun, read).
619
+
620
+ %% 批量传输
621
+ batch_transfer(SshRef, Files, Direction, Concurrency) ->
622
+ %% Concurrency 个并发 channel
623
+ Pool = [open_sftp_channel(SshRef) || _ <- lists:seq(1, Concurrency)],
624
+ Tasks = [{Chan, File, Direction} || File <- Files, Chan <- Pool],
625
+ poolboy:transaction(Tasks, fun(Task) -> do_transfer(Task) end).
626
+ ```
627
+
628
+ ### 4.3 SSH 算法配置
629
+
630
+ 对照 FR-CONN-001 量化指标:
631
+
632
+ | 类别 | Erlang 算法配置 |
633
+ |------|----------------|
634
+ | **密钥交换** | `curve25519-sha256@libssh.org`, `curve25519-sha256`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group16-sha512`, `diffie-hellman-group14-sha256`(≥ 7 种) |
635
+ | **公钥** | `ssh-ed25519`, `rsa-sha2-512`, `rsa-sha2-256`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`(≥ 6 种) |
636
+ | **对称加密** | `aes256-gcm@openssh.com`, `aes128-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes256-ctr`, `aes192-ctr`, `aes128-ctr`(≥ 6 种) |
637
+ | **MAC** | `hmac-sha2-256`, `hmac-sha2-512`, `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-512-etm@openssh.com`(≥ 4 种) |
638
+
639
+ ```erlang
640
+ %% build_ssh_options/1 生成的算法选项
641
+ build_ssh_options(Spec) ->
642
+ [
643
+ {preferred_algorithms, #{
644
+ kex => ['curve25519-sha256@libssh.org',
645
+ 'curve25519-sha256',
646
+ 'ecdh-sha2-nistp256',
647
+ 'ecdh-sha2-nistp384',
648
+ 'ecdh-sha2-nistp521',
649
+ 'diffie-hellman-group16-sha512',
650
+ 'diffie-hellman-group14-sha256'],
651
+ public_key => ['ssh-ed25519',
652
+ 'rsa-sha2-512',
653
+ 'rsa-sha2-256',
654
+ 'ecdsa-sha2-nistp256',
655
+ 'ecdsa-sha2-nistp384',
656
+ 'ecdsa-sha2-nistp521'],
657
+ cipher => ['aes256-gcm@openssh.com',
658
+ 'aes128-gcm@openssh.com',
659
+ 'chacha20-poly1305@openssh.com',
660
+ 'aes256-ctr','aes192-ctr','aes128-ctr'],
661
+ mac => ['hmac-sha2-256',
662
+ 'hmac-sha2-512',
663
+ 'hmac-sha2-256-etm@openssh.com',
664
+ 'hmac-sha2-512-etm@openssh.com']
665
+ }},
666
+ {user, binary_to_list(maps:get(<<"user">>, Spec))},
667
+ {silently_accept_hosts, false},
668
+ {user_dir, binary_to_list(maps:get(<<"key_dir">>, Spec, <<"/tmp">>))}
669
+ ] ++ build_auth_options(Spec).
670
+ ```
671
+
672
+ ### 4.4 连接池管理
673
+
674
+ #### 4.4.1 ssh_conn_pool_sup — 连接池 Supervisor
675
+
676
+ ```erlang
677
+ -module(ssh_conn_pool_sup).
678
+ -behaviour(supervisor).
679
+
680
+ %% simple_one_for_one 策略,按需创建连接进程
681
+ init([]) ->
682
+ SupFlags = #{strategy => simple_one_for_one,
683
+ intensity => 100,
684
+ period => 60},
685
+ ChildSpec = #{id => ssh_conn_worker,
686
+ start => {ssh_conn_worker, start_link, []},
687
+ restart => temporary,
688
+ shutdown => 10000,
689
+ type => worker,
690
+ modules => [ssh_conn_worker]},
691
+ {ok, {SupFlags, [ChildSpec]}}.
692
+
693
+ %% 获取所有活跃连接
694
+ all_workers() ->
695
+ [Pid || {_, Pid, _, _} <- supervisor:which_children(?MODULE)].
696
+ ```
697
+
698
+ #### 4.4.2 连接 ID 分配
699
+
700
+ ```erlang
701
+ %% 短 ID + UUID 后缀,方便调试同时全局唯一
702
+ gen_conn_id() ->
703
+ iolist_to_binary(["conn_", integer_to_list(unix_ts()), "_",
704
+ binary:part(uuid:get_v4(), {0, 8})]).
705
+ ```
706
+
707
+ ### 4.5 监督树设计
708
+
709
+ ```erlang
710
+ %% ssh_core_sup.erl 顶层监督树
711
+ init([]) ->
712
+ SupFlags = #{strategy => one_for_one,
713
+ intensity => 10,
714
+ period => 60},
715
+ Children = [
716
+ #{
717
+ id => ssh_known_hosts,
718
+ start => {ssh_known_hosts, start_link, []},
719
+ restart => permanent,
720
+ type => worker
721
+ },
722
+ #{
723
+ id => ssh_ipc_gateway,
724
+ start => {ssh_ipc_gateway, start_link, [ListenOpts]},
725
+ restart => permanent,
726
+ type => worker
727
+ },
728
+ #{
729
+ id => ssh_conn_pool_sup,
730
+ start => {ssh_conn_pool_sup, start_link, []},
731
+ restart => permanent,
732
+ type => supervisor
733
+ },
734
+ #{
735
+ id => ssh_sftp_engine_sup,
736
+ start => {ssh_sftp_engine_sup, start_link, []},
737
+ restart => permanent,
738
+ type => supervisor
739
+ },
740
+ #{
741
+ id => ssh_keepalive_mgr,
742
+ start => {ssh_keepalive_mgr, start_link, [#{interval => 30000}]},
743
+ restart => permanent,
744
+ type => worker
745
+ }
746
+ ],
747
+ {ok, {SupFlags, Children}}.
748
+ ```
749
+
750
+ 监督树结构:
751
+
752
+ ```
753
+ ssh_core_sup (one_for_one, intensity 10/60s)
754
+ ├── ssh_known_hosts (permanent worker)
755
+ ├── ssh_ipc_gateway (permanent worker)
756
+ ├── ssh_conn_pool_sup (permanent supervisor)
757
+ │ └── [动态] ssh_conn_worker × N (temporary, simple_one_for_one)
758
+ │ └── [动态] ssh_channel_fsm (temporary, 子进程)
759
+ ├── ssh_sftp_engine_sup (permanent supervisor)
760
+ │ └── [动态] ssh_sftp_session × M (temporary)
761
+ └── ssh_keepalive_mgr (permanent worker)
762
+ ```
763
+
764
+ 设计要点:
765
+ - 连接工作进程设为 `temporary`,进程退出不自动重启,由 Ruby 端决定是否重连——避免反复冲击目标服务器。
766
+ - IPC 网关设为 `permanent`,崩溃自动重启不影响已有连接。
767
+ - 保活管理器设为 `permanent`,崩溃后重新初始化,重新扫描所有连接。
768
+
769
+ ### 4.6 Erlang Release 打包
770
+
771
+ 使用 Rebar3 release 打包独立可运行的 Erlang 运行时,跨平台预编译。
772
+
773
+ ```
774
+ ssh_core/
775
+ ├── rebar.config
776
+ ├── apps/
777
+ │ └── ssh_core/
778
+ │ └── src/ (上述所有 .erl)
779
+ └── config/
780
+ ├── vm.args % VM 启动参数
781
+ └── sys.config % 应用配置
782
+ ```
783
+
784
+ ```erlang
785
+ %% rebar.config 核心配置
786
+ {erl_opts, [debug_info]}.
787
+ {deps, [
788
+ {jsx, "3.1.0"} % JSON 解析
789
+ ]}.
790
+
791
+ {relx, [
792
+ {release, {ssh_core, "1.0.0"}, [ssh_core, jsx, runtime_tools]},
793
+ {mode, dev},
794
+ {include_erts, true}, % 打包 ERTS,目标机无需装 Erlang
795
+ {include_src, false},
796
+ {overlay, [
797
+ {mkdir, "{{output_dir}}/logs"},
798
+ {copy, "config/vm.args", "{{output_dir}}/releases/1.0.0/vm.args"},
799
+ {copy, "config/sys.config", "{{output_dir}}/releases/1.0.0/sys.config"}
800
+ ]}
801
+ ]}.
802
+ ```
803
+
804
+ 跨平台编译矩阵:
805
+
806
+ | 平台 | 编译环境 | 产物 |
807
+ |------|----------|------|
808
+ | Windows x64 | Windows + MSVC + Rebar3 | `ssh_core_1.0.0_windows_x64.zip` |
809
+ | macOS x64/ARM64 | macOS + Clang + Rebar3 | `ssh_core_1.0.0_macos_universal.tar.gz` |
810
+ | Linux x64 | Ubuntu 20.04 + GCC + Rebar3 | `ssh_core_1.0.0_linux_x64.tar.gz` |
811
+
812
+ ---
813
+
814
+ ## 5 Ruby 调度界面设计
815
+
816
+ ### 5.1 模块结构
817
+
818
+ Ruby 侧作为 network-infra-utility gem 的一部分,放在 `service/ssh/` 目录。
819
+
820
+ ```
821
+ service/ssh/
822
+ ├── design/
823
+ │ ├── SSH连接客户端功能需求文档.md
824
+ │ └── 软件设计文档.md ← 本文档
825
+ ├── lib/
826
+ │ └── network_infra_utility/
827
+ │ └── ssh/
828
+ │ ├── version.rb # 版本号
829
+ │ ├── client.rb # 主入口,生命周期管理
830
+ │ ├── ipc/
831
+ │ │ ├── client.rb # JSON-RPC 客户端
832
+ │ │ ├── protocol.rb # JSON-RPC 协议封装
833
+ │ │ └── event_listener.rb # 异步事件监听
834
+ │ ├── session/
835
+ │ │ ├── manager.rb # 会话管理器
836
+ │ │ ├── session.rb # 单个会话对象
837
+ │ │ ├── tree.rb # 会话树形分组
838
+ │ │ └── history.rb # 最近连接与历史
839
+ │ ├── terminal/
840
+ │ │ ├── emulator.rb # ANSI/xterm 转义解析
841
+ │ │ ├── renderer.rb # 屏幕渲染
842
+ │ │ ├── buffer.rb # 回滚缓冲区
843
+ │ │ ├── theme.rb # 配色方案
844
+ │ │ └── input.rb # 输入处理
845
+ │ ├── file/
846
+ │ │ ├── sftp_browser.rb # SFTP 浏览器
847
+ │ │ ├── transfer.rb # 文件传输管理
848
+ │ │ └── watcher.rb # 远程文件编辑监听
849
+ │ ├── security/
850
+ │ │ ├── vault.rb # 凭据加密存储
851
+ │ │ ├── key_manager.rb # 密钥管理
852
+ │ │ └── host_key.rb # 主机密钥校验
853
+ │ ├── automation/
854
+ │ │ ├── macro_engine.rb # 登录宏引擎
855
+ │ │ ├── batch_exec.rb # 批量执行
856
+ │ │ └── snippet_manager.rb# 代码片段管理
857
+ │ ├── network/
858
+ │ │ ├── port_forward.rb # 端口转发规则
859
+ │ │ ├── proxy.rb # 代理配置
860
+ │ │ └── diagnostics.rb # 连接诊断
861
+ │ └── config/
862
+ │ ├── settings.rb # 全局设置
863
+ │ ├── import_export.rb # 会话导入导出
864
+ │ └── schema.rb # 配置数据模型
865
+ ├── bin/
866
+ │ └── ssh-client # CLI 入口脚本
867
+ ├── spec/
868
+ │ ├── ipc/ # IPC 通信测试
869
+ │ ├── session/ # 会话管理测试
870
+ │ ├── terminal/ # 终端仿真测试
871
+ │ └── ...
872
+ └── ssh_client.gemspec
873
+ ```
874
+
875
+ ### 5.2 核心模块设计
876
+
877
+ #### 5.2.1 SSH::Client — 主入口
878
+
879
+ ```ruby
880
+ # frozen_string_literal: true
881
+
882
+ require_relative "ipc/client"
883
+ require_relative "session/manager"
884
+ require_relative "terminal/emulator"
885
+ require_relative "security/vault"
886
+ require_relative "config/settings"
887
+
888
+ module NetworkInfraUtility
889
+ module SSH
890
+ # SSH 客户端主入口,管理 Erlang 引擎生命周期与模块协调。
891
+ #
892
+ # 用法:
893
+ # client = SSH::Client.new
894
+ # client.start_engine # 启动 Erlang 后端
895
+ # session = client.connect(host: "10.0.0.1", user: "admin")
896
+ # session.terminal.puts("show version")
897
+ class Client
898
+ attr_reader :ipc, :sessions, :vault, :settings
899
+
900
+ ENGINE_BINARY = File.expand_path("../ext/ssh_core/bin/ssh_core", __dir__)
901
+ ENGINE_STARTUP_TIMEOUT = 10 # 秒
902
+
903
+ def initialize
904
+ @settings = Config::Settings.new
905
+ @vault = Security::Vault.new(@settings.master_password)
906
+ @sessions = Session::Manager.new(self)
907
+ @ipc = IPC::Client.new
908
+ @engine_pid = nil
909
+ end
910
+
911
+ # 启动 Erlang SSH 核心引擎,建立 IPC 连接。
912
+ def start_engine
913
+ @engine_pid = spawn_engine
914
+ wait_for_ipcReady(ENGINE_STARTUP_TIMEOUT)
915
+ @ipc.connect(read_ipc_endpoint)
916
+ end
917
+
918
+ # 发起 SSH 连接。
919
+ # @param spec [Hash] 连接参数 {host, port, user, auth, jumps, ...}
920
+ # @return [Session] 会话对象
921
+ def connect(spec)
922
+ spec = @vault.resolve_credentials(spec)
923
+ conn_id = @ipc.call("conn.connect", spec)
924
+ @sessions.create(conn_id, spec)
925
+ end
926
+
927
+ def stop
928
+ @sessions.disconnect_all
929
+ @ipc.close
930
+ Process.kill("TERM", @engine_pid) if @engine_pid
931
+ end
932
+
933
+ private
934
+
935
+ def spawn_engine
936
+ log_dir = @settings.log_dir
937
+ FileUtils.mkdir_p(log_dir)
938
+ Process.spawn(
939
+ ENGINE_BINARY, "foreground",
940
+ "--config", @settings.config_path,
941
+ out: File.join(log_dir, "engine.log"),
942
+ err: File.join(log_dir, "engine.err")
943
+ )
944
+ end
945
+
946
+ # Erlang 引擎启动时将 IPC 端口写入临时文件,Ruby 读取。
947
+ def read_ipc_endpoint
948
+ endpoint_file = File.join(Dir.tmpdir, "ssh_core_#{Process.uid}.endpoint")
949
+ deadline = Time.now + ENGINE_STARTUP_TIMEOUT
950
+ loop do
951
+ return File.read(endpoint_file).strip if File.exist?(endpoint_file)
952
+ raise "Erlang engine startup timeout" if Time.now > deadline
953
+ sleep 0.1
954
+ end
955
+ end
956
+
957
+ def wait_for_ipc_ready(timeout)
958
+ deadline = Time.now + timeout
959
+ loop do
960
+ return if @ipc.connected?
961
+ raise "IPC connection timeout" if Time.now > deadline
962
+ sleep 0.1
963
+ end
964
+ end
965
+ end
966
+ end
967
+ end
968
+ ```
969
+
970
+ #### 5.2.2 IPC::Client — JSON-RPC 客户端
971
+
972
+ ```ruby
973
+ # frozen_string_literal: true
974
+
975
+ require "json"
976
+ require "socket"
977
+
978
+ module NetworkInfraUtility
979
+ module SSH
980
+ module IPC
981
+ # JSON-RPC 2.0 客户端,与 Erlang SSH 核心引擎通信。
982
+ #
983
+ # 同步调用用 #call,异步事件推送通过 #on_event 注册回调。
984
+ class Client
985
+ attr_reader :connected
986
+
987
+ def initialize
988
+ @socket = nil
989
+ @request_id = 0
990
+ @pending = {} # id => Queue
991
+ @callbacks = {} # event_name => [Proc]
992
+ @listener_thread = nil
993
+ @connected = false
994
+ end
995
+
996
+ # 连接到 Erlang IPC 网关。
997
+ # @param endpoint [String] "unix:/path/to/sock" 或 "tcp://127.0.0.1:port"
998
+ def connect(endpoint)
999
+ @socket = open_socket(endpoint)
1000
+ @connected = true
1001
+ start_event_listener
1002
+ end
1003
+
1004
+ # 同步 RPC 调用。
1005
+ # @param method [String] 方法名,如 "conn.connect"
1006
+ # @param params [Hash] 参数
1007
+ # @param timeout [Integer] 超时秒数
1008
+ # @return 调用结果
1009
+ # @raise [RPCError] 远程返回错误
1010
+ def call(method, params = {}, timeout = 30)
1011
+ id = next_request_id
1012
+ request = {jsonrpc: "2.0", id: id, method: method, params: params}
1013
+ queue = Queue.new
1014
+ @pending[id] = queue
1015
+ @socket.puts(JSON.generate(request))
1016
+ result = queue.pop(timeout: timeout)
1017
+ raise RPCError, result[:error] if result[:error]
1018
+ result[:result]
1019
+ ensure
1020
+ @pending.delete(id)
1021
+ end
1022
+
1023
+ # 注册异步事件回调。
1024
+ # @param event [String] 事件名,如 "channel.data"
1025
+ # @param block [Proc] 回调
1026
+ def on_event(event, &block)
1027
+ (@callbacks[event] ||= []) << block
1028
+ end
1029
+
1030
+ def close
1031
+ @connected = false
1032
+ @listener_thread&.kill
1033
+ @socket&.close
1034
+ end
1035
+
1036
+ private
1037
+
1038
+ def open_socket(endpoint)
1039
+ if endpoint.start_with?("unix:")
1040
+ UNIXSocket.new(endpoint.sub("unix:", ""))
1041
+ elsif endpoint.start_with?("tcp://")
1042
+ uri = URI(endpoint)
1043
+ TCPSocket.new(uri.host, uri.port)
1044
+ else
1045
+ raise ArgumentError, "Unknown endpoint format: #{endpoint}"
1046
+ end
1047
+ end
1048
+
1049
+ def start_event_listener
1050
+ @listener_thread = Thread.new do
1051
+ loop do
1052
+ line = @socket.gets
1053
+ break unless line
1054
+ msg = JSON.parse(line, symbolize_names: true)
1055
+ handle_message(msg)
1056
+ end
1057
+ end
1058
+ end
1059
+
1060
+ def handle_message(msg)
1061
+ if msg[:id] && @pending[msg[:id]]
1062
+ # 同步响应
1063
+ @pending[msg[:id]] << msg
1064
+ elsif msg[:method]
1065
+ # 异步推送
1066
+ callbacks = @callbacks[msg[:method]]
1067
+ callbacks&.each { |cb| cb.call(msg[:params]) }
1068
+ end
1069
+ end
1070
+
1071
+ def next_request_id
1072
+ @request_id += 1
1073
+ end
1074
+ end
1075
+
1076
+ class RPCError < StandardError; end
1077
+ end
1078
+ end
1079
+ end
1080
+ ```
1081
+
1082
+ #### 5.2.3 Session::Manager — 会话管理器
1083
+
1084
+ ```ruby
1085
+ # frozen_string_literal: true
1086
+
1087
+ module NetworkInfraUtility
1088
+ module SSH
1089
+ module Session
1090
+ # 会话管理器,维护全部活跃会话的生命周期。
1091
+ # 对应需求 FR-SESS-001 ~ FR-SESS-006。
1092
+ class Manager
1093
+ include Enumerable
1094
+
1095
+ def initialize(client)
1096
+ @client = client
1097
+ @sessions = {} # conn_id => Session
1098
+ @tree = Tree.new # 分组树
1099
+ @history = History.new
1100
+ end
1101
+
1102
+ # 发起连接并创建会话。
1103
+ def connect(spec)
1104
+ conn_id = @client.ipc.call("conn.connect", build_connect_spec(spec))
1105
+ create(conn_id, spec)
1106
+ end
1107
+
1108
+ # 从已有连接 ID 创建会话对象。
1109
+ def create(conn_id, spec)
1110
+ session = Session.new(@client, conn_id, spec)
1111
+ @sessions[conn_id] = session
1112
+ @history.record(session)
1113
+ session
1114
+ end
1115
+
1116
+ def get(conn_id)
1117
+ @sessions[conn_id]
1118
+ end
1119
+
1120
+ def disconnect(conn_id)
1121
+ @sessions[conn_id]&.disconnect
1122
+ @sessions.delete(conn_id)
1123
+ end
1124
+
1125
+ def disconnect_all
1126
+ @sessions.each_value(&:disconnect)
1127
+ @sessions.clear
1128
+ end
1129
+
1130
+ def each(&block)
1131
+ @sessions.each_value(&block)
1132
+ end
1133
+
1134
+ # 搜索会话。对应 FR-SESS-004。
1135
+ # @param query [String] 搜索关键词
1136
+ # @param fields [Array<Symbol>] 搜索字段,默认 :name, :host, :ip, :tags
1137
+ def search(query, fields = %i[name host ip tags])
1138
+ query_lower = query.downcase
1139
+ select do |s|
1140
+ fields.any? do |f|
1141
+ val = s.send(f)
1142
+ val.is_a?(Array) ? val.any? { |v| v.downcase.include?(query_lower) }
1143
+ : val.to_s.downcase.include?(query_lower)
1144
+ end
1145
+ end
1146
+ end
1147
+
1148
+ # 广播输入到多个会话。对应 FR-TERM-008。
1149
+ # @param conn_ids [Array<String>] 目标连接 ID
1150
+ # @param text [String] 要发送的文本
1151
+ def broadcast(conn_ids, text)
1152
+ conn_ids.each { |id| @sessions[id]&.terminal&.send(text) }
1153
+ end
1154
+
1155
+ attr_reader :tree, :history
1156
+ end
1157
+ end
1158
+ end
1159
+ end
1160
+ ```
1161
+
1162
+ #### 5.2.4 Session — 会话对象
1163
+
1164
+ ```ruby
1165
+ # frozen_string_literal: true
1166
+
1167
+ module NetworkInfraUtility
1168
+ module SSH
1169
+ module Session
1170
+ # 单个 SSH 会话,封装终端、文件、端口转发等子能力。
1171
+ class Session
1172
+ attr_reader :conn_id, :spec, :name, :host, :port, :user
1173
+ attr_reader :terminal, :file_manager, :port_forward
1174
+ attr_accessor :tags, :group
1175
+
1176
+ # 会话状态
1177
+ STATUSES = %i[connecting authenticating connected disconnected error].freeze
1178
+
1179
+ def initialize(client, conn_id, spec)
1180
+ @client = client
1181
+ @conn_id = conn_id
1182
+ @spec = spec
1183
+ @name = spec[:name] || "#{spec[:user]}@#{spec[:host]}"
1184
+ @host = spec[:host]
1185
+ @port = spec[:port] || 22
1186
+ @user = spec[:user]
1187
+ @tags = spec[:tags] || []
1188
+ @group = spec[:group]
1189
+ @status = :connected
1190
+ @terminal = nil
1191
+ @file_manager = nil
1192
+ @port_forward = nil
1193
+ end
1194
+
1195
+ # 打开终端通道。
1196
+ def open_terminal(term_type = "xterm-256color", cols = 80, rows = 24)
1197
+ channel_id = @client.ipc.call("channel.open", {
1198
+ conn_id: @conn_id,
1199
+ type: "shell",
1200
+ term: term_type,
1201
+ cols: cols,
1202
+ rows: rows
1203
+ })
1204
+ @terminal = Terminal::Emulator.new(@client, @conn_id, channel_id, cols, rows)
1205
+ @terminal
1206
+ end
1207
+
1208
+ # 打开 SFTP 文件管理器。对应 FR-FILE-001。
1209
+ def open_file_manager
1210
+ sftp_id = @client.ipc.call("sftp.open", {conn_id: @conn_id})
1211
+ @file_manager = File::SftpBrowser.new(@client, sftp_id)
1212
+ @file_manager
1213
+ end
1214
+
1215
+ # 添加端口转发规则。对应 FR-NET-001。
1216
+ def add_port_forward(type, local_port, remote_host, remote_port)
1217
+ rule_id = @client.ipc.call("portfwd.add", {
1218
+ conn_id: @conn_id,
1219
+ type: type.to_s,
1220
+ local_port: local_port,
1221
+ remote_host: remote_host,
1222
+ remote_port: remote_port
1223
+ })
1224
+ (@port_forward ||= []) << {id: rule_id, type: type, local_port: local_port,
1225
+ remote_host: remote_host, remote_port: remote_port}
1226
+ rule_id
1227
+ end
1228
+
1229
+ def disconnect
1230
+ @client.ipc.call("conn.disconnect", {id: @conn_id})
1231
+ @status = :disconnected
1232
+ end
1233
+
1234
+ def connected?
1235
+ @status == :connected
1236
+ end
1237
+ end
1238
+ end
1239
+ end
1240
+ end
1241
+ ```
1242
+
1243
+ #### 5.2.5 Terminal::Emulator — 终端仿真器
1244
+
1245
+ ```ruby
1246
+ # frozen_string_literal: true
1247
+
1248
+ module NetworkInfraUtility
1249
+ module SSH
1250
+ module Terminal
1251
+ # ANSI/xterm 转义序列解析与终端状态维护。
1252
+ # 对应需求 FR-TERM-001、FR-TERM-002、FR-TERM-007、FR-TERM-010。
1253
+ class Emulator
1254
+ attr_reader :cols, :rows, :cursor_x, :cursor_y, :buffer, :theme
1255
+
1256
+ # 终端类型映射
1257
+ TERM_TYPES = {
1258
+ "xterm-256color" => {colors: 256, truecolor: true},
1259
+ "vt100" => {colors: 16, truecolor: false},
1260
+ "vt220" => {colors: 16, truecolor: false}
1261
+ }.freeze
1262
+
1263
+ def initialize(client, conn_id, channel_id, cols, rows)
1264
+ @client = client
1265
+ @conn_id = conn_id
1266
+ @channel_id = channel_id
1267
+ @cols = cols
1268
+ @rows = rows
1269
+ @cursor_x = 0
1270
+ @cursor_y = 0
1271
+ @buffer = Buffer.new(rows, max_lines: 10000) # FR-TERM-007
1272
+ @theme = Theme.default
1273
+ @parser = AnsiParser.new(@buffer, @theme)
1274
+
1275
+ # 注册 SSH 数据推送回调
1276
+ @client.ipc.on_event("channel.data") do |params|
1277
+ next unless params[:id] == @channel_id
1278
+ data = Base64.decode64(params[:data])
1279
+ @parser.feed(data)
1280
+ end
1281
+ end
1282
+
1283
+ # 发送数据到 SSH 通道。
1284
+ def send(data)
1285
+ @client.ipc.call("channel.send", {
1286
+ id: @channel_id,
1287
+ data: Base64.encode64(data)
1288
+ })
1289
+ end
1290
+
1291
+ # 发送一行(带换行)。
1292
+ def puts(text)
1293
+ send("#{text}\r")
1294
+ end
1295
+
1296
+ # 窗口大小变更。
1297
+ def resize(cols, rows)
1298
+ @cols = cols
1299
+ @rows = rows
1300
+ @buffer.resize(cols, rows)
1301
+ @client.ipc.call("channel.window_change", {
1302
+ id: @channel_id, cols: cols, rows: rows
1303
+ })
1304
+ end
1305
+
1306
+ # 设置配色主题。对应 FR-TERM-002。
1307
+ def theme=(theme_name)
1308
+ @theme = Theme.load(theme_name)
1309
+ end
1310
+
1311
+ # 搜索缓冲区内容。对应 FR-TERM-005、FR-TERM-007。
1312
+ # @param pattern [String] 正则或关键词
1313
+ # @return [Array<Match>] 匹配结果
1314
+ def search(pattern)
1315
+ @buffer.search(pattern)
1316
+ end
1317
+
1318
+ # 导出缓冲区内容为文本。
1319
+ def export(path = nil)
1320
+ text = @buffer.to_text
1321
+ File.write(path, text) if path
1322
+ text
1323
+ end
1324
+ end
1325
+ end
1326
+ end
1327
+ end
1328
+ ```
1329
+
1330
+ #### 5.2.6 Terminal::Buffer — 回滚缓冲区
1331
+
1332
+ ```ruby
1333
+ # frozen_string_literal: true
1334
+
1335
+ module NetworkInfraUtility
1336
+ module SSH
1337
+ module Terminal
1338
+ # 终端回滚缓冲区,支持大容量历史和快速搜索。
1339
+ # 对应需求 FR-TERM-007:默认 ≥ 10000 行,可配置 1000–100000。
1340
+ class Buffer
1341
+ attr_reader :rows, :cols, :scrollback_lines
1342
+
1343
+ def initialize(rows, max_lines: 10000)
1344
+ @rows = rows
1345
+ @cols = cols || 80
1346
+ @scrollback_lines = max_lines
1347
+ @lines = [] # 回滚行(已滚出屏幕)
1348
+ @screen = [] # 当前屏幕行
1349
+ @rows.times { @screen << Line.new(@cols) }
1350
+ end
1351
+
1352
+ # 写入字符到当前光标位置(由 AnsiParser 调用)。
1353
+ def write_char(char, x, y, style = {})
1354
+ line = @screen[y] || (@screen[y] = Line.new(@cols))
1355
+ line.write_char(char, x, style)
1356
+ end
1357
+
1358
+ # 换行——将屏幕首行移入回滚区。
1359
+ def newline
1360
+ @lines << @screen.shift
1361
+ @screen << Line.new(@cols)
1362
+ trim_scrollback
1363
+ end
1364
+
1365
+ # 回滚行数 + 屏幕行数。
1366
+ def total_lines
1367
+ @lines.size + @screen.size
1368
+ end
1369
+
1370
+ # 搜索全部内容(回滚 + 屏幕)。
1371
+ # @param pattern [String] 正则表达式
1372
+ # @return [Array<Match>]
1373
+ def search(pattern)
1374
+ regex = Regexp.new(pattern)
1375
+ matches = []
1376
+ all_lines.each_with_index do |line, idx|
1377
+ line.to_s.scan(regex) do
1378
+ m = Regexp.last_match
1379
+ matches << Match.new(line: idx, start: m.begin(0), end: m.end(0), text: m[0])
1380
+ end
1381
+ end
1382
+ matches
1383
+ end
1384
+
1385
+ # 导出纯文本。
1386
+ def to_text
1387
+ all_lines.map(&:to_s).join("\n")
1388
+ end
1389
+
1390
+ def resize(cols, rows)
1391
+ @cols = cols
1392
+ if rows > @rows
1393
+ (rows - @rows).times { @screen << Line.new(@cols) }
1394
+ elsif rows < @rows
1395
+ (@rows - rows).times { @lines << @screen.shift }
1396
+ trim_scrollback
1397
+ end
1398
+ @rows = rows
1399
+ end
1400
+
1401
+ private
1402
+
1403
+ def trim_scrollback
1404
+ while @lines.size > @scrollback_lines
1405
+ @lines.shift
1406
+ end
1407
+ end
1408
+
1409
+ def all_lines
1410
+ @lines + @screen
1411
+ end
1412
+ end
1413
+
1414
+ # 一行字符,每个字符附带样式信息。
1415
+ class Line
1416
+ attr_reader :cells
1417
+
1418
+ def initialize(cols)
1419
+ @cells = Array.new(cols) { Cell.new }
1420
+ end
1421
+
1422
+ def write_char(char, x, style = {})
1423
+ @cells[x] = Cell.new(char, style) if x < @cells.size
1424
+ end
1425
+
1426
+ def to_s
1427
+ @cells.map(&:char).join.rstrip
1428
+ end
1429
+ end
1430
+
1431
+ # 单个字符单元。
1432
+ Cell = Struct.new(:char, :style) do
1433
+ def initialize(char = " ", style = {})
1434
+ super
1435
+ end
1436
+ end
1437
+
1438
+ # 搜索匹配结果。
1439
+ Match = Struct.new(:line, :start, :end, :text)
1440
+ end
1441
+ end
1442
+ end
1443
+ ```
1444
+
1445
+ #### 5.2.7 Security::Vault — 凭据加密存储
1446
+
1447
+ ```ruby
1448
+ # frozen_string_literal: true
1449
+
1450
+ require "openssl"
1451
+ require "yaml"
1452
+ require "securerandom"
1453
+
1454
+ module NetworkInfraUtility
1455
+ module SSH
1456
+ module Security
1457
+ # 凭据加密存储,AES-256-GCM。
1458
+ # 对应需求 FR-SEC-001。
1459
+ class Vault
1460
+ PBKDF2_ITERATIONS = 100_000
1461
+ SALT_LENGTH = 32
1462
+ KEY_LENGTH = 32 # AES-256
1463
+ NONCE_LENGTH = 12 # GCM nonce
1464
+
1465
+ def initialize(master_password)
1466
+ @master_password = master_password
1467
+ @cache = {} # 解密后的凭据缓存
1468
+ end
1469
+
1470
+ # 加密凭据并存储到配置文件。
1471
+ # @param key [String] 凭据标识,如 "session_001_password"
1472
+ # @param value [String] 明文凭据
1473
+ def store(key, value)
1474
+ salt = SecureRandom.random_bytes(SALT_LENGTH)
1475
+ derived_key = derive_key(@master_password, salt)
1476
+ nonce = SecureRandom.random_bytes(NONCE_LENGTH)
1477
+ cipher = OpenSSL::Cipher::AES.new(256, :GCM)
1478
+ cipher.encrypt
1479
+ cipher.key = derived_key
1480
+ cipher.iv = nonce
1481
+ encrypted = cipher.update(value) + cipher.final
1482
+ tag = cipher.auth_tag
1483
+
1484
+ entry = {
1485
+ salt: Base64.encode64(salt),
1486
+ nonce: Base64.encode64(nonce),
1487
+ tag: Base64.encode64(tag),
1488
+ data: Base64.encode64(encrypted)
1489
+ }
1490
+ write_to_store(key, entry)
1491
+ end
1492
+
1493
+ # 读取并解密凭据。
1494
+ # @param key [String] 凭据标识
1495
+ # @return [String] 明文凭据
1496
+ def load(key)
1497
+ return @cache[key] if @cache[key]
1498
+
1499
+ entry = read_from_store(key)
1500
+ return nil unless entry
1501
+
1502
+ salt = Base64.decode64(entry[:salt])
1503
+ derived_key = derive_key(@master_password, salt)
1504
+ nonce = Base64.decode64(entry[:nonce])
1505
+ tag = Base64.decode64(entry[:tag])
1506
+ encrypted = Base64.decode64(entry[:data])
1507
+
1508
+ cipher = OpenSSL::Cipher::AES.new(256, :GCM)
1509
+ cipher.decrypt
1510
+ cipher.key = derived_key
1511
+ cipher.iv = nonce
1512
+ cipher.auth_tag = tag
1513
+
1514
+ plaintext = cipher.update(encrypted) + cipher.final
1515
+ @cache[key] = plaintext
1516
+ plaintext
1517
+ end
1518
+
1519
+ # 在连接规格中解析凭据引用。
1520
+ # spec 中 password 可能是 "~vault:session_001_password"
1521
+ def resolve_credentials(spec)
1522
+ spec.each do |k, v|
1523
+ if v.is_a?(String) && v.start_with?("~vault:")
1524
+ spec[k] = load(v.sub("~vault:", ""))
1525
+ end
1526
+ end
1527
+ spec
1528
+ end
1529
+
1530
+ private
1531
+
1532
+ def derive_key(password, salt)
1533
+ OpenSSL::PKCS5.pbkdf2_hmac(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256")
1534
+ end
1535
+
1536
+ def write_to_store(key, entry)
1537
+ store = load_store
1538
+ store[key] = entry
1539
+ File.write(store_path, YAML.dump(store))
1540
+ set_file_permissions
1541
+ end
1542
+
1543
+ def read_from_store(key)
1544
+ load_store[key]
1545
+ end
1546
+
1547
+ def load_store
1548
+ return {} unless File.exist?(store_path)
1549
+ YAML.load_file(store_path)
1550
+ end
1551
+
1552
+ def store_path
1553
+ File.join(Dir.home, ".network-infra-utility", "vault.yml")
1554
+ end
1555
+
1556
+ def set_file_permissions
1557
+ path = store_path
1558
+ if RUBY_PLATFORM.include?("mswin") || RUBY_PLATFORM.include?("mingw")
1559
+ # Windows: ACL 限制当前用户
1560
+ system("icacls \"#{path}\" /inheritance:r /grant:r \"#{ENV['USERNAME']}:R\"")
1561
+ else
1562
+ File.chmod(0o600, path)
1563
+ end
1564
+ end
1565
+ end
1566
+ end
1567
+ end
1568
+ end
1569
+ ```
1570
+
1571
+ #### 5.2.8 Automation::MacroEngine — 登录宏引擎
1572
+
1573
+ ```ruby
1574
+ # frozen_string_literal: true
1575
+
1576
+ module NetworkInfraUtility
1577
+ module SSH
1578
+ module Automation
1579
+ # 登录宏引擎,连接后自动执行预设命令序列。
1580
+ # 对应需求 FR-AUTO-001。
1581
+ class MacroEngine
1582
+ # 单步定义
1583
+ Step = Struct.new(:action, :wait_pattern, :delay, :on_fail, keyword_init: true)
1584
+
1585
+ def initialize(session)
1586
+ @session = session
1587
+ @steps = []
1588
+ @running = false
1589
+ @abort = false
1590
+ end
1591
+
1592
+ # 添加一步。
1593
+ # @param action [String] 要发送的命令
1594
+ # @param wait_pattern [String, Regexp] 等待匹配的文本/正则
1595
+ # @param delay [Float] 发送前延迟(秒)
1596
+ # @param on_fail [:continue, :abort, :ask] 失败时行为
1597
+ def add_step(action:, wait_pattern: nil, delay: 0, on_fail: :continue)
1598
+ raise "Too many steps (max 50)" if @steps.size >= 50
1599
+ @steps << Step.new(action: action, wait_pattern: wait_pattern,
1600
+ delay: delay, on_fail: on_fail)
1601
+ end
1602
+
1603
+ # 执行宏。
1604
+ def run(on_progress: nil)
1605
+ @running = true
1606
+ @steps.each_with_index do |step, i|
1607
+ break if @abort
1608
+ on_progress&.call(step, i + 1, @steps.size)
1609
+ sleep step.delay if step.delay > 0
1610
+ @session.terminal.send(step.action)
1611
+ if step.wait_pattern
1612
+ result = wait_for(step.wait_pattern, timeout: 30)
1613
+ handle_step_result(step, result)
1614
+ end
1615
+ end
1616
+ ensure
1617
+ @running = false
1618
+ end
1619
+
1620
+ def abort
1621
+ @abort = true
1622
+ end
1623
+
1624
+ private
1625
+
1626
+ def wait_for(pattern, timeout:)
1627
+ regex = pattern.is_a?(Regexp) ? pattern : Regexp.new(Regexp.escape(pattern))
1628
+ deadline = Time.now + timeout
1629
+ accumulated = ""
1630
+ loop do
1631
+ return :matched if accumulated.match?(regex)
1632
+ return :timeout if Time.now > deadline
1633
+ line = @session.terminal.buffer.last_line&.to_s || ""
1634
+ accumulated << line + "\n"
1635
+ sleep 0.1
1636
+ end
1637
+ end
1638
+
1639
+ def handle_step_result(step, result)
1640
+ case [result, step.on_fail]
1641
+ when [:matched, any], [:timeout, :continue]
1642
+ # 继续
1643
+ when [:timeout, :abort]
1644
+ @abort = true
1645
+ when [:timeout, :ask]
1646
+ # 询问用户——CLI 版输出提示,TUI 版弹窗
1647
+ yield(:ask, step) if block_given?
1648
+ end
1649
+ end
1650
+ end
1651
+ end
1652
+ end
1653
+ end
1654
+ ```
1655
+
1656
+ #### 5.2.9 Automation::BatchExec — 批量执行
1657
+
1658
+ ```ruby
1659
+ # frozen_string_literal: true
1660
+
1661
+ module NetworkInfraUtility
1662
+ module SSH
1663
+ module Automation
1664
+ # 批量命令执行,多会话同时下发。
1665
+ # 对应需求 FR-AUTO-003。
1666
+ class BatchExec
1667
+ # 执行模式
1668
+ PARALLEL = :parallel
1669
+ SERIAL = :serial
1670
+
1671
+ def initialize(client)
1672
+ @client = client
1673
+ end
1674
+
1675
+ # 批量执行命令。
1676
+ # @param sessions [Array<Session>] 目标会话列表
1677
+ # @param command [String] 要执行的命令
1678
+ # @param mode [:parallel, :serial] 执行模式
1679
+ # @param timeout [Integer] 单会话超时秒数
1680
+ # @return [Hash<conn_id => Result>]
1681
+ def execute(sessions, command, mode: PARALLEL, timeout: 30)
1682
+ if mode == PARALLEL
1683
+ execute_parallel(sessions, command, timeout)
1684
+ else
1685
+ execute_serial(sessions, command, timeout)
1686
+ end
1687
+ end
1688
+
1689
+ private
1690
+
1691
+ def execute_parallel(sessions, command, timeout)
1692
+ threads = sessions.map do |s|
1693
+ Thread.new { [s.conn_id, run_on_session(s, command, timeout)] }
1694
+ end
1695
+ results = threads.map(&:value)
1696
+ results.to_h
1697
+ end
1698
+
1699
+ def execute_serial(sessions, command, timeout)
1700
+ sessions.map do |s|
1701
+ [s.conn_id, run_on_session(s, command, timeout)]
1702
+ end.to_h
1703
+ end
1704
+
1705
+ def run_on_session(session, command, timeout)
1706
+ chan_id = @client.ipc.call("channel.open", {
1707
+ conn_id: session.conn_id, type: "exec", command: command
1708
+ })
1709
+ collector = ResultCollector.new
1710
+ @client.ipc.on_event("channel.data") do |params|
1711
+ next unless params[:id] == chan_id
1712
+ collector << Base64.decode64(params[:data])
1713
+ end
1714
+ collector.wait(timeout)
1715
+ end
1716
+ end
1717
+
1718
+ # 结果收集器,收集单个会话的命令输出。
1719
+ ResultCollector = Struct.new(:output, :done) do
1720
+ def initialize
1721
+ self.output = +""
1722
+ self.done = false
1723
+ @mutex = Mutex.new
1724
+ @cv = ConditionVariable.new
1725
+ end
1726
+
1727
+ def <<(data)
1728
+ @mutex.synchronize { output << data }
1729
+ end
1730
+
1731
+ def wait(timeout)
1732
+ deadline = Time.now + timeout
1733
+ @mutex.synchronize do
1734
+ @cv.wait(@mutex, [deadline - Time.now, 0].max) until done || Time.now >= deadline
1735
+ end
1736
+ output.to_s
1737
+ end
1738
+
1739
+ def finish!
1740
+ @mutex.synchronize do
1741
+ self.done = true
1742
+ @cv.broadcast
1743
+ end
1744
+ end
1745
+ end
1746
+ end
1747
+ end
1748
+ end
1749
+ ```
1750
+
1751
+ ### 5.3 CLI 入口设计
1752
+
1753
+ V1.0 先交付交互式 CLI,后续升级为 TUI 多面板。
1754
+
1755
+ ```ruby
1756
+ #!/usr/bin/env ruby
1757
+ # frozen_string_literal: true
1758
+
1759
+ require "thor"
1760
+ require_relative "../lib/network_infra_utility/ssh/client"
1761
+
1762
+ module NetworkInfraUtility
1763
+ module SSH
1764
+ class CLI < Thor
1765
+ desc "connect HOST", "连接到 SSH 服务器"
1766
+ option :user, aliases: "-u", required: true
1767
+ option :port, aliases: "-p", type: :numeric, default: 22
1768
+ option :key, aliases: "-i", desc: "私钥文件路径"
1769
+ option :jump, aliases: "-J", desc: "跳板机 (user@host:port)"
1770
+ option :name, desc: "会话名称"
1771
+
1772
+ def connect(host)
1773
+ client = Client.new
1774
+ client.start_engine
1775
+
1776
+ spec = {
1777
+ host: host, port: options[:port], user: options[:user],
1778
+ name: options[:name]
1779
+ }
1780
+ spec[:key_path] = options[:key] if options[:key]
1781
+ spec[:jumps] = parse_jumps(options[:jump]) if options[:jump]
1782
+
1783
+ session = client.connect(spec)
1784
+ terminal = session.open_terminal
1785
+
1786
+ # 进入交互模式
1787
+ start_interactive(session, terminal)
1788
+ rescue => e
1789
+ STDERR.puts "Error: #{e.message}"
1790
+ exit 1
1791
+ ensure
1792
+ client&.stop
1793
+ end
1794
+
1795
+ desc "batch FILE", "批量在多个会话上执行命令"
1796
+ option :command, aliases: "-c", required: true
1797
+ option :serial, type: :boolean, default: false
1798
+
1799
+ def batch(file)
1800
+ # 从文件读取会话列表,批量执行
1801
+ sessions = load_sessions_from_file(file)
1802
+ client = Client.new
1803
+ client.start_engine
1804
+ sessions.map! { |spec| client.connect(spec).open_terminal }
1805
+
1806
+ batch = Automation::BatchExec.new(client)
1807
+ results = batch.execute(sessions, options[:command],
1808
+ mode: options[:serial] ? :serial : :parallel)
1809
+ print_results(results)
1810
+ end
1811
+
1812
+ private
1813
+
1814
+ def start_interactive(session, terminal)
1815
+ # V1.0: 简单的读-发-收循环
1816
+ # V2.0: 升级为 curses 多面板 TUI
1817
+ loop do
1818
+ line = STDIN.gets
1819
+ break if line.nil? || line.chomp == "exit"
1820
+ terminal.puts(line.chomp)
1821
+ end
1822
+ end
1823
+
1824
+ def parse_jumps(jump_str)
1825
+ jump_str.split(",").map do |j|
1826
+ if j =~ /\A([^@]+)@([^:]+):(\d+)\z/
1827
+ {user: $1, host: $2, port: $3.to_i}
1828
+ else
1829
+ raise "Invalid jump format: #{j} (expected user@host:port)"
1830
+ end
1831
+ end
1832
+ end
1833
+ end
1834
+ end
1835
+ end
1836
+
1837
+ NetworkInfraUtility::SSH::CLI.start(ARGV)
1838
+ ```
1839
+
1840
+ ---
1841
+
1842
+ ## 6 通信协议设计
1843
+
1844
+ ### 6.1 JSON-RPC 2.0 消息格式
1845
+
1846
+ Erlang 与 Ruby 之间所有通信均使用 JSON-RPC 2.0,每条消息以换行符 `\n` 分隔。
1847
+
1848
+ #### 6.1.1 请求
1849
+
1850
+ ```json
1851
+ {"jsonrpc":"2.0","id":1,"method":"conn.connect","params":{"host":"10.0.0.1","port":22,"user":"admin","auth":{"type":"password","value":"~vault:sec_001"}}}
1852
+ ```
1853
+
1854
+ #### 6.1.2 响应
1855
+
1856
+ ```json
1857
+ {"jsonrpc":"2.0","id":1,"result":{"conn_id":"conn_1691000000_a1b2c3d4","fingerprint":"SHA256:AbC..."}}
1858
+ ```
1859
+
1860
+ #### 6.1.3 错误响应
1861
+
1862
+ ```json
1863
+ {"jsonrpc":"2.0","id":1,"error":{"code":-32001,"message":"Authentication failed","data":{"reason":"publickey rejected"}}}
1864
+ ```
1865
+
1866
+ #### 6.1.4 异步推送(无 id)
1867
+
1868
+ ```json
1869
+ {"jsonrpc":"2.0","method":"channel.data","params":{"id":"ch_001","data":"dGVzdA=="}}
1870
+ ```
1871
+
1872
+ ### 6.2 错误码定义
1873
+
1874
+ | 错误码 | 含义 | 说明 |
1875
+ |--------|------|------|
1876
+ | -32700 | Parse error | JSON 解析失败 |
1877
+ | -32600 | Invalid Request | 请求格式不合法 |
1878
+ | -32601 | Method not found | 方法未注册 |
1879
+ | -32602 | Invalid params | 参数校验失败 |
1880
+ | -32603 | Internal error | Erlang 内部异常 |
1881
+ | -32001 | Connection error | SSH 连接失败 |
1882
+ | -32002 | Authentication error | 认证失败 |
1883
+ | -32003 | Channel error | 通道操作失败 |
1884
+ | -32004 | Host key error | 主机密钥校验失败 |
1885
+ | -32005 | Timeout | 操作超时 |
1886
+ | -32006 | SFTP error | SFTP 操作失败 |
1887
+ | -32007 | Port forward error | 端口转发失败 |
1888
+
1889
+ ### 6.3 RPC 方法清单
1890
+
1891
+ #### 6.3.1 连接管理
1892
+
1893
+ | 方法 | 方向 | 参数 | 返回 | 对应需求 |
1894
+ |------|------|------|------|----------|
1895
+ | `conn.connect` | Ruby→Erlang | `{host, port, user, auth, jumps?, proxy?, keepalive?}` | `{conn_id, fingerprint}` | FR-CONN-001/002/003/004 |
1896
+ | `conn.disconnect` | Ruby→Erlang | `{id}` | `{ok}` | — |
1897
+ | `conn.list` | Ruby→Erlang | `{}` | `[{conn_id, host, state}]` | — |
1898
+ | `conn.reconnect` | Ruby→Erlang | `{id}` | `{conn_id}` | FR-CONN-005 |
1899
+ | `conn.ready` | Erlang→Ruby | `{conn_id}` | — | 连接就绪通知 |
1900
+ | `conn.closed` | Erlang→Ruby | `{conn_id, reason}` | — | 连接断开通知 |
1901
+
1902
+ #### 6.3.2 通道管理
1903
+
1904
+ | 方法 | 方向 | 参数 | 返回 | 对应需求 |
1905
+ |------|------|------|------|----------|
1906
+ | `channel.open` | Ruby→Erlang | `{conn_id, type, term?, cols?, rows?, command?}` | `{channel_id}` | — |
1907
+ | `channel.send` | Ruby→Erlang | `{id, data}` | `{ok}` | — |
1908
+ | `channel.close` | Ruby→Erlang | `{id}` | `{ok}` | — |
1909
+ | `channel.window_change` | Ruby→Erlang | `{id, cols, rows}` | `{ok}` | FR-TERM-001 |
1910
+ | `channel.data` | Erlang→Ruby | `{id, data}` | — | 终端数据推送 |
1911
+ | `channel.eof` | Erlang→Ruby | `{id, reason}` | — | 通道关闭通知 |
1912
+
1913
+ #### 6.3.3 认证
1914
+
1915
+ | 方法 | 方向 | 参数 | 返回 | 对应需求 |
1916
+ |------|------|------|------|----------|
1917
+ | `auth.prompt` | Erlang→Ruby | `{conn_id, prompts[]}` | `{responses[]}` | FR-CONN-002 键盘交互 |
1918
+ | `hostkey.verify` | Erlang→Ruby | `{conn_id, host, port, fingerprint, key_type}` | `{accept/once/reject}` | FR-SEC-002 |
1919
+
1920
+ #### 6.3.4 端口转发
1921
+
1922
+ | 方法 | 方向 | 参数 | 返回 | 对应需求 |
1923
+ |------|------|------|------|----------|
1924
+ | `portfwd.add` | Ruby→Erlang | `{conn_id, type, local_port, remote_host, remote_port}` | `{rule_id}` | FR-NET-001 |
1925
+ | `portfwd.remove` | Ruby→Erlang | `{conn_id, rule_id}` | `{ok}` | FR-NET-001 |
1926
+ | `portfwd.list` | Ruby→Erlang | `{conn_id}` | `[{rule_id, type, ...}]` | FR-NET-001 |
1927
+
1928
+ #### 6.3.5 SFTP
1929
+
1930
+ | 方法 | 方向 | 参数 | 返回 | 对应需求 |
1931
+ |------|------|------|------|----------|
1932
+ | `sftp.open` | Ruby→Erlang | `{conn_id}` | `{sftp_id}` | FR-FILE-001 |
1933
+ | `sftp.list_dir` | Ruby→Erlang | `{sftp_id, path}` | `{entries[{name, type, size, mtime, perms}]}` | FR-FILE-001 |
1934
+ | `sftp.download` | Ruby→Erlang | `{sftp_id, remote, local}` | `{ok, transferred}` | FR-FILE-001 |
1935
+ | `sftp.upload` | Ruby→Erlang | `{sftp_id, local, remote}` | `{ok, transferred}` | FR-FILE-001 |
1936
+ | `sftp.mkdir` | Ruby→Erlang | `{sftp_id, path}` | `{ok}` | FR-FILE-001 |
1937
+ | `sftp.remove` | Ruby→Erlang | `{sftp_id, path}` | `{ok}` | FR-FILE-001 |
1938
+ | `sftp.stat` | Ruby→Erlang | `{sftp_id, path}` | `{size, mtime, perms, type}` | FR-FILE-001 |
1939
+ | `sftp.progress` | Erlang→Ruby | `{sftp_id, transferred, total, speed}` | — | 传输进度推送 |
1940
+
1941
+ #### 6.3.6 管理与诊断
1942
+
1943
+ | 方法 | 方向 | 参数 | 返回 | 对应需求 |
1944
+ |------|------|------|------|----------|
1945
+ | `engine.ping` | Ruby→Erlang | `{}` | `{ok, timestamp}` | 心跳检测 |
1946
+ | `engine.stats` | Ruby→Erlang | `{}` | `{connections, channels, uptime, memory}` | NFR-PERF-005/006 |
1947
+ | `engine.shutdown` | Ruby→Erlang | `{}` | `{ok}` | 优雅关闭 |
1948
+
1949
+ ### 6.4 数据编码约定
1950
+
1951
+ | 数据类型 | 编码方式 | 说明 |
1952
+ |----------|----------|------|
1953
+ | 终端原始字节 | Base64 | 二进制安全,跨语言无坑 |
1954
+ | 文件路径 | UTF-8 字符串 | 直接传递 |
1955
+ | 时间戳 | Unix 毫秒整数 | 避免时区问题 |
1956
+ | 密钥指纹 | 字符串 `SHA256:base64` | 与 OpenSSH 一致 |
1957
+ | 认证凭据 | `~vault:<key>` 引用 | Ruby 解析后替换为明文,仅在 IPC 中短暂传输 |
1958
+
1959
+ ### 6.5 IPC 性能保障
1960
+
1961
+ | 指标 | 目标 | 措施 |
1962
+ |------|------|------|
1963
+ | 单次 RPC 往返延迟 | ≤ 1ms(Unix Socket) | 本地 IPC,无网络开销 |
1964
+ | 终端数据推送延迟 | ≤ 5ms | base64 编解码 + JSON 序列化约 0.2ms |
1965
+ | 消息吞吐 | ≥ 10000 msg/s | Erlang 端无需序列化整行,直接 `jsx:encode` |
1966
+ | 大数据流传输 | 不阻塞小消息 | SFTP 数据使用独立 channel,不与终端复用 |
1967
+
1968
+ ---
1969
+
1970
+ ## 7 数据模型设计
1971
+
1972
+ ### 7.1 会话配置模型
1973
+
1974
+ ```yaml
1975
+ # ~/.network-infra-utility/sessions.yml
1976
+ version: 1
1977
+ groups:
1978
+ - id: grp_datacenter_a
1979
+ name: "A机房"
1980
+ parent: null
1981
+ groups:
1982
+ - id: grp_datacenter_a_core
1983
+ name: "核心交换"
1984
+ parent: grp_datacenter_a
1985
+ sessions:
1986
+ - id: sess_001
1987
+ name: "核心交换机-01"
1988
+ group: grp_datacenter_a_core
1989
+ host: "10.0.0.1"
1990
+ port: 22
1991
+ user: "admin"
1992
+ auth:
1993
+ type: publickey
1994
+ key_path: "~/.ssh/id_ed25519"
1995
+ passphrase_ref: "~vault:sess_001_key_pass"
1996
+ tags: ["core", "cisco"]
1997
+ terminal:
1998
+ type: "xterm-256color"
1999
+ theme: "solarized-dark"
2000
+ font: "JetBrains Mono"
2001
+ font_size: 14
2002
+ scrollback: 10000
2003
+ encoding: "utf-8"
2004
+ keepalive:
2005
+ interval: 30
2006
+ count_max: 3
2007
+ proxy:
2008
+ type: "socks5"
2009
+ host: "127.0.0.1"
2010
+ port: 1080
2011
+ jumps:
2012
+ - host: "172.16.0.1"
2013
+ port: 22
2014
+ user: "jump"
2015
+ auth:
2016
+ type: password
2017
+ password_ref: "~vault:jump_001_pass"
2018
+ port_forwards:
2019
+ - type: local
2020
+ local_port: 8080
2021
+ remote_host: "127.0.0.1"
2022
+ remote_port: 80
2023
+ enabled: true
2024
+ macro:
2025
+ enabled: true
2026
+ steps:
2027
+ - action: "terminal length 0"
2028
+ wait_pattern: "#"
2029
+ delay: 1
2030
+ - action: "show version"
2031
+ wait_pattern: "#"
2032
+ delay: 0
2033
+ log:
2034
+ enabled: true
2035
+ path: "~/ssh-logs/sess_001"
2036
+ max_size: 100MB
2037
+ rotate: 10
2038
+ ```
2039
+
2040
+ ### 7.2 Erlang 内部状态模型
2041
+
2042
+ ```erlang
2043
+ %% 连接规格映射
2044
+ -type connect_spec() :: #{
2045
+ host => binary(),
2046
+ port => pos_integer(),
2047
+ user => binary(),
2048
+ auth => #{
2049
+ type => password | publickey | keyboard_interactive,
2050
+ password => binary() | undefined,
2051
+ key_path => binary() | undefined,
2052
+ passphrase => binary() | undefined,
2053
+ chain => [atom()] %% 认证回退链
2054
+ },
2055
+ jumps => [connect_spec()] | undefined,
2056
+ proxy => #{
2057
+ type => http | socks5,
2058
+ host => binary(),
2059
+ port => pos_integer(),
2060
+ auth => map() | undefined
2061
+ } | undefined,
2062
+ keepalive => #{
2063
+ interval => pos_integer(),
2064
+ count_max => pos_integer()
2065
+ },
2066
+ algorithms => map() | undefined %% 可选自定义算法列表
2067
+ }.
2068
+ ```
2069
+
2070
+ ### 7.3 配置文件布局
2071
+
2072
+ ```
2073
+ ~/.network-infra-utility/
2074
+ ├── settings.yml # 全局设置(主题、快捷键、默认终端参数)
2075
+ ├── sessions.yml # 会话与分组配置
2076
+ ├── vault.yml # 加密凭据存储(权限 600)
2077
+ ├── known_hosts.yml # 已知主机密钥
2078
+ ├── snippets/ # 代码片段库
2079
+ │ ├── network.yml
2080
+ │ └── linux.yml
2081
+ ├── themes/ # 自定义主题
2082
+ │ └── custom.yml
2083
+ ├── macros/ # 宏脚本
2084
+ │ └── cisco_login.yml
2085
+ └── logs/ # 会话日志
2086
+ ├── sess_001/
2087
+ │ ├── 20260802_153000.log
2088
+ │ └── 20260802_160000.log
2089
+ └── ...
2090
+ ```
2091
+
2092
+ ### 7.4 关键数据结构对比
2093
+
2094
+ | 数据 | Ruby 侧表示 | Erlang 侧表示 | 持久化位置 |
2095
+ |------|------------|---------------|-----------|
2096
+ | 会话配置 | `Session` 对象 | 不持久化(每次 connect 传入) | `sessions.yml` |
2097
+ | 凭据 | Vault 引用 `~vault:key` | 运行时明文(仅 IPC 传输) | `vault.yml`(加密) |
2098
+ | 已知主机 | `Hash<host, key>` | ETS 表 | `known_hosts.yml` |
2099
+ | 代码片段 | `Array<Snippet>` | 不管理 | `snippets/*.yml` |
2100
+ | 会话日志 | 文件句柄 | 不持久化 | `logs/` 目录 |
2101
+ | 终端缓冲 | `Buffer` 对象 | 不持久化 | 内存中 |
2102
+
2103
+ ---
2104
+
2105
+ ## 8 安全设计
2106
+
2107
+ ### 8.1 安全架构总览
2108
+
2109
+ ```
2110
+ ┌─ 用户 ────────────────────────────────────────┐
2111
+ │ 主密码(不存储,仅内存) │
2112
+ └──────┬────────────────────────────────────────┘
2113
+ │ PBKDF2/Argon2 派生
2114
+ ▼
2115
+ ┌─ Ruby Vault 层 ───────────────────────────────┐
2116
+ │ AES-256-GCM 加密存储 vault.yml │
2117
+ │ 凭据仅在 connect 时解密,经 IPC 传入 Erlang │
2118
+ └──────┬────────────────────────────────────────┘
2119
+ │ JSON-RPC (本地 IPC, 不经网络)
2120
+ ▼
2121
+ ┌─ Erlang 引擎层 ───────────────────────────────┐
2122
+ │ 认证凭据仅在 ssh:connect 调用期间存活 │
2123
+ │ 连接建立后凭据从内存清除 │
2124
+ │ SSH 加密由 OTP ssh 应用保证 │
2125
+ └──────┬────────────────────────────────────────┘
2126
+ │ SSH 加密通道
2127
+ ▼
2128
+ ┌─ 远程 SSH 服务器 ─────────────────────────────┐
2129
+ └────────────────────────────────────────────────┘
2130
+ ```
2131
+
2132
+ ### 8.2 凭据安全 — FR-SEC-001
2133
+
2134
+ | 环节 | 措施 | 验收标准 |
2135
+ |------|------|----------|
2136
+ | 主密码存储 | PBKDF2-SHA256 迭代 ≥ 100000 次,仅存哈希 | NFR-SEC-002 |
2137
+ | 凭据存储 | AES-256-GCM 加密,随机 salt + nonce + auth_tag | FR-SEC-001 ①② |
2138
+ | 配置文件 | vault.yml 权限 600(Unix)/ ACL 限制(Windows) | FR-SEC-001 ④ |
2139
+ | 内存安全 | 凭据解密后仅存在连接建立期间,连接成功后 GC 清除 | NFR-SEC-001 |
2140
+ | 日志安全 | 日志中不记录明文凭据,配置中用 `~vault:` 引用替代替 | NFR-SEC-001 |
2141
+ | IPC 传输 | Unix Socket 本地通信,不经网络;Windows TCP 仅绑定 127.0.0.1 | — |
2142
+
2143
+ ### 8.3 主机密钥验证 — FR-SEC-002
2144
+
2145
+ ```erlang
2146
+ %% 首次连接流程
2147
+ 1. Erlang 连接时获取服务器公钥指纹
2148
+ 2. 调用 hostkey.verify 推送到 Ruby
2149
+ 3. Ruby 查询 known_hosts:
2150
+ a. 已知且匹配 → 自动接受
2151
+ b. 已知但不匹配 → 弹窗告警 "主机密钥变更!可能存在中间人攻击"
2152
+ c. 未知 → 弹窗确认 "首次连接,指纹 SHA256:xxx,是否信任?"
2153
+ 4. 用户确认后 Ruby 回复 accept/reject
2154
+ 5. accept 后 Ruby 将指纹写入 known_hosts.yml
2155
+ ```
2156
+
2157
+ ### 8.4 IPC 认证
2158
+
2159
+ | 步骤 | 机制 |
2160
+ |------|------|
2161
+ | 1. Erlang 引擎启动 | 生成随机 32 字节令牌,与监听端点一起写入临时文件 |
2162
+ | 2. 文件权限 | 临时文件权限设为 600,路径包含 UID 防止其他用户读取 |
2163
+ | 3. Ruby 读取 | 读取端点和令牌 |
2164
+ | 4. 首次 RPC | Ruby 在 params 中携带 `auth_token` |
2165
+ | 5. Erlang 校验 | 匹配后绑定 TCP 连接,后续请求无需再传 |
2166
+ | 6. 连接关闭 | 令牌失效,防止重放 |
2167
+
2168
+ ### 8.5 依赖安全 — NFR-SEC-003
2169
+
2170
+ | 依赖 | 来源 | 安全策略 |
2171
+ |------|------|----------|
2172
+ | Erlang/OTP ssh | OTP 内置 | 跟随 OTP 安全更新 |
2173
+ | jsx | hex.pm | 发布前 `rebar3 audit` 检查 CVE |
2174
+ | Ruby stdlib | 内置 | 跟随 Ruby 安全更新 |
2175
+ | thor | RubyGems | `bundle audit` 检查 |
2176
+
2177
+ ---
2178
+
2179
+ ## 9 需求映射矩阵
2180
+
2181
+ ### 9.1 功能需求映射
2182
+
2183
+ 下表将每条功能需求映射到实现层和核心模块:
2184
+
2185
+ | 需求编号 | 功能名称 | Erlang 模块 | Ruby 模块 | 优先级 | 版本 |
2186
+ |----------|----------|-------------|-----------|--------|------|
2187
+ | FR-CONN-001 | SSH2 协议支持 | ssh_conn_worker + OTP ssh | IPC::Client | P0 | V1.0 |
2188
+ | FR-CONN-002 | 多认证方式 | ssh_auth_engine | Security::Vault | P0 | V1.0 |
2189
+ | FR-CONN-003 | 跳板机跳转 | ssh_jump_chain | Session::Manager | P0 | V1.0 |
2190
+ | FR-CONN-004 | 连接保活 | ssh_keepalive_mgr | Config::Settings | P0 | V1.0 |
2191
+ | FR-CONN-005 | 自动重连 | ssh_conn_worker | Session::Manager | P1 | V2.0 |
2192
+ | FR-CONN-006 | 多协议支持 | 扩展 conn_worker | Session::Manager | P1 | V2.0 |
2193
+ | FR-CONN-007 | MOSH 支持 | 新 mosh_engine | — | P2 | V3.0 |
2194
+ | FR-SESS-001 | 标签页与窗口拆分 | — | Session::Manager + TUI | P0 | V1.0 |
2195
+ | FR-SESS-002 | 会话分组与树形管理 | — | Session::Tree | P0 | V1.0 |
2196
+ | FR-SESS-003 | 会话导入导出 | — | Config::ImportExport | P1 | V2.0 |
2197
+ | FR-SESS-004 | 会话搜索 | — | Session::Manager#search | P1 | V2.0 |
2198
+ | FR-SESS-005 | 最近连接列表 | — | Session::History | P1 | V2.0 |
2199
+ | FR-SESS-006 | 会话锁屏 | — | TUI 层 | P2 | V3.0 |
2200
+ | FR-FILE-001 | SFTP 可视化浏览器 | ssh_sftp_engine | File::SftpBrowser | P0 | V1.0 |
2201
+ | FR-FILE-002 | SCP 命令传输 | ssh_sftp_engine | File::Transfer | P0 | V1.0 |
2202
+ | FR-FILE-003 | 双面板同步浏览 | ssh_channel_fsm | File::SftpBrowser | P1 | V2.0 |
2203
+ | FR-FILE-004 | ZMODEM | ssh_channel_fsm | File::Zmodem | P1 | V2.0 |
2204
+ | FR-FILE-005 | 批量文件传输 | ssh_sftp_engine | File::Transfer | P1 | V2.0 |
2205
+ | FR-FILE-006 | 远程文件编辑 | — | File::Watcher | P2 | V3.0 |
2206
+ | FR-SEC-001 | 凭据加密存储 | — | Security::Vault | P0 | V1.0 |
2207
+ | FR-SEC-002 | 主机密钥验证 | ssh_known_hosts | Security::HostKey | P0 | V1.0 |
2208
+ | FR-SEC-003 | SSH Agent 集成 | ssh_auth_engine | Security::KeyManager | P1 | V2.0 |
2209
+ | FR-SEC-004 | 密钥对生成 | — | Security::KeyManager | P1 | V2.0 |
2210
+ | FR-SEC-005 | 凭据同步加密 | — | Security::Vault | P2 | V3.0 |
2211
+ | FR-SEC-006 | 2FA/MFA 支持 | ssh_auth_engine | Security::OTP | P2 | V3.0 |
2212
+ | FR-NET-001 | 端口转发 | ssh_port_fwd | Network::PortForward | P0 | V1.0 |
2213
+ | FR-NET-002 | SOCKS5/HTTP 代理 | ssh_conn_worker | Network::Proxy | P1 | V2.0 |
2214
+ | FR-NET-003 | X11 转发 | ssh_port_fwd | — | P2 | V3.0 |
2215
+ | FR-NET-004 | 连接诊断 | — | Network::Diagnostics | P1 | V2.0 |
2216
+ | FR-NET-005 | 流量统计 | ssh_conn_worker | Network::Stats | P2 | V3.0 |
2217
+ | FR-TERM-001 | 终端仿真 | ssh_channel_fsm | Terminal::Emulator | P0 | V1.0 |
2218
+ | FR-TERM-002 | 主题与配色 | — | Terminal::Theme | P0 | V1.0 |
2219
+ | FR-TERM-003 | 字体配置 | — | Terminal::Renderer | P0 | V1.0 |
2220
+ | FR-TERM-004 | 快捷键定制 | — | TUI 层 | P1 | V2.0 |
2221
+ | FR-TERM-005 | 搜索与高亮 | — | Terminal::Buffer | P1 | V2.0 |
2222
+ | FR-TERM-006 | 日志记录 | ssh_channel_fsm | Terminal::Logger | P0 | V1.0 |
2223
+ | FR-TERM-007 | 回滚缓冲区 | — | Terminal::Buffer | P0 | V1.0 |
2224
+ | FR-TERM-008 | 输入同步/广播 | — | Session::Manager#broadcast | P1 | V2.0 |
2225
+ | FR-TERM-009 | 代码片段管理 | — | Automation::SnippetManager | P1 | V2.0 |
2226
+ | FR-TERM-010 | 智能选中 | — | Terminal::Emulator | P1 | V2.0 |
2227
+ | FR-TERM-011 | 窗口透明度 | — | GUI 层 | P2 | V3.0 |
2228
+ | FR-TERM-012 | 光标样式 | — | Terminal::Renderer | P2 | V3.0 |
2229
+ | FR-AUTO-001 | 登录宏/脚本 | — | Automation::MacroEngine | P0 | V1.0 |
2230
+ | FR-AUTO-002 | 会话录放 | — | Automation::Recorder | P2 | V3.0 |
2231
+ | FR-AUTO-003 | 命令批量执行 | ssh_channel_fsm | Automation::BatchExec | P1 | V2.0 |
2232
+ | FR-AUTO-004 | 本地 Shell 集成 | — | TUI 层 | P1 | V2.0 |
2233
+ | FR-AUTO-005 | 脚本引擎扩展 | — | Automation::ScriptEngine | P2 | V3.0 |
2234
+ | FR-COLLAB-001 | 跨平台支持 | Rebar3 跨平台编译 | Ruby 跨平台 | P1 | V2.0 |
2235
+ | FR-COLLAB-002 | 配置云同步 | — | Config::Sync | P2 | V3.0 |
2236
+ | FR-COLLAB-003 | 团队共享 | — | Config::TeamShare | P2 | V3.0 |
2237
+ | FR-COLLAB-004 | 便携版 | — | 打包脚本 | P2 | V3.0 |
2238
+
2239
+ ### 9.2 非功能需求映射
2240
+
2241
+ | 编号 | 指标 | 实现措施 |
2242
+ |------|------|----------|
2243
+ | NFR-PERF-001 | 连接 ≤ 2s | OTP ssh 握手优化,curve25519 优先 |
2244
+ | NFR-PERF-002 | 渲染 ≥ 30fps | Ruby 差分渲染,只重绘变化行 |
2245
+ | NFR-PERF-003 | 输入延迟 ≤ 50ms | Unix Socket IPC ≤ 1ms + Erlang 直发 |
2246
+ | NFR-PERF-004 | SFTP ≥ 50MB/s | Erlang 并行 read/write + 独立通道 |
2247
+ | NFR-PERF-005 | 10 会话 ≤ 500MB | Erlang 轻量进程,每连接约 2-4MB |
2248
+ | NFR-PERF-006 | ≥ 50 并发 | Erlang 进程模型天生支持数万并发 |
2249
+ | NFR-PERF-007 | 搜索 ≤ 500ms | Buffer 索引优化,二分查找 |
2250
+ | NFR-PERF-008 | 冷启动 ≤ 3s | Erlang release 预编译,Ruby 延迟加载 |
2251
+ | NFR-REL-001 | 72h 无崩溃 | OTP 监督树自动恢复,Ruby 线程异常隔离 |
2252
+ | NFR-REL-002 | 重连 ≥ 95% | 指数退避重连 + 会话恢复 |
2253
+ | NFR-REL-003 | 校验 100% | SFTP 传输后 MD5/SHA-256 校验 |
2254
+ | NFR-SEC-001 | 无明文凭据 | Vault 加密 + `~vault:` 引用 |
2255
+ | NFR-SEC-002 | 加密强度 | PBKDF2 ≥ 100000 / Argon2id |
2256
+ | NFR-SEC-003 | 无 CVE | 发布前 `rebar3 audit` + `bundle audit` |
2257
+ | NFR-USE-001 | 5 分钟上手 | CLI 向导 + 文档 |
2258
+ | NFR-USE-002 | 快捷键覆盖 | 全操作快捷键映射 |
2259
+ | NFR-USE-003 | 中英双语 | i18n 资源文件 |
2260
+ | NFR-COMP-001 | OS 兼容 | Erlang 跨平台编译 + Ruby 跨平台 |
2261
+ | NFR-COMP-002 | 服务端兼容 | OTP ssh 兼容 OpenSSH 7.0+、Dropbear、Cisco IOS |
2262
+ | NFR-COMP-003 | HiDPI 适配 | V2.0 TUI 层处理 DPI 缩放 |
2263
+
2264
+ ### 9.3 需求覆盖统计
2265
+
2266
+ | 层 | V1.0 覆盖 | V2.0 覆盖 | V3.0 覆盖 | 总计 |
2267
+ |----|----------|----------|----------|------|
2268
+ | 功能需求 | 17/51 | 19/51 | 15/51 | 51/51 |
2269
+ | 非功能需求 | 14/20 | 4/20 | 2/20 | 20/20 |
2270
+
2271
+ ---
2272
+
2273
+ ## 10 部署与打包
2274
+
2275
+ ### 10.1 Erlang 引擎分发
2276
+
2277
+ Erlang 引擎以预编译 release 形式分发,与 Ruby gem 解耦:
2278
+
2279
+ ```
2280
+ ssh-core-ext/
2281
+ ├── windows-x64/
2282
+ │ └── ssh_core.exe + erts-15.x/
2283
+ ├── macos-universal/
2284
+ │ └── ssh_core + erts-15.x/
2285
+ ├── linux-x64/
2286
+ │ └── ssh_core + erts-15.x/
2287
+ └── VERSION # 版本信息
2288
+ ```
2289
+
2290
+ Ruby gem 在安装时或运行时检测平台,下载对应预编译引擎到 `ext/ssh_core/` 目录。
2291
+
2292
+ ### 10.2 Ruby gem 打包
2293
+
2294
+ ```ruby
2295
+ # ssh_client.gemspec
2296
+ Gem::Specification.new do |spec|
2297
+ spec.name = "network-infra-utility-ssh"
2298
+ spec.version = "1.0.0"
2299
+ spec.summary = "SSH 连接客户端 - Erlang 核心 + Ruby 调度"
2300
+ spec.authors = ["Numeron"]
2301
+ spec.files = Dir["lib/**/*.rb", "bin/*", "ext/ssh_core/**/*"]
2302
+ spec.bindir = "bin"
2303
+ spec.executables = ["ssh-client"]
2304
+ spec.add_dependency "thor", "~> 1.2"
2305
+ spec.add_development_dependency "rspec", "~> 3.12"
2306
+ end
2307
+ ```
2308
+
2309
+ ### 10.3 启动流程
2310
+
2311
+ ```
2312
+ 1. 用户执行 ssh-client connect 10.0.0.1 -u admin
2313
+ 2. Ruby CLI 解析参数
2314
+ 3. SSH::Client.new → 加载配置
2315
+ 4. client.start_engine
2316
+ a. 检测平台 → 选择对应 ssh_core 二进制
2317
+ b. Process.spawn 启动 Erlang 引擎
2318
+ c. Erlang 引擎初始化 → 写入 IPC 端点文件
2319
+ d. Ruby 读取端点 → 建立 IPC 连接
2320
+ 5. client.connect(spec)
2321
+ a. Vault 解密凭据
2322
+ b. IPC RPC 调用 conn.connect
2323
+ c. Erlang 建立 SSH 连接
2324
+ d. 返回 conn_id
2325
+ 6. session.open_terminal
2326
+ a. IPC RPC 调用 channel.open
2327
+ b. Ruby 注册 channel.data 推送回调
2328
+ 7. 进入交互模式
2329
+ ```
2330
+
2331
+ ### 10.4 CI/CD 流水线
2332
+
2333
+ ```
2334
+ GitHub Actions / 自建 CI
2335
+ ├── Erlang 编译矩阵 (windows/macos/linux)
2336
+ │ ├── rebar3 compile
2337
+ │ ├── rebar3 ct (Common Test)
2338
+ │ ├── rebar3 release
2339
+ │ └── 上传预编译二进制
2340
+ ├── Ruby 测试
2341
+ │ ├── bundle install
2342
+ │ ├── rspec spec/
2343
+ │ └── rubocop
2344
+ ├── 集成测试
2345
+ │ ├── 启动 Erlang 引擎 + Ruby 客户端
2346
+ │ ├── 连接 Docker 内 SSH 服务器
2347
+ │ └── 执行端到端测试脚本
2348
+ └── 发布
2349
+ ├── 打包 Ruby gem (含预编译 Erlang 引擎)
2350
+ └── 发布到 RubyGems / 内部 gem 服务器
2351
+ ```
2352
+
2353
+ ---
2354
+
2355
+ ## 11 三期迭代设计
2356
+
2357
+ ### 11.1 V1.0 — MVP 核心(P0 需求)
2358
+
2359
+ **目标**:可用的单用户 SSH 终端,CLI 交互。
2360
+
2361
+ **Erlang 侧交付**:
2362
+
2363
+ | 模块 | 状态 | 说明 |
2364
+ |------|------|------|
2365
+ | ssh_core_sup | 全量 | 监督树 |
2366
+ | ssh_ipc_gateway | 全量 | JSON-RPC 网关 |
2367
+ | ssh_conn_worker | 全量 | SSH2 连接 |
2368
+ | ssh_auth_engine | 全量 | 密码+公钥+键盘交互 |
2369
+ | ssh_jump_chain | 全量 | 多级跳板 |
2370
+ | ssh_channel_fsm | 全量 | shell 通道 |
2371
+ | ssh_keepalive_mgr | 全量 | 保活 + 断连检测 |
2372
+ | ssh_port_fwd | 全量 | 3 种端口转发 |
2373
+ | ssh_sftp_engine | 全量 | SFTP 浏览 + 传输 |
2374
+ | ssh_known_hosts | 全量 | 主机密钥校验 |
2375
+
2376
+ **Ruby 侧交付**:
2377
+
2378
+ | 模块 | 状态 | 说明 |
2379
+ |------|------|------|
2380
+ | SSH::Client | 全量 | 主入口 + 引擎管理 |
2381
+ | IPC::Client | 全量 | JSON-RPC 客户端 |
2382
+ | Session::Manager | 全量 | 会话管理 |
2383
+ | Session::Tree | 全量 | 分组树 |
2384
+ | Terminal::Emulator | 基础 | xterm-256color 转义解析 |
2385
+ | Terminal::Buffer | 全量 | 回滚缓冲 10000 行 |
2386
+ | Terminal::Theme | 全量 | 内置 10 套配色 |
2387
+ | Security::Vault | 全量 | AES-256-GCM |
2388
+ | Security::HostKey | 全量 | 主机密钥管理 |
2389
+ | Automation::MacroEngine | 全量 | 登录宏 |
2390
+ | CLI | 基础 | Thor 命令行 |
2391
+
2392
+ **V1.0 验收标准**:
2393
+ - 连接 OpenSSH 服务器,交互终端可用
2394
+ - 密码 + 公钥 + 跳板连接可用
2395
+ - SFTP 浏览和文件传输可用
2396
+ - 端口转发 3 种类型可用
2397
+ - 凭据加密存储 + 主机密钥校验
2398
+ - 17 条 P0 需求全部满足量化指标
2399
+
2400
+ ### 11.2 V2.0 — 效率增强(P1 需求)
2401
+
2402
+ **新增内容**:
2403
+
2404
+ | 方向 | 模块 | 说明 |
2405
+ |------|------|------|
2406
+ | TUI 多面板 | Curses UI + 标签页 + 窗口拆分 | FR-SESS-001 |
2407
+ | 自动重连 | ssh_conn_worker 扩展 | FR-CONN-005 |
2408
+ | 会话导入导出 | Config::ImportExport | FR-SESS-003 |
2409
+ | 会话搜索 | Session::Manager#search | FR-SESS-004 |
2410
+ | 批量文件传输 | File::Transfer 扩展 | FR-FILE-005 |
2411
+ | ZMODEM | File::Zmodem | FR-FILE-004 |
2412
+ | SSH Agent | Security::KeyManager | FR-SEC-003 |
2413
+ | 密钥生成 | Security::KeyManager | FR-SEC-004 |
2414
+ | 代理连接 | Network::Proxy | FR-NET-002 |
2415
+ | 连接诊断 | Network::Diagnostics | FR-NET-004 |
2416
+ | 搜索高亮 | Terminal::Buffer#search 扩展 | FR-TERM-005 |
2417
+ | 广播输入 | Session::Manager#broadcast | FR-TERM-008 |
2418
+ | 代码片段 | Automation::SnippetManager | FR-TERM-009 |
2419
+ | 批量执行 | Automation::BatchExec | FR-AUTO-003 |
2420
+ | 跨平台 | 三平台编译 + 测试 | FR-COLLAB-001 |
2421
+
2422
+ ### 11.3 V3.0 — 差异化竞争力(P2 需求)
2423
+
2424
+ **新增内容**:
2425
+
2426
+ | 方向 | 模块 | 说明 |
2427
+ |------|------|------|
2428
+ | MOSH | mosh_engine (Erlang) | FR-CONN-007 |
2429
+ | 会话锁屏 | TUI 层 | FR-SESS-006 |
2430
+ | 远程文件编辑 | File::Watcher | FR-FILE-006 |
2431
+ | 凭据云同步 | Config::Sync + 端到端加密 | FR-SEC-005 |
2432
+ | 2FA/MFA | Security::OTP | FR-SEC-006 |
2433
+ | X11 转发 | ssh_port_fwd 扩展 | FR-NET-003 |
2434
+ | 流量统计 | Network::Stats | FR-NET-005 |
2435
+ | 脚本引擎 | Automation::ScriptEngine (Lua) | FR-AUTO-005 |
2436
+ | 会话录放 | Automation::Recorder | FR-AUTO-002 |
2437
+ | 团队共享 | Config::TeamShare | FR-COLLAB-003 |
2438
+ | 便携版 | 打包脚本 | FR-COLLAB-004 |
2439
+
2440
+ ### 11.4 迭代节奏
2441
+
2442
+ | 里程碑 | 时间 | 内容 |
2443
+ |--------|------|------|
2444
+ | M1 | V1.0 W1-2 | Erlang 引擎骨架 + IPC 通路 |
2445
+ | M2 | V1.0 W3-4 | SSH 连接 + 认证 + 跳板 |
2446
+ | M3 | V1.0 W5-6 | Ruby 调度层 + CLI |
2447
+ | M4 | V1.0 W7-8 | 终端仿真 + SFTP + 端口转发 |
2448
+ | M5 | V1.0 W9-10 | 凭据加密 + 已知主机 + 登录宏 |
2449
+ | V1.0 Release | W10 | 17 条 P0 全量验收 |
2450
+ | — | — | — |
2451
+ | M6 | V2.0 W1-4 | TUI 多面板 + 会话管理增强 |
2452
+ | M7 | V2.0 W5-8 | 文件增强 + 网络增强 |
2453
+ | V2.0 Release | W8 | 19 条 P1 全量验收 |
2454
+ | — | — | — |
2455
+ | V3.0 | W1-10 | 15 条 P2 分批交付 |
2456
+
2457
+ ---
2458
+
2459
+ ## 附录 A:设计决策记录
2460
+
2461
+ ### ADR-001 SSH 协议栈选择 Erlang/OTP
2462
+
2463
+ - **日期**:2026-08-02
2464
+ - **状态**:已决定
2465
+ - **背景**:需要在 Windows/macOS/Linux 三平台支持高并发 SSH 连接(≥ 50),连接延迟 ≤ 2s
2466
+ - **决策**:使用 Erlang/OTP 内置 ssh 应用作为协议栈
2467
+ - **理由**:OTP ssh 是官方维护的成熟 SSH2 实现,进程模型天然适合并发连接管理,跨平台编译稳定
2468
+ - **后果**:引入 Erlang/Ruby 双语言栈,增加打包复杂度,但换来得连接层面可靠性
2469
+
2470
+ ### ADR-002 IPC 协议选择 JSON-RPC 2.0
2471
+
2472
+ - **日期**:2026-08-02
2473
+ - **状态**:已决定
2474
+ - **背景**:Erlang 与 Ruby 之间需要高效、可靠的进程间通信
2475
+ - **决策**:使用 JSON-RPC 2.0 over Unix Socket / TCP
2476
+ - **理由**:JSON-RPC 语言无关、调试友好;Unix Socket 零拷贝低延迟;JSON 序列化对终端数据性能损失 < 1ms 可接受
2477
+ - **代价**:base64 编码增加约 33% 数据量,终端高频小包场景可通过批量推送优化
2478
+
2479
+ ### ADR-003 UI 分阶段:CLI → TUI → GUI
2480
+
2481
+ - **日期**:2026-08-02
2482
+ - **状态**:已决定
2483
+ - **背景**:全量 TUI/GUI 开发周期长,需快速验证核心功能链路
2484
+ - **决策**:V1.0 交付 Thor CLI,V2.0 升级 Curses TUI,V3.0 评估 GUI 需求
2485
+ - **理由**:降低首版复杂度,先跑通 "连接 → 终端 → 文件" 全链路再增强交互
2486
+
2487
+ ### ADR-004 凭据存储使用 Ruby 侧 Vault 而非 Erlang 管理
2488
+
2489
+ - **日期**:2026-08-02
2490
+ - **状态**:已决定
2491
+ - **背景**:凭据加密存储可在 Erlang 或 Ruby 任一侧实现
2492
+ - **决策**:Ruby 侧用 AES-256-GCM 管理,连接时解密后经 IPC 传入 Erlang
2493
+ - **理由**:Ruby OpenSSL 生态成熟,与配置管理在同一层更内聚;Erlang 仅作为协议执行层保持无状态