network-infra-utility 0.3.0 → 0.6.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/GUIDE.md +212 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +70 -0
  6. data/Rakefile +1 -1
  7. data/bin/dns-query +836 -0
  8. data/bin/geo-doc +135 -0
  9. data/bin/geo-get +1 -1
  10. data/bin/geo-update +429 -0
  11. data/document/ASNum/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +242 -0
  12. data/document/DNSQuery/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +248 -0
  13. 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
  14. data/document/IP/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +297 -0
  15. data/document/MAC/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +296 -0
  16. 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
  17. data/network-infra-utility.gemspec +4 -2
  18. data/network.rb +3 -1
  19. data/service/geodb/geodb.rb +278 -1
  20. data/service/ssh/README.md +955 -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 +117 -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 +142 -1
@@ -0,0 +1,30 @@
1
+ [package]
2
+ name = "ssh_core_rs"
3
+ version = "1.0.0"
4
+ edition = "2021"
5
+ description = "Rust SSH core engine — IPC-compatible replacement for Erlang ssh_core"
6
+
7
+ [[bin]]
8
+ name = "ssh_core_rs"
9
+ path = "src/main.rs"
10
+
11
+ [dependencies]
12
+ # 禁用默认特性(aws-lc-rs 需要 NASM/C11),改用 ring(纯 Rust 加密后端)
13
+ # 不启用 async-trait:russh 0.61 的 Handler trait 使用原生 AFIT(impl Future),
14
+ # 启用 async-trait 会导致 impl 与 trait 的生命周期签名不匹配(E0195)
15
+ russh = { version = "0.61", default-features = false, features = ["ring", "rsa", "flate2"] }
16
+ tokio = { version = "1", features = ["full"] }
17
+ serde = { version = "1", features = ["derive"] }
18
+ serde_json = "1"
19
+ base64 = "0.22"
20
+ tracing = "0.1"
21
+ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
22
+ thiserror = "1"
23
+ dashmap = "6"
24
+ rand = "0.8"
25
+ sha2 = "0.10"
26
+ russh-sftp = "2.4.0"
27
+
28
+ [profile.release]
29
+ opt-level = 3
30
+ lto = true
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # ssh_core_rs engine launcher for Linux/macOS
3
+ # Starts the Rust SSH core engine in foreground mode.
4
+ # Called by Ruby Client.spawn_engine via Process.spawn.
5
+ #
6
+ # This is the Rust-based replacement for the Erlang ssh_core engine.
7
+ # It implements the exact same IPC protocol, so Ruby can switch between
8
+ # the two backends by changing the engine_binary_path in client.rb.
9
+
10
+ set -e
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
+ SRC_DIR="$(dirname "$SCRIPT_DIR")"
14
+
15
+ # Delete stale endpoint file so Ruby can detect fresh startup
16
+ ENDPOINT_FILE="/tmp/ssh_core_$(id -u).endpoint"
17
+ rm -f "$ENDPOINT_FILE"
18
+
19
+ # Check if pre-built binary exists
20
+ BINARY="$SRC_DIR/target/release/ssh_core_rs"
21
+ if [ -f "$BINARY" ]; then
22
+ exec "$BINARY"
23
+ fi
24
+
25
+ # Build if not yet compiled
26
+ echo "Building ssh_core_rs (release)..." >&2
27
+ if ! command -v cargo &> /dev/null; then
28
+ echo "Error: cargo not found in PATH. Please install Rust toolchain." >&2
29
+ exit 1
30
+ fi
31
+ cargo build --release --manifest-path "$SRC_DIR/Cargo.toml"
32
+
33
+ # Run the engine in foreground (blocks until shutdown)
34
+ exec "$BINARY"
@@ -0,0 +1,40 @@
1
+ @echo off
2
+ REM ssh_core_rs engine launcher for Windows
3
+ REM Starts the Rust SSH core engine in foreground mode.
4
+ REM Called by Ruby Client.spawn_engine via Process.spawn.
5
+ REM
6
+ REM This is the Rust-based replacement for the Erlang ssh_core engine.
7
+ REM It implements the exact same IPC protocol, so Ruby can switch between
8
+ REM the two backends by changing the engine_binary_path in client.rb.
9
+
10
+ setlocal
11
+
12
+ set SCRIPT_DIR=%~dp0
13
+ set SRC_DIR=%SCRIPT_DIR%..
14
+
15
+ REM Delete stale endpoint file so Ruby can detect fresh startup
16
+ set EP_FILE=%TEMP%\ssh_core_%USERNAME%.endpoint
17
+ if exist "%EP_FILE%" del "%EP_FILE%"
18
+
19
+ REM Check if pre-built binary exists
20
+ set BINARY=%SRC_DIR%\target\release\ssh_core_rs.exe
21
+ if exist "%BINARY%" goto :run
22
+
23
+ REM Build if not yet compiled
24
+ echo Building ssh_core_rs (release)...
25
+ where cargo >nul 2>nul
26
+ if %ERRORLEVEL% neq 0 (
27
+ echo Error: cargo not found in PATH. Please install Rust toolchain.
28
+ exit /b 1
29
+ )
30
+ cargo build --release --manifest-path "%SRC_DIR%\Cargo.toml"
31
+ if %ERRORLEVEL% neq 0 (
32
+ echo Error: cargo build failed.
33
+ exit /b 1
34
+ )
35
+
36
+ :run
37
+ REM Run the engine in foreground (blocks until shutdown)
38
+ "%BINARY%"
39
+
40
+ endlocal
@@ -0,0 +1,296 @@
1
+ //! 通道管理器 — 管理所有 SSH 通道的打开、发送、关闭和窗口变更。
2
+ //! 对应 Erlang 端的 ssh_channel_stm.erl。
3
+ //!
4
+ //! 核心职责:
5
+ //! - channel.open:打开 shell/exec/subsystem 通道,请求 PTY,返回 channel_id
6
+ //! - channel.send:Base64 解码后向通道发送数据
7
+ //! - channel.close:关闭通道
8
+ //! - channel.window_change:变更 PTY 窗口大小
9
+ //! - 通过 handler 的 ConnShared 注册数据接收器,推送 channel.data / channel.eof
10
+
11
+ use std::collections::HashMap;
12
+ use std::sync::Arc;
13
+
14
+ use russh::Channel;
15
+ use russh::client;
16
+ use serde_json::{json, Value};
17
+ use tokio::sync::{mpsc, RwLock};
18
+ use tracing::debug;
19
+
20
+ use crate::codec;
21
+ use crate::coalesce::Coalescer;
22
+ use crate::conn::ConnManager;
23
+ use crate::handler::{ChannelData, ConnShared};
24
+ use crate::proto;
25
+
26
+ /// 通道管理器
27
+ pub struct ChannelManager {
28
+ /// IPC channel_id → 通道状态
29
+ channels: RwLock<HashMap<String, ChannelEntry>>,
30
+ /// 连接管理器引用
31
+ conn_mgr: Arc<ConnManager>,
32
+ /// coalesce 引用(用于数据推送)
33
+ coalesce: Arc<Coalescer>,
34
+ }
35
+
36
+ /// 单个通道的状态
37
+ struct ChannelEntry {
38
+ conn_id: String,
39
+ russh_ch_id: russh::ChannelId,
40
+ /// 通道对象(通过 Arc<Mutex> 保护,可 clone Arc)
41
+ channel: Arc<tokio::sync::Mutex<Channel<client::Msg>>>,
42
+ /// 数据接收任务 handle
43
+ data_task: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
44
+ }
45
+
46
+ impl ChannelManager {
47
+ pub fn new(conn_mgr: Arc<ConnManager>, coalesce: Arc<Coalescer>) -> Arc<Self> {
48
+ Arc::new(Self {
49
+ channels: RwLock::new(HashMap::new()),
50
+ conn_mgr,
51
+ coalesce,
52
+ })
53
+ }
54
+
55
+ /// RPC: channel.open
56
+ pub async fn rpc_open(&self, params: Value) -> Result<Value, String> {
57
+ let conn_id = proto::get_str(&params, "conn_id")
58
+ .ok_or("missing conn_id")?
59
+ .to_string();
60
+ let ch_type = proto::get_str(&params, "type").unwrap_or("shell");
61
+ let cols = proto::get_num(&params, "cols").unwrap_or(80.0) as u32;
62
+ let rows = proto::get_num(&params, "rows").unwrap_or(24.0) as u32;
63
+ let term_type = proto::get_str(&params, "term").unwrap_or("xterm-256color");
64
+ let command = proto::get_str(&params, "command");
65
+
66
+ // 获取连接的 handle 和 shared
67
+ let (handle, shared) = self
68
+ .conn_mgr
69
+ .get_handle_and_shared(&conn_id)
70
+ .await
71
+ .ok_or("conn_not_found")?;
72
+
73
+ // 打开 session 通道
74
+ let channel = handle
75
+ .channel_open_session()
76
+ .await
77
+ .map_err(|e| format!("channel_open_failed: {}", e))?;
78
+
79
+ let russh_ch_id = channel.id();
80
+ let ipc_ch_id = codec::gen_channel_id(&conn_id);
81
+
82
+ // 根据类型初始化通道
83
+ match ch_type {
84
+ "shell" => {
85
+ // 请求 PTY
86
+ channel
87
+ .request_pty(true, term_type, cols, rows, 0, 0, &[])
88
+ .await
89
+ .map_err(|e| format!("pty_request_failed: {}", e))?;
90
+ // 请求 shell
91
+ channel
92
+ .request_shell(true)
93
+ .await
94
+ .map_err(|e| format!("shell_request_failed: {}", e))?;
95
+ }
96
+ "exec" => {
97
+ let cmd = command.ok_or("missing command for exec")?;
98
+ channel
99
+ .exec(true, cmd)
100
+ .await
101
+ .map_err(|e| format!("exec_failed: {}", e))?;
102
+ }
103
+ "subsystem" => {
104
+ let subsystem = command.ok_or("missing subsystem name")?;
105
+ channel
106
+ .request_subsystem(true, subsystem)
107
+ .await
108
+ .map_err(|e| format!("subsystem_failed: {}", e))?;
109
+ }
110
+ _ => return Err(format!("unsupported_channel_type: {}", ch_type)),
111
+ }
112
+
113
+ // 注册数据接收器到 ConnShared(handler 的 data 回调会通过此 tx 推送数据)
114
+ let (data_tx, data_rx) = mpsc::unbounded_channel::<ChannelData>();
115
+ shared.register_channel_tx(russh_ch_id, data_tx).await;
116
+
117
+ // 启动数据接收循环(从 handler 转发的数据推送到 coalesce / IPC)
118
+ let coalesce = Arc::clone(&self.coalesce);
119
+ let ipc_ch_id_clone = ipc_ch_id.clone();
120
+ let conn_id_clone = conn_id.clone();
121
+ let shared_clone = Arc::clone(&shared);
122
+ let russh_id_for_close = russh_ch_id;
123
+ let data_task = tokio::spawn(async move {
124
+ Self::data_receive_loop(
125
+ data_rx,
126
+ coalesce,
127
+ ipc_ch_id_clone,
128
+ conn_id_clone,
129
+ shared_clone,
130
+ russh_id_for_close,
131
+ )
132
+ .await;
133
+ });
134
+
135
+ // 保存通道状态
136
+ let entry = ChannelEntry {
137
+ conn_id: conn_id.clone(),
138
+ russh_ch_id,
139
+ channel: Arc::new(tokio::sync::Mutex::new(channel)),
140
+ data_task: tokio::sync::Mutex::new(Some(data_task)),
141
+ };
142
+ self.channels
143
+ .write()
144
+ .await
145
+ .insert(ipc_ch_id.clone(), entry);
146
+
147
+ debug!(
148
+ "channel.open: conn={} ipc_ch={} russh_ch={} type={}",
149
+ conn_id, ipc_ch_id, russh_ch_id, ch_type
150
+ );
151
+
152
+ Ok(json!({
153
+ "channel_id": ipc_ch_id
154
+ }))
155
+ }
156
+
157
+ /// RPC: channel.send
158
+ pub async fn rpc_send(&self, params: Value) -> Result<Value, String> {
159
+ let ch_id = proto::get_str(&params, "id")
160
+ .ok_or("missing id")?
161
+ .to_string();
162
+ let b64_data = proto::get_str(&params, "data")
163
+ .ok_or("missing data")?;
164
+
165
+ let data = codec::decode_b64(b64_data).map_err(|e| format!("base64_decode_failed: {}", e))?;
166
+
167
+ let channel_arc = {
168
+ let channels = self.channels.read().await;
169
+ channels
170
+ .get(&ch_id)
171
+ .map(|e| Arc::clone(&e.channel))
172
+ .ok_or("channel_not_found")?
173
+ };
174
+
175
+ // 通过 russh Channel 发送数据
176
+ let ch = channel_arc.lock().await;
177
+ ch.data(data.as_slice())
178
+ .await
179
+ .map_err(|e| format!("send_failed: {}", e))?;
180
+
181
+ debug!("channel.send: ch={} bytes={}", ch_id, data.len());
182
+ Ok(json!({"ok": true}))
183
+ }
184
+
185
+ /// RPC: channel.close
186
+ pub async fn rpc_close(&self, params: Value) -> Result<Value, String> {
187
+ let ch_id = proto::get_str(&params, "id")
188
+ .ok_or("missing id")?
189
+ .to_string();
190
+
191
+ let entry = self.channels.write().await.remove(&ch_id);
192
+ match entry {
193
+ Some(e) => {
194
+ // 关闭 russh 通道
195
+ {
196
+ let ch = e.channel.lock().await;
197
+ let _ = ch.close().await;
198
+ }
199
+
200
+ // 注销数据接收器
201
+ // 获取 shared 来注销——需要通过 conn_mgr
202
+ if let Some((_handle, shared)) = self.conn_mgr.get_handle_and_shared(&e.conn_id).await {
203
+ shared.unregister_channel_tx(e.russh_ch_id).await;
204
+ }
205
+
206
+ // 取消数据接收任务
207
+ if let Some(task) = e.data_task.lock().await.take() {
208
+ task.abort();
209
+ }
210
+
211
+ debug!("channel.close: ch={}", ch_id);
212
+ Ok(json!({"ok": true}))
213
+ }
214
+ None => Err("channel_not_found".to_string()),
215
+ }
216
+ }
217
+
218
+ /// RPC: channel.window_change
219
+ pub async fn rpc_window_change(&self, params: Value) -> Result<Value, String> {
220
+ let ch_id = proto::get_str(&params, "id")
221
+ .ok_or("missing id")?
222
+ .to_string();
223
+ let cols = proto::get_num(&params, "cols").unwrap_or(80.0) as u32;
224
+ let rows = proto::get_num(&params, "rows").unwrap_or(24.0) as u32;
225
+
226
+ let channel_arc = {
227
+ let channels = self.channels.read().await;
228
+ channels
229
+ .get(&ch_id)
230
+ .map(|e| Arc::clone(&e.channel))
231
+ .ok_or("channel_not_found")?
232
+ };
233
+
234
+ let ch = channel_arc.lock().await;
235
+ ch.window_change(cols as u32, rows as u32, 0, 0)
236
+ .await
237
+ .map_err(|e| format!("window_change_failed: {}", e))?;
238
+
239
+ debug!("channel.window_change: ch={} cols={} rows={}", ch_id, cols, rows);
240
+ Ok(json!({"ok": true}))
241
+ }
242
+
243
+ /// 数据接收循环:从 handler 转发的 ChannelData 推送到 coalesce / IPC
244
+ async fn data_receive_loop(
245
+ mut data_rx: mpsc::UnboundedReceiver<ChannelData>,
246
+ coalesce: Arc<Coalescer>,
247
+ ipc_ch_id: String,
248
+ conn_id: String,
249
+ shared: Arc<ConnShared>,
250
+ russh_ch_id: russh::ChannelId,
251
+ ) {
252
+ while let Some(data) = data_rx.recv().await {
253
+ match data {
254
+ ChannelData::Stdout { data } => {
255
+ // 推送到 coalesce(批量合并推送)
256
+ coalesce.enqueue(&ipc_ch_id, data).await;
257
+ }
258
+ ChannelData::Extended { data, ext } => {
259
+ // 扩展数据(stderr)——直接推送,不走 coalesce
260
+ let b64 = codec::encode_b64(&data);
261
+ shared
262
+ .gateway
263
+ .push_event(
264
+ "channel.extended_data",
265
+ json!({
266
+ "id": ipc_ch_id,
267
+ "data": b64,
268
+ "ext": ext
269
+ }),
270
+ )
271
+ .await;
272
+ }
273
+ ChannelData::Eof { reason } => {
274
+ // 推送 channel.eof
275
+ shared
276
+ .gateway
277
+ .push_event(
278
+ "channel.eof",
279
+ json!({
280
+ "id": ipc_ch_id,
281
+ "reason": reason
282
+ }),
283
+ )
284
+ .await;
285
+ // 收到 EOF 后,先 flush coalesce 中该通道的剩余数据
286
+ coalesce.flush_channel(&ipc_ch_id).await;
287
+ break;
288
+ }
289
+ }
290
+ }
291
+
292
+ // 注销数据接收器
293
+ shared.unregister_channel_tx(russh_ch_id).await;
294
+ debug!("conn {} channel {} data receive loop ended", conn_id, ipc_ch_id);
295
+ }
296
+ }
@@ -0,0 +1,143 @@
1
+ //! 高频数据合并器 — 8ms tick 或 16KB watermark 刷新 batch。
2
+ //! 对应 Erlang 端的 ssh_ipc_coalesce.erl。
3
+ //!
4
+ //! 核心设计(与 Erlang 端完全对齐):
5
+ //! - 每个通道的数据通过 enqueue() 添加到 buffer
6
+ //! - 每 COALESCE_TICK_MS(8ms)定时刷出一个 channel.data.batch 推送
7
+ //! - 当 buffer 总大小达到 COALESCE_WATERMARK_BYTES(16KB)时立即刷新
8
+ //! - flush_channel() 用于通道关闭时单独 flush 该通道的剩余数据
9
+
10
+ use std::collections::HashMap;
11
+ use std::sync::Arc;
12
+
13
+ use serde_json::{json, Value};
14
+ use tokio::sync::Mutex;
15
+ use tokio::time::{interval, Duration};
16
+ use tracing::trace;
17
+
18
+ use crate::codec;
19
+ use crate::gateway::Gateway;
20
+
21
+ /// 合并 tick 间隔(毫秒)
22
+ const COALESCE_TICK_MS: u64 = 8;
23
+
24
+ /// 合并 watermark(字节)
25
+ const COALESCE_WATERMARK_BYTES: usize = 16 * 1024;
26
+
27
+ /// 缓冲区中单个通道的数据
28
+ struct ChannelBuffer {
29
+ /// 累积的数据块
30
+ data: Vec<u8>,
31
+ }
32
+
33
+ /// 合并器状态
34
+ struct CoalesceState {
35
+ /// channel_id → buffer
36
+ buffer: HashMap<String, ChannelBuffer>,
37
+ /// 缓冲区总大小
38
+ total_size: usize,
39
+ }
40
+
41
+ /// 合并器
42
+ pub struct Coalescer {
43
+ state: Mutex<CoalesceState>,
44
+ gateway: Arc<Gateway>,
45
+ }
46
+
47
+ impl Coalescer {
48
+ pub fn new(gateway: Arc<Gateway>) -> Arc<Self> {
49
+ Arc::new(Self {
50
+ state: Mutex::new(CoalesceState {
51
+ buffer: HashMap::new(),
52
+ total_size: 0,
53
+ }),
54
+ gateway,
55
+ })
56
+ }
57
+
58
+ /// 启动定时 flush 循环
59
+ pub fn start(self: &Arc<Self>) {
60
+ let this = Arc::clone(self);
61
+ tokio::spawn(async move {
62
+ let mut ticker = interval(Duration::from_millis(COALESCE_TICK_MS));
63
+ ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
64
+
65
+ loop {
66
+ ticker.tick().await;
67
+ this.flush().await;
68
+ }
69
+ });
70
+ }
71
+
72
+ /// 入队数据(由 channel.rs 调用)
73
+ pub async fn enqueue(&self, channel_id: &str, data: Vec<u8>) {
74
+ let mut state = self.state.lock().await;
75
+
76
+ let entry = state
77
+ .buffer
78
+ .entry(channel_id.to_string())
79
+ .or_insert(ChannelBuffer { data: Vec::new() });
80
+ entry.data.extend_from_slice(&data);
81
+ state.total_size += data.len();
82
+
83
+ // 达到 watermark 立即 flush
84
+ if state.total_size >= COALESCE_WATERMARK_BYTES {
85
+ drop(state);
86
+ self.flush().await;
87
+ }
88
+ }
89
+
90
+ /// 刷出所有通道的累积数据为 channel.data.batch 推送
91
+ pub async fn flush(&self) {
92
+ let mut state = self.state.lock().await;
93
+ if state.buffer.is_empty() {
94
+ return;
95
+ }
96
+
97
+ // 取出 buffer
98
+ let buffer = std::mem::take(&mut state.buffer);
99
+ state.total_size = 0;
100
+ drop(state);
101
+
102
+ // 构建 batch items
103
+ let items: Vec<Value> = buffer
104
+ .into_iter()
105
+ .map(|(ch_id, buf)| {
106
+ let b64 = codec::encode_b64(&buf.data);
107
+ json!({
108
+ "id": ch_id,
109
+ "data": b64
110
+ })
111
+ })
112
+ .collect();
113
+
114
+ if !items.is_empty() {
115
+ trace!("coalesce flush: {} channels", items.len());
116
+ self.gateway
117
+ .push_batch(json!({ "items": items }))
118
+ .await;
119
+ }
120
+ }
121
+
122
+ /// 单独 flush 指定通道的剩余数据(通道关闭时调用)
123
+ pub async fn flush_channel(&self, channel_id: &str) {
124
+ let mut state = self.state.lock().await;
125
+
126
+ if let Some(buf) = state.buffer.remove(channel_id) {
127
+ state.total_size = state.total_size.saturating_sub(buf.data.len());
128
+
129
+ let b64 = codec::encode_b64(&buf.data);
130
+ drop(state);
131
+
132
+ // 单独推送该通道的数据
133
+ self.gateway
134
+ .push_batch(json!({
135
+ "items": [{
136
+ "id": channel_id,
137
+ "data": b64
138
+ }]
139
+ }))
140
+ .await;
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,71 @@
1
+ //! 编解码工具:Base64、ID 生成、SHA-256 指纹。
2
+ //! 对应 Erlang 端的 ssh_codec.erl。
3
+
4
+ use base64::Engine;
5
+ use rand::RngCore;
6
+ use sha2::{Digest, Sha256};
7
+ use std::time::{SystemTime, UNIX_EPOCH};
8
+
9
+ /// 当前 Unix 时间戳(毫秒)
10
+ pub fn unix_ms() -> u64 {
11
+ SystemTime::now()
12
+ .duration_since(UNIX_EPOCH)
13
+ .unwrap_or_default()
14
+ .as_millis() as u64
15
+ }
16
+
17
+ /// Base64 编码
18
+ pub fn encode_b64(data: &[u8]) -> String {
19
+ base64::engine::general_purpose::STANDARD.encode(data)
20
+ }
21
+
22
+ /// Base64 解码
23
+ pub fn decode_b64(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
24
+ base64::engine::general_purpose::STANDARD.decode(s)
25
+ }
26
+
27
+ /// 生成连接 ID: conn_<unix_ms>_<8hex>
28
+ pub fn gen_conn_id() -> String {
29
+ let ts = unix_ms();
30
+ let mut buf = [0u8; 4];
31
+ rand::thread_rng().fill_bytes(&mut buf);
32
+ let rand_hex: String = buf.iter().map(|b| format!("{:02x}", b)).collect();
33
+ format!("conn_{}_{}", ts, rand_hex)
34
+ }
35
+
36
+ /// 生成通道 ID: ch_<conn_short>_<seq>
37
+ pub fn gen_channel_id(conn_id: &str) -> String {
38
+ let conn_short: String = conn_id.chars().take(16).collect();
39
+ let seq = unix_ms() % 100_000;
40
+ format!("ch_{}_{}", conn_short, seq)
41
+ }
42
+
43
+ /// 生成 SFTP 会话 ID: sftp_<conn_short>_<ts>
44
+ pub fn gen_sftp_id(conn_id: &str) -> String {
45
+ let conn_short: String = conn_id.chars().take(16).collect();
46
+ let ts = unix_ms();
47
+ format!("sftp_{}_{}", conn_short, ts)
48
+ }
49
+
50
+ /// 生成端口转发规则 ID: fwd_<conn_short>_<seq>
51
+ pub fn gen_rule_id(conn_id: &str) -> String {
52
+ let conn_short: String = conn_id.chars().take(16).collect();
53
+ let ts = unix_ms();
54
+ format!("fwd_{}_{}", conn_short, ts)
55
+ }
56
+
57
+ /// 计算公钥的 SHA-256 指纹(OpenSSH 格式)。
58
+ /// 输入为公钥的 DER 编码字节。
59
+ pub fn fingerprint(public_key_der: &[u8]) -> String {
60
+ let mut hasher = Sha256::new();
61
+ hasher.update(public_key_der);
62
+ let hash = hasher.finalize();
63
+ format!("SHA256:{}", encode_b64(&hash))
64
+ }
65
+
66
+ /// 生成认证 Token(32 字节随机数 Base64 编码)
67
+ pub fn gen_auth_token() -> String {
68
+ let mut buf = [0u8; 32];
69
+ rand::thread_rng().fill_bytes(&mut buf);
70
+ encode_b64(&buf)
71
+ }