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,378 @@
1
+ //! 端口转发管理器 — 管理本地/远程端口转发规则。
2
+ //! 对应 Erlang 端的 ssh_port_fwd.erl。
3
+ //!
4
+ //! 核心设计:
5
+ //! - 本地转发:tokio TcpListener 监听本地端口 → SSH channel_open_direct_tcpip → 远程
6
+ //! - 远程转发:handle.tcpip_forward() 请求 SSH 服务器在远端监听 → 推送转发数据
7
+ //! - 每条规则有唯一 rule_id,可动态添加/删除/列举
8
+ //! - 本地转发的 listener task 在规则删除时通过 AbortHandle 取消
9
+
10
+ use std::collections::HashMap;
11
+ use std::sync::Arc;
12
+
13
+ use russh::client;
14
+ use serde_json::{json, Value};
15
+ use tokio::io::{AsyncReadExt, AsyncWriteExt};
16
+ use tokio::net::{TcpListener, TcpStream};
17
+ use tokio::sync::RwLock;
18
+ use tokio::task::AbortHandle;
19
+ use tracing::{debug, info, warn};
20
+
21
+ use crate::codec;
22
+ use crate::conn::ConnManager;
23
+ use crate::handler::SshHandler;
24
+ use crate::proto;
25
+
26
+ /// 端口转发类型
27
+ #[derive(Debug, Clone, Copy, PartialEq)]
28
+ enum FwdType {
29
+ Local,
30
+ Remote,
31
+ }
32
+
33
+ impl FwdType {
34
+ fn as_str(&self) -> &'static str {
35
+ match self {
36
+ FwdType::Local => "local",
37
+ FwdType::Remote => "remote",
38
+ }
39
+ }
40
+
41
+ fn from_str(s: &str) -> Option<Self> {
42
+ match s {
43
+ "local" => Some(FwdType::Local),
44
+ "remote" => Some(FwdType::Remote),
45
+ _ => None,
46
+ }
47
+ }
48
+ }
49
+
50
+ /// 转发规则
51
+ struct FwdRule {
52
+ rule_id: String,
53
+ conn_id: String,
54
+ fwd_type: FwdType,
55
+ local_port: u16,
56
+ remote_host: String,
57
+ remote_port: u16,
58
+ /// 本地转发:listener task 的 abort handle
59
+ abort_handle: Option<AbortHandle>,
60
+ }
61
+
62
+ /// 端口转发管理器
63
+ pub struct PortFwdManager {
64
+ /// rule_id → 转发规则
65
+ rules: RwLock<HashMap<String, FwdRule>>,
66
+ /// 连接管理器引用
67
+ conn_mgr: Arc<ConnManager>,
68
+ }
69
+
70
+ impl PortFwdManager {
71
+ pub fn new(conn_mgr: Arc<ConnManager>) -> Arc<Self> {
72
+ Arc::new(Self {
73
+ rules: RwLock::new(HashMap::new()),
74
+ conn_mgr,
75
+ })
76
+ }
77
+
78
+ /// RPC: portfwd.add — 添加端口转发规则
79
+ pub async fn rpc_add(&self, params: Value) -> Result<Value, String> {
80
+ let conn_id = proto::get_str(&params, "conn_id")
81
+ .ok_or("missing conn_id")?
82
+ .to_string();
83
+ let type_str = proto::get_str(&params, "type").unwrap_or("local");
84
+ let local_port = proto::get_num(&params, "local_port").unwrap_or(0.0) as u16;
85
+ let remote_host = proto::get_str(&params, "remote_host")
86
+ .unwrap_or("127.0.0.1")
87
+ .to_string();
88
+ let remote_port = proto::get_num(&params, "remote_port").unwrap_or(0.0) as u16;
89
+
90
+ let fwd_type = FwdType::from_str(type_str)
91
+ .ok_or_else(|| format!("invalid_fwd_type: {}", type_str))?;
92
+
93
+ if local_port == 0 {
94
+ return Err("missing_local_port".to_string());
95
+ }
96
+
97
+ let rule_id = codec::gen_rule_id(&conn_id);
98
+
99
+ match fwd_type {
100
+ FwdType::Local => {
101
+ self.add_local_forward(&conn_id, &rule_id, local_port, &remote_host, remote_port)
102
+ .await?;
103
+ }
104
+ FwdType::Remote => {
105
+ self.add_remote_forward(&conn_id, &rule_id, local_port, &remote_host, remote_port)
106
+ .await?;
107
+ }
108
+ }
109
+
110
+ info!("portfwd.add: conn={} rule={} type={} local_port={} -> {}:{}",
111
+ conn_id, rule_id, fwd_type.as_str(), local_port, remote_host, remote_port);
112
+
113
+ Ok(json!({
114
+ "rule_id": rule_id
115
+ }))
116
+ }
117
+
118
+ /// RPC: portfwd.remove — 删除端口转发规则
119
+ pub async fn rpc_remove(&self, params: Value) -> Result<Value, String> {
120
+ let rule_id = proto::get_str(&params, "rule_id")
121
+ .ok_or("missing rule_id")?
122
+ .to_string();
123
+
124
+ let mut rules = self.rules.write().await;
125
+ let rule = rules.remove(&rule_id);
126
+ match rule {
127
+ Some(r) => {
128
+ // 本地转发:abort listener task
129
+ if let Some(handle) = r.abort_handle {
130
+ handle.abort();
131
+ }
132
+
133
+ // 远程转发:请求服务器取消转发
134
+ if r.fwd_type == FwdType::Remote {
135
+ if let Some(handle) = self.conn_mgr.get_handle(&r.conn_id).await {
136
+ let _ = handle
137
+ .cancel_tcpip_forward(&r.remote_host, r.local_port.into())
138
+ .await;
139
+ }
140
+ }
141
+
142
+ info!("portfwd.remove: rule={}", rule_id);
143
+ Ok(json!({"ok": true}))
144
+ }
145
+ None => Err("rule_not_found".to_string()),
146
+ }
147
+ }
148
+
149
+ /// RPC: portfwd.list — 列出所有转发规则
150
+ pub async fn rpc_list(&self, params: Value) -> Result<Value, String> {
151
+ let conn_id = proto::get_str(&params, "conn_id");
152
+ let rules = self.rules.read().await;
153
+ let items: Vec<Value> = rules
154
+ .iter()
155
+ .filter(|(_, r)| conn_id.is_none() || Some(r.conn_id.as_str()) == conn_id)
156
+ .map(|(id, r)| {
157
+ json!({
158
+ "rule_id": id,
159
+ "conn_id": r.conn_id,
160
+ "type": r.fwd_type.as_str(),
161
+ "local_port": r.local_port,
162
+ "remote_host": r.remote_host,
163
+ "remote_port": r.remote_port,
164
+ })
165
+ })
166
+ .collect();
167
+
168
+ Ok(json!({"rules": Value::Array(items)}))
169
+ }
170
+
171
+ /// 添加本地端口转发
172
+ async fn add_local_forward(
173
+ &self,
174
+ conn_id: &str,
175
+ rule_id: &str,
176
+ local_port: u16,
177
+ remote_host: &str,
178
+ remote_port: u16,
179
+ ) -> Result<(), String> {
180
+ // 绑定本地 TCP 监听
181
+ let listener = TcpListener::bind(("127.0.0.1", local_port))
182
+ .await
183
+ .map_err(|e| format!("listen_failed: {}", e))?;
184
+
185
+ let actual_port = listener
186
+ .local_addr()
187
+ .map(|a| a.port())
188
+ .unwrap_or(local_port);
189
+
190
+ let handle = self
191
+ .conn_mgr
192
+ .get_handle(conn_id)
193
+ .await
194
+ .ok_or("conn_not_found")?;
195
+
196
+ let remote_host = remote_host.to_string();
197
+ let conn_id_owned = conn_id.to_string();
198
+
199
+ // 启动 listener task
200
+ let task = tokio::spawn({
201
+ let rh = remote_host.clone();
202
+ async move {
203
+ Self::local_forward_loop(listener, handle, rh, remote_port, conn_id_owned).await;
204
+ }
205
+ });
206
+
207
+ let abort_handle = task.abort_handle();
208
+
209
+ let rule = FwdRule {
210
+ rule_id: rule_id.to_string(),
211
+ conn_id: conn_id.to_string(),
212
+ fwd_type: FwdType::Local,
213
+ local_port: actual_port,
214
+ remote_host: remote_host.to_string(),
215
+ remote_port,
216
+ abort_handle: Some(abort_handle),
217
+ };
218
+ self.rules.write().await.insert(rule_id.to_string(), rule);
219
+
220
+ Ok(())
221
+ }
222
+
223
+ /// 添加远程端口转发
224
+ async fn add_remote_forward(
225
+ &self,
226
+ conn_id: &str,
227
+ rule_id: &str,
228
+ remote_port: u16,
229
+ local_host: &str,
230
+ local_port: u16,
231
+ ) -> Result<(), String> {
232
+ let handle = self
233
+ .conn_mgr
234
+ .get_handle(conn_id)
235
+ .await
236
+ .ok_or("conn_not_found")?;
237
+
238
+ // 请求 SSH 服务器在远端监听
239
+ handle
240
+ .tcpip_forward(local_host, local_port.into())
241
+ .await
242
+ .map_err(|e| format!("tcpip_forward_failed: {}", e))?;
243
+
244
+ let rule = FwdRule {
245
+ rule_id: rule_id.to_string(),
246
+ conn_id: conn_id.to_string(),
247
+ fwd_type: FwdType::Remote,
248
+ local_port: remote_port,
249
+ remote_host: local_host.to_string(),
250
+ remote_port: local_port,
251
+ abort_handle: None,
252
+ };
253
+ self.rules.write().await.insert(rule_id.to_string(), rule);
254
+
255
+ Ok(())
256
+ }
257
+
258
+ /// 本地转发循环:接受 TCP 连接 → SSH direct_tcpip → 双向数据转发
259
+ async fn local_forward_loop(
260
+ listener: TcpListener,
261
+ handle: Arc<client::Handle<SshHandler>>,
262
+ remote_host: String,
263
+ remote_port: u16,
264
+ conn_id: String,
265
+ ) {
266
+ loop {
267
+ match listener.accept().await {
268
+ Ok((client_sock, client_addr)) => {
269
+ debug!("portfwd: accepted connection from {} on conn {}", client_addr, conn_id);
270
+
271
+ let handle = Arc::clone(&handle);
272
+ let rh = remote_host.clone();
273
+ let rp = remote_port;
274
+
275
+ // 每个连接独立 spawn 一个转发任务
276
+ tokio::spawn(async move {
277
+ Self::handle_forward_connection(
278
+ client_sock,
279
+ handle,
280
+ rh,
281
+ rp,
282
+ )
283
+ .await;
284
+ });
285
+ }
286
+ Err(e) => {
287
+ warn!("portfwd: accept failed on conn {}: {}", conn_id, e);
288
+ break;
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ /// 处理单个本地转发连接
295
+ async fn handle_forward_connection(
296
+ mut client_sock: TcpStream,
297
+ handle: Arc<client::Handle<SshHandler>>,
298
+ remote_host: String,
299
+ remote_port: u16,
300
+ ) {
301
+ // 通过 SSH 打开 direct_tcpip 通道
302
+ let channel = match handle
303
+ .channel_open_direct_tcpip(&remote_host, remote_port.into(), "127.0.0.1", 0)
304
+ .await
305
+ {
306
+ Ok(ch) => ch,
307
+ Err(e) => {
308
+ warn!("portfwd: direct_tcpip failed to {}:{}: {}", remote_host, remote_port, e);
309
+ return;
310
+ }
311
+ };
312
+
313
+ // 获取通道的读写流
314
+ let (mut ch_reader, mut ch_writer) = {
315
+ let stream = channel.into_stream();
316
+ tokio::io::split(stream)
317
+ };
318
+
319
+ // 拆分 TCP socket 为读写两半,避免双重借用
320
+ let (mut sock_reader, mut sock_writer) = tokio::io::split(&mut client_sock);
321
+
322
+ // 双向数据转发
323
+ let client_to_remote = async {
324
+ let mut buf = [0u8; 32768];
325
+ loop {
326
+ match sock_reader.read(&mut buf).await {
327
+ Ok(0) => break, // EOF
328
+ Ok(n) => {
329
+ if ch_writer.write_all(&buf[..n]).await.is_err() {
330
+ break;
331
+ }
332
+ let _ = ch_writer.flush().await;
333
+ }
334
+ Err(_) => break,
335
+ }
336
+ }
337
+ };
338
+
339
+ let remote_to_client = async {
340
+ let mut buf = [0u8; 32768];
341
+ loop {
342
+ match ch_reader.read(&mut buf).await {
343
+ Ok(0) => break, // EOF
344
+ Ok(n) => {
345
+ if sock_writer.write_all(&buf[..n]).await.is_err() {
346
+ break;
347
+ }
348
+ let _ = sock_writer.flush().await;
349
+ }
350
+ Err(_) => break,
351
+ }
352
+ }
353
+ };
354
+
355
+ tokio::join!(client_to_remote, remote_to_client);
356
+
357
+ let _ = client_sock.shutdown().await;
358
+ }
359
+
360
+ /// 清理指定连接的所有转发规则(连接断开时调用)
361
+ pub async fn cleanup_conn(&self, conn_id: &str) {
362
+ let mut rules = self.rules.write().await;
363
+ let to_remove: Vec<String> = rules
364
+ .iter()
365
+ .filter(|(_, r)| r.conn_id == conn_id)
366
+ .map(|(k, _)| k.clone())
367
+ .collect();
368
+
369
+ for id in &to_remove {
370
+ if let Some(rule) = rules.remove(id) {
371
+ if let Some(handle) = rule.abort_handle {
372
+ handle.abort();
373
+ }
374
+ warn!("portfwd rule {} cleaned up (conn {} disconnected)", id, conn_id);
375
+ }
376
+ }
377
+ }
378
+ }
@@ -0,0 +1,198 @@
1
+ //! JSON-RPC 2.0 协议编解码与分帧。
2
+ //! 对应 Erlang 端的 ssh_ipc_proto.erl。
3
+ //!
4
+ //! 协议规定:
5
+ //! - 消息格式:JSON-RPC 2.0,每条消息以 \n 分隔
6
+ //! - 有 id 的消息为 request(Ruby→Rust)或 response(双向)
7
+ //! - 无 id 的消息为 notification(推送,Rust→Ruby)
8
+ //! - 最大消息 2MB
9
+
10
+ use serde_json::{Map, Value};
11
+
12
+ /// 协议版本
13
+ pub const PROTOCOL_VER: &str = "1.0";
14
+ /// 消息分隔符
15
+ pub const MSG_DELIMITER: u8 = b'\n';
16
+ /// 最大消息大小(2MB)
17
+ pub const MAX_MSG_SIZE: usize = 2 * 1024 * 1024;
18
+
19
+ /// 编码 JSON-RPC 请求(带 id 和 method)
20
+ pub fn encode_request(id: i64, method: &str, params: &Value) -> String {
21
+ serde_json::json!({
22
+ "jsonrpc": "2.0",
23
+ "id": id,
24
+ "method": method,
25
+ "params": params
26
+ })
27
+ .to_string()
28
+ }
29
+
30
+ /// 编码 JSON-RPC 成功响应
31
+ pub fn encode_response(id: i64, result: &Value) -> String {
32
+ serde_json::json!({
33
+ "jsonrpc": "2.0",
34
+ "id": id,
35
+ "result": result
36
+ })
37
+ .to_string()
38
+ }
39
+
40
+ /// 编码 JSON-RPC 错误响应
41
+ pub fn encode_error(id: i64, code: i64, message: &str) -> String {
42
+ serde_json::json!({
43
+ "jsonrpc": "2.0",
44
+ "id": id,
45
+ "error": {
46
+ "code": code,
47
+ "message": message
48
+ }
49
+ })
50
+ .to_string()
51
+ }
52
+
53
+ /// 编码推送通知(无 id)
54
+ pub fn encode_push(method: &str, params: &Value) -> String {
55
+ serde_json::json!({
56
+ "jsonrpc": "2.0",
57
+ "method": method,
58
+ "params": params
59
+ })
60
+ .to_string()
61
+ }
62
+
63
+ /// 给消息追加帧分隔符
64
+ pub fn frame(msg: &str) -> Vec<u8> {
65
+ let mut data = msg.as_bytes().to_vec();
66
+ data.push(MSG_DELIMITER);
67
+ data
68
+ }
69
+
70
+ /// 从缓冲区中分割出完整帧。
71
+ /// 返回 (完整帧列表, 剩余不完整数据)
72
+ pub fn unframe(buffer: &[u8]) -> (Vec<Vec<u8>>, Vec<u8>) {
73
+ let mut frames = Vec::new();
74
+ let mut start = 0;
75
+
76
+ for (i, &byte) in buffer.iter().enumerate() {
77
+ if byte == MSG_DELIMITER {
78
+ // 检查前面是否有 \r(处理 \r\n 的情况)
79
+ let end = if i > start && buffer[i - 1] == b'\r' {
80
+ i - 1
81
+ } else {
82
+ i
83
+ };
84
+ if end > start {
85
+ frames.push(buffer[start..end].to_vec());
86
+ }
87
+ start = i + 1;
88
+ }
89
+ }
90
+
91
+ let rest = if start < buffer.len() {
92
+ buffer[start..].to_vec()
93
+ } else {
94
+ Vec::new()
95
+ };
96
+
97
+ (frames, rest)
98
+ }
99
+
100
+ /// 消息分类
101
+ #[derive(Debug, Clone)]
102
+ pub enum Message {
103
+ /// Ruby→Rust 请求(有 id 和 method)
104
+ Request { id: i64, method: String, params: Value },
105
+ /// 对 Rust→Ruby 反向请求的响应(有 id,无 method)
106
+ Response { id: i64, result: Option<Value>, error: Option<Value> },
107
+ /// 推送通知(无 id,有 method)
108
+ Notification { method: String, params: Value },
109
+ }
110
+
111
+ /// 解码一条 JSON-RPC 消息
112
+ pub fn decode(data: &[u8]) -> Result<Message, String> {
113
+ let text = std::str::from_utf8(data).map_err(|e| format!("utf8 error: {}", e))?;
114
+ let val: Value = serde_json::from_str(text).map_err(|e| format!("json error: {}", e))?;
115
+
116
+ let obj = val.as_object().ok_or("not a json object")?;
117
+
118
+ // 校验 jsonrpc 版本
119
+ let ver = obj
120
+ .get("jsonrpc")
121
+ .and_then(|v| v.as_str())
122
+ .ok_or("missing jsonrpc field")?;
123
+ if ver != "2.0" {
124
+ return Err(format!("unsupported jsonrpc version: {}", ver));
125
+ }
126
+
127
+ let id = obj.get("id");
128
+
129
+ match id {
130
+ Some(id_val) => {
131
+ let id_num = id_val.as_i64().ok_or("id is not an integer")?;
132
+
133
+ if obj.contains_key("method") {
134
+ // 有 id 和 method → Ruby→Rust 请求
135
+ let method = obj
136
+ .get("method")
137
+ .and_then(|v| v.as_str())
138
+ .ok_or("method not a string")?
139
+ .to_string();
140
+ let params = obj.get("params").cloned().unwrap_or(Value::Null);
141
+ Ok(Message::Request {
142
+ id: id_num,
143
+ method,
144
+ params,
145
+ })
146
+ } else {
147
+ // 有 id 无 method → 反向 RPC 的响应
148
+ let result = obj.get("result").cloned();
149
+ let error = obj.get("error").cloned();
150
+ Ok(Message::Response {
151
+ id: id_num,
152
+ result,
153
+ error,
154
+ })
155
+ }
156
+ }
157
+ None => {
158
+ // 无 id → 推送通知
159
+ let method = obj
160
+ .get("method")
161
+ .and_then(|v| v.as_str())
162
+ .ok_or("notification missing method")?
163
+ .to_string();
164
+ let params = obj.get("params").cloned().unwrap_or(Value::Null);
165
+ Ok(Message::Notification { method, params })
166
+ }
167
+ }
168
+ }
169
+
170
+ /// 从 Value 中提取字符串字段(兼容 String key)
171
+ pub fn get_str<'a>(params: &'a Value, key: &str) -> Option<&'a str> {
172
+ params.get(key).and_then(|v| v.as_str())
173
+ }
174
+
175
+ /// 从 Value 中提取数值字段
176
+ pub fn get_num(params: &Value, key: &str) -> Option<f64> {
177
+ params.get(key).and_then(|v| v.as_f64())
178
+ }
179
+
180
+ /// 从 Value 中提取布尔字段
181
+ pub fn get_bool(params: &Value, key: &str) -> Option<bool> {
182
+ params.get(key).and_then(|v| v.as_bool())
183
+ }
184
+
185
+ /// 构建成功响应的 params(Erlang 风格 {ok, #{}}
186
+ pub fn ok_result(fields: Map<String, Value>) -> Value {
187
+ Value::Object(fields)
188
+ }
189
+
190
+ /// 构建简单 ok 响应
191
+ pub fn ok_true() -> Value {
192
+ serde_json::json!({"ok": true})
193
+ }
194
+
195
+ /// 构建错误对象
196
+ pub fn error_obj(message: &str) -> Value {
197
+ serde_json::json!({"message": message})
198
+ }