在 Rust 中通过 TLS 重定向 stdio

Redirect stdio over TLS in Rust

我正在尝试复制 ncat 中的“-e”选项,以将 Rust 中的 stdio 重定向到远程 ncat 侦听器。

我可以通过使用 dup2 在 TcpStream 上完成,然后在 Rust 中执行“/bin/sh”命令。但是,我不知道如何通过 TLS 进行重定向,因为重定向似乎需要文件描述符,而 TlsStream 似乎没有提供。

任何人都可以对此提出建议吗?

编辑 2020 年 11 月 2 日

Rust 论坛中有人友好地与我分享了一个解决方案 (https://users.rust-lang.org/t/redirect-stdio-pipes-and-file-descriptors/50751/8),现在我正在努力研究如何通过 TLS 连接重定向 stdio。

let mut command_output = std::process::Command::new("/bin/sh")
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .stderr(Stdio::piped())
    .spawn()
    .expect("cannot execute command");

let mut command_stdin = command_output.stdin.unwrap();
println!("command_stdin {}", command_stdin.as_raw_fd());

let copy_stdin_thread = std::thread::spawn(move || {
    io::copy(&mut io::stdin(), &mut command_stdin)
});
        
let mut command_stdout = command_output.stdout.unwrap();
println!("command_stdout {}", command_stdout.as_raw_fd());

let copy_stdout_thread = std::thread::spawn(move || {
   io::copy(&mut command_stdout, &mut io::stdout())
});

let command_stderr = command_output.stderr.unwrap();
println!("command_stderr {}", command_stderr.as_raw_fd());

let copy_stderr_thread = std::thread::spawn(move || {
    io::copy(&mut command_stderr, &mut io::stderr())
});

copy_stdin_thread.join().unwrap()?;
copy_stdout_thread.join().unwrap()?;
copy_stderr_thread.join().unwrap()?;

这个问题和这个答案不是特定于 Rust 的。

您注意到一个重要的事实,即重定向进程的 I/O 必须是文件描述符。 您的应用程序中的一种可能解决方案是

  • 使用socketpair(PF_LOCAL, SOCK_STREAM, 0, fd)
    • 这提供了两个连接的双向文件描述符
  • 在此套接字对的一端使用 dup2() 用于重定向进程的 I/O(就像您对未加密的 TCP 流所做的那样)
  • 同时观看另一端和 TLS 流(例如,以 select() 类似的方式)以便
    • 从套接字对接收可用的内容并将其发送到 TLS 流,
    • 接收来自 TLS 流的可用内容并将其发送到套接字对。

请注意,select() 在 TLS 流(实际上是其底层文件描述符)上有点棘手,因为一些字节可能已经被接收(在其底层文件描述符上)并在内部缓冲区中解密,同时尚未被应用程序使用。 在尝试新的 select() 之前,您必须询问 TSL 流的接收缓冲区是否为空。 为此 watch/recv/send 循环使用异步或线程解决方案可能比依赖 select() 类解决方案更容易。


编辑,问题中编辑后

既然你现在有了一个依赖三个不同管道的解决方案,你就可以忘记关于 socketpair() 的一切。

在您的示例的每个线程中调用 std::io::copy() 是一个简单的循环,它从第一个参数接收一些字节并将它们发送到第二个。 您的 TlsStream 可能是执行所有加密的 I/O 操作(发送和接收)的单一结构,因此您将无法向多线程提供对它的 &mut 引用。

最好的方法可能是编写自己的循环来尝试检测新的传入字节,然后将它们分派到适当的目的地。 如上文所述,我会为此使用 select()。 不幸的是,据我所知,在 Rust 中,我们必须依赖 low-level 特性作为 libc(在异步世界中可能还有我不知道的其他高级解决方案......) .

为了展示主要思想,我在下面制作了一个(不是这样的)最小示例;它肯定远非完美,所以 « 小心处理 » ;^) (它依赖于native-tls and libc

从 openssl 访问它会得到这个

$ openssl s_client -connect localhost:9876
CONNECTED(00000003)
Can't use SSL_get_servername
...
    Extended master secret: yes
---
hello
/bin/sh: line 1: hello: command not found
df
Filesystem     1K-blocks      Used Available Use% Mounted on
dev              4028936         0   4028936   0% /dev
run              4038472      1168   4037304   1% /run
/dev/sda5       30832548  22074768   7168532  76% /
tmpfs            4038472    234916   3803556   6% /dev/shm
tmpfs               4096         0      4096   0% /sys/fs/cgroup
tmpfs            4038472         4   4038468   1% /tmp
/dev/sda6      338368556 219588980 101568392  69% /home
tmpfs             807692        56    807636   1% /run/user/9223
exit
read:errno=0
fn main() {
    let args: Vec<_> = std::env::args().collect();
    let use_simple = args.len() == 2 && args[1] == "s";

    let mut file = std::fs::File::open("server.pfx").unwrap();
    let mut identity = vec![];
    use std::io::Read;
    file.read_to_end(&mut identity).unwrap();
    let identity =
        native_tls::Identity::from_pkcs12(&identity, "dummy").unwrap();

    let listener = std::net::TcpListener::bind("0.0.0.0:9876").unwrap();
    let acceptor = native_tls::TlsAcceptor::new(identity).unwrap();
    let acceptor = std::sync::Arc::new(acceptor);

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                let acceptor = acceptor.clone();
                std::thread::spawn(move || {
                    let stream = acceptor.accept(stream).unwrap();
                    if use_simple {
                        simple_client(stream);
                    } else {
                        redirect_shell(stream);
                    }
                });
            }
            Err(_) => {
                println!("accept failure");
                break;
            }
        }
    }
}

