Add support for proxying outgoing WebSocket connections

This commit is contained in:
Travis Burtrum 2022-03-25 01:32:10 -04:00
parent 4f5938e0ce
commit c0a8adc3e0
7 changed files with 65 additions and 58 deletions

View File

@ -18,7 +18,7 @@ xmpp-proxy in reverse proxy (incoming) mode will:
xmpp-proxy in outgoing mode will: xmpp-proxy in outgoing mode will:
1. listen on any number of interfaces/ports 1. listen on any number of interfaces/ports
2. accept any plain-text TCP connection from a local XMPP server or client 2. accept any plain-text TCP or WebSocket connection from a local XMPP server or client
3. look up the required SRV records 3. look up the required SRV records
4. connect to a real XMPP server across the internet over STARTTLS, Direct TLS, QUIC, or WebSocket 4. connect to a real XMPP server across the internet over STARTTLS, Direct TLS, QUIC, or WebSocket
5. fallback to next SRV target or defaults as required to fully connect 5. fallback to next SRV target or defaults as required to fully connect

View File

@ -2,21 +2,21 @@
#[cfg(feature = "websocket")] #[cfg(feature = "websocket")]
use crate::{from_ws, to_ws_new}; use crate::{from_ws, to_ws_new};
use crate::{slicesubsequence::SliceSubsequence, trace, StanzaFilter, StanzaRead::*, StanzaReader, StanzaWrite::*}; use crate::{slicesubsequence::SliceSubsequence, trace, AsyncReadAndWrite, StanzaFilter, StanzaRead::*, StanzaReader, StanzaWrite::*};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
#[cfg(feature = "websocket")] #[cfg(feature = "websocket")]
use futures_util::{ use futures_util::{
stream::{SplitSink, SplitStream}, stream::{SplitSink, SplitStream},
SinkExt, TryStreamExt, SinkExt, TryStreamExt,
}; };
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufStream}; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
#[cfg(feature = "websocket")] #[cfg(feature = "websocket")]
use tokio_tungstenite::{tungstenite::Message::*, WebSocketStream}; use tokio_tungstenite::{tungstenite::Message::*, WebSocketStream};
#[cfg(feature = "websocket")] #[cfg(feature = "websocket")]
type WsWr = SplitSink<WebSocketStream<BufStream<tokio_rustls::TlsStream<tokio::net::TcpStream>>>, tokio_tungstenite::tungstenite::Message>; type WsWr = SplitSink<WebSocketStream<Box<dyn AsyncReadAndWrite + Unpin + Send>>, tokio_tungstenite::tungstenite::Message>;
#[cfg(feature = "websocket")] #[cfg(feature = "websocket")]
type WsRd = SplitStream<WebSocketStream<BufStream<tokio_rustls::TlsStream<tokio::net::TcpStream>>>>; type WsRd = SplitStream<WebSocketStream<Box<dyn AsyncReadAndWrite + Unpin + Send>>>;
pub enum StanzaWrite { pub enum StanzaWrite {
AsyncWrite(Box<dyn AsyncWrite + Unpin + Send>), AsyncWrite(Box<dyn AsyncWrite + Unpin + Send>),

View File

@ -4,6 +4,7 @@ pub use stanzafilter::*;
mod slicesubsequence; mod slicesubsequence;
use slicesubsequence::*; use slicesubsequence::*;
use anyhow::bail;
use std::net::SocketAddr; use std::net::SocketAddr;
pub use log::{debug, error, info, log_enabled, trace}; pub use log::{debug, error, info, log_enabled, trace};
@ -21,6 +22,29 @@ pub fn c2s(is_c2s: bool) -> &'static str {
} }
} }
pub async fn first_bytes_match(stream: &tokio::net::TcpStream, p: &mut [u8], matcher: fn(&[u8]) -> bool) -> anyhow::Result<bool> {
// sooo... I don't think peek here can be used for > 1 byte without this timer craziness... can it?
let len = p.len();
// wait up to 10 seconds until len bytes have been read
use std::time::{Duration, Instant};
let duration = Duration::from_secs(10);
let now = Instant::now();
loop {
let n = stream.peek(p).await?;
if n == len {
break; // success
}
if n == 0 {
bail!("not enough bytes");
}
if Instant::now() - now > duration {
bail!("less than {} bytes in 10 seconds, closed connection?", len);
}
}
Ok(matcher(p))
}
#[derive(Clone)] #[derive(Clone)]
pub struct Context<'a> { pub struct Context<'a> {
conn_id: String, conn_id: String,

View File

@ -5,10 +5,14 @@ async fn handle_outgoing_connection(stream: tokio::net::TcpStream, client_addr:
let mut in_filter = StanzaFilter::new(config.max_stanza_size_bytes); let mut in_filter = StanzaFilter::new(config.max_stanza_size_bytes);
let (in_rd, in_wr) = tokio::io::split(stream); let is_ws = first_bytes_match(&stream, &mut in_filter.buf[0..3], |p| p == b"GET").await?;
let mut in_rd = StanzaRead::new(in_rd); let (mut in_rd, mut in_wr) = if is_ws {
let mut in_wr = StanzaWrite::new(in_wr); incoming_websocket_connection(Box::new(stream), config.max_stanza_size_bytes).await?
} else {
let (in_rd, in_wr) = tokio::io::split(stream);
(StanzaRead::new(in_rd), StanzaWrite::new(in_wr))
};
// now read to figure out client vs server // now read to figure out client vs server
let (stream_open, is_c2s) = stream_preamble(&mut in_rd, &mut in_wr, client_addr.log_to(), &mut in_filter).await?; let (stream_open, is_c2s) = stream_preamble(&mut in_rd, &mut in_wr, client_addr.log_to(), &mut in_filter).await?;

View File

@ -83,35 +83,13 @@ async fn handle_tls_connection(mut stream: tokio::net::TcpStream, client_addr: &
let mut in_filter = StanzaFilter::new(config.max_stanza_size_bytes); let mut in_filter = StanzaFilter::new(config.max_stanza_size_bytes);
let direct_tls = {
// sooo... I don't think peek here can be used for > 1 byte without this timer
// craziness... can it? this could be switched to only peek 1 byte and assume
// a leading 0x16 is TLS, it would *probably* be ok ?
//let mut p = [0u8; 3];
let p = &mut in_filter.buf[0..3];
// wait up to 10 seconds until 3 bytes have been read
use std::time::{Duration, Instant};
let duration = Duration::from_secs(10);
let now = Instant::now();
loop {
let n = stream.peek(p).await?;
if n == 3 {
break; // success
}
if n == 0 {
bail!("not enough bytes");
}
if Instant::now() - now > duration {
bail!("less than 3 bytes in 10 seconds, closed connection?");
}
}
/* TLS packet starts with a record "Hello" (0x16), followed by version /* TLS packet starts with a record "Hello" (0x16), followed by version
* (0x03 0x00-0x03) (RFC6101 A.1) * (0x03 0x00-0x03) (RFC6101 A.1)
* This means we reject SSLv2 and lower, which is actually a good thing (RFC6176) * This means we reject SSLv2 and lower, which is actually a good thing (RFC6176)
*
* could just check the leading 0x16 is TLS, it would *probably* be ok ?
*/ */
p[0] == 0x16 && p[1] == 0x03 && p[2] <= 0x03 let direct_tls = first_bytes_match(&stream, &mut in_filter.buf[0..3], |p| p[0] == 0x16 && p[1] == 0x03 && p[2] <= 0x03).await?;
};
client_addr.set_proto(if direct_tls { "directtls-in" } else { "starttls-in" }); client_addr.set_proto(if direct_tls { "directtls-in" } else { "starttls-in" });
info!("{} direct_tls sniffed", client_addr.log_from()); info!("{} direct_tls sniffed", client_addr.log_from());
@ -218,7 +196,7 @@ async fn handle_tls_connection(mut stream: tokio::net::TcpStream, client_addr: &
}; };
if websocket { if websocket {
handle_websocket_connection(stream, config, server_certs, local_addr, client_addr, in_filter).await handle_websocket_connection(Box::new(stream), config, server_certs, local_addr, client_addr, in_filter).await
} else { } else {
let (in_rd, in_wr) = tokio::io::split(stream); let (in_rd, in_wr) = tokio::io::split(stream);
shuffle_rd_wr_filter(StanzaRead::already_buffered(in_rd), StanzaWrite::new(in_wr), config, server_certs, local_addr, client_addr, in_filter).await shuffle_rd_wr_filter(StanzaRead::already_buffered(in_rd), StanzaWrite::new(in_wr), config, server_certs, local_addr, client_addr, in_filter).await

View File

@ -15,8 +15,22 @@ fn ws_cfg(max_stanza_size_bytes: usize) -> Option<WebSocketConfig> {
}) })
} }
pub trait AsyncReadAndWrite: tokio::io::AsyncRead + tokio::io::AsyncWrite {}
impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite> AsyncReadAndWrite for T {}
pub async fn incoming_websocket_connection(stream: Box<dyn AsyncReadAndWrite + Unpin + Send>, max_stanza_size_bytes: usize) -> Result<(StanzaRead, StanzaWrite)> {
// accept the websocket
// todo: check SEC_WEBSOCKET_PROTOCOL or ORIGIN ?
let stream = tokio_tungstenite::accept_async_with_config(stream, ws_cfg(max_stanza_size_bytes)).await?;
let (in_wr, in_rd) = stream.split();
Ok((StanzaRead::WebSocketRead(in_rd), StanzaWrite::WebSocketClientWrite(in_wr)))
}
pub async fn handle_websocket_connection( pub async fn handle_websocket_connection(
stream: BufStream<tokio_rustls::TlsStream<tokio::net::TcpStream>>, stream: Box<dyn AsyncReadAndWrite + Unpin + Send>,
config: CloneableConfig, config: CloneableConfig,
server_certs: ServerCerts, server_certs: ServerCerts,
local_addr: SocketAddr, local_addr: SocketAddr,
@ -26,22 +40,9 @@ pub async fn handle_websocket_connection(
client_addr.set_proto("websocket-in"); client_addr.set_proto("websocket-in");
info!("{} connected", client_addr.log_from()); info!("{} connected", client_addr.log_from());
// accept the websocket let (in_rd, in_wr) = incoming_websocket_connection(stream, config.max_stanza_size_bytes).await?;
// todo: check SEC_WEBSOCKET_PROTOCOL or ORIGIN ?
let stream = tokio_tungstenite::accept_async_with_config(stream, ws_cfg(config.max_stanza_size_bytes)).await?;
let (in_wr, in_rd) = stream.split(); shuffle_rd_wr_filter(in_rd, in_wr, config, server_certs, local_addr, client_addr, in_filter).await
shuffle_rd_wr_filter(
StanzaRead::WebSocketRead(in_rd),
StanzaWrite::WebSocketClientWrite(in_wr),
config,
server_certs,
local_addr,
client_addr,
in_filter,
)
.await
} }
pub fn from_ws(stanza: String) -> String { pub fn from_ws(stanza: String) -> String {
@ -97,7 +98,6 @@ pub fn to_ws_new(buf: &[u8], mut end_of_first_tag: usize, is_c2s: bool) -> Resul
use rustls::ServerName; use rustls::ServerName;
use std::convert::TryFrom; use std::convert::TryFrom;
use tokio::io::BufStream;
use tokio_tungstenite::tungstenite::client::IntoClientRequest; use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use tokio_tungstenite::tungstenite::http::header::{ORIGIN, SEC_WEBSOCKET_PROTOCOL}; use tokio_tungstenite::tungstenite::http::header::{ORIGIN, SEC_WEBSOCKET_PROTOCOL};
@ -113,9 +113,10 @@ pub async fn websocket_connect(target: SocketAddr, server_name: &str, url: &Uri,
let stream = tokio::net::TcpStream::connect(target).await?; let stream = tokio::net::TcpStream::connect(target).await?;
let stream = config.connector.connect(dnsname, stream).await?; let stream = config.connector.connect(dnsname, stream).await?;
let stream: tokio_rustls::TlsStream<tokio::net::TcpStream> = stream.into(); //let stream: tokio_rustls::TlsStream<tokio::net::TcpStream> = stream.into();
// todo: tokio_tungstenite seems to have a bug, if the write buffer is non-zero, it'll hang forever, even though we always flush, investigate // todo: tokio_tungstenite seems to have a bug, if the write buffer is non-zero, it'll hang forever, even though we always flush, investigate
let stream = BufStream::with_capacity(crate::IN_BUFFER_SIZE, 0, stream); //let stream = BufStream::with_capacity(crate::IN_BUFFER_SIZE, 0, stream);
let stream: Box<dyn AsyncReadAndWrite + Unpin + Send> = Box::new(stream);
let (stream, _) = tokio_tungstenite::client_async_with_config(request, stream, ws_cfg(config.max_stanza_size_bytes)).await?; let (stream, _) = tokio_tungstenite::client_async_with_config(request, stream, ws_cfg(config.max_stanza_size_bytes)).await?;

View File

@ -3,7 +3,7 @@
incoming_listen = [ "0.0.0.0:5222", "0.0.0.0:5269", "0.0.0.0:443" ] incoming_listen = [ "0.0.0.0:5222", "0.0.0.0:5269", "0.0.0.0:443" ]
# interfaces to listen for reverse proxy QUIC XMPP connections on, should be open to the internet # interfaces to listen for reverse proxy QUIC XMPP connections on, should be open to the internet
quic_listen = [ "0.0.0.0:443" ] quic_listen = [ "0.0.0.0:443" ]
# interfaces to listen for outgoing proxy TCP XMPP connections on, should be localhost # interfaces to listen for outgoing proxy TCP or WebSocket XMPP connections on, should be localhost
outgoing_listen = [ "127.0.0.1:15270" ] outgoing_listen = [ "127.0.0.1:15270" ]
# these ports shouldn't do any TLS, but should assume any connection from xmpp-proxy is secure # these ports shouldn't do any TLS, but should assume any connection from xmpp-proxy is secure