【问题标题】:Redirect stdio over TLS in Rust在 Rust 中通过 TLS 重定向 stdio
【发布时间】:2021-02-08 08:42:27
【问题描述】:

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

我可以在 TcpStream 上使用 dup2 然后在 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()?;

【问题讨论】:

    标签: ssl rust file-descriptor stdio


    【解决方案1】:

    这个问题和这个答案并不是 Rust 特有的。

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

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

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


    编辑,在问题中的版本之后

    由于您现在有一个依赖于三个不同管道的解决方案,您可以忘记关于 socketpair() 的所有内容。

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

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

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

    从 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();
    }
    

    【讨论】:

    • 嗨@prog-fh,有人设法提供了一种通过管道重定向stdio的解决方案。现在我仍然不确定是否通过 TLS 发送该标准输出。
    • 嗨@localacct,我进行了相应的编辑以匹配您找到的(部分)解决方案。
    猜你喜欢
    • 1970-01-01
    • 2021-11-10
    • 1970-01-01
    • 1970-01-01
    • 2010-12-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多