fn simple_client(mut stream: native_tls::TlsStream<std::net::TcpStream>) {
    let mut buffer = [0_u8; 100];
    let mut count = 0;
    loop {
        use std::io::Read;
        if let Ok(sz_r) = stream.read(&mut buffer) {
            if sz_r == 0 {
                println!("EOF");
                break;
            }
            println!(
                "received <{}>",
                std::str::from_utf8(&buffer[0..sz_r]).unwrap_or("???")
            );
            let reply = format!("message {} is {} bytes long\n", count, sz_r);
            count += 1;
            use std::io::Write;
            if stream.write_all(reply.as_bytes()).is_err() {
                println!("write failure");
                break;
            }
        } else {
            println!("read failure");
            break;
        }
    }
}

fn redirect_shell(mut stream: native_tls::TlsStream<std::net::TcpStream>) {
    // start child process
    let mut child = std::process::Command::new("/bin/sh")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .expect("cannot execute command");
    // access useful I/O and file descriptors
    let stdin = child.stdin.as_mut().unwrap();
    let stdout = child.stdout.as_mut().unwrap();
    let stderr = child.stderr.as_mut().unwrap();
    use std::os::unix::io::AsRawFd;
    let stream_fd = stream.get_ref().as_raw_fd();
    let stdout_fd = stdout.as_raw_fd();
    let stderr_fd = stderr.as_raw_fd();
    // main send/recv loop
    use std::io::{Read, Write};
    let mut buffer = [0_u8; 100];
    loop {
        // no need to wait for new incoming bytes on tcp-stream
        // if some are already decoded in the tls-stream
        let already_buffered = match stream.buffered_read_size() {
            Ok(sz) if sz > 0 => true,
            _ => false,
        };
        // prepare file descriptors to be watched for by select()
        let mut fdset =
            unsafe { std::mem::MaybeUninit::uninit().assume_init() };
        let mut max_fd = -1;
        unsafe { libc::FD_ZERO(&mut fdset) };
        unsafe { libc::FD_SET(stdout_fd, &mut fdset) };
        max_fd = std::cmp::max(max_fd, stdout_fd);
        unsafe { libc::FD_SET(stderr_fd, &mut fdset) };
        max_fd = std::cmp::max(max_fd, stderr_fd);
        if !already_buffered {
            // see above
            unsafe { libc::FD_SET(stream_fd, &mut fdset) };
            max_fd = std::cmp::max(max_fd, stream_fd);
        }
        // block this thread until something new happens
        // on these file-descriptors (don't wait if some bytes
        // are already decoded in the tls-stream)
        let mut zero_timeout =
            unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
        unsafe {
            libc::select(
                max_fd + 1,
                &mut fdset,
                std::ptr::null_mut(),
                std::ptr::null_mut(),
                if already_buffered {
                    &mut zero_timeout
                } else {
                    std::ptr::null_mut()
                },
            )
        };
        // this thread is not blocked any more,
        // try to handle what happened on the file descriptors
        if unsafe { libc::FD_ISSET(stdout_fd, &mut fdset) } {
            // something new happened on stdout,
            // try to receive some bytes an send them through the tls-stream
            if let Ok(sz_r) = stdout.read(&mut buffer) {
                if sz_r == 0 {
                    println!("EOF detected on stdout");
                    break;
                }
                if stream.write_all(&buffer[0..sz_r]).is_err() {
                    println!("write failure on tls-stream");
                    break;
                }
            } else {
                println!("read failure on process stdout");
                break;
            }
        }
        if unsafe { libc::FD_ISSET(stderr_fd, &mut fdset) } {
            // something new happened on stderr,
            // try to receive some bytes an send them through the tls-stream
            if let Ok(sz_r) = stderr.read(&mut buffer) {
                if sz_r == 0 {
                    println!("EOF detected on stderr");
                    break;
                }
                if stream.write_all(&buffer[0..sz_r]).is_err() {
                    println!("write failure on tls-stream");
                    break;
                }
            } else {
                println!("read failure on process stderr");
                break;
            }
        }
        if already_buffered
            || unsafe { libc::FD_ISSET(stream_fd, &mut fdset) }
        {
            // something new happened on the tls-stream
            // (or some bytes were already buffered),
            // try to receive some bytes an send them on stdin
            if let Ok(sz_r) = stream.read(&mut buffer) {
                if sz_r == 0 {
                    println!("EOF detected on tls-stream");
                    break;
                }
                if stdin.write_all(&buffer[0..sz_r]).is_err() {
                    println!("write failure on stdin");
                    break;
                }
            } else {
                println!("read failure on tls-stream");
                break;
            }
        }
    }
    let _ = child.wait();
}