Rust反向Shell按上箭头无法实时回填历史命令解决方案咨询
问题根因
默认Linux终端工作在规范模式(Canonical Mode):内核会缓存所有输入直到遇到回车才返回给应用,同时自动回显输入字符,方向键、退格等特殊按键由内核预处理,不会交给上层程序处理,因此你无法实时捕获上箭头按键实现历史命令回填。
之前使用rustyline的缺陷是它完全接管了行输入逻辑,自带自定义提示符,会覆盖远端Shell返回的[root@arch ~]$这类原生提示符。
实现方案
要实现rlwrap -cAr nc -lvnp 55100的同等效果,无需引入rustyline、也不需要本地存储历史,只需按以下步骤实现:
- 切换终端为原始模式(Raw Mode):禁用规范模式、默认回显、特殊字符预处理,每个按键按下后会实时提交给程序处理
- 内存级维护三个状态:当前输入行缓冲区、光标位置、历史命令队列(完全在内存存储,程序退出后自动销毁,不会写入本地磁盘)
- 手动处理所有按键事件:
- 可打印字符:追加到输入缓冲区,手动回显到终端
- 退格键:删除缓冲区末尾字符,输出
\x08 \x08控制序列擦除终端上的对应字符 - 上/下方向键:切换到对应历史命令后,先擦除当前行的已输入内容,再输出历史命令内容,同步更新当前输入缓冲区
- 回车键:将当前缓冲区内容加
\n发送给远端Shell,把输入内容加入历史队列,清空当前缓冲区
- 程序退出前强制恢复终端默认配置,避免终端行为异常。
完整实现代码
首先Cargo.toml依赖:
[package] name = "reverse_shell_listener" version = "0.1.0" edition = "2021" [dependencies] nix = "0.26.2" ctrlc = "3.4.0"
主程序代码:
use nix::sys::termios; use std::io::{self, Read, Write}; use std::net::TcpListener; use std::thread; fn main() -> io::Result<()> { // 保存原始终端属性,退出时恢复 let orig_termios = termios::tcgetattr(io::stdin())?; let mut raw_termios = orig_termios.clone(); termios::cfmakeraw(&mut raw_termios); termios::tcsetattr(io::stdin(), termios::SetArg::TCSANOW, &raw_termios)?; // 退出回调,恢复终端属性 let orig_termios_clone = orig_termios.clone(); ctrlc::set_handler(move || { termios::tcsetattr(io::stdin(), termios::SetArg::TCSANOW, &orig_termios_clone).unwrap(); std::process::exit(0); }).unwrap(); // 启动监听 let listener = TcpListener::bind("0.0.0.0:55100")?; println!("Started listener"); let (mut stream, _) = listener.accept()?; println!("Connection received"); // 启动读线程:把远端Shell返回的内容输出到本地终端 let mut stream_clone = stream.try_clone()?; let orig_termios_clone = orig_termios.clone(); thread::spawn(move || { let mut buf = [0; 1024]; loop { let n = stream_clone.read(&mut buf).unwrap(); if n == 0 { println!("\r\nConnection lost"); termios::tcsetattr(io::stdin(), termios::SetArg::TCSANOW, &orig_termios_clone).unwrap(); std::process::exit(0x0100); } io::stdout().write_all(&buf[..n]).unwrap(); io::stdout().flush().unwrap(); } }); // 输入状态维护 let mut input_buf = String::new(); let mut history: Vec<String> = Vec::new(); let mut history_idx = 0; // 处理本地输入 let mut stdin = io::stdin(); let mut key_buf = [0; 4]; loop { let n = stdin.read(&mut key_buf)?; match &key_buf[..n] { // 回车键 b"\n" | b"\r" => { // 输出换行 print!("\r\n"); io::stdout().flush()?; // 发送到远端 stream.write_all(input_buf.as_bytes())?; stream.write_all(b"\n")?; stream.flush()?; // 加入历史,清空缓冲区 if !input_buf.is_empty() { history.push(input_buf.clone()); } input_buf.clear(); history_idx = history.len(); } // 退格键 b"\x7f" | b"\x08" => { if !input_buf.is_empty() { input_buf.pop(); // 擦除终端上的字符 print!("\x08 \x08"); io::stdout().flush()?; } } // 上箭头 b"\x1b[A" => { if !history.is_empty() && history_idx > 0 { history_idx -= 1; // 擦除当前行输入 for _ in 0..input_buf.len() { print!("\x08 \x08"); } // 加载历史命令 input_buf = history[history_idx].clone(); print!("{}", input_buf); io::stdout().flush()?; } } // 下箭头 b"\x1b[B" => { if history_idx < history.len() { history_idx += 1; // 擦除当前行输入 for _ in 0..input_buf.len() { print!("\x08 \x08"); } // 加载历史命令,到最新则为空 input_buf = if history_idx == history.len() { String::new() } else { history[history_idx].clone() }; print!("{}", input_buf); io::stdout().flush()?; } } // 普通可打印字符 _ => { if let Ok(s) = String::from_utf8(key_buf[..n].to_vec()) { input_buf.push_str(&s); print!("{}", s); io::stdout().flush()?; } } } } }
不需要ctrlc依赖的话可以删除对应逻辑,改用自定义信号处理即可,不影响核心功能。
内容的提问来源于stack exchange,提问作者user16139080
相关产品推荐
相关产品推荐

