Перенаправить stdio через TLS в Rust

Я пытаюсь воспроизвести параметр -e в ncat, чтобы перенаправить stdio в Rust на удаленный слушатель ncat.

Я могу сделать это через TcpStream, используя dup2, а затем выполнив команду /bin/sh в Rust. Однако я не знаю, как это сделать через TLS, поскольку для перенаправления, похоже, требуются файловые дескрипторы, которые TlsStream, похоже, не предоставляет.

Кто-нибудь может посоветовать по этому поводу?

EDIT 2 Nov 2020

Кто-то на форуме Rust любезно поделился со мной решением (https://users.rust-lang.org/t/redirect-stdio-pipes-and-file-descriptors/50751/8), и теперь я пытаюсь работать над тем, как перенаправить stdio через соединение TLS.

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()?;

person localacct    schedule 26.10.2020    source источник


Ответы (1)


Этот вопрос и этот ответ не относятся к Rust.

Вы заметили важный факт, что ввод-вывод перенаправленного процесса должен быть файловым дескриптором. Одним из возможных решений в вашем приложении является

  • use socketpair(PF_LOCAL, SOCK_STREAM, 0, fd)
    • this provides two connected bidirectional file descriptors
  • используйте dup2() на одном конце этой пары сокетов для ввода-вывода перенаправленного процесса (как вы сделали бы с незашифрованным потоком TCP)
  • watch both the other end and the TLS stream (in a select()-like manner for example) in order to
    • receive what becomes available from the socketpair and send it to the TLS stream,
    • получать то, что становится доступным из потока TLS, и отправлять его в пару сокетов.

Обратите внимание, что select() в потоке TLS (на самом деле его базовый файловый дескриптор) немного сложна, потому что некоторые байты, возможно, уже были получены (в его базовом файловом дескрипторе) и расшифрованы во внутреннем буфере, пока еще не используются приложением. Вы должны спросить поток TSL, пуст ли его приемный буфер, прежде чем пробовать на нем новый select(). Использование асинхронного или многопоточного решения для этого цикла просмотра/получения/отправки, вероятно, проще, чем полагаться на решение, подобное select().


редактировать, после редакции в вопросе

Поскольку теперь у вас есть решение, основанное на трех разных каналах, вы можете забыть обо всем, что касается socketpair().

Вызов std::io::copy() в каждом потоке вашего примера является простой цикл, который получает несколько байтов из своего первого параметра и отправляет их второму. Ваш TlsStream, вероятно, представляет собой единую структуру, выполняющую все зашифрованные операции ввода-вывода (отправка и получение), поэтому вы не сможете предоставить ссылку &mut на него для ваших нескольких потоков.

Лучше всего, вероятно, написать свой собственный цикл, пытающийся обнаружить новые входящие байты, а затем отправить их в соответствующее место назначения. Как объяснялось выше, я бы использовал для этого select(). К сожалению, насколько я знаю, в Rust мы должны полагаться на низкоуровневые функции, такие как libc (могут быть другие высокоуровневые решения, о которых я не знаю в асинхронном мире...).

Я привел (не очень) минимальный пример ниже, чтобы показать основную идею; он, безусловно, далек от совершенства, поэтому «обращайтесь с осторожностью» ;^) (он опирается на native-tls и 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();
}
person prog-fh    schedule 26.10.2020
comment
Привет @prog-fh, кому-то удалось найти решение для перенаправления stdio по каналам. Теперь я все еще не уверен в отправке этого stdio через TLS. - person localacct; 02.11.2020
comment
Привет @localacct, я отредактировал соответствующим образом, чтобы оно соответствовало (частичному) решению, которое вы нашли. - person prog-fh; 02.11.2020