-
Notifications
You must be signed in to change notification settings - Fork 65
Add TLS/TCP and DTLS/UDP support for both server and client #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
db77ba8
63c69ad
9eee810
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| pub mod tls; | ||
| use colored::Colorize; | ||
| use rustyline::error::ReadlineError; | ||
| use std::io::{stdin, stdout, Read, Result, Write}; | ||
|
|
@@ -17,8 +18,12 @@ pub struct Opts { | |
| pub exec: Option<String>, | ||
| pub block_signals: bool, | ||
| pub mode: Mode, | ||
| pub protocol: crate::input::Protocol, | ||
| pub cert: Option<String>, | ||
| pub key: Option<String>, | ||
| } | ||
|
|
||
| #[derive(Debug, Clone, Copy)] | ||
| pub enum Mode { | ||
| Normal, | ||
| Interactive, | ||
|
|
@@ -108,51 +113,135 @@ fn block_signals(should_block: bool) -> Result<()> { | |
| } | ||
| // Listen on given host and port | ||
| pub fn listen(opts: &Opts) -> rustyline::Result<()> { | ||
| let listener = TcpListener::bind(format!("{}:{}", opts.host, opts.port))?; | ||
| match opts.protocol { | ||
| crate::input::Protocol::Tcp => { | ||
| let listener = TcpListener::bind(format!("{}:{}", opts.host, opts.port))?; | ||
|
|
||
| #[cfg(not(unix))] | ||
| { | ||
| if let Mode::Interactive = opts.mode { | ||
| print_feature_not_supported(); | ||
|
|
||
| exit(1); | ||
| } | ||
| } | ||
|
|
||
| log::info!("Listening on {}:{}", opts.host.green(), opts.port.cyan()); | ||
|
|
||
| let (mut stream, _) = listener.accept()?; | ||
| #[cfg(not(unix))] | ||
| { | ||
| if let Mode::Interactive = opts.mode { | ||
| print_feature_not_supported(); | ||
| exit(1); | ||
| } | ||
| } | ||
|
|
||
| match &opts.mode { | ||
| Mode::Interactive => { | ||
| // It exists it if isn't unix above | ||
| block_signals(opts.block_signals)?; | ||
| log::info!("Listening on {}:{}", opts.host.green(), opts.port.cyan()); | ||
| let (mut stream, _) = listener.accept()?; | ||
|
|
||
| #[cfg(unix)] | ||
| { | ||
| termios_handler::setup_fd()?; | ||
| listen_tcp_normal(stream, opts)?; | ||
| match &opts.mode { | ||
| Mode::Interactive => { | ||
| block_signals(opts.block_signals)?; | ||
| #[cfg(unix)] | ||
| { | ||
| termios_handler::setup_fd()?; | ||
| listen_tcp_normal(stream, opts)?; | ||
| } | ||
| } | ||
| Mode::LocalInteractive => { | ||
| let t = pipe_thread(stream.try_clone()?, stdout()); | ||
| print_connection_received(); | ||
| readline_decorator(|command| { | ||
| stream | ||
| .write_all((command + "\n").as_bytes()) | ||
| .expect("Failed to send TCP."); | ||
| })?; | ||
| t.join().unwrap(); | ||
| } | ||
| Mode::Normal => { | ||
| block_signals(opts.block_signals)?; | ||
| listen_tcp_normal(stream, opts)?; | ||
| } | ||
| } | ||
| } | ||
| Mode::LocalInteractive => { | ||
| let t = pipe_thread(stream.try_clone()?, stdout()); | ||
|
|
||
| crate::input::Protocol::Tls => { | ||
| use std::fs; | ||
| use std::sync::Arc; | ||
| use rustls::ServerConfig; | ||
| use rustls::pki_types::CertificateDer; | ||
| use crate::listener::tls::accept_tls; | ||
| let cert_path = opts.cert.as_ref().expect("TLS listener requires --cert"); | ||
| let key_path = opts.key.as_ref().expect("TLS listener requires --key"); | ||
| let cert_data = fs::read(cert_path).expect("Failed to read cert file"); | ||
| let key_data = fs::read(key_path).expect("Failed to read key file"); | ||
|
Comment on lines
+164
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Second suggestion: Take a page from SSH et al, and check the permissions on any files being read, whether from a default location or one passed in via (SSH recommends, but doesn't enforce, that |
||
| let certs = vec![CertificateDer::from(cert_data)]; | ||
| use rustls::pki_types::PrivatePkcs8KeyDer; | ||
| let key = PrivatePkcs8KeyDer::from(key_data).into(); | ||
| let config = ServerConfig::builder() | ||
| .with_no_client_auth() | ||
| .with_single_cert(certs, key) | ||
| .expect("bad cert/key"); | ||
| let config = Arc::new(config); | ||
| let listener = TcpListener::bind(format!("{}:{}", opts.host, opts.port))?; | ||
| log::info!("Listening (TLS) on {}:{}", opts.host.green(), opts.port.cyan()); | ||
| let (stream, _) = listener.accept()?; | ||
| let mut tls_stream = accept_tls(stream, config)?; | ||
| let (stdin_thread, stdout_thread) = ( | ||
| pipe_thread(stdin(), tls_stream.get_mut().try_clone()?), | ||
| pipe_thread(tls_stream, stdout()), | ||
| ); | ||
| print_connection_received(); | ||
|
|
||
| readline_decorator(|command| { | ||
| stream | ||
| .write_all((command + "\n").as_bytes()) | ||
| .expect("Failed to send TCP."); | ||
| })?; | ||
|
|
||
| t.join().unwrap(); | ||
| stdin_thread.join().unwrap(); | ||
| stdout_thread.join().unwrap(); | ||
| } | ||
| Mode::Normal => { | ||
| block_signals(opts.block_signals)?; | ||
| listen_tcp_normal(stream, opts)?; | ||
| crate::input::Protocol::Udp => { | ||
| log::error!("UDP listener not implemented"); | ||
| exit(1); | ||
| } | ||
| crate::input::Protocol::Dtls => { | ||
| use std::fs; | ||
| use udp_dtls::Identity; | ||
| use crate::listener::tls::accept_dtls; | ||
| let cert_path = opts.cert.as_ref().expect("DTLS listener requires --cert (PKCS12)"); | ||
| let pkcs12_data = fs::read(cert_path).expect("Failed to read PKCS12 file"); | ||
| let identity = Identity::from_pkcs12(&pkcs12_data, "").expect("Invalid PKCS12"); | ||
| let socket = std::net::UdpSocket::bind(format!("{}:{}", opts.host, opts.port))?; | ||
| log::info!("Listening (DTLS) on {}:{}", opts.host.green(), opts.port.cyan()); | ||
| use std::sync::{Arc, Mutex}; | ||
| let dtls_stream = accept_dtls(socket, identity)?; | ||
| let stream = Arc::new(Mutex::new(dtls_stream)); | ||
| let stream_writer = Arc::clone(&stream); | ||
| let stdin_thread = std::thread::spawn(move || { | ||
| let mut stdin = stdin(); | ||
| let mut buf = [0u8; 4096]; | ||
| loop { | ||
| let n = match stdin.read(&mut buf) { | ||
| Ok(0) => break, | ||
| Ok(n) => n, | ||
| Err(_) => break, | ||
| }; | ||
| if let Ok(mut s) = stream_writer.lock() { | ||
| if s.write_all(&buf[..n]).is_err() { | ||
| break; | ||
| } | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
| }); | ||
| let stream_reader = Arc::clone(&stream); | ||
| let stdout_thread = std::thread::spawn(move || { | ||
| let mut stdout = stdout(); | ||
| let mut buf = [0u8; 4096]; | ||
| loop { | ||
| let n = match stream_reader.lock() { | ||
| Ok(mut s) => match s.read(&mut buf) { | ||
| Ok(0) => break, | ||
| Ok(n) => n, | ||
| Err(_) => break, | ||
| }, | ||
| Err(_) => break, | ||
| }; | ||
| if stdout.write_all(&buf[..n]).is_err() { | ||
| break; | ||
| } | ||
| let _ = stdout.flush(); | ||
| } | ||
| }); | ||
| print_connection_received(); | ||
| stdin_thread.join().unwrap(); | ||
| stdout_thread.join().unwrap(); | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| use std::io::{Read, Write, Result}; | ||
| use std::net::{TcpStream, UdpSocket, SocketAddr}; | ||
| use std::sync::Arc; | ||
|
|
||
| use rustls::{ClientConfig, ServerConfig, StreamOwned, ServerConnection, ClientConnection}; | ||
| use rustls::pki_types::ServerName; | ||
| use udp_dtls::{DtlsAcceptor, DtlsConnector, Identity, Certificate, DtlsStream}; | ||
|
|
||
| // Minimal wrapper to adapt UdpSocket to Read/Write for udp-dtls | ||
| #[derive(Debug)] | ||
| pub struct UdpSocketChannel(pub UdpSocket); | ||
|
|
||
| impl Read for UdpSocketChannel { | ||
| fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { | ||
| self.0.recv(buf) | ||
| } | ||
| } | ||
|
|
||
| impl Write for UdpSocketChannel { | ||
| fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { | ||
| self.0.send(buf) | ||
| } | ||
| fn flush(&mut self) -> std::io::Result<()> { | ||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| pub fn accept_tls(stream: TcpStream, config: Arc<ServerConfig>) -> Result<StreamOwned<ServerConnection, TcpStream>> { | ||
| let conn = ServerConnection::new(config).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; | ||
| Ok(StreamOwned::new(conn, stream)) | ||
| } | ||
|
|
||
| pub fn connect_tls(stream: TcpStream, config: Arc<ClientConfig>, server_name: &str) -> Result<StreamOwned<ClientConnection, TcpStream>> { | ||
| let server_name = ServerName::try_from(server_name.to_owned()) | ||
| .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid server name"))?; | ||
| let conn = ClientConnection::new(config, server_name) | ||
| .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; | ||
| Ok(StreamOwned::new(conn, stream)) | ||
| } | ||
|
|
||
| pub fn accept_dtls(socket: UdpSocket, identity: Identity) -> Result<DtlsStream<UdpSocketChannel>> { | ||
| let acceptor = DtlsAcceptor::builder(identity) | ||
| .build() | ||
| .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; | ||
| let channel = UdpSocketChannel(socket); | ||
| let stream = acceptor.accept(channel) | ||
| .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{:?}", e)))?; | ||
| Ok(stream) | ||
| } | ||
|
|
||
| pub fn connect_dtls(socket: UdpSocket, addr: SocketAddr, identity: Identity, peer_cert: Certificate) -> Result<DtlsStream<UdpSocketChannel>> { | ||
| let connector = DtlsConnector::builder() | ||
| .identity(identity) | ||
| .add_root_certificate(peer_cert) | ||
| .build() | ||
| .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; | ||
| let channel = UdpSocketChannel(socket); | ||
| let stream = connector.connect(&addr.to_string(), channel) | ||
| .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{:?}", e)))?; | ||
| Ok(stream) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion:
Establish a standard location for these files (analogous to of
$HOME/.ssh/), preferably in a path like$XDG_CONFIG_DIR/rustcat1. The files.../server-cert.der2 and.../server-key.der, stored in that directory, could be loaded automatically if--protocol tlsis used but no--certand/or no--keyargument (respectively) is given. To save the user having to type them every time.(Another command line flag could disable automatic file loading even in the absence of
--certor--key, for the paranoid or in scripts. Something like--no-user-defaults,--ignore-user-keys, etc.)Notes
$XDG_CONFIG_DIRas either the contents of that envvar, or$HOME/.config/if no such environment variable is set.default-cert.der/default-key.der, you get the idea.)