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.
- checksums.yaml +4 -4
- data/.gitignore +21 -0
- data/GUIDE.md +212 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +70 -0
- data/Rakefile +1 -1
- data/bin/dns-query +836 -0
- data/bin/geo-doc +135 -0
- data/bin/geo-get +1 -1
- data/bin/geo-update +429 -0
- data/document/ASNum/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +242 -0
- data/document/DNSQuery/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +248 -0
- 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
- data/document/IP/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +297 -0
- data/document/MAC/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +296 -0
- 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
- data/network-infra-utility.gemspec +4 -2
- data/network.rb +3 -1
- data/service/geodb/geodb.rb +278 -1
- data/service/ssh/README.md +955 -0
- data/service/ssh/bin/ssh-client +198 -0
- 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
- 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
- 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
- data/service/ssh/ext/ssh_core/bin/ssh_core.cmd +28 -0
- data/service/ssh/ext/ssh_core/config/sys.config +0 -0
- data/service/ssh/ext/ssh_core/config/vm.args +0 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/CHECKSUM +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/LICENSE +21 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/README.md +696 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/VERSION +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/contents.tar.gz +0 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/metadata.config +15 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.config +17 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.lock +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.app.src +10 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.erl +506 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.erl +393 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.hrl +18 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_consult.erl +81 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_decoder.erl +1909 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_encoder.erl +116 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_parser.erl +1214 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_json.erl +408 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_term.erl +389 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_verify.erl +121 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx.erl +506 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.erl +393 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.hrl +18 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_consult.erl +81 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_decoder.erl +1909 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_encoder.erl +116 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_parser.erl +1214 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_json.erl +408 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_term.erl +389 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_verify.erl +121 -0
- data/service/ssh/ext/ssh_core/rebar.config +24 -0
- data/service/ssh/ext/ssh_core/rebar.lock +1 -0
- data/service/ssh/ext/ssh_core/src/ssh_auth_engine.erl +156 -0
- data/service/ssh/ext/ssh_core/src/ssh_channel_stm.erl +232 -0
- data/service/ssh/ext/ssh_core/src/ssh_codec.erl +83 -0
- data/service/ssh/ext/ssh_core/src/ssh_conn_sup.erl +48 -0
- data/service/ssh/ext/ssh_core/src/ssh_conn_worker.erl +535 -0
- data/service/ssh/ext/ssh_core/src/ssh_core.app.src +36 -0
- data/service/ssh/ext/ssh_core/src/ssh_core_app.erl +11 -0
- data/service/ssh/ext/ssh_core/src/ssh_core_sup.erl +46 -0
- data/service/ssh/ext/ssh_core/src/ssh_infra_sup.erl +117 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc.hrl +80 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_coalesce.erl +94 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_gateway.erl +467 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_proto.erl +95 -0
- data/service/ssh/ext/ssh_core/src/ssh_jump_chain.erl +101 -0
- data/service/ssh/ext/ssh_core/src/ssh_keepalive_mgr.erl +222 -0
- data/service/ssh/ext/ssh_core/src/ssh_known_hosts_proxy.erl +67 -0
- data/service/ssh/ext/ssh_core/src/ssh_port_fwd.erl +225 -0
- data/service/ssh/ext/ssh_core/src/ssh_sftp_session.erl +250 -0
- data/service/ssh/ext/ssh_core/src/ssh_sftp_sup.erl +62 -0
- data/service/ssh/ext/ssh_core_rs/Cargo.lock +2345 -0
- data/service/ssh/ext/ssh_core_rs/Cargo.toml +30 -0
- data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs +34 -0
- data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs.cmd +40 -0
- data/service/ssh/ext/ssh_core_rs/src/channel.rs +296 -0
- data/service/ssh/ext/ssh_core_rs/src/coalesce.rs +143 -0
- data/service/ssh/ext/ssh_core_rs/src/codec.rs +71 -0
- data/service/ssh/ext/ssh_core_rs/src/conn.rs +628 -0
- data/service/ssh/ext/ssh_core_rs/src/gateway.rs +389 -0
- data/service/ssh/ext/ssh_core_rs/src/handler.rs +293 -0
- data/service/ssh/ext/ssh_core_rs/src/keepalive.rs +194 -0
- data/service/ssh/ext/ssh_core_rs/src/main.rs +351 -0
- data/service/ssh/ext/ssh_core_rs/src/portfwd.rs +378 -0
- data/service/ssh/ext/ssh_core_rs/src/proto.rs +198 -0
- data/service/ssh/ext/ssh_core_rs/src/sftp.rs +294 -0
- data/service/ssh/lib/network_infra_utility/ssh/automation/macro_engine.rb +213 -0
- data/service/ssh/lib/network_infra_utility/ssh/client.rb +257 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/schema.rb +90 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/settings.rb +103 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/store.rb +90 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/coalesce.rb +83 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/errors.rb +36 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/router.rb +212 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/transport.rb +81 -0
- data/service/ssh/lib/network_infra_utility/ssh/security/host_key.rb +211 -0
- data/service/ssh/lib/network_infra_utility/ssh/security/vault.rb +211 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/history.rb +56 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/manager.rb +92 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/session.rb +109 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/tree.rb +95 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/ansi_parser.rb +435 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/buffer.rb +78 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/emulator.rb +159 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/logger.rb +195 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/screen.rb +212 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/theme.rb +127 -0
- data/service/ssh/lib/network_infra_utility/ssh/version.rb +7 -0
- data/service/ssh/lib/network_infra_utility/ssh.rb +44 -0
- data/support/basic/as_num.rb +221 -0
- data/support/basic/mac_address.rb +281 -0
- data/version.rb +1 -1
- 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(¶ms, "conn_id")
|
|
58
|
+
.ok_or("missing conn_id")?
|
|
59
|
+
.to_string();
|
|
60
|
+
let ch_type = proto::get_str(¶ms, "type").unwrap_or("shell");
|
|
61
|
+
let cols = proto::get_num(¶ms, "cols").unwrap_or(80.0) as u32;
|
|
62
|
+
let rows = proto::get_num(¶ms, "rows").unwrap_or(24.0) as u32;
|
|
63
|
+
let term_type = proto::get_str(¶ms, "term").unwrap_or("xterm-256color");
|
|
64
|
+
let command = proto::get_str(¶ms, "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(¶ms, "id")
|
|
160
|
+
.ok_or("missing id")?
|
|
161
|
+
.to_string();
|
|
162
|
+
let b64_data = proto::get_str(¶ms, "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(¶ms, "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(¶ms, "id")
|
|
221
|
+
.ok_or("missing id")?
|
|
222
|
+
.to_string();
|
|
223
|
+
let cols = proto::get_num(¶ms, "cols").unwrap_or(80.0) as u32;
|
|
224
|
+
let rows = proto::get_num(¶ms, "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
|
+
}
|