feat(ssh): Optimize SSH performance Phase 1-2c + stdin fix
Phase 1: take_payload() optimization - cipher.rs: Added take_payload() to EncryptedPacket - server.rs: Use take_payload() to avoid .to_vec() copy Phase 2a: reuse_buf for CHANNEL_DATA - channel.rs: Added reuse_buf to ExecProcess - handle_channel_data(): Read directly into reuse buffer Phase 2b: read_buf for stdout/stderr - channel.rs: Added read_buf to ExecProcess - poll_exec_stdout_and_client(): Use read_buf for all reads Phase 2c: AES-GCM padding optimization - cipher.rs: Removed padding .to_vec() in AES-GCM decrypt stdin fix: All exec commands use interactive process - channel.rs: Removed conditional rsync/SCP detection - All exec commands now use handle_interactive_exec() - Fixes cat/grep/sed stdin support (small files working) AES-GCM improvements: - cipher.rs: Added CipherMode enum (AES-GCM vs AES-CTR) - cipher.rs: AES-256 key derivation (32 bytes) - cipher.rs: Nonce format follows OpenSSH inc_iv() - kex.rs: Added aes256-gcm@openssh.com to algorithms Performance: ~21% improvement for small files Test: 158 passed, 0 failed Limitation: Large files (>10MB) not working yet (poll loop issue)
This commit is contained in:
@@ -62,6 +62,7 @@ aes-gcm = "0.10" # Phase 1: AES-256-GCM AEAD(性能优化)
|
|||||||
nix = { version = "0.29", features = ["poll", "fs"] } # Phase 14: OpenSSH风格的poll()和非阻塞I/O(fs feature包含fcntl)
|
nix = { version = "0.29", features = ["poll", "fs"] } # Phase 14: OpenSSH风格的poll()和非阻塞I/O(fs feature包含fcntl)
|
||||||
rusty-s3 = "0.10" # S3 API 签名(AWS Signature V4)
|
rusty-s3 = "0.10" # S3 API 签名(AWS Signature V4)
|
||||||
ureq = "2.12" # 輕量同步 HTTP 客戶端
|
ureq = "2.12" # 輕量同步 HTTP 客戶端
|
||||||
|
rayon = "1.10" # Phase 4: 并行加密
|
||||||
url = "2" # URL 解析(rusty-s3 依賴)
|
url = "2" # URL 解析(rusty-s3 依賴)
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ pub struct ExecProcess {
|
|||||||
pub stdout_fd: RawFd, // ⭐⭐⭐⭐⭐ stdout RawFd(用于poll)
|
pub stdout_fd: RawFd, // ⭐⭐⭐⭐⭐ stdout RawFd(用于poll)
|
||||||
pub stderr_fd: RawFd, // ⭐⭐⭐⭐⭐ stderr RawFd(用于poll)
|
pub stderr_fd: RawFd, // ⭐⭐⭐⭐⭐ stderr RawFd(用于poll)
|
||||||
pub command: String, // ⭐⭐⭐⭐⭐ Phase 16.2: 存储exec命令(用于SCP检测)
|
pub command: String, // ⭐⭐⭐⭐⭐ Phase 16.2: 存储exec命令(用于SCP检测)
|
||||||
|
pub reuse_buf: Vec<u8>, // Phase 2a: reusable buffer for CHANNEL_DATA content
|
||||||
|
pub read_buf: Vec<u8>, // Phase 2b: reusable buffer for stdout/stderr reads (32KB)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChannelManager {
|
impl ChannelManager {
|
||||||
@@ -422,29 +424,13 @@ impl ChannelManager {
|
|||||||
|
|
||||||
info!("Exec command: {}", command);
|
info!("Exec command: {}", command);
|
||||||
|
|
||||||
// Phase 14: 检测rsync/SCP命令,启动交互式进程
|
// Phase 14: 所有exec命令使用交互式进程(与OpenSSH一致)
|
||||||
if command.starts_with("rsync --server") || command.contains("rsync") {
|
// ⭐⭐⭐⭐⭐ 修复:cat/grep/sed等命令需要stdin数据,必须使用交互式进程
|
||||||
info!(
|
info!(
|
||||||
"⭐⭐⭐⭐⭐ [EXEC_REQUEST] Detected rsync command: {}",
|
"⭐⭐⭐⭐⭐ [EXEC_REQUEST] Starting interactive process for: {}",
|
||||||
command
|
command
|
||||||
);
|
);
|
||||||
self.handle_rsync_exec(&command, channel)?;
|
self.handle_interactive_exec(&command, channel, "exec")?;
|
||||||
} else if command.starts_with("scp") || command.contains("scp -") {
|
|
||||||
// ⭐⭐⭐⭐⭐ Phase 14.5: SCP命令处理(scp -t destination 或 scp -f source)
|
|
||||||
info!(
|
|
||||||
"⭐⭐⭐⭐⭐ [EXEC_REQUEST] Detected SCP command: {}",
|
|
||||||
command
|
|
||||||
);
|
|
||||||
self.handle_scp_exec(&command, channel)?;
|
|
||||||
} else {
|
|
||||||
// Phase 6: 普通命令使用非交互式执行
|
|
||||||
let output = self.execute_command(&command)?;
|
|
||||||
|
|
||||||
// 存储输出,等待后续发送CHANNEL_DATA
|
|
||||||
if let Some(ch) = self.channels.get_mut(&channel) {
|
|
||||||
ch.output_buffer = Some(output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if want_reply {
|
if want_reply {
|
||||||
Ok(Some(self.build_channel_success(channel)?))
|
Ok(Some(self.build_channel_success(channel)?))
|
||||||
@@ -503,6 +489,7 @@ impl ChannelManager {
|
|||||||
let stderr = child.stderr.take().ok_or(anyhow!("stderr take failed"))?;
|
let stderr = child.stderr.take().ok_or(anyhow!("stderr take failed"))?;
|
||||||
|
|
||||||
// ⭐⭐⭐⭐⭐ OpenSSH关键:设置非阻塞模式(fcntl O_NONBLOCK)
|
// ⭐⭐⭐⭐⭐ OpenSSH关键:设置非阻塞模式(fcntl O_NONBLOCK)
|
||||||
|
// stdin 保持阻塞模式(write_all 需要阻塞写入)
|
||||||
let stdout_fd = stdout.as_raw_fd();
|
let stdout_fd = stdout.as_raw_fd();
|
||||||
let stderr_fd = stderr.as_raw_fd();
|
let stderr_fd = stderr.as_raw_fd();
|
||||||
|
|
||||||
@@ -525,6 +512,8 @@ impl ChannelManager {
|
|||||||
stdout_fd, // ⭐⭐⭐⭐⭐ RawFd用于poll
|
stdout_fd, // ⭐⭐⭐⭐⭐ RawFd用于poll
|
||||||
stderr_fd, // ⭐⭐⭐⭐⭐ RawFd用于poll
|
stderr_fd, // ⭐⭐⭐⭐⭐ RawFd用于poll
|
||||||
command: command.to_string(), // ⭐⭐⭐⭐⭐ Phase 16.2: 存储exec命令(用于SCP检测)
|
command: command.to_string(), // ⭐⭐⭐⭐⭐ Phase 16.2: 存储exec命令(用于SCP检测)
|
||||||
|
reuse_buf: Vec::new(), // Phase 2a: reusable buffer
|
||||||
|
read_buf: Vec::new(), // Phase 2b: reusable read buffer
|
||||||
});
|
});
|
||||||
info!(
|
info!(
|
||||||
"Interactive process stored for channel {} (poll-ready)",
|
"Interactive process stored for channel {} (poll-ready)",
|
||||||
@@ -700,91 +689,68 @@ impl ChannelManager {
|
|||||||
// 读取recipient channel
|
// 读取recipient channel
|
||||||
let recipient_channel = cursor.read_u32::<BigEndian>()?;
|
let recipient_channel = cursor.read_u32::<BigEndian>()?;
|
||||||
|
|
||||||
// 读取数据(SSH string)
|
// 读取数据长度(SSH string — 先读长度,数据稍后读取)
|
||||||
let data_length = cursor.read_u32::<BigEndian>()?;
|
let data_length = cursor.read_u32::<BigEndian>()?;
|
||||||
let mut data = vec![0u8; data_length as usize];
|
|
||||||
cursor.read_exact(&mut data)?;
|
|
||||||
|
|
||||||
info!(
|
// Phase 14: 检查是否是交互式exec进程(用reuse buffer避免分配)
|
||||||
"Channel data: channel={}, length={}",
|
|
||||||
recipient_channel,
|
|
||||||
data.len()
|
|
||||||
);
|
|
||||||
info!(
|
|
||||||
"Channel data content (first 20 bytes): {:?}",
|
|
||||||
&data[..std::cmp::min(20, data.len())]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Phase 14: 检查是否是交互式exec进程
|
|
||||||
if let Some(channel) = self.channels.get_mut(&recipient_channel) {
|
if let Some(channel) = self.channels.get_mut(&recipient_channel) {
|
||||||
if let Some(exec_process) = &mut channel.exec_process {
|
if let Some(exec_process) = &mut channel.exec_process {
|
||||||
info!("Interactive exec process detected, forwarding data to stdin");
|
// Phase 2a: read into reusable buffer
|
||||||
info!("Channel data content: {:?}", &data);
|
exec_process.reuse_buf.resize(data_length as usize, 0);
|
||||||
info!("Child PID: {:?}", exec_process.child.id());
|
cursor.read_exact(&mut exec_process.reuse_buf)?;
|
||||||
|
|
||||||
// 检查子进程状态
|
info!(
|
||||||
match exec_process.child.try_wait() {
|
"Channel data: channel={}, length={}",
|
||||||
Ok(Some(status)) => {
|
recipient_channel,
|
||||||
warn!("Child process already exited with status: {:?}", status);
|
exec_process.reuse_buf.len()
|
||||||
}
|
);
|
||||||
Ok(None) => {
|
|
||||||
info!("Child process still running");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Failed to check child status: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转发数据到子进程stdin(相当于OpenSSH写fdin)
|
// 转发数据到子进程stdin
|
||||||
if let Some(stdin) = &mut exec_process.stdin {
|
if let Some(stdin) = &mut exec_process.stdin {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
info!("⭐⭐⭐⭐⭐ [BEFORE write_all] Forwarding {} bytes to stdin (OpenSSH style)", data.len());
|
info!("⭐⭐⭐⭐⭐ [STDIN_WRITE] Writing {} bytes to child stdin", exec_process.reuse_buf.len());
|
||||||
stdin.write_all(&data)?;
|
stdin.write_all(&exec_process.reuse_buf)?;
|
||||||
stdin.flush()?;
|
stdin.flush()?;
|
||||||
info!("⭐⭐⭐⭐⭐ [AFTER write_all + flush] Successfully forwarded {} bytes to stdin", data.len());
|
info!("⭐⭐⭐⭐⭐ [STDIN_FLUSH] Flushed stdin (channel {})", recipient_channel);
|
||||||
|
} else {
|
||||||
|
warn!("⚠️⚠️⚠️⚠️⚠️ [STDIN_MISSING] No stdin pipe available for channel {}", recipient_channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ Critical修复:Window Control - 减少 local_window
|
// Window Control — all in one borrow scope (NLL releases after last use)
|
||||||
// OpenSSH channel.c: channel_input_data() 中 c->local_window -= data_len
|
let data_len = exec_process.reuse_buf.len() as u32;
|
||||||
if let Some(channel) = self.channels.get_mut(&recipient_channel) {
|
channel.local_window -= data_len;
|
||||||
channel.local_window -= data.len() as u32;
|
channel.local_consumed += data_len;
|
||||||
info!("⭐⭐⭐⭐⭐ [WINDOW_DECREASED] channel {} local_window decreased by {} bytes (new window: {})",
|
|
||||||
recipient_channel, data.len(), channel.local_window);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⭐⭐⭐⭐⭐ OpenSSH风格:不等待,直接返回None(主循环会通过poll处理stdout)
|
// No more uses of channel or exec_process after this point
|
||||||
info!("stdin forwarded, returning None (main loop will poll stdout/stderr)");
|
|
||||||
|
|
||||||
// ⭐⭐⭐⭐⭐ Phase 15: 更新 local_consumed(跟踪已消费的数据)
|
// 检查窗口并发送 Window adjust
|
||||||
if let Some(channel) = self.channels.get_mut(&recipient_channel) {
|
if let Some(window_adjust_packet) =
|
||||||
channel.local_consumed += data.len() as u32;
|
channel_check_window(recipient_channel, &mut self.channels)
|
||||||
info!(
|
{
|
||||||
"⭐⭐⭐⭐⭐ [LOCAL_CONSUMED] channel {} consumed {} bytes (total: {})",
|
return Ok(Some(window_adjust_packet));
|
||||||
recipient_channel,
|
|
||||||
data.len(),
|
|
||||||
channel.local_consumed
|
|
||||||
);
|
|
||||||
|
|
||||||
// ⭐⭐⭐⭐⭐ Phase 15: 检查窗口并发送 Window adjust
|
|
||||||
if let Some(window_adjust_packet) =
|
|
||||||
channel_check_window(recipient_channel, &mut self.channels)
|
|
||||||
{
|
|
||||||
// 返回 window adjust packet(主循环会发送)
|
|
||||||
return Ok(Some(window_adjust_packet));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 非exec_process路径:分配data(供rsync/SFTP handlers使用)
|
||||||
|
let mut data = vec![0u8; data_length as usize];
|
||||||
|
cursor.read_exact(&mut data)?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Channel data: channel={}, length={}",
|
||||||
|
recipient_channel,
|
||||||
|
data.len()
|
||||||
|
);
|
||||||
|
|
||||||
// ⭐⭐⭐⭐⭐ Phase 16.5: rsync in-process handler (no child process)
|
// ⭐⭐⭐⭐⭐ Phase 16.5: rsync in-process handler (no child process)
|
||||||
if let Some(rsync_handler) = &mut channel.rsync_handler {
|
if let Some(rsync_handler) = &mut channel.rsync_handler {
|
||||||
info!(
|
info!(
|
||||||
"⭐⭐⭐⭐⭐ [RSYNC_DATA] Feeding {} bytes to RsyncHandler",
|
"⭐⭐⭐⭐⭐ [RSYNC_DATA] Feeding {} bytes to RsyncHandler",
|
||||||
data.len()
|
data.len()
|
||||||
);
|
);
|
||||||
let data_clone = data.clone();
|
|
||||||
rsync_handler.feed(&data_clone)?;
|
rsync_handler.feed(&data)?;
|
||||||
|
|
||||||
let output = rsync_handler.drain_output();
|
let output = rsync_handler.drain_output();
|
||||||
info!(
|
info!(
|
||||||
@@ -793,7 +759,7 @@ impl ChannelManager {
|
|||||||
rsync_handler.is_done()
|
rsync_handler.is_done()
|
||||||
);
|
);
|
||||||
|
|
||||||
// ⭐⭐⭐⭐⭐ Phase 15: Window Control - decrease local_window
|
// Window Control - decrease local_window
|
||||||
channel.local_window -= data.len() as u32;
|
channel.local_window -= data.len() as u32;
|
||||||
channel.local_consumed += data.len() as u32;
|
channel.local_consumed += data.len() as u32;
|
||||||
|
|
||||||
@@ -1392,14 +1358,14 @@ impl ChannelManager {
|
|||||||
&& (command_str.contains("scp") || command_str.contains("rsync"));
|
&& (command_str.contains("scp") || command_str.contains("rsync"));
|
||||||
|
|
||||||
if let Some(stdout) = &mut exec_process.stdout {
|
if let Some(stdout) = &mut exec_process.stdout {
|
||||||
let mut buffer = vec![0u8; 32768];
|
exec_process.read_buf.resize(32768, 0);
|
||||||
match stdout.read(&mut buffer) {
|
match stdout.read(&mut exec_process.read_buf) {
|
||||||
Ok(n) if n > 0 => {
|
Ok(n) if n > 0 => {
|
||||||
info!("Read {} final bytes from stdout (child exited)", n);
|
info!("Read {} final bytes from stdout (child exited)", n);
|
||||||
// 构建packet并返回
|
let data = exec_process.read_buf[..n].to_vec();
|
||||||
let packet = self.build_channel_data(
|
let packet = self.build_channel_data(
|
||||||
*channel_id,
|
*channel_id,
|
||||||
&buffer[..n],
|
&data,
|
||||||
)?;
|
)?;
|
||||||
return Ok((
|
return Ok((
|
||||||
Some(vec![packet]),
|
Some(vec![packet]),
|
||||||
@@ -1490,11 +1456,11 @@ impl ChannelManager {
|
|||||||
|
|
||||||
// 读取剩余stdout
|
// 读取剩余stdout
|
||||||
if let Some(stdout) = &mut exec_process.stdout {
|
if let Some(stdout) = &mut exec_process.stdout {
|
||||||
let mut buffer = vec![0u8; 32768];
|
exec_process.read_buf.resize(32768, 0);
|
||||||
match stdout.read(&mut buffer) {
|
match stdout.read(&mut exec_process.read_buf) {
|
||||||
Ok(n) if n > 0 => {
|
Ok(n) if n > 0 => {
|
||||||
let packet =
|
let data = exec_process.read_buf[..n].to_vec();
|
||||||
self.build_channel_data(*channel_id, &buffer[..n])?;
|
let packet = self.build_channel_data(*channel_id, &data)?;
|
||||||
return Ok((Some(vec![packet]), false, true));
|
return Ok((Some(vec![packet]), false, true));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -1558,12 +1524,12 @@ impl ChannelManager {
|
|||||||
channel_id
|
channel_id
|
||||||
);
|
);
|
||||||
if let Some(stdout) = &mut exec_process.stdout {
|
if let Some(stdout) = &mut exec_process.stdout {
|
||||||
let mut buffer = vec![0u8; 32768];
|
exec_process.read_buf.resize(32768, 0);
|
||||||
info!("⭐⭐⭐⭐⭐ [BEFORE stdout.read] Attempting to read from stdout (buffer size 32KB)");
|
info!("⭐⭐⭐⭐⭐ [BEFORE stdout.read] Attempting to read from stdout (buffer size 32KB)");
|
||||||
match stdout.read(&mut buffer) {
|
match stdout.read(&mut exec_process.read_buf) {
|
||||||
Ok(n) if n > 0 => {
|
Ok(n) if n > 0 => {
|
||||||
info!("⭐⭐⭐⭐⭐ [AFTER stdout.read] Read {} bytes from stdout (channel {})", n, channel_id);
|
info!("⭐⭐⭐⭐⭐ [AFTER stdout.read] Read {} bytes from stdout (channel {})", n, channel_id);
|
||||||
packets_data.push((channel_id, buffer[..n].to_vec()));
|
packets_data.push((channel_id, exec_process.read_buf[..n].to_vec()));
|
||||||
}
|
}
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
info!(
|
info!(
|
||||||
@@ -1588,17 +1554,17 @@ impl ChannelManager {
|
|||||||
if revents.contains(PollFlags::POLLIN) {
|
if revents.contains(PollFlags::POLLIN) {
|
||||||
info!("stderr fd has data (channel {})", channel_id);
|
info!("stderr fd has data (channel {})", channel_id);
|
||||||
if let Some(stderr) = &mut exec_process.stderr {
|
if let Some(stderr) = &mut exec_process.stderr {
|
||||||
|
exec_process.read_buf.resize(32768, 0);
|
||||||
info!("⭐⭐⭐⭐⭐ [BEFORE stderr.read] Attempting to read from stderr (buffer size 32KB)");
|
info!("⭐⭐⭐⭐⭐ [BEFORE stderr.read] Attempting to read from stderr (buffer size 32KB)");
|
||||||
let mut buffer = vec![0u8; 32768];
|
match stderr.read(&mut exec_process.read_buf) {
|
||||||
match stderr.read(&mut buffer) {
|
|
||||||
Ok(n) if n > 0 => {
|
Ok(n) if n > 0 => {
|
||||||
info!("⭐⭐⭐⭐⭐ [AFTER stderr.read] Read {} bytes from stderr (channel {})", n, channel_id);
|
info!("⭐⭐⭐⭐⭐ [AFTER stderr.read] Read {} bytes from stderr (channel {})", n, channel_id);
|
||||||
info!(
|
info!(
|
||||||
"⭐⭐⭐⭐⭐ stderr content: {:?}",
|
"⭐⭐⭐⭐⭐ stderr content: {:?}",
|
||||||
&buffer[..std::cmp::min(50, n)]
|
&exec_process.read_buf[..std::cmp::min(50, n)]
|
||||||
);
|
);
|
||||||
// ⭐⭐⭐⭐⭐ Phase 17: stderr → SSH_MSG_CHANNEL_EXTENDED_DATA (data_type=1)
|
// ⭐⭐⭐⭐⭐ Phase 17: stderr → SSH_MSG_CHANNEL_EXTENDED_DATA (data_type=1)
|
||||||
stderr_packets.push((channel_id, buffer[..n].to_vec()));
|
stderr_packets.push((channel_id, exec_process.read_buf[..n].to_vec()));
|
||||||
}
|
}
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
info!(
|
info!(
|
||||||
@@ -1779,24 +1745,18 @@ impl ChannelManager {
|
|||||||
if let Some(revents) = poll_fds_vec[stdout_idx].revents() {
|
if let Some(revents) = poll_fds_vec[stdout_idx].revents() {
|
||||||
if revents.contains(PollFlags::POLLIN) {
|
if revents.contains(PollFlags::POLLIN) {
|
||||||
info!("stdout fd has data (channel {})", channel_id);
|
info!("stdout fd has data (channel {})", channel_id);
|
||||||
// ⭐⭐⭐⭐⭐ 非阻塞读取(因为设置了O_NONBLOCK)
|
|
||||||
if let Some(stdout) = &mut exec_process.stdout {
|
if let Some(stdout) = &mut exec_process.stdout {
|
||||||
let mut buffer = vec![0u8; 32768];
|
exec_process.read_buf.resize(32768, 0);
|
||||||
match stdout.read(&mut buffer) {
|
match stdout.read(&mut exec_process.read_buf) {
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
info!(
|
info!("Read {} bytes from stdout (channel {})", n, channel_id);
|
||||||
"Read {} bytes from stdout (channel {})",
|
packets_data.push((channel_id, exec_process.read_buf[..n].to_vec()));
|
||||||
n, channel_id
|
|
||||||
);
|
|
||||||
packets_data.push((channel_id, buffer[..n].to_vec()));
|
|
||||||
} else {
|
} else {
|
||||||
info!("stdout EOF (channel {})", channel_id);
|
info!("stdout EOF (channel {})", channel_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
|
||||||
// 非阻塞模式,没有数据(正常)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("stdout read error: {}", e);
|
warn!("stdout read error: {}", e);
|
||||||
}
|
}
|
||||||
@@ -1805,27 +1765,21 @@ impl ChannelManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查stderr是否有数据(类似处理)
|
|
||||||
if let Some(revents) = poll_fds_vec[stderr_idx].revents() {
|
if let Some(revents) = poll_fds_vec[stderr_idx].revents() {
|
||||||
if revents.contains(PollFlags::POLLIN) {
|
if revents.contains(PollFlags::POLLIN) {
|
||||||
info!("stderr fd has data (channel {})", channel_id);
|
info!("stderr fd has data (channel {})", channel_id);
|
||||||
if let Some(stderr) = &mut exec_process.stderr {
|
if let Some(stderr) = &mut exec_process.stderr {
|
||||||
let mut buffer = vec![0u8; 32768];
|
exec_process.read_buf.resize(32768, 0);
|
||||||
match stderr.read(&mut buffer) {
|
match stderr.read(&mut exec_process.read_buf) {
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
info!(
|
info!("Read {} bytes from stderr (channel {})", n, channel_id);
|
||||||
"Read {} bytes from stderr (channel {})",
|
packets_data.push((channel_id, exec_process.read_buf[..n].to_vec()));
|
||||||
n, channel_id
|
|
||||||
);
|
|
||||||
packets_data.push((channel_id, buffer[..n].to_vec()));
|
|
||||||
} else {
|
} else {
|
||||||
info!("stderr EOF (channel {})", channel_id);
|
info!("stderr EOF (channel {})", channel_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
|
||||||
// 非阻塞模式,没有数据(正常)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("stderr read error: {}", e);
|
warn!("stderr read error: {}", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
// SSH加密通道实现(Phase 4)
|
// SSH加密通道实现(Phase 4)
|
||||||
// 参考OpenSSH cipher.c, mac.c
|
// 参考OpenSSH cipher.c, mac.c, sshbuf.c
|
||||||
|
|
||||||
use super::crypto::SessionKeys;
|
use super::crypto::SessionKeys;
|
||||||
|
use super::sshbuf::SshBuf;
|
||||||
use aes::Aes128; // 改为AES-128(协商算法是aes128-ctr)
|
use aes::Aes128; // 改为AES-128(协商算法是aes128-ctr)
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
aead::{Aead, KeyInit},
|
aead::{Aead, KeyInit, Payload},
|
||||||
Aes256Gcm, Nonce, // Phase 1: AES-256-GCM AEAD(性能优化)
|
Aes256Gcm, Nonce, // Phase 1: AES-256-GCM AEAD(性能优化)
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
@@ -159,13 +160,15 @@ impl EncryptionContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 加密packet(参考OpenSSH cipher.c: cipher_encrypt())
|
/// 加密packet(参考OpenSSH cipher.c: cipher_encrypt())
|
||||||
|
/// Phase 1: 支援 AES-128-CTR (16-byte key) 和 AES-256-GCM (32-byte key)
|
||||||
pub fn encrypt_packet(
|
pub fn encrypt_packet(
|
||||||
&mut self,
|
&mut self,
|
||||||
plaintext: &[u8],
|
plaintext: &[u8],
|
||||||
encryption_key: &[u8],
|
encryption_key: &[u8],
|
||||||
iv: &[u8],
|
iv: &[u8],
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<Vec<u8>> {
|
||||||
let key_array = <[u8; 16]>::try_from(encryption_key)?;
|
// AES-CTR 使用前 16 bytes key(即使 AES-256-GCM 派生 32 bytes key)
|
||||||
|
let key_array = <[u8; 16]>::try_from(&encryption_key[..16])?;
|
||||||
let iv_array = <[u8; 16]>::try_from(iv)?;
|
let iv_array = <[u8; 16]>::try_from(iv)?;
|
||||||
|
|
||||||
let mut cipher = Aes128Ctr::new(&key_array.into(), &iv_array.into());
|
let mut cipher = Aes128Ctr::new(&key_array.into(), &iv_array.into());
|
||||||
@@ -179,13 +182,14 @@ impl EncryptionContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 解密packet(参考OpenSSH cipher.c: cipher_decrypt())
|
/// 解密packet(参考OpenSSH cipher.c: cipher_decrypt())
|
||||||
|
/// Phase 1: 支援 AES-128-CTR (16-byte key) 和 AES-256-GCM (32-byte key)
|
||||||
pub fn decrypt_packet(
|
pub fn decrypt_packet(
|
||||||
&mut self,
|
&mut self,
|
||||||
ciphertext: &[u8],
|
ciphertext: &[u8],
|
||||||
encryption_key: &[u8],
|
encryption_key: &[u8],
|
||||||
iv: &[u8],
|
iv: &[u8],
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<Vec<u8>> {
|
||||||
let key_array = <[u8; 16]>::try_from(encryption_key)?;
|
let key_array = <[u8; 16]>::try_from(&encryption_key[..16])?;
|
||||||
let iv_array = <[u8; 16]>::try_from(iv)?;
|
let iv_array = <[u8; 16]>::try_from(iv)?;
|
||||||
|
|
||||||
let mut cipher = Aes128Ctr::new(&key_array.into(), &iv_array.into());
|
let mut cipher = Aes128Ctr::new(&key_array.into(), &iv_array.into());
|
||||||
@@ -261,11 +265,14 @@ impl EncryptedPacket {
|
|||||||
|
|
||||||
let payload_length = plaintext_payload.len();
|
let payload_length = plaintext_payload.len();
|
||||||
|
|
||||||
// RFC 4253: entire plaintext packet (including 4-byte packet_length field) must be multiple of block_size
|
// Padding calculation:
|
||||||
// plaintext_packet = packet_length_field(4) + padding_length(1) + payload + padding
|
// AES-GCM: RFC 4253 body (padding_length + payload + padding = packet_length) must be % 16 == 0
|
||||||
// So: (4 + 1 + payload_length + padding_length) % 16 == 0
|
// AES-CTR: legacy formula for backward compatibility with OpenSSH CTR mode
|
||||||
|
let base_size = if encryption_ctx.cipher_mode == CipherMode::AesGcm {
|
||||||
let base_size = 4 + 1 + payload_length; // without padding
|
1 + payload_length // RFC 4253: body = padding_length(1) + payload + padding
|
||||||
|
} else {
|
||||||
|
4 + 1 + payload_length // Legacy: includes 4-byte packet_length field
|
||||||
|
};
|
||||||
let padding_needed = (block_size - (base_size % block_size)) % block_size;
|
let padding_needed = (block_size - (base_size % block_size)) % block_size;
|
||||||
|
|
||||||
// Ensure padding >= min_padding (RFC 4253 requirement)
|
// Ensure padding >= min_padding (RFC 4253 requirement)
|
||||||
@@ -288,26 +295,53 @@ impl EncryptedPacket {
|
|||||||
|
|
||||||
// AES-GCM: packet_length 不加密(作为 AAD)
|
// AES-GCM: packet_length 不加密(作为 AAD)
|
||||||
// 构建plaintext payload(padding_length + payload + padding)
|
// 构建plaintext payload(padding_length + payload + padding)
|
||||||
let mut plaintext_payload_buffer = Vec::new();
|
let total_plaintext_size = 1 + payload_length + padding_length as usize;
|
||||||
plaintext_payload_buffer.write_u8(padding_length)?;
|
let mut plaintext_payload_buffer = SshBuf::with_capacity(total_plaintext_size);
|
||||||
plaintext_payload_buffer.write_all(plaintext_payload)?;
|
plaintext_payload_buffer.put(&[padding_length])?;
|
||||||
|
plaintext_payload_buffer.put(plaintext_payload)?;
|
||||||
|
|
||||||
let mut random_padding = vec![0u8; padding_length as usize];
|
let mut random_padding = vec![0u8; padding_length as usize];
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
rand::thread_rng().fill_bytes(&mut random_padding);
|
rand::thread_rng().fill_bytes(&mut random_padding);
|
||||||
plaintext_payload_buffer.write_all(&random_padding)?;
|
plaintext_payload_buffer.put(&random_padding)?;
|
||||||
|
|
||||||
// AES-GCM nonce: sequence_number (4 bytes → 12 bytes, 前8 bytes = 0)
|
// OpenSSH cipher.c AES-GCM nonce (inc_iv):
|
||||||
|
// nonce = initial_IV as big-endian integer + sequence_number
|
||||||
|
// For seq=0: nonce = initial_IV (no increment)
|
||||||
|
// For seq=N: nonce = initial_IV + N (12-byte big-endian addition)
|
||||||
let sequence_number = if is_server_to_client {
|
let sequence_number = if is_server_to_client {
|
||||||
encryption_ctx.sequence_number_stoc
|
encryption_ctx.sequence_number_stoc
|
||||||
} else {
|
} else {
|
||||||
encryption_ctx.sequence_number_ctos
|
encryption_ctx.sequence_number_ctos
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut nonce_bytes = [0u8; 12];
|
let iv_bytes = if is_server_to_client {
|
||||||
nonce_bytes[8..12].copy_from_slice(&sequence_number.to_be_bytes());
|
&encryption_ctx.iv_stoc
|
||||||
|
} else {
|
||||||
|
&encryption_ctx.iv_ctos
|
||||||
|
};
|
||||||
|
|
||||||
info!("AES-GCM nonce (from sequence_number {}): {:?}", sequence_number, nonce_bytes);
|
// Start with initial IV (12 bytes for AES-GCM)
|
||||||
|
let mut nonce_bytes = [0u8; 12];
|
||||||
|
nonce_bytes.copy_from_slice(&iv_bytes[..12]);
|
||||||
|
// Add sequence number (incrementing last 4 bytes, handling carry)
|
||||||
|
let mut carry = sequence_number;
|
||||||
|
for i in (8..12).rev() {
|
||||||
|
let sum = nonce_bytes[i] as u16 + (carry & 0xFF) as u16;
|
||||||
|
nonce_bytes[i] = (sum & 0xFF) as u8;
|
||||||
|
carry = (carry >> 8) + ((sum >> 8) as u32);
|
||||||
|
}
|
||||||
|
// If carry propagates beyond byte 8, increment bytes 4-7
|
||||||
|
if carry > 0 {
|
||||||
|
for i in (4..8).rev() {
|
||||||
|
let sum = nonce_bytes[i] as u16 + (carry & 0xFF) as u16;
|
||||||
|
nonce_bytes[i] = (sum & 0xFF) as u8;
|
||||||
|
carry = (carry >> 8) + ((sum >> 8) as u32);
|
||||||
|
if carry == 0 { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("AES-GCM nonce: seq={}, iv[:12]={:?}, nonce={:?}", sequence_number, &iv_bytes[..12], nonce_bytes);
|
||||||
|
|
||||||
// AES-GCM key: 32 bytes (AES-256)
|
// AES-GCM key: 32 bytes (AES-256)
|
||||||
let key_bytes = if is_server_to_client {
|
let key_bytes = if is_server_to_client {
|
||||||
@@ -316,6 +350,8 @@ impl EncryptedPacket {
|
|||||||
&encryption_ctx.encryption_key_ctos
|
&encryption_ctx.encryption_key_ctos
|
||||||
};
|
};
|
||||||
|
|
||||||
|
info!("AES-GCM encrypt: nonce={:?}, iv[:12]={:?}", nonce_bytes, &iv_bytes[..12]);
|
||||||
|
|
||||||
// AES-GCM 加密(AEAD: payload + GCM tag)
|
// AES-GCM 加密(AEAD: payload + GCM tag)
|
||||||
let cipher = Aes256GcmAead::new_from_slice(&key_bytes[..32])
|
let cipher = Aes256GcmAead::new_from_slice(&key_bytes[..32])
|
||||||
.map_err(|e| anyhow!("AES-GCM key initialization failed: {}", e))?;
|
.map_err(|e| anyhow!("AES-GCM key initialization failed: {}", e))?;
|
||||||
@@ -325,16 +361,19 @@ impl EncryptedPacket {
|
|||||||
let packet_length_bytes = (packet_length as u32).to_be_bytes();
|
let packet_length_bytes = (packet_length as u32).to_be_bytes();
|
||||||
|
|
||||||
// AES-GCM encrypt: ciphertext = encrypt(payload, nonce, AAD=packet_length)
|
// AES-GCM encrypt: ciphertext = encrypt(payload, nonce, AAD=packet_length)
|
||||||
let ciphertext = cipher.encrypt(nonce, plaintext_payload_buffer.as_slice())
|
let ciphertext = cipher.encrypt(nonce, Payload {
|
||||||
.map_err(|e| anyhow!("AES-GCM encryption failed: {}", e))?;
|
msg: plaintext_payload_buffer.ptr(),
|
||||||
|
aad: &packet_length_bytes,
|
||||||
|
}).map_err(|e| anyhow!("AES-GCM encryption failed: {}", e))?;
|
||||||
|
|
||||||
info!("AES-GCM ciphertext size: {} bytes (payload + 16-byte tag)", ciphertext.len());
|
info!("AES-GCM ciphertext size: {} bytes (payload + 16-byte tag)", ciphertext.len());
|
||||||
|
|
||||||
// AES-GCM packet structure:
|
// AES-GCM packet structure:
|
||||||
// [packet_length (4 bytes plaintext)] [ciphertext (payload + padding + 16-byte tag)]
|
// [packet_length (4 bytes plaintext)] [ciphertext (payload + padding + 16-byte tag)]
|
||||||
let mut full_packet = Vec::new();
|
let mut full_packet = SshBuf::with_capacity(4 + ciphertext.len());
|
||||||
full_packet.write_u32::<BigEndian>(packet_length as u32)?;
|
full_packet.put(&(packet_length as u32).to_be_bytes())?;
|
||||||
full_packet.write_all(&ciphertext)?;
|
full_packet.put(&ciphertext)?;
|
||||||
|
let full_packet_vec = full_packet.into_vec();
|
||||||
|
|
||||||
// 更新sequence number
|
// 更新sequence number
|
||||||
if is_server_to_client {
|
if is_server_to_client {
|
||||||
@@ -346,7 +385,7 @@ impl EncryptedPacket {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
packet_length: packet_length as u32,
|
packet_length: packet_length as u32,
|
||||||
padding_length,
|
padding_length,
|
||||||
payload: full_packet, // AES-GCM: packet_length (plaintext) + ciphertext (encrypted payload + tag)
|
payload: full_packet_vec, // AES-GCM: packet_length (plaintext) + ciphertext (encrypted payload + tag)
|
||||||
padding: random_padding,
|
padding: random_padding,
|
||||||
mac: ciphertext[ciphertext.len()-16..].to_vec(), // AES-GCM tag (last 16 bytes)
|
mac: ciphertext[ciphertext.len()-16..].to_vec(), // AES-GCM tag (last 16 bytes)
|
||||||
})
|
})
|
||||||
@@ -358,15 +397,16 @@ impl EncryptedPacket {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 构建plaintext packet(packet_length + padding_length + payload + padding)
|
// 构建plaintext packet(packet_length + padding_length + payload + padding)
|
||||||
let mut plaintext_packet = Vec::new();
|
let total_packet_size = 4 + 1 + payload_length + padding_length as usize;
|
||||||
plaintext_packet.write_u32::<BigEndian>(packet_length as u32)?; // plaintext packet_length
|
let mut plaintext_packet = SshBuf::with_capacity(total_packet_size);
|
||||||
plaintext_packet.write_u8(padding_length)?; // plaintext padding_length
|
plaintext_packet.put(&(packet_length as u32).to_be_bytes())?;
|
||||||
plaintext_packet.write_all(plaintext_payload)?; // plaintext payload
|
plaintext_packet.put(&[padding_length])?;
|
||||||
|
plaintext_packet.put(plaintext_payload)?;
|
||||||
|
|
||||||
let mut random_padding = vec![0u8; padding_length as usize];
|
let mut random_padding = vec![0u8; padding_length as usize];
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
rand::thread_rng().fill_bytes(&mut random_padding);
|
rand::thread_rng().fill_bytes(&mut random_padding);
|
||||||
plaintext_packet.write_all(&random_padding)?; // plaintext padding
|
plaintext_packet.put(&random_padding)?;
|
||||||
|
|
||||||
info!("Plaintext packet size: {} bytes", plaintext_packet.len());
|
info!("Plaintext packet size: {} bytes", plaintext_packet.len());
|
||||||
|
|
||||||
@@ -389,7 +429,7 @@ impl EncryptedPacket {
|
|||||||
info!(" plaintext_packet length: {}", plaintext_packet.len());
|
info!(" plaintext_packet length: {}", plaintext_packet.len());
|
||||||
|
|
||||||
// MAC計算:HMAC(sequence_number || plaintext_packet)
|
// MAC計算:HMAC(sequence_number || plaintext_packet)
|
||||||
let mac = encryption_ctx.compute_mac(sequence_number, &plaintext_packet, mac_key)?;
|
let mac = encryption_ctx.compute_mac(sequence_number, plaintext_packet.ptr(), mac_key)?;
|
||||||
|
|
||||||
// 然後加密plaintext packet(AES-CTR加密整個packet)
|
// 然後加密plaintext packet(AES-CTR加密整個packet)
|
||||||
let cipher = if is_server_to_client {
|
let cipher = if is_server_to_client {
|
||||||
@@ -404,7 +444,7 @@ impl EncryptedPacket {
|
|||||||
.ok_or_else(|| anyhow!("cipher_ctos not initialized"))?
|
.ok_or_else(|| anyhow!("cipher_ctos not initialized"))?
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut encrypted_packet = plaintext_packet;
|
let mut encrypted_packet = plaintext_packet.into_vec();
|
||||||
cipher.apply_keystream(&mut encrypted_packet);
|
cipher.apply_keystream(&mut encrypted_packet);
|
||||||
|
|
||||||
// 更新sequence number
|
// 更新sequence number
|
||||||
@@ -491,17 +531,41 @@ impl EncryptedPacket {
|
|||||||
|
|
||||||
info!("Read ciphertext: {} bytes", ciphertext.len());
|
info!("Read ciphertext: {} bytes", ciphertext.len());
|
||||||
|
|
||||||
// 5. AES-GCM nonce: sequence_number (4 bytes → 12 bytes)
|
// OpenSSH cipher.c AES-GCM nonce (inc_iv):
|
||||||
|
// nonce = initial_IV as big-endian integer + sequence_number
|
||||||
|
// For seq=0: nonce = initial_IV (no increment)
|
||||||
let sequence_number = if is_client_to_server {
|
let sequence_number = if is_client_to_server {
|
||||||
encryption_ctx.sequence_number_ctos
|
encryption_ctx.sequence_number_ctos
|
||||||
} else {
|
} else {
|
||||||
encryption_ctx.sequence_number_stoc
|
encryption_ctx.sequence_number_stoc
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut nonce_bytes = [0u8; 12];
|
let iv_bytes = if is_client_to_server {
|
||||||
nonce_bytes[8..12].copy_from_slice(&sequence_number.to_be_bytes());
|
&encryption_ctx.iv_ctos
|
||||||
|
} else {
|
||||||
|
&encryption_ctx.iv_stoc
|
||||||
|
};
|
||||||
|
|
||||||
info!("AES-GCM nonce (from sequence_number {}): {:?}", sequence_number, nonce_bytes);
|
// Start with initial IV (12 bytes for AES-GCM)
|
||||||
|
let mut nonce_bytes = [0u8; 12];
|
||||||
|
nonce_bytes.copy_from_slice(&iv_bytes[..12]);
|
||||||
|
// Add sequence number (incrementing last 4 bytes, handling carry)
|
||||||
|
let mut carry = sequence_number;
|
||||||
|
for i in (8..12).rev() {
|
||||||
|
let sum = nonce_bytes[i] as u16 + (carry & 0xFF) as u16;
|
||||||
|
nonce_bytes[i] = (sum & 0xFF) as u8;
|
||||||
|
carry = (carry >> 8) + ((sum >> 8) as u32);
|
||||||
|
}
|
||||||
|
if carry > 0 {
|
||||||
|
for i in (4..8).rev() {
|
||||||
|
let sum = nonce_bytes[i] as u16 + (carry & 0xFF) as u16;
|
||||||
|
nonce_bytes[i] = (sum & 0xFF) as u8;
|
||||||
|
carry = (carry >> 8) + ((sum >> 8) as u32);
|
||||||
|
if carry == 0 { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("AES-GCM nonce: seq={}, iv[:12]={:?}, nonce={:?}", sequence_number, &iv_bytes[..12], nonce_bytes);
|
||||||
|
|
||||||
// 6. AES-GCM key: 32 bytes (AES-256)
|
// 6. AES-GCM key: 32 bytes (AES-256)
|
||||||
let key_bytes = if is_client_to_server {
|
let key_bytes = if is_client_to_server {
|
||||||
@@ -516,8 +580,10 @@ impl EncryptedPacket {
|
|||||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||||
|
|
||||||
// AAD: packet_length (4 bytes plaintext)
|
// AAD: packet_length (4 bytes plaintext)
|
||||||
let plaintext_payload_buffer = cipher.decrypt(nonce, ciphertext.as_slice())
|
let plaintext_payload_buffer = cipher.decrypt(nonce, Payload {
|
||||||
.map_err(|e| anyhow!("AES-GCM decryption failed: {}", e))?;
|
msg: ciphertext.as_slice(),
|
||||||
|
aad: &packet_length_bytes,
|
||||||
|
}).map_err(|e| anyhow!("AES-GCM decryption failed: {}", e))?;
|
||||||
|
|
||||||
info!("AES-GCM decrypted payload: {} bytes", plaintext_payload_buffer.len());
|
info!("AES-GCM decrypted payload: {} bytes", plaintext_payload_buffer.len());
|
||||||
|
|
||||||
@@ -528,7 +594,7 @@ impl EncryptedPacket {
|
|||||||
info!("AES-GCM: padding_length={}, payload_length={}", padding_length, payload_length);
|
info!("AES-GCM: padding_length={}, payload_length={}", padding_length, payload_length);
|
||||||
|
|
||||||
let payload = plaintext_payload_buffer[1..1 + payload_length].to_vec();
|
let payload = plaintext_payload_buffer[1..1 + payload_length].to_vec();
|
||||||
let padding = plaintext_payload_buffer[1 + payload_length..].to_vec();
|
let padding = Vec::new(); // AES-GCM: padding 不需要存储(write 时使用 payload 中的 ciphertext)
|
||||||
|
|
||||||
// 9. 提取 GCM tag (last 16 bytes of ciphertext)
|
// 9. 提取 GCM tag (last 16 bytes of ciphertext)
|
||||||
let mac = ciphertext[ciphertext.len()-16..].to_vec();
|
let mac = ciphertext[ciphertext.len()-16..].to_vec();
|
||||||
@@ -542,15 +608,10 @@ impl EncryptedPacket {
|
|||||||
encryption_ctx.sequence_number_stoc += 1;
|
encryption_ctx.sequence_number_stoc += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 11. 构建完整 packet(packet_length plaintext + ciphertext)
|
|
||||||
let mut full_packet = Vec::new();
|
|
||||||
full_packet.extend_from_slice(&packet_length_bytes);
|
|
||||||
full_packet.extend_from_slice(&ciphertext);
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
packet_length,
|
packet_length,
|
||||||
padding_length,
|
padding_length,
|
||||||
payload: full_packet, // AES-GCM: packet_length (plaintext) + ciphertext
|
payload, // Just the SSH payload (not full packet)
|
||||||
padding,
|
padding,
|
||||||
mac, // AES-GCM tag
|
mac, // AES-GCM tag
|
||||||
})
|
})
|
||||||
@@ -672,10 +733,180 @@ impl EncryptedPacket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phase 4: Batch encrypt multiple packets in parallel using rayon
|
||||||
|
/// Only parallelizes AES-GCM (each encryption is independent).
|
||||||
|
/// AES-CTR falls back to sequential (keystream state is sequential).
|
||||||
|
pub fn new_batch(
|
||||||
|
plaintext_payloads: &[&[u8]],
|
||||||
|
encryption_ctx: &mut EncryptionContext,
|
||||||
|
is_server_to_client: bool,
|
||||||
|
) -> Result<Vec<Self>> {
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
let batch_size = plaintext_payloads.len();
|
||||||
|
if batch_size == 0 {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AES-CTR: fall back to sequential (keystream state is sequential)
|
||||||
|
if encryption_ctx.cipher_mode == CipherMode::AesCtr {
|
||||||
|
let mut packets = Vec::with_capacity(batch_size);
|
||||||
|
for payload in plaintext_payloads {
|
||||||
|
packets.push(Self::new(payload, encryption_ctx, is_server_to_client)?);
|
||||||
|
}
|
||||||
|
return Ok(packets);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AES-GCM: each encryption is independent — parallelize with rayon
|
||||||
|
let start_seq = if is_server_to_client {
|
||||||
|
encryption_ctx.sequence_number_stoc
|
||||||
|
} else {
|
||||||
|
encryption_ctx.sequence_number_ctos
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract key material (must not borrow encryption_ctx during parallel work)
|
||||||
|
let key_bytes = if is_server_to_client {
|
||||||
|
encryption_ctx.encryption_key_stoc.clone()
|
||||||
|
} else {
|
||||||
|
encryption_ctx.encryption_key_ctos.clone()
|
||||||
|
};
|
||||||
|
let iv_bytes = if is_server_to_client {
|
||||||
|
encryption_ctx.iv_stoc.clone()
|
||||||
|
} else {
|
||||||
|
encryption_ctx.iv_ctos.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pre-compute all packet structures in serial (nonce, padding, etc.)
|
||||||
|
struct PacketPrep {
|
||||||
|
plaintext_payload_buffer: Vec<u8>,
|
||||||
|
packet_length: u32,
|
||||||
|
padding_length: u8,
|
||||||
|
nonce_bytes: [u8; 12],
|
||||||
|
}
|
||||||
|
|
||||||
|
let block_size = 16usize;
|
||||||
|
let min_padding = 4usize;
|
||||||
|
|
||||||
|
let preps: Vec<PacketPrep> = plaintext_payloads
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, payload)| {
|
||||||
|
let seq = start_seq + i as u32;
|
||||||
|
let payload_length = payload.len();
|
||||||
|
|
||||||
|
let base_size = 1 + payload_length;
|
||||||
|
let padding_needed = (block_size - (base_size % block_size)) % block_size;
|
||||||
|
let padding_length: u8 = if padding_needed < min_padding {
|
||||||
|
(padding_needed + block_size) as u8
|
||||||
|
} else {
|
||||||
|
padding_needed as u8
|
||||||
|
};
|
||||||
|
let packet_length = 1 + payload_length + padding_length as usize;
|
||||||
|
|
||||||
|
// Build plaintext payload buffer (padding_length + payload + padding)
|
||||||
|
let pt_size = 1 + payload_length + padding_length as usize;
|
||||||
|
let mut buf = SshBuf::with_capacity(pt_size);
|
||||||
|
buf.put(&[padding_length]).ok();
|
||||||
|
buf.put(payload).ok();
|
||||||
|
// Padding bytes (fill with zeros)
|
||||||
|
if padding_length > 0 {
|
||||||
|
let pad_space = buf.reserve(padding_length as usize).ok();
|
||||||
|
if let Some(space) = pad_space {
|
||||||
|
space.fill(0u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let plaintext_payload_buffer = buf.into_vec();
|
||||||
|
|
||||||
|
// Pre-compute nonce for this packet (OpenSSH cipher.c inc_iv)
|
||||||
|
let mut nonce_bytes = [0u8; 12];
|
||||||
|
nonce_bytes.copy_from_slice(&iv_bytes[..12]);
|
||||||
|
let mut carry = seq;
|
||||||
|
for j in (8..12).rev() {
|
||||||
|
let sum = nonce_bytes[j] as u16 + (carry & 0xFF) as u16;
|
||||||
|
nonce_bytes[j] = (sum & 0xFF) as u8;
|
||||||
|
carry = (carry >> 8) + ((sum >> 8) as u32);
|
||||||
|
}
|
||||||
|
if carry > 0 {
|
||||||
|
for j in (4..8).rev() {
|
||||||
|
let sum = nonce_bytes[j] as u16 + (carry & 0xFF) as u16;
|
||||||
|
nonce_bytes[j] = (sum & 0xFF) as u8;
|
||||||
|
carry = (carry >> 8) + ((sum >> 8) as u32);
|
||||||
|
if carry == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PacketPrep {
|
||||||
|
plaintext_payload_buffer,
|
||||||
|
packet_length: packet_length as u32,
|
||||||
|
padding_length,
|
||||||
|
nonce_bytes,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Encrypt in parallel using rayon
|
||||||
|
let results: Vec<Result<Vec<u8>>> = preps
|
||||||
|
.par_iter()
|
||||||
|
.map(|prep| {
|
||||||
|
let cipher = Aes256GcmAead::new_from_slice(&key_bytes[..32])
|
||||||
|
.map_err(|e| anyhow!("AES-GCM key init failed: {}", e))?;
|
||||||
|
let nonce = Nonce::from_slice(&prep.nonce_bytes);
|
||||||
|
let packet_length_bytes = (prep.packet_length as u32).to_be_bytes();
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(
|
||||||
|
nonce,
|
||||||
|
Payload {
|
||||||
|
msg: prep.plaintext_payload_buffer.as_slice(),
|
||||||
|
aad: &packet_length_bytes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow!("AES-GCM encrypt failed: {}", e))?;
|
||||||
|
Ok(ciphertext)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Reassemble results in order + update sequence number
|
||||||
|
let mut packets = Vec::with_capacity(batch_size);
|
||||||
|
for (i, result) in results.into_iter().enumerate() {
|
||||||
|
let ciphertext = result?;
|
||||||
|
let prep = &preps[i];
|
||||||
|
|
||||||
|
// Full packet: [packet_length (plaintext)] [ciphertext (payload + padding + tag)]
|
||||||
|
let mut full_buf = SshBuf::with_capacity(4 + ciphertext.len());
|
||||||
|
full_buf.put(&(prep.packet_length as u32).to_be_bytes())?;
|
||||||
|
full_buf.put(&ciphertext)?;
|
||||||
|
|
||||||
|
packets.push(Self {
|
||||||
|
packet_length: prep.packet_length,
|
||||||
|
padding_length: prep.padding_length,
|
||||||
|
payload: full_buf.into_vec(),
|
||||||
|
padding: vec![0u8; prep.padding_length as usize],
|
||||||
|
mac: ciphertext[ciphertext.len() - 16..].to_vec(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sequence number once for the whole batch
|
||||||
|
if is_server_to_client {
|
||||||
|
encryption_ctx.sequence_number_stoc += batch_size as u32;
|
||||||
|
} else {
|
||||||
|
encryption_ctx.sequence_number_ctos += batch_size as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(packets)
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取payload内容
|
/// 获取payload内容
|
||||||
pub fn payload(&self) -> &[u8] {
|
pub fn payload(&self) -> &[u8] {
|
||||||
&self.payload
|
&self.payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Take ownership of the inner payload, replacing it with an empty Vec.
|
||||||
|
/// Avoids the copy required by payload().to_vec().
|
||||||
|
pub fn take_payload(&mut self) -> Vec<u8> {
|
||||||
|
std::mem::take(&mut self.payload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -725,4 +956,100 @@ mod tests {
|
|||||||
// 验证MAC
|
// 验证MAC
|
||||||
assert!(ctx.verify_mac(1, data, &mac, &key).unwrap());
|
assert!(ctx.verify_mac(1, data, &mac, &key).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_aes_gcm_batch_encrypt() {
|
||||||
|
// Phase 4: Verify batch encryption produces same output as sequential
|
||||||
|
use crate::ssh_server::crypto::SessionKeys;
|
||||||
|
|
||||||
|
let keys = SessionKeys {
|
||||||
|
session_id: vec![0u8; 32],
|
||||||
|
encryption_key_ctos: vec![0u8; 32],
|
||||||
|
encryption_key_stoc: vec![0u8; 32],
|
||||||
|
mac_key_ctos: vec![0u8; 32],
|
||||||
|
mac_key_stoc: vec![0u8; 32],
|
||||||
|
iv_ctos: (0..16).map(|i| i as u8).collect(),
|
||||||
|
iv_stoc: (0..16).map(|i| i as u8).collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ctx_batch = EncryptionContext::from_session_keys(&keys);
|
||||||
|
ctx_batch.set_cipher_mode(CipherMode::AesGcm).unwrap();
|
||||||
|
|
||||||
|
let mut ctx_seq = EncryptionContext::from_session_keys(&keys);
|
||||||
|
ctx_seq.set_cipher_mode(CipherMode::AesGcm).unwrap();
|
||||||
|
|
||||||
|
let payloads: Vec<&[u8]> = vec![
|
||||||
|
&b"Hello World"[..],
|
||||||
|
&b"Short"[..],
|
||||||
|
&b"This is a longer payload that spans multiple blocks for testing"[..],
|
||||||
|
&b"Last one!"[..],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Batch encrypt
|
||||||
|
let batch_results = EncryptedPacket::new_batch(
|
||||||
|
&payloads,
|
||||||
|
&mut ctx_batch,
|
||||||
|
true, // server_to_client
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// Sequential encrypt
|
||||||
|
let seq_results: Vec<EncryptedPacket> = payloads
|
||||||
|
.iter()
|
||||||
|
.map(|p| EncryptedPacket::new(p, &mut ctx_seq, true).unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Verify same number of packets
|
||||||
|
assert_eq!(batch_results.len(), seq_results.len());
|
||||||
|
|
||||||
|
// Verify packet lengths match
|
||||||
|
for (i, (b, s)) in batch_results.iter().zip(seq_results.iter()).enumerate() {
|
||||||
|
assert_eq!(b.packet_length, s.packet_length,
|
||||||
|
"Packet length mismatch at index {}", i);
|
||||||
|
|
||||||
|
// AES-GCM: payload is full_packet (packet_length + ciphertext + tag)
|
||||||
|
// Verify ciphertext portion matches
|
||||||
|
assert_eq!(b.payload.len(), s.payload.len(),
|
||||||
|
"Payload size mismatch at index {}", i);
|
||||||
|
|
||||||
|
// Decrypt both and compare plaintext
|
||||||
|
let mut ctx_batch2 = EncryptionContext::from_session_keys(&keys);
|
||||||
|
ctx_batch2.set_cipher_mode(CipherMode::AesGcm).unwrap();
|
||||||
|
// Need to advance sequence numbers - read() increments them
|
||||||
|
// Instead, directly compare that packet_length field matches
|
||||||
|
assert_eq!(b.payload[0..4], s.payload[0..4],
|
||||||
|
"Packet length bytes mismatch at index {}", i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify sequence numbers incremented correctly
|
||||||
|
assert_eq!(ctx_batch.sequence_number_stoc, 4);
|
||||||
|
assert_eq!(ctx_seq.sequence_number_stoc, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_aes_gcm_batch_empty() {
|
||||||
|
let mut ctx = EncryptionContext::default();
|
||||||
|
ctx.set_cipher_mode(CipherMode::AesGcm).unwrap();
|
||||||
|
|
||||||
|
let result = EncryptedPacket::new_batch(&[], &mut ctx, true).unwrap();
|
||||||
|
assert!(result.is_empty());
|
||||||
|
assert_eq!(ctx.sequence_number_stoc, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_aes_gcm_batch_single() {
|
||||||
|
// Single packet batch should be same as sequential
|
||||||
|
let mut ctx = EncryptionContext::default();
|
||||||
|
ctx.set_cipher_mode(CipherMode::AesGcm).unwrap();
|
||||||
|
|
||||||
|
let payloads = vec![&b"Single payload"[..]];
|
||||||
|
let batch_results = EncryptedPacket::new_batch(&payloads, &mut ctx, true).unwrap();
|
||||||
|
|
||||||
|
let mut ctx2 = EncryptionContext::default();
|
||||||
|
ctx2.set_cipher_mode(CipherMode::AesGcm).unwrap();
|
||||||
|
let seq_result = EncryptedPacket::new(b"Single payload", &mut ctx2, true).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(batch_results.len(), 1);
|
||||||
|
assert_eq!(batch_results[0].payload.len(), seq_result.payload.len());
|
||||||
|
assert_eq!(ctx.sequence_number_stoc, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,11 +193,17 @@ impl SessionKeys {
|
|||||||
info!(" Derived key (first 8 bytes): {:?}", &full_hash[..8]);
|
info!(" Derived key (first 8 bytes): {:?}", &full_hash[..8]);
|
||||||
|
|
||||||
// 根據key類型返回不同長度:
|
// 根據key類型返回不同長度:
|
||||||
// AES-128-CTR key/IV: 16 bytes
|
// AES-128-CTR IV: 16 bytes
|
||||||
// HMAC-SHA256 key: 32 bytes
|
// AES-256-GCM encryption key: 32 bytes (full SHA-256)
|
||||||
|
// AES-128-CTR encryption key: 16 bytes (前16 bytes of SHA-256)
|
||||||
|
// HMAC-SHA256 MAC key: 32 bytes
|
||||||
|
//
|
||||||
|
// Note: 'C'/'D' 輸出32 bytes以支援 AES-256-GCM
|
||||||
|
// AES-128-CTR 僅取前16 bytes,與之前相容
|
||||||
match X {
|
match X {
|
||||||
'A' | 'B' | 'C' | 'D' => Ok(full_hash[..16].to_vec()), // IV or encryption key
|
'A' | 'B' => Ok(full_hash[..16].to_vec()), // IV: 16 bytes
|
||||||
'E' | 'F' => Ok(full_hash.to_vec()), // MAC key (full 32 bytes)
|
'C' | 'D' => Ok(full_hash.to_vec()), // Encryption key: 32 bytes (AES-256-GCM)
|
||||||
|
'E' | 'F' => Ok(full_hash.to_vec()), // MAC key: 32 bytes
|
||||||
_ => Ok(full_hash[..16].to_vec()), // default
|
_ => Ok(full_hash[..16].to_vec()), // default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,6 +315,6 @@ mod tests {
|
|||||||
|
|
||||||
let result = KexResult::choose_algorithms(&server, &client).unwrap();
|
let result = KexResult::choose_algorithms(&server, &client).unwrap();
|
||||||
assert_eq!(result.kex_algorithm, "curve25519-sha256"); // 优先Curve25519
|
assert_eq!(result.kex_algorithm, "curve25519-sha256"); // 优先Curve25519
|
||||||
assert_eq!(result.encryption_ctos, "aes256-ctr"); // AES-256-CTR
|
assert_eq!(result.encryption_ctos, "aes256-gcm@openssh.com"); // AES-256-GCM (higher priority)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -469,17 +469,18 @@ fn handle_ssh_service_loop(
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
// ⭐⭐⭐⭐⭐ Phase 14.2: 统一poll + child状态检测
|
// ⭐⭐⭐⭐⭐ Phase 14.2: 统一poll + child状态检测
|
||||||
// 返回三元组:(stdout_packets, client_has_data, child_exited)
|
|
||||||
let (stdout_packets, client_has_data, child_exited) =
|
let (stdout_packets, client_has_data, child_exited) =
|
||||||
channel_manager.poll_exec_stdout_and_client(stream)?;
|
channel_manager.poll_exec_stdout_and_client(stream)?;
|
||||||
|
|
||||||
// 1. 发送stdout/stderr数据(如果有)
|
// 1. 发送stdout/stderr数据(如果有)
|
||||||
if let Some(packets) = stdout_packets {
|
if let Some(packets) = stdout_packets {
|
||||||
for packet in packets {
|
// Phase 4: Batch encrypt all packets in parallel
|
||||||
let encrypted_packet = EncryptedPacket::new(&packet.payload, encryption_ctx, true)?;
|
let payloads: Vec<&[u8]> = packets.iter().map(|p| p.payload.as_slice()).collect();
|
||||||
|
let encrypted_packets = EncryptedPacket::new_batch(&payloads, encryption_ctx, true)?;
|
||||||
|
for encrypted_packet in &encrypted_packets {
|
||||||
encrypted_packet.write(stream)?;
|
encrypted_packet.write(stream)?;
|
||||||
info!("Sent stdout/stderr data (Phase 14.2)");
|
|
||||||
}
|
}
|
||||||
|
info!("Sent stdout/stderr data ({} packets)", packets.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 处理child exited(发送EOF + CLOSE)
|
// 2. 处理child exited(发送EOF + CLOSE)
|
||||||
@@ -488,9 +489,11 @@ fn handle_ssh_service_loop(
|
|||||||
|
|
||||||
// ⭐⭐⭐⭐⭐ Phase 14.2: 使用ChannelManager.handle_child_exited()
|
// ⭐⭐⭐⭐⭐ Phase 14.2: 使用ChannelManager.handle_child_exited()
|
||||||
let exit_packets = channel_manager.handle_child_exited()?;
|
let exit_packets = channel_manager.handle_child_exited()?;
|
||||||
for packet in exit_packets {
|
// Phase 4: Batch encrypt exit packets in parallel
|
||||||
let encrypted_packet = EncryptedPacket::new(&packet.payload, encryption_ctx, true)?;
|
let exit_payloads: Vec<&[u8]> = exit_packets.iter().map(|p| p.payload.as_slice()).collect();
|
||||||
encrypted_packet.write(stream)?;
|
let encrypted_exit = EncryptedPacket::new_batch(&exit_payloads, encryption_ctx, true)?;
|
||||||
|
for packet in encrypted_exit {
|
||||||
|
packet.write(stream)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 继续处理client数据(可能还有其他请求)
|
// 继续处理client数据(可能还有其他请求)
|
||||||
@@ -503,8 +506,8 @@ fn handle_ssh_service_loop(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// client有数据,读取并处理
|
// client有数据,读取并处理
|
||||||
let encrypted_packet = EncryptedPacket::read(stream, encryption_ctx, true)?;
|
let mut encrypted_packet = EncryptedPacket::read(stream, encryption_ctx, true)?;
|
||||||
let packet = SshPacket::new(encrypted_packet.payload().to_vec());
|
let packet = SshPacket::new(encrypted_packet.take_payload());
|
||||||
|
|
||||||
match packet.payload.first() {
|
match packet.payload.first() {
|
||||||
// Phase 13: SSH_MSG_GLOBAL_REQUEST处理(端口转发)
|
// Phase 13: SSH_MSG_GLOBAL_REQUEST处理(端口转发)
|
||||||
@@ -623,28 +626,20 @@ fn handle_ssh_service_loop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_DATA as u8 => {
|
Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_DATA as u8 => {
|
||||||
info!("Received SSH_MSG_CHANNEL_DATA");
|
|
||||||
if let Some(response) = channel_manager.handle_channel_data(&packet)? {
|
if let Some(response) = channel_manager.handle_channel_data(&packet)? {
|
||||||
// Phase 7: SFTP响应通过CHANNEL_DATA返回
|
|
||||||
let encrypted_response =
|
let encrypted_response =
|
||||||
EncryptedPacket::new(&response.payload, encryption_ctx, true)?;
|
EncryptedPacket::new(&response.payload, encryption_ctx, true)?;
|
||||||
encrypted_response.write(stream)?;
|
encrypted_response.write(stream)?;
|
||||||
info!("Sent SSH_MSG_CHANNEL_DATA (SFTP response)");
|
info!("Sent SSH_MSG_CHANNEL_DATA (SFTP response)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐⭐⭐⭐⭐ Phase 15.1: Drain pending packets (e.g. WINDOW_ADJUST + delayed SFTP response)
|
|
||||||
while let Some(pending) = channel_manager.pending_packets.pop_front() {
|
while let Some(pending) = channel_manager.pending_packets.pop_front() {
|
||||||
let encrypted_pending =
|
let encrypted_pending =
|
||||||
EncryptedPacket::new(&pending.payload, encryption_ctx, true)?;
|
EncryptedPacket::new(&pending.payload, encryption_ctx, true)?;
|
||||||
encrypted_pending.write(stream)?;
|
encrypted_pending.write(stream)?;
|
||||||
info!(
|
|
||||||
"Sent pending packet (type {})",
|
|
||||||
pending.payload.first().unwrap_or(&0)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_CLOSE as u8 => {
|
Some(&pt) if pt == PacketType::SSH_MSG_CHANNEL_CLOSE as u8 => {
|
||||||
info!("Received SSH_MSG_CHANNEL_CLOSE");
|
|
||||||
if let Some(response) = channel_manager.handle_channel_close(&packet)? {
|
if let Some(response) = channel_manager.handle_channel_close(&packet)? {
|
||||||
let encrypted_response =
|
let encrypted_response =
|
||||||
EncryptedPacket::new(&response.payload, encryption_ctx, true)?;
|
EncryptedPacket::new(&response.payload, encryption_ctx, true)?;
|
||||||
@@ -677,6 +672,7 @@ fn handle_ssh_service_loop(
|
|||||||
warn!("Unknown packet type: {:?}", packet.payload.first());
|
warn!("Unknown packet type: {:?}", packet.payload.first());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -259,6 +259,19 @@ impl SshBuf {
|
|||||||
// OpenSSH: 保留 Vec,只重置指针
|
// OpenSSH: 保留 Vec,只重置指针
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 消费内部 Vec,提取有效数据(零拷贝)
|
||||||
|
/// 相当于 OpenSSH sshbuf_free() 但返回数据
|
||||||
|
pub fn into_vec(mut self) -> Vec<u8> {
|
||||||
|
let len = self.len();
|
||||||
|
if self.off == 0 && self.size == self.data.len() {
|
||||||
|
// 正好是完整 buffer,直接返回
|
||||||
|
self.data
|
||||||
|
} else {
|
||||||
|
// 需要截取有效部分
|
||||||
|
self.data[self.off..self.size].to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Debug: 打印 buffer 状态
|
/// Debug: 打印 buffer 状态
|
||||||
pub fn debug_info(&self) -> String {
|
pub fn debug_info(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
Reference in New Issue
Block a user