Massive refactoring

This commit is contained in:
Travis Burtrum 2022-07-16 23:23:01 -04:00
parent 4498559c08
commit e553b4da14
23 changed files with 941 additions and 825 deletions

View File

@ -39,7 +39,7 @@ fn main() {
for (mut key, value) in env::vars() { for (mut key, value) in env::vars() {
//writeln!(&mut w, "{key}: {value}", ).unwrap(); //writeln!(&mut w, "{key}: {value}", ).unwrap();
if value == "1" && key.starts_with("CARGO_FEATURE_") { if value == "1" && key.starts_with("CARGO_FEATURE_") {
let mut key = key.split_off(14).replace("_", "-"); let mut key = key.split_off(14).replace('_', "-");
key.make_ascii_lowercase(); key.make_ascii_lowercase();
if allowed_features.contains(&key.as_str()) { if allowed_features.contains(&key.as_str()) {
features.push(key); features.push(key);

33
src/common/ca_roots.rs Normal file
View File

@ -0,0 +1,33 @@
#[cfg(feature = "tokio-rustls")]
use tokio_rustls::webpki::{TlsServerTrustAnchors, TrustAnchor};
#[cfg(all(feature = "webpki-roots", not(feature = "rustls-native-certs")))]
pub use webpki_roots::TLS_SERVER_ROOTS;
#[cfg(all(feature = "rustls-native-certs", not(feature = "webpki-roots")))]
lazy_static::lazy_static! {
pub static ref TLS_SERVER_ROOTS: TlsServerTrustAnchors<'static> = {
// we need these to stick around for 'static, this is only called once so no problem
let certs = Box::leak(Box::new(rustls_native_certs::load_native_certs().expect("could not load platform certs")));
let root_cert_store = Box::leak(Box::new(Vec::new()));
for cert in certs {
// some system CAs are invalid, ignore those
if let Ok(ta) = TrustAnchor::try_from_cert_der(&cert.0) {
root_cert_store.push(ta);
}
}
TlsServerTrustAnchors(root_cert_store)
};
}
pub fn root_cert_store() -> rustls::RootCertStore {
use rustls::{OwnedTrustAnchor, RootCertStore};
let mut root_cert_store = RootCertStore::empty();
root_cert_store.add_server_trust_anchors(
TLS_SERVER_ROOTS
.0
.iter()
.map(|ta| OwnedTrustAnchor::from_subject_spki_name_constraints(ta.subject, ta.spki, ta.name_constraints)),
);
root_cert_store
}

47
src/common/certs_key.rs Normal file
View File

@ -0,0 +1,47 @@
use std::sync::{Arc, RwLock};
use anyhow::Result;
use rustls::{sign::CertifiedKey, SignatureScheme};
pub struct CertsKey {
#[cfg(feature = "rustls-pemfile")]
pub inner: Result<RwLock<Arc<CertifiedKey>>>,
}
impl CertsKey {
pub fn new(certified_key: Result<CertifiedKey>) -> Self {
CertsKey {
#[cfg(feature = "rustls-pemfile")]
inner: certified_key.map(|c| RwLock::new(Arc::new(c))),
}
}
}
#[cfg(feature = "rustls-pemfile")]
impl rustls::server::ResolvesServerCert for CertsKey {
fn resolve(&self, _: rustls::server::ClientHello) -> Option<Arc<rustls::sign::CertifiedKey>> {
self.inner.as_ref().map(|rwl| rwl.read().expect("CertKey poisoned?").clone()).ok()
}
}
#[cfg(feature = "rustls-pemfile")]
impl rustls::client::ResolvesClientCert for CertsKey {
fn resolve(&self, _: &[&[u8]], _: &[SignatureScheme]) -> Option<Arc<CertifiedKey>> {
self.inner.as_ref().map(|rwl| rwl.read().expect("CertKey poisoned?").clone()).ok()
}
fn has_certs(&self) -> bool {
self.inner.is_ok()
}
}
#[cfg(not(feature = "rustls-pemfile"))]
impl rustls::client::ResolvesClientCert for CertsKey {
fn resolve(&self, _: &[&[u8]], _: &[SignatureScheme]) -> Option<Arc<CertifiedKey>> {
None
}
fn has_certs(&self) -> bool {
false
}
}

198
src/common/incoming.rs Normal file
View File

@ -0,0 +1,198 @@
use crate::{
common::{c2s, certs_key::CertsKey, shuffle_rd_wr_filter_only, stream_preamble, to_str, ALPN_XMPP_CLIENT, ALPN_XMPP_SERVER},
context::Context,
in_out::{StanzaRead, StanzaWrite},
slicesubsequence::SliceSubsequence,
stanzafilter::StanzaFilter,
};
use anyhow::{anyhow, bail, Result};
use log::trace;
use rustls::{Certificate, ServerConfig, ServerConnection};
use std::{io::Write, net::SocketAddr, sync::Arc};
use tokio::io::{AsyncWriteExt, ReadHalf, WriteHalf};
#[derive(Clone)]
pub struct CloneableConfig {
pub max_stanza_size_bytes: usize,
#[cfg(feature = "s2s-incoming")]
pub s2s_target: Option<SocketAddr>,
#[cfg(feature = "c2s-incoming")]
pub c2s_target: Option<SocketAddr>,
pub proxy: bool,
}
pub fn server_config(certs_key: Arc<CertsKey>) -> Result<ServerConfig> {
if let Err(e) = &certs_key.inner {
bail!("invalid cert/key: {}", e);
}
let config = ServerConfig::builder().with_safe_defaults();
#[cfg(feature = "s2s")]
let config = config.with_client_cert_verifier(Arc::new(crate::verify::AllowAnonymousOrAnyCert));
#[cfg(not(feature = "s2s"))]
let config = config.with_no_client_auth();
let mut config = config.with_cert_resolver(certs_key);
// todo: will connecting without alpn work then?
config.alpn_protocols.push(ALPN_XMPP_CLIENT.to_vec());
config.alpn_protocols.push(ALPN_XMPP_SERVER.to_vec());
Ok(config)
}
#[cfg(not(feature = "s2s-incoming"))]
pub type ServerCerts = ();
#[cfg(feature = "s2s-incoming")]
#[derive(Clone)]
pub enum ServerCerts {
Tls(&'static ServerConnection),
#[cfg(feature = "quic")]
Quic(quinn::Connection),
}
#[cfg(feature = "s2s-incoming")]
impl ServerCerts {
pub fn peer_certificates(&self) -> Option<Vec<Certificate>> {
match self {
ServerCerts::Tls(c) => c.peer_certificates().map(|c| c.to_vec()),
#[cfg(feature = "quic")]
ServerCerts::Quic(c) => c.peer_identity().and_then(|v| v.downcast::<Vec<Certificate>>().ok()).map(|v| v.to_vec()),
}
}
pub fn sni(&self) -> Option<String> {
match self {
ServerCerts::Tls(c) => c.sni_hostname().map(|s| s.to_string()),
#[cfg(feature = "quic")]
ServerCerts::Quic(c) => c.handshake_data().and_then(|v| v.downcast::<quinn::crypto::rustls::HandshakeData>().ok()).and_then(|h| h.server_name),
}
}
pub fn alpn(&self) -> Option<Vec<u8>> {
match self {
ServerCerts::Tls(c) => c.alpn_protocol().map(|s| s.to_vec()),
#[cfg(feature = "quic")]
ServerCerts::Quic(c) => c.handshake_data().and_then(|v| v.downcast::<quinn::crypto::rustls::HandshakeData>().ok()).and_then(|h| h.protocol),
}
}
pub fn is_tls(&self) -> bool {
match self {
ServerCerts::Tls(_) => true,
#[cfg(feature = "quic")]
ServerCerts::Quic(_) => false,
}
}
}
pub async fn shuffle_rd_wr(in_rd: StanzaRead, in_wr: StanzaWrite, config: CloneableConfig, server_certs: ServerCerts, local_addr: SocketAddr, client_addr: &mut Context<'_>) -> Result<()> {
let filter = StanzaFilter::new(config.max_stanza_size_bytes);
shuffle_rd_wr_filter(in_rd, in_wr, config, server_certs, local_addr, client_addr, filter).await
}
pub async fn shuffle_rd_wr_filter(
mut in_rd: StanzaRead,
mut in_wr: StanzaWrite,
config: CloneableConfig,
server_certs: ServerCerts,
local_addr: SocketAddr,
client_addr: &mut Context<'_>,
mut in_filter: StanzaFilter,
) -> Result<()> {
// now read to figure out client vs server
let (stream_open, is_c2s) = stream_preamble(&mut in_rd, &mut in_wr, client_addr.log_from(), &mut in_filter).await?;
client_addr.set_c2s_stream_open(is_c2s, &stream_open);
#[cfg(feature = "s2s-incoming")]
{
trace!(
"{} connected: sni: {:?}, alpn: {:?}, tls-not-quic: {}",
client_addr.log_from(),
server_certs.sni(),
server_certs.alpn().map(|a| String::from_utf8_lossy(&a).to_string()),
server_certs.is_tls(),
);
if !is_c2s {
// for s2s we need this
use std::time::SystemTime;
let domain = stream_open
.extract_between(b" from='", b"'")
.or_else(|_| stream_open.extract_between(b" from=\"", b"\""))
.and_then(|b| Ok(std::str::from_utf8(b)?))?;
let (_, cert_verifier) = crate::srv::get_xmpp_connections(domain, is_c2s).await?;
let certs = server_certs.peer_certificates().ok_or_else(|| anyhow!("no client cert auth for s2s incoming from {}", domain))?;
// todo: send stream error saying cert is invalid
cert_verifier.verify_cert(&certs[0], &certs[1..], SystemTime::now())?;
}
drop(server_certs);
}
let (out_rd, out_wr) = open_incoming(&config, local_addr, client_addr, &stream_open, is_c2s, &mut in_filter).await?;
drop(stream_open);
shuffle_rd_wr_filter_only(
in_rd,
in_wr,
StanzaRead::new(out_rd),
StanzaWrite::new(out_wr),
is_c2s,
config.max_stanza_size_bytes,
client_addr,
in_filter,
)
.await
}
async fn open_incoming(
config: &CloneableConfig,
local_addr: SocketAddr,
client_addr: &mut Context<'_>,
stream_open: &[u8],
is_c2s: bool,
in_filter: &mut StanzaFilter,
) -> Result<(ReadHalf<tokio::net::TcpStream>, WriteHalf<tokio::net::TcpStream>)> {
let target = if is_c2s {
#[cfg(not(feature = "c2s-incoming"))]
bail!("incoming c2s connection but lacking compile-time support");
#[cfg(feature = "c2s-incoming")]
config.c2s_target
} else {
#[cfg(not(feature = "s2s-incoming"))]
bail!("incoming s2s connection but lacking compile-time support");
#[cfg(feature = "s2s-incoming")]
config.s2s_target
}
.ok_or_else(|| anyhow!("incoming connection but `{}_target` not defined", c2s(is_c2s)))?;
client_addr.set_to_addr(target);
let out_stream = tokio::net::TcpStream::connect(target).await?;
let (out_rd, mut out_wr) = tokio::io::split(out_stream);
if config.proxy {
/*
https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n
PROXY TCP6 ffff:f...f:ffff ffff:f...f:ffff 65535 65535\r\n
PROXY TCP6 SOURCE_IP DEST_IP SOURCE_PORT DEST_PORT\r\n
*/
// tokio AsyncWrite doesn't have write_fmt so have to go through this buffer for some crazy reason
//write!(out_wr, "PROXY TCP{} {} {} {} {}\r\n", if client_addr.is_ipv4() { '4' } else {'6' }, client_addr.ip(), local_addr.ip(), client_addr.port(), local_addr.port())?;
write!(
&mut in_filter.buf[0..],
"PROXY TCP{} {} {} {} {}\r\n",
if client_addr.client_addr().is_ipv4() { '4' } else { '6' },
client_addr.client_addr().ip(),
local_addr.ip(),
client_addr.client_addr().port(),
local_addr.port()
)?;
let end_idx = &(&in_filter.buf[0..]).first_index_of(b"\n")? + 1;
trace!("{} '{}'", client_addr.log_from(), to_str(&in_filter.buf[0..end_idx]));
out_wr.write_all(&in_filter.buf[0..end_idx]).await?;
}
trace!("{} '{}'", client_addr.log_from(), to_str(stream_open));
out_wr.write_all(stream_open).await?;
out_wr.flush().await?;
Ok((out_rd, out_wr))
}

144
src/common/mod.rs Normal file
View File

@ -0,0 +1,144 @@
use crate::{
context::Context,
in_out::{StanzaRead, StanzaWrite},
slicesubsequence::SliceSubsequence,
stanzafilter::StanzaFilter,
};
use anyhow::{bail, Result};
use log::{info, trace};
use rustls::{
sign::{RsaSigningKey, SigningKey},
Certificate, PrivateKey,
};
use std::{fs::File, io, io::BufReader, sync::Arc};
#[cfg(feature = "incoming")]
pub mod incoming;
#[cfg(feature = "outgoing")]
pub mod outgoing;
#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
pub mod ca_roots;
pub mod certs_key;
pub const IN_BUFFER_SIZE: usize = 8192;
pub const ALPN_XMPP_CLIENT: &[u8] = b"xmpp-client";
pub const ALPN_XMPP_SERVER: &[u8] = b"xmpp-server";
pub fn to_str(buf: &[u8]) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(buf)
}
pub fn c2s(is_c2s: bool) -> &'static str {
if is_c2s {
"c2s"
} else {
"s2s"
}
}
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))
}
pub async fn stream_preamble(in_rd: &mut StanzaRead, in_wr: &mut StanzaWrite, client_addr: &'_ str, in_filter: &mut StanzaFilter) -> Result<(Vec<u8>, bool)> {
let mut stream_open = Vec::new();
while let Ok(Some((buf, _))) = in_rd.next(in_filter, client_addr, in_wr).await {
trace!("{} received pre-<stream:stream> stanza: '{}'", client_addr, to_str(buf));
if buf.starts_with(b"<?xml ") {
stream_open.extend_from_slice(buf);
} else if buf.starts_with(b"<stream:stream ") {
stream_open.extend_from_slice(buf);
return Ok((stream_open, buf.contains_seq(br#" xmlns="jabber:client""#) || buf.contains_seq(br#" xmlns='jabber:client'"#)));
} else {
bail!("bad pre-<stream:stream> stanza: {}", to_str(buf));
}
}
bail!("stream ended before open")
}
#[allow(clippy::too_many_arguments)]
pub async fn shuffle_rd_wr_filter_only(
mut in_rd: StanzaRead,
mut in_wr: StanzaWrite,
mut out_rd: StanzaRead,
mut out_wr: StanzaWrite,
is_c2s: bool,
max_stanza_size_bytes: usize,
client_addr: &mut Context<'_>,
mut in_filter: StanzaFilter,
) -> Result<()> {
let mut out_filter = StanzaFilter::new(max_stanza_size_bytes);
loop {
tokio::select! {
Ok(ret) = in_rd.next(&mut in_filter, client_addr.log_to(), &mut in_wr) => {
match ret {
None => break,
Some((buf, eoft)) => {
trace!("{} '{}'", client_addr.log_from(), to_str(buf));
out_wr.write_all(is_c2s, buf, eoft, client_addr.log_from()).await?;
out_wr.flush().await?;
}
}
},
Ok(ret) = out_rd.next(&mut out_filter, client_addr.log_from(), &mut out_wr) => {
match ret {
None => break,
Some((buf, eoft)) => {
trace!("{} '{}'", client_addr.log_to(), to_str(buf));
in_wr.write_all(is_c2s, buf, eoft, client_addr.log_to()).await?;
in_wr.flush().await?;
}
}
},
}
}
info!("{} disconnected", client_addr.log_from());
Ok(())
}
#[cfg(feature = "rustls-pemfile")]
pub fn read_certified_key(tls_key: &str, tls_cert: &str) -> Result<rustls::sign::CertifiedKey> {
use rustls_pemfile::{certs, read_all, Item};
let tls_key = read_all(&mut BufReader::new(File::open(tls_key)?))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?
.into_iter()
.flat_map(|item| match item {
Item::RSAKey(der) => RsaSigningKey::new(&PrivateKey(der)).ok().map(Arc::new).map(|r| r as Arc<dyn SigningKey>),
Item::PKCS8Key(der) => rustls::sign::any_supported_type(&PrivateKey(der)).ok(),
Item::ECKey(der) => rustls::sign::any_supported_type(&PrivateKey(der)).ok(),
_ => None,
})
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?;
let tls_certs = certs(&mut BufReader::new(File::open(tls_cert)?))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert"))
.map(|mut certs| certs.drain(..).map(Certificate).collect())?;
Ok(rustls::sign::CertifiedKey::new(tls_certs, tls_key))
}

54
src/common/outgoing.rs Normal file
View File

@ -0,0 +1,54 @@
use crate::{
common::{certs_key::CertsKey, ALPN_XMPP_CLIENT, ALPN_XMPP_SERVER},
verify::XmppServerCertVerifier,
};
use rustls::ClientConfig;
use std::sync::Arc;
use tokio_rustls::TlsConnector;
#[derive(Clone)]
pub struct OutgoingConfig {
pub max_stanza_size_bytes: usize,
pub certs_key: Arc<CertsKey>,
}
impl OutgoingConfig {
pub fn with_custom_certificate_verifier(&self, is_c2s: bool, cert_verifier: XmppServerCertVerifier) -> OutgoingVerifierConfig {
let config = match is_c2s {
false => ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(cert_verifier))
.with_client_cert_resolver(self.certs_key.clone()),
_ => ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(cert_verifier))
.with_no_client_auth(),
};
let mut config_alpn = config.clone();
config_alpn.alpn_protocols.push(if is_c2s { ALPN_XMPP_CLIENT } else { ALPN_XMPP_SERVER }.to_vec());
let config_alpn = Arc::new(config_alpn);
let connector_alpn: TlsConnector = config_alpn.clone().into();
let connector: TlsConnector = Arc::new(config).into();
OutgoingVerifierConfig {
max_stanza_size_bytes: self.max_stanza_size_bytes,
config_alpn,
connector_alpn,
connector,
}
}
}
#[derive(Clone)]
pub struct OutgoingVerifierConfig {
pub max_stanza_size_bytes: usize,
pub config_alpn: Arc<ClientConfig>,
pub connector_alpn: TlsConnector,
pub connector: TlsConnector,
}

112
src/context.rs Normal file
View File

@ -0,0 +1,112 @@
use crate::{
common::{c2s, to_str},
slicesubsequence::SliceSubsequence,
};
use log::{info, log_enabled};
use std::net::SocketAddr;
#[derive(Clone)]
pub struct Context<'a> {
conn_id: String,
log_from: String,
log_to: String,
proto: &'a str,
is_c2s: Option<bool>,
to: Option<String>,
to_addr: Option<SocketAddr>,
from: Option<String>,
client_addr: SocketAddr,
}
impl<'a> Context<'a> {
pub fn new(proto: &'static str, client_addr: SocketAddr) -> Context {
let (log_to, log_from, conn_id) = if log_enabled!(log::Level::Info) {
#[cfg(feature = "logging")]
let conn_id = {
use rand::{distributions::Alphanumeric, thread_rng, Rng};
thread_rng().sample_iter(&Alphanumeric).take(10).map(char::from).collect()
};
#[cfg(not(feature = "logging"))]
let conn_id = "".to_string();
(
format!("{}: ({} <- ({}-unk)):", conn_id, client_addr, proto),
format!("{}: ({} -> ({}-unk)):", conn_id, client_addr, proto),
conn_id,
)
} else {
("".to_string(), "".to_string(), "".to_string())
};
Context {
conn_id,
log_from,
log_to,
proto,
client_addr,
is_c2s: None,
to: None,
to_addr: None,
from: None,
}
}
fn re_calc(&mut self) {
// todo: make this good
self.log_from = format!(
"{}: ({} ({}) -> ({}-{}) -> {} ({})):",
self.conn_id,
self.client_addr,
if self.from.is_some() { self.from.as_ref().unwrap() } else { "unk" },
self.proto,
if self.is_c2s.is_some() { c2s(self.is_c2s.unwrap()) } else { "unk" },
if self.to_addr.is_some() { self.to_addr.as_ref().unwrap().to_string() } else { "unk".to_string() },
if self.to.is_some() { self.to.as_ref().unwrap() } else { "unk" },
);
self.log_to = self.log_from.replace(" -> ", " <- ");
}
pub fn log_from(&self) -> &str {
&self.log_from
}
pub fn log_to(&self) -> &str {
&self.log_to
}
pub fn client_addr(&self) -> &SocketAddr {
&self.client_addr
}
pub fn set_proto(&mut self, proto: &'static str) {
if log_enabled!(log::Level::Info) {
self.proto = proto;
self.to_addr = None;
self.re_calc();
}
}
pub fn set_c2s_stream_open(&mut self, is_c2s: bool, stream_open: &[u8]) {
if log_enabled!(log::Level::Info) {
self.is_c2s = Some(is_c2s);
self.from = stream_open
.extract_between(b" from='", b"'")
.or_else(|_| stream_open.extract_between(b" from=\"", b"\""))
.map(|b| to_str(b).to_string())
.ok();
self.to = stream_open
.extract_between(b" to='", b"'")
.or_else(|_| stream_open.extract_between(b" to=\"", b"\""))
.map(|b| to_str(b).to_string())
.ok();
self.re_calc();
info!("{} stream data set", &self.log_from());
}
}
pub fn set_to_addr(&mut self, to_addr: SocketAddr) {
if log_enabled!(log::Level::Info) {
self.to_addr = Some(to_addr);
self.re_calc();
}
}
}

View File

@ -1,14 +1,20 @@
// Box<dyn AsyncWrite + Unpin + Send>, Box<dyn AsyncRead + Unpin + Send> // Box<dyn AsyncWrite + Unpin + Send>, Box<dyn AsyncRead + Unpin + Send>
#[cfg(feature = "websocket")] #[cfg(feature = "websocket")]
use crate::{from_ws, to_ws_new, AsyncReadAndWrite}; use crate::websocket::{from_ws, to_ws_new, AsyncReadAndWrite};
use crate::{slicesubsequence::SliceSubsequence, trace, StanzaFilter, StanzaRead::*, StanzaReader, StanzaWrite::*}; use crate::{
common::IN_BUFFER_SIZE,
in_out::{StanzaRead::*, StanzaWrite::*},
slicesubsequence::SliceSubsequence,
stanzafilter::{StanzaFilter, StanzaReader},
};
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 log::trace;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader}; 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};
@ -75,7 +81,7 @@ impl StanzaRead {
#[inline(always)] #[inline(always)]
pub fn new<T: 'static + AsyncRead + Unpin + Send>(rd: T) -> Self { pub fn new<T: 'static + AsyncRead + Unpin + Send>(rd: T) -> Self {
// we naively read 1 byte at a time, which buffering significantly speeds up // we naively read 1 byte at a time, which buffering significantly speeds up
AsyncRead(StanzaReader(Box::new(BufReader::with_capacity(crate::IN_BUFFER_SIZE, rd)))) AsyncRead(StanzaReader(Box::new(BufReader::with_capacity(IN_BUFFER_SIZE, rd))))
} }
#[inline(always)] #[inline(always)]

View File

@ -1,201 +1,28 @@
mod stanzafilter;
pub use stanzafilter::*;
mod slicesubsequence;
use slicesubsequence::*;
use anyhow::bail; use anyhow::bail;
use log::info;
use std::net::SocketAddr; use std::net::SocketAddr;
pub use log::{debug, error, info, log_enabled, trace}; pub mod common;
pub mod slicesubsequence;
pub mod stanzafilter;
#[cfg(feature = "s2s-incoming")] #[cfg(feature = "quic")]
use rustls::{Certificate, ServerConnection}; pub mod quic;
pub fn to_str(buf: &[u8]) -> std::borrow::Cow<'_, str> { #[cfg(feature = "tls")]
String::from_utf8_lossy(buf) pub mod tls;
}
pub fn c2s(is_c2s: bool) -> &'static str { #[cfg(feature = "outgoing")]
if is_c2s { pub mod outgoing;
"c2s"
} else {
"s2s"
}
}
pub async fn first_bytes_match(stream: &tokio::net::TcpStream, p: &mut [u8], matcher: fn(&[u8]) -> bool) -> anyhow::Result<bool> { #[cfg(any(feature = "s2s-incoming", feature = "outgoing"))]
// sooo... I don't think peek here can be used for > 1 byte without this timer craziness... can it? pub mod srv;
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)) #[cfg(feature = "websocket")]
} pub mod websocket;
#[derive(Clone)] #[cfg(any(feature = "s2s-incoming", feature = "outgoing"))]
pub struct Context<'a> { pub mod verify;
conn_id: String,
log_from: String,
log_to: String,
proto: &'a str,
is_c2s: Option<bool>,
to: Option<String>,
to_addr: Option<SocketAddr>,
from: Option<String>,
client_addr: SocketAddr,
}
impl<'a> Context<'a> { mod context;
pub fn new(proto: &'static str, client_addr: SocketAddr) -> Context { pub mod in_out;
let (log_to, log_from, conn_id) = if log_enabled!(log::Level::Info) {
#[cfg(feature = "logging")]
let conn_id = {
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
thread_rng().sample_iter(&Alphanumeric).take(10).map(char::from).collect()
};
#[cfg(not(feature = "logging"))]
let conn_id = "".to_string();
(
format!("{}: ({} <- ({}-unk)):", conn_id, client_addr, proto),
format!("{}: ({} -> ({}-unk)):", conn_id, client_addr, proto),
conn_id,
)
} else {
("".to_string(), "".to_string(), "".to_string())
};
Context {
conn_id,
log_from,
log_to,
proto,
client_addr,
is_c2s: None,
to: None,
to_addr: None,
from: None,
}
}
fn re_calc(&mut self) {
// todo: make this good
self.log_from = format!(
"{}: ({} ({}) -> ({}-{}) -> {} ({})):",
self.conn_id,
self.client_addr,
if self.from.is_some() { self.from.as_ref().unwrap() } else { "unk" },
self.proto,
if self.is_c2s.is_some() { c2s(self.is_c2s.unwrap()) } else { "unk" },
if self.to_addr.is_some() { self.to_addr.as_ref().unwrap().to_string() } else { "unk".to_string() },
if self.to.is_some() { self.to.as_ref().unwrap() } else { "unk" },
);
self.log_to = self.log_from.replace(" -> ", " <- ");
}
pub fn log_from(&self) -> &str {
&self.log_from
}
pub fn log_to(&self) -> &str {
&self.log_to
}
pub fn client_addr(&self) -> &SocketAddr {
&self.client_addr
}
pub fn set_proto(&mut self, proto: &'static str) {
if log_enabled!(log::Level::Info) {
self.proto = proto;
self.to_addr = None;
self.re_calc();
}
}
pub fn set_c2s_stream_open(&mut self, is_c2s: bool, stream_open: &[u8]) {
if log_enabled!(log::Level::Info) {
self.is_c2s = Some(is_c2s);
self.from = stream_open
.extract_between(b" from='", b"'")
.or_else(|_| stream_open.extract_between(b" from=\"", b"\""))
.map(|b| to_str(b).to_string())
.ok();
self.to = stream_open
.extract_between(b" to='", b"'")
.or_else(|_| stream_open.extract_between(b" to=\"", b"\""))
.map(|b| to_str(b).to_string())
.ok();
self.re_calc();
info!("{} stream data set", &self.log_from());
}
}
pub fn set_to_addr(&mut self, to_addr: SocketAddr) {
if log_enabled!(log::Level::Info) {
self.to_addr = Some(to_addr);
self.re_calc();
}
}
}
#[cfg(not(feature = "s2s-incoming"))]
pub type ServerCerts = ();
#[cfg(feature = "s2s-incoming")]
#[derive(Clone)]
pub enum ServerCerts {
Tls(&'static ServerConnection),
#[cfg(feature = "quic")]
Quic(quinn::Connection),
}
#[cfg(feature = "s2s-incoming")]
impl ServerCerts {
pub fn peer_certificates(&self) -> Option<Vec<Certificate>> {
match self {
ServerCerts::Tls(c) => c.peer_certificates().map(|c| c.to_vec()),
#[cfg(feature = "quic")]
ServerCerts::Quic(c) => c.peer_identity().and_then(|v| v.downcast::<Vec<Certificate>>().ok()).map(|v| v.to_vec()),
}
}
pub fn sni(&self) -> Option<String> {
match self {
ServerCerts::Tls(c) => c.sni_hostname().map(|s| s.to_string()),
#[cfg(feature = "quic")]
ServerCerts::Quic(c) => c.handshake_data().and_then(|v| v.downcast::<quinn::crypto::rustls::HandshakeData>().ok()).and_then(|h| h.server_name),
}
}
pub fn alpn(&self) -> Option<Vec<u8>> {
match self {
ServerCerts::Tls(c) => c.alpn_protocol().map(|s| s.to_vec()),
#[cfg(feature = "quic")]
ServerCerts::Quic(c) => c.handshake_data().and_then(|v| v.downcast::<quinn::crypto::rustls::HandshakeData>().ok()).and_then(|h| h.protocol),
}
}
pub fn is_tls(&self) -> bool {
match self {
ServerCerts::Tls(_) => true,
#[cfg(feature = "quic")]
ServerCerts::Quic(_) => false,
}
}
}

View File

@ -1,112 +1,14 @@
#![deny(clippy::all)] #![deny(clippy::all)]
use anyhow::Result;
use std::ffi::OsString;
use std::fs::File;
use std::io;
use std::io::{BufReader, Read, Write};
use std::iter::Iterator;
use std::net::SocketAddr;
use std::path::Path;
use std::sync::{Arc, RwLock};
use die::{die, Die}; use die::{die, Die};
use log::{debug, error, info};
use serde_derive::Deserialize; use serde_derive::Deserialize;
use std::{ffi::OsString, fs::File, io::Read, iter::Iterator, net::SocketAddr, path::Path, sync::Arc};
use tokio::io::{AsyncWriteExt, ReadHalf, WriteHalf};
use tokio::net::TcpListener;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use xmpp_proxy::common::certs_key::CertsKey;
#[cfg(feature = "rustls")]
use rustls::{
sign::{CertifiedKey, RsaSigningKey, SigningKey},
Certificate, ClientConfig, PrivateKey, ServerConfig, SignatureScheme,
};
#[cfg(feature = "tokio-rustls")]
use tokio_rustls::{
webpki::{DnsNameRef, TlsServerTrustAnchors, TrustAnchor},
TlsConnector,
};
use anyhow::{anyhow, bail, Result};
mod slicesubsequence;
use slicesubsequence::*;
pub use xmpp_proxy::*;
#[cfg(feature = "quic")]
mod quic;
#[cfg(feature = "quic")]
use crate::quic::*;
#[cfg(feature = "tls")]
mod tls;
#[cfg(feature = "tls")]
use crate::tls::*;
#[cfg(feature = "outgoing")] #[cfg(feature = "outgoing")]
mod outgoing; use xmpp_proxy::{common::outgoing::OutgoingConfig, outgoing::spawn_outgoing_listener};
#[cfg(feature = "outgoing")]
use crate::outgoing::*;
#[cfg(any(feature = "s2s-incoming", feature = "outgoing"))]
mod srv;
#[cfg(any(feature = "s2s-incoming", feature = "outgoing"))]
use crate::srv::*;
#[cfg(feature = "websocket")]
mod websocket;
#[cfg(feature = "websocket")]
use crate::websocket::*;
#[cfg(any(feature = "s2s-incoming", feature = "outgoing"))]
mod verify;
#[cfg(any(feature = "s2s-incoming", feature = "outgoing"))]
use crate::verify::*;
mod in_out;
pub use crate::in_out::*;
const IN_BUFFER_SIZE: usize = 8192;
// todo: split these out to outgoing module
const ALPN_XMPP_CLIENT: &[u8] = b"xmpp-client";
const ALPN_XMPP_SERVER: &[u8] = b"xmpp-server";
#[cfg(all(feature = "webpki-roots", not(feature = "rustls-native-certs")))]
pub use webpki_roots::TLS_SERVER_ROOTS;
#[cfg(all(feature = "rustls-native-certs", not(feature = "webpki-roots")))]
lazy_static::lazy_static! {
static ref TLS_SERVER_ROOTS: TlsServerTrustAnchors<'static> = {
// we need these to stick around for 'static, this is only called once so no problem
let certs = Box::leak(Box::new(rustls_native_certs::load_native_certs().expect("could not load platform certs")));
let root_cert_store = Box::leak(Box::new(Vec::new()));
for cert in certs {
// some system CAs are invalid, ignore those
if let Ok(ta) = TrustAnchor::try_from_cert_der(&cert.0) {
root_cert_store.push(ta);
}
}
TlsServerTrustAnchors(root_cert_store)
};
}
#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
pub fn root_cert_store() -> rustls::RootCertStore {
use rustls::{OwnedTrustAnchor, RootCertStore};
let mut root_cert_store = RootCertStore::empty();
root_cert_store.add_server_trust_anchors(
TLS_SERVER_ROOTS
.0
.iter()
.map(|ta| OwnedTrustAnchor::from_subject_spki_name_constraints(ta.subject, ta.spki, ta.name_constraints)),
);
root_cert_store
}
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
struct Config { struct Config {
@ -123,87 +25,6 @@ struct Config {
log_style: Option<String>, log_style: Option<String>,
} }
#[derive(Clone)]
pub struct CloneableConfig {
max_stanza_size_bytes: usize,
#[cfg(feature = "s2s-incoming")]
s2s_target: Option<SocketAddr>,
#[cfg(feature = "c2s-incoming")]
c2s_target: Option<SocketAddr>,
proxy: bool,
}
struct CertsKey {
#[cfg(feature = "rustls-pemfile")]
inner: Result<RwLock<Arc<rustls::sign::CertifiedKey>>>,
}
impl CertsKey {
fn new(main_config: &Config) -> Self {
CertsKey {
#[cfg(feature = "rustls-pemfile")]
inner: main_config.certs_key().map(|c| RwLock::new(Arc::new(c))),
}
}
#[cfg(all(unix, any(feature = "incoming", feature = "s2s-outgoing")))]
fn spawn_refresh_task(&'static self, cfg_path: OsString) -> Option<JoinHandle<Result<()>>> {
if self.inner.is_err() {
None
} else {
Some(tokio::spawn(async move {
use tokio::signal::unix::{signal, SignalKind};
let mut stream = signal(SignalKind::hangup())?;
loop {
stream.recv().await;
info!("got SIGHUP");
match Config::parse(&cfg_path).and_then(|c| c.certs_key()) {
Ok(cert_key) => {
if let Ok(rwl) = self.inner.as_ref() {
let cert_key = Arc::new(cert_key);
let mut certs_key = rwl.write().expect("CertKey poisoned?");
*certs_key = cert_key;
drop(certs_key);
info!("reloaded cert/key successfully!");
}
}
Err(e) => error!("invalid config/cert/key on SIGHUP: {}", e),
};
}
}))
}
}
}
#[cfg(feature = "rustls-pemfile")]
impl rustls::server::ResolvesServerCert for CertsKey {
fn resolve(&self, _: rustls::server::ClientHello) -> Option<Arc<rustls::sign::CertifiedKey>> {
self.inner.as_ref().map(|rwl| rwl.read().expect("CertKey poisoned?").clone()).ok()
}
}
#[cfg(feature = "rustls-pemfile")]
impl rustls::client::ResolvesClientCert for CertsKey {
fn resolve(&self, _: &[&[u8]], _: &[SignatureScheme]) -> Option<Arc<CertifiedKey>> {
self.inner.as_ref().map(|rwl| rwl.read().expect("CertKey poisoned?").clone()).ok()
}
fn has_certs(&self) -> bool {
self.inner.is_ok()
}
}
#[cfg(not(feature = "rustls-pemfile"))]
impl rustls::client::ResolvesClientCert for CertsKey {
fn resolve(&self, _: &[&[u8]], _: &[SignatureScheme]) -> Option<Arc<CertifiedKey>> {
None
}
fn has_certs(&self) -> bool {
false
}
}
impl Config { impl Config {
fn parse<P: AsRef<Path>>(path: P) -> Result<Config> { fn parse<P: AsRef<Path>>(path: P) -> Result<Config> {
let mut f = File::open(path)?; let mut f = File::open(path)?;
@ -212,8 +33,9 @@ impl Config {
Ok(toml::from_str(&input)?) Ok(toml::from_str(&input)?)
} }
fn get_cloneable_cfg(&self) -> CloneableConfig { #[cfg(feature = "incoming")]
CloneableConfig { fn get_cloneable_cfg(&self) -> xmpp_proxy::common::incoming::CloneableConfig {
xmpp_proxy::common::incoming::CloneableConfig {
max_stanza_size_bytes: self.max_stanza_size_bytes, max_stanza_size_bytes: self.max_stanza_size_bytes,
#[cfg(feature = "s2s-incoming")] #[cfg(feature = "s2s-incoming")]
s2s_target: self.s2s_target, s2s_target: self.s2s_target,
@ -238,268 +60,41 @@ impl Config {
#[cfg(feature = "rustls-pemfile")] #[cfg(feature = "rustls-pemfile")]
fn certs_key(&self) -> Result<rustls::sign::CertifiedKey> { fn certs_key(&self) -> Result<rustls::sign::CertifiedKey> {
use rustls_pemfile::{certs, read_all, Item}; xmpp_proxy::common::read_certified_key(&self.tls_key, &self.tls_cert)
let tls_key = read_all(&mut BufReader::new(File::open(&self.tls_key)?))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?
.into_iter()
.flat_map(|item| match item {
Item::RSAKey(der) => RsaSigningKey::new(&PrivateKey(der)).ok().map(Arc::new).map(|r| r as Arc<dyn SigningKey>),
Item::PKCS8Key(der) => rustls::sign::any_supported_type(&PrivateKey(der)).ok(),
Item::ECKey(der) => rustls::sign::any_supported_type(&PrivateKey(der)).ok(),
_ => None,
})
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?;
let tls_certs = certs(&mut BufReader::new(File::open(&self.tls_cert)?))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert"))
.map(|mut certs| certs.drain(..).map(Certificate).collect())?;
Ok(rustls::sign::CertifiedKey::new(tls_certs, tls_key))
} }
#[cfg(feature = "incoming")] #[cfg(not(feature = "rustls-pemfile"))]
fn server_config(&self, certs_key: Arc<CertsKey>) -> Result<ServerConfig> { fn certs_key(&self) -> Result<rustls::sign::CertifiedKey> {
if let Err(e) = &certs_key.inner { anyhow::bail!("rustls-pemfile disabled at compile time")
bail!("invalid cert/key: {}", e);
}
let config = ServerConfig::builder().with_safe_defaults();
#[cfg(feature = "s2s")]
let config = config.with_client_cert_verifier(Arc::new(AllowAnonymousOrAnyCert));
#[cfg(not(feature = "s2s"))]
let config = config.with_no_client_auth();
let mut config = config.with_cert_resolver(certs_key);
// todo: will connecting without alpn work then?
config.alpn_protocols.push(ALPN_XMPP_CLIENT.to_vec());
config.alpn_protocols.push(ALPN_XMPP_SERVER.to_vec());
Ok(config)
} }
} }
#[derive(Clone)] #[cfg(all(unix, any(feature = "incoming", feature = "s2s-outgoing")))]
#[cfg(feature = "outgoing")] fn spawn_refresh_task(certs_key: &'static CertsKey, cfg_path: OsString) -> Option<JoinHandle<Result<()>>> {
pub struct OutgoingConfig { if certs_key.inner.is_err() {
max_stanza_size_bytes: usize, None
certs_key: Arc<CertsKey>,
}
#[cfg(feature = "outgoing")]
impl OutgoingConfig {
pub fn with_custom_certificate_verifier(&self, is_c2s: bool, cert_verifier: XmppServerCertVerifier) -> OutgoingVerifierConfig {
let config = match is_c2s {
false => ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(cert_verifier))
.with_client_cert_resolver(self.certs_key.clone()),
_ => ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(cert_verifier))
.with_no_client_auth(),
};
let mut config_alpn = config.clone();
config_alpn.alpn_protocols.push(if is_c2s { ALPN_XMPP_CLIENT } else { ALPN_XMPP_SERVER }.to_vec());
let config_alpn = Arc::new(config_alpn);
let connector_alpn: TlsConnector = config_alpn.clone().into();
let connector: TlsConnector = Arc::new(config).into();
OutgoingVerifierConfig {
max_stanza_size_bytes: self.max_stanza_size_bytes,
config_alpn,
connector_alpn,
connector,
}
}
}
#[derive(Clone)]
#[cfg(feature = "outgoing")]
pub struct OutgoingVerifierConfig {
pub max_stanza_size_bytes: usize,
pub config_alpn: Arc<ClientConfig>,
pub connector_alpn: TlsConnector,
pub connector: TlsConnector,
}
#[cfg(feature = "incoming")]
async fn shuffle_rd_wr(in_rd: StanzaRead, in_wr: StanzaWrite, config: CloneableConfig, server_certs: ServerCerts, local_addr: SocketAddr, client_addr: &mut Context<'_>) -> Result<()> {
let filter = StanzaFilter::new(config.max_stanza_size_bytes);
shuffle_rd_wr_filter(in_rd, in_wr, config, server_certs, local_addr, client_addr, filter).await
}
#[cfg(feature = "incoming")]
async fn shuffle_rd_wr_filter(
mut in_rd: StanzaRead,
mut in_wr: StanzaWrite,
config: CloneableConfig,
server_certs: ServerCerts,
local_addr: SocketAddr,
client_addr: &mut Context<'_>,
mut in_filter: StanzaFilter,
) -> Result<()> {
// now read to figure out client vs server
let (stream_open, is_c2s) = stream_preamble(&mut in_rd, &mut in_wr, client_addr.log_from(), &mut in_filter).await?;
client_addr.set_c2s_stream_open(is_c2s, &stream_open);
#[cfg(feature = "s2s-incoming")]
{
trace!(
"{} connected: sni: {:?}, alpn: {:?}, tls-not-quic: {}",
client_addr.log_from(),
server_certs.sni(),
server_certs.alpn().map(|a| String::from_utf8_lossy(&a).to_string()),
server_certs.is_tls(),
);
if !is_c2s {
// for s2s we need this
use std::time::SystemTime;
let domain = stream_open
.extract_between(b" from='", b"'")
.or_else(|_| stream_open.extract_between(b" from=\"", b"\""))
.and_then(|b| Ok(std::str::from_utf8(b)?))?;
let (_, cert_verifier) = get_xmpp_connections(domain, is_c2s).await?;
let certs = server_certs.peer_certificates().ok_or_else(|| anyhow!("no client cert auth for s2s incoming from {}", domain))?;
// todo: send stream error saying cert is invalid
cert_verifier.verify_cert(&certs[0], &certs[1..], SystemTime::now())?;
}
drop(server_certs);
}
let (out_rd, out_wr) = open_incoming(&config, local_addr, client_addr, &stream_open, is_c2s, &mut in_filter).await?;
drop(stream_open);
shuffle_rd_wr_filter_only(
in_rd,
in_wr,
StanzaRead::new(out_rd),
StanzaWrite::new(out_wr),
is_c2s,
config.max_stanza_size_bytes,
client_addr,
in_filter,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn shuffle_rd_wr_filter_only(
mut in_rd: StanzaRead,
mut in_wr: StanzaWrite,
mut out_rd: StanzaRead,
mut out_wr: StanzaWrite,
is_c2s: bool,
max_stanza_size_bytes: usize,
client_addr: &mut Context<'_>,
mut in_filter: StanzaFilter,
) -> Result<()> {
let mut out_filter = StanzaFilter::new(max_stanza_size_bytes);
loop {
tokio::select! {
Ok(ret) = in_rd.next(&mut in_filter, client_addr.log_to(), &mut in_wr) => {
match ret {
None => break,
Some((buf, eoft)) => {
trace!("{} '{}'", client_addr.log_from(), to_str(buf));
out_wr.write_all(is_c2s, buf, eoft, client_addr.log_from()).await?;
out_wr.flush().await?;
}
}
},
Ok(ret) = out_rd.next(&mut out_filter, client_addr.log_from(), &mut out_wr) => {
match ret {
None => break,
Some((buf, eoft)) => {
trace!("{} '{}'", client_addr.log_to(), to_str(buf));
in_wr.write_all(is_c2s, buf, eoft, client_addr.log_to()).await?;
in_wr.flush().await?;
}
}
},
}
}
info!("{} disconnected", client_addr.log_from());
Ok(())
}
#[cfg(feature = "incoming")]
async fn open_incoming(
config: &CloneableConfig,
local_addr: SocketAddr,
client_addr: &mut Context<'_>,
stream_open: &[u8],
is_c2s: bool,
in_filter: &mut StanzaFilter,
) -> Result<(ReadHalf<tokio::net::TcpStream>, WriteHalf<tokio::net::TcpStream>)> {
let target = if is_c2s {
#[cfg(not(feature = "c2s-incoming"))]
bail!("incoming c2s connection but lacking compile-time support");
#[cfg(feature = "c2s-incoming")]
config.c2s_target
} else { } else {
#[cfg(not(feature = "s2s-incoming"))] Some(tokio::spawn(async move {
bail!("incoming s2s connection but lacking compile-time support"); use tokio::signal::unix::{signal, SignalKind};
#[cfg(feature = "s2s-incoming")] let mut stream = signal(SignalKind::hangup())?;
config.s2s_target loop {
stream.recv().await;
info!("got SIGHUP");
match Config::parse(&cfg_path).and_then(|c| c.certs_key()) {
Ok(cert_key) => {
if let Ok(rwl) = certs_key.inner.as_ref() {
let cert_key = Arc::new(cert_key);
let mut certs_key = rwl.write().expect("CertKey poisoned?");
*certs_key = cert_key;
drop(certs_key);
info!("reloaded cert/key successfully!");
}
}
Err(e) => error!("invalid config/cert/key on SIGHUP: {}", e),
};
}
}))
} }
.ok_or_else(|| anyhow!("incoming connection but `{}_target` not defined", c2s(is_c2s)))?;
client_addr.set_to_addr(target);
let out_stream = tokio::net::TcpStream::connect(target).await?;
let (out_rd, mut out_wr) = tokio::io::split(out_stream);
if config.proxy {
/*
https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n
PROXY TCP6 ffff:f...f:ffff ffff:f...f:ffff 65535 65535\r\n
PROXY TCP6 SOURCE_IP DEST_IP SOURCE_PORT DEST_PORT\r\n
*/
// tokio AsyncWrite doesn't have write_fmt so have to go through this buffer for some crazy reason
//write!(out_wr, "PROXY TCP{} {} {} {} {}\r\n", if client_addr.is_ipv4() { '4' } else {'6' }, client_addr.ip(), local_addr.ip(), client_addr.port(), local_addr.port())?;
write!(
&mut in_filter.buf[0..],
"PROXY TCP{} {} {} {} {}\r\n",
if client_addr.client_addr().is_ipv4() { '4' } else { '6' },
client_addr.client_addr().ip(),
local_addr.ip(),
client_addr.client_addr().port(),
local_addr.port()
)?;
let end_idx = &(&in_filter.buf[0..]).first_index_of(b"\n")? + 1;
trace!("{} '{}'", client_addr.log_from(), to_str(&in_filter.buf[0..end_idx]));
out_wr.write_all(&in_filter.buf[0..end_idx]).await?;
}
trace!("{} '{}'", client_addr.log_from(), to_str(stream_open));
out_wr.write_all(stream_open).await?;
out_wr.flush().await?;
Ok((out_rd, out_wr))
}
pub async fn stream_preamble(in_rd: &mut StanzaRead, in_wr: &mut StanzaWrite, client_addr: &'_ str, in_filter: &mut StanzaFilter) -> Result<(Vec<u8>, bool)> {
let mut stream_open = Vec::new();
while let Ok(Some((buf, _))) = in_rd.next(in_filter, client_addr, in_wr).await {
trace!("{} received pre-<stream:stream> stanza: '{}'", client_addr, to_str(buf));
if buf.starts_with(b"<?xml ") {
stream_open.extend_from_slice(buf);
} else if buf.starts_with(b"<stream:stream ") {
stream_open.extend_from_slice(buf);
return Ok((stream_open, buf.contains_seq(br#" xmlns="jabber:client""#) || buf.contains_seq(br#" xmlns='jabber:client'"#)));
} else {
bail!("bad pre-<stream:stream> stanza: {}", to_str(buf));
}
}
bail!("stream ended before open")
} }
#[tokio::main] #[tokio::main]
@ -533,18 +128,23 @@ async fn main() {
die!("log_level or log_style defined in config but logging disabled at compile-time"); die!("log_level or log_style defined in config but logging disabled at compile-time");
} }
#[cfg(feature = "incoming")]
let config = main_config.get_cloneable_cfg(); let config = main_config.get_cloneable_cfg();
let certs_key = Arc::new(CertsKey::new(&main_config)); let certs_key = Arc::new(CertsKey::new(main_config.certs_key()));
let mut handles: Vec<JoinHandle<Result<()>>> = Vec::new(); let mut handles: Vec<JoinHandle<Result<()>>> = Vec::new();
if !main_config.incoming_listen.is_empty() { if !main_config.incoming_listen.is_empty() {
#[cfg(all(any(feature = "tls", feature = "websocket"), feature = "incoming"))] #[cfg(all(any(feature = "tls", feature = "websocket"), feature = "incoming"))]
{ {
use xmpp_proxy::{
common::incoming::server_config,
tls::incoming::{spawn_tls_listener, tls_acceptor},
};
if main_config.c2s_target.is_none() && main_config.s2s_target.is_none() { if main_config.c2s_target.is_none() && main_config.s2s_target.is_none() {
die!("one of c2s_target/s2s_target must be defined if incoming_listen is non-empty"); die!("one of c2s_target/s2s_target must be defined if incoming_listen is non-empty");
} }
let acceptor = main_config.tls_acceptor(certs_key.clone()).die("invalid cert/key ?"); let acceptor = tls_acceptor(server_config(certs_key.clone()).die("invalid cert/key ?"));
for listener in main_config.incoming_listen.iter() { for listener in main_config.incoming_listen.iter() {
handles.push(spawn_tls_listener(listener.parse().die("invalid listener address"), config.clone(), acceptor.clone())); handles.push(spawn_tls_listener(listener.parse().die("invalid listener address"), config.clone(), acceptor.clone()));
} }
@ -555,10 +155,14 @@ async fn main() {
if !main_config.quic_listen.is_empty() { if !main_config.quic_listen.is_empty() {
#[cfg(all(feature = "quic", feature = "incoming"))] #[cfg(all(feature = "quic", feature = "incoming"))]
{ {
use xmpp_proxy::{
common::incoming::server_config,
quic::incoming::{quic_server_config, spawn_quic_listener},
};
if main_config.c2s_target.is_none() && main_config.s2s_target.is_none() { if main_config.c2s_target.is_none() && main_config.s2s_target.is_none() {
die!("one of c2s_target/s2s_target must be defined if quic_listen is non-empty"); die!("one of c2s_target/s2s_target must be defined if quic_listen is non-empty");
} }
let quic_config = main_config.quic_server_config(certs_key.clone()).die("invalid cert/key ?"); let quic_config = quic_server_config(server_config(certs_key.clone()).die("invalid cert/key ?"));
for listener in main_config.quic_listen.iter() { for listener in main_config.quic_listen.iter() {
handles.push(spawn_quic_listener(listener.parse().die("invalid listener address"), config.clone(), quic_config.clone())); handles.push(spawn_quic_listener(listener.parse().die("invalid listener address"), config.clone(), quic_config.clone()));
} }
@ -581,8 +185,11 @@ async fn main() {
die!("all of incoming_listen, quic_listen, outgoing_listen empty, nothing to do, exiting..."); die!("all of incoming_listen, quic_listen, outgoing_listen empty, nothing to do, exiting...");
} }
#[cfg(all(unix, any(feature = "incoming", feature = "s2s-outgoing")))] #[cfg(all(unix, any(feature = "incoming", feature = "s2s-outgoing")))]
if let Some(refresh_task) = Box::leak(Box::new(certs_key.clone())).spawn_refresh_task(cfg_path) { {
handles.push(refresh_task); let certs_key = Box::leak(Box::new(certs_key.clone()));
if let Some(refresh_task) = spawn_refresh_task(certs_key, cfg_path) {
handles.push(refresh_task);
}
} }
info!("xmpp-proxy started"); info!("xmpp-proxy started");

View File

@ -1,4 +1,16 @@
use crate::*; use crate::{
common::{first_bytes_match, outgoing::OutgoingConfig, shuffle_rd_wr_filter_only, stream_preamble},
context::Context,
in_out::{StanzaRead, StanzaWrite},
slicesubsequence::SliceSubsequence,
srv::srv_connect,
stanzafilter::StanzaFilter,
};
use anyhow::Result;
use die::Die;
use log::{error, info};
use std::net::SocketAddr;
use tokio::{net::TcpListener, task::JoinHandle};
async fn handle_outgoing_connection(stream: tokio::net::TcpStream, client_addr: &mut Context<'_>, config: OutgoingConfig) -> Result<()> { async fn handle_outgoing_connection(stream: tokio::net::TcpStream, client_addr: &mut Context<'_>, config: OutgoingConfig) -> Result<()> {
info!("{} connected", client_addr.log_from()); info!("{} connected", client_addr.log_from());
@ -7,7 +19,7 @@ async fn handle_outgoing_connection(stream: tokio::net::TcpStream, client_addr:
#[cfg(feature = "websocket")] #[cfg(feature = "websocket")]
let (mut in_rd, mut in_wr) = if first_bytes_match(&stream, &mut in_filter.buf[0..3], |p| p == b"GET").await? { let (mut in_rd, mut in_wr) = if first_bytes_match(&stream, &mut in_filter.buf[0..3], |p| p == b"GET").await? {
incoming_websocket_connection(Box::new(stream), config.max_stanza_size_bytes).await? crate::websocket::incoming_websocket_connection(Box::new(stream), config.max_stanza_size_bytes).await?
} else { } else {
let (in_rd, in_wr) = tokio::io::split(stream); let (in_rd, in_wr) = tokio::io::split(stream);
(StanzaRead::new(in_rd), StanzaWrite::new(in_wr)) (StanzaRead::new(in_rd), StanzaWrite::new(in_wr))

View File

@ -1,40 +1,16 @@
use crate::*; use crate::{
use futures::StreamExt; common::incoming::{shuffle_rd_wr, CloneableConfig, ServerCerts},
use quinn::{ServerConfig, TransportConfig}; context::Context,
use std::{net::SocketAddr, sync::Arc}; in_out::{StanzaRead, StanzaWrite},
};
use anyhow::Result; use anyhow::Result;
use die::Die;
use futures::StreamExt;
use log::{error, info};
use quinn::ServerConfig;
use std::{net::SocketAddr, sync::Arc};
use tokio::task::JoinHandle;
#[cfg(feature = "outgoing")]
pub async fn quic_connect(target: SocketAddr, server_name: &str, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
let bind_addr = "0.0.0.0:0".parse().unwrap();
let client_cfg = config.config_alpn;
let mut endpoint = quinn::Endpoint::client(bind_addr)?;
endpoint.set_default_client_config(quinn::ClientConfig::new(client_cfg));
// connect to server
let quinn::NewConnection { connection, .. } = endpoint.connect(target, server_name)?.await?;
trace!("quic connected: addr={}", connection.remote_address());
let (wrt, rd) = connection.open_bi().await?;
Ok((StanzaWrite::new(wrt), StanzaRead::new(rd)))
}
#[cfg(feature = "incoming")]
impl Config {
pub fn quic_server_config(&self, cert_key: Arc<CertsKey>) -> Result<ServerConfig> {
let transport_config = TransportConfig::default();
// todo: configure transport_config here if needed
let server_config = self.server_config(cert_key)?;
let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(server_config));
server_config.transport = Arc::new(transport_config);
Ok(server_config)
}
}
#[cfg(feature = "incoming")]
pub fn spawn_quic_listener(local_addr: SocketAddr, config: CloneableConfig, server_config: ServerConfig) -> JoinHandle<Result<()>> { pub fn spawn_quic_listener(local_addr: SocketAddr, config: CloneableConfig, server_config: ServerConfig) -> JoinHandle<Result<()>> {
let (_endpoint, mut incoming) = quinn::Endpoint::server(server_config, local_addr).die("cannot listen on port/interface"); let (_endpoint, mut incoming) = quinn::Endpoint::server(server_config, local_addr).die("cannot listen on port/interface");
tokio::spawn(async move { tokio::spawn(async move {
@ -43,7 +19,7 @@ pub fn spawn_quic_listener(local_addr: SocketAddr, config: CloneableConfig, serv
let config = config.clone(); let config = config.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Ok(mut new_conn) = incoming_conn.await { if let Ok(mut new_conn) = incoming_conn.await {
let client_addr = crate::Context::new("quic-in", new_conn.connection.remote_address()); let client_addr = Context::new("quic-in", new_conn.connection.remote_address());
#[cfg(feature = "s2s-incoming")] #[cfg(feature = "s2s-incoming")]
let server_certs = ServerCerts::Quic(new_conn.connection); let server_certs = ServerCerts::Quic(new_conn.connection);
@ -70,3 +46,12 @@ pub fn spawn_quic_listener(local_addr: SocketAddr, config: CloneableConfig, serv
Ok(()) Ok(())
}) })
} }
pub fn quic_server_config(server_config: rustls::ServerConfig) -> ServerConfig {
let transport_config = quinn::TransportConfig::default();
// todo: configure transport_config here if needed
let mut server_config = ServerConfig::with_crypto(Arc::new(server_config));
server_config.transport = Arc::new(transport_config);
server_config
}

5
src/quic/mod.rs Normal file
View File

@ -0,0 +1,5 @@
#[cfg(feature = "incoming")]
pub mod incoming;
#[cfg(feature = "outgoing")]
pub mod outgoing;

23
src/quic/outgoing.rs Normal file
View File

@ -0,0 +1,23 @@
use std::net::SocketAddr;
use crate::{
common::outgoing::OutgoingVerifierConfig,
in_out::{StanzaRead, StanzaWrite},
};
use anyhow::Result;
use log::trace;
pub async fn quic_connect(target: SocketAddr, server_name: &str, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
let bind_addr = "0.0.0.0:0".parse().unwrap();
let client_cfg = config.config_alpn;
let mut endpoint = quinn::Endpoint::client(bind_addr)?;
endpoint.set_default_client_config(quinn::ClientConfig::new(client_cfg));
// connect to server
let quinn::NewConnection { connection, .. } = endpoint.connect(target, server_name)?.await?;
trace!("quic connected: addr={}", connection.remote_address());
let (wrt, rd) = connection.open_bi().await?;
Ok((StanzaWrite::new(wrt), StanzaRead::new(rd)))
}

View File

@ -1,22 +1,33 @@
#![allow(clippy::upper_case_acronyms)] #![allow(clippy::upper_case_acronyms)]
use std::cmp::Ordering; #[cfg(feature = "outgoing")]
use std::convert::TryFrom; use crate::common::outgoing::{OutgoingConfig, OutgoingVerifierConfig};
use std::net::{IpAddr, SocketAddr}; use crate::{
common::{stream_preamble, to_str},
use data_encoding::BASE64; context::Context,
use ring::digest::{Algorithm, Context as DigestContext, SHA256, SHA512}; in_out::{StanzaRead, StanzaWrite},
slicesubsequence::SliceSubsequence,
use trust_dns_resolver::error::ResolveError; stanzafilter::{StanzaFilter, StanzaReader},
use trust_dns_resolver::lookup::{SrvLookup, TxtLookup}; verify::XmppServerCertVerifier,
use trust_dns_resolver::{IntoName, TokioAsyncResolver}; };
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use tokio_rustls::webpki::DnsName; use data_encoding::BASE64;
use log::{debug, error, trace};
use ring::digest::{Algorithm, Context as DigestContext, SHA256, SHA512};
use serde::Deserialize;
use std::{
cmp::Ordering,
convert::TryFrom,
net::{IpAddr, SocketAddr},
};
use tokio_rustls::webpki::{DnsName, DnsNameRef};
#[cfg(feature = "websocket")] #[cfg(feature = "websocket")]
use tokio_tungstenite::tungstenite::http::Uri; use tokio_tungstenite::tungstenite::http::Uri;
use trust_dns_resolver::{
use crate::*; error::ResolveError,
lookup::{SrvLookup, TxtLookup},
IntoName, TokioAsyncResolver,
};
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref RESOLVER: TokioAsyncResolver = make_resolver(); static ref RESOLVER: TokioAsyncResolver = make_resolver();
@ -165,7 +176,7 @@ impl XmppConnection {
&self, &self,
domain: &str, domain: &str,
stream_open: &[u8], stream_open: &[u8],
in_filter: &mut crate::StanzaFilter, in_filter: &mut StanzaFilter,
client_addr: &mut Context<'_>, client_addr: &mut Context<'_>,
config: OutgoingVerifierConfig, config: OutgoingVerifierConfig,
) -> Result<(StanzaWrite, StanzaRead, SocketAddr, &'static str)> { ) -> Result<(StanzaWrite, StanzaRead, SocketAddr, &'static str)> {
@ -184,28 +195,28 @@ impl XmppConnection {
debug!("{} trying ip {}", client_addr.log_from(), to_addr); debug!("{} trying ip {}", client_addr.log_from(), to_addr);
match self.conn_type { match self.conn_type {
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
XmppConnectionType::StartTLS => match crate::starttls_connect(to_addr, domain, stream_open, in_filter, config.clone()).await { XmppConnectionType::StartTLS => match crate::tls::outgoing::starttls_connect(to_addr, domain, stream_open, in_filter, config.clone()).await {
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "starttls-out")), Ok((wr, rd)) => return Ok((wr, rd, to_addr, "starttls-out")),
Err(e) => error!("starttls connection failed to IP {} from SRV {}, error: {}", to_addr, self.target, e), Err(e) => error!("starttls connection failed to IP {} from SRV {}, error: {}", to_addr, self.target, e),
}, },
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
XmppConnectionType::DirectTLS => match crate::tls_connect(to_addr, domain, config.clone()).await { XmppConnectionType::DirectTLS => match crate::tls::outgoing::tls_connect(to_addr, domain, config.clone()).await {
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "directtls-out")), Ok((wr, rd)) => return Ok((wr, rd, to_addr, "directtls-out")),
Err(e) => error!("direct tls connection failed to IP {} from SRV {}, error: {}", to_addr, self.target, e), Err(e) => error!("direct tls connection failed to IP {} from SRV {}, error: {}", to_addr, self.target, e),
}, },
#[cfg(feature = "quic")] #[cfg(feature = "quic")]
XmppConnectionType::QUIC => match crate::quic_connect(to_addr, domain, config.clone()).await { XmppConnectionType::QUIC => match crate::quic::outgoing::quic_connect(to_addr, domain, config.clone()).await {
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "quic-out")), Ok((wr, rd)) => return Ok((wr, rd, to_addr, "quic-out")),
Err(e) => error!("quic connection failed to IP {} from SRV {}, error: {}", to_addr, self.target, e), Err(e) => error!("quic connection failed to IP {} from SRV {}, error: {}", to_addr, self.target, e),
}, },
#[cfg(feature = "websocket")] #[cfg(feature = "websocket")]
// todo: when websocket is found via DNS, we need to validate cert against domain, *not* target, this is a security problem with XEP-0156, we are doing it the secure but likely unexpected way here for now // todo: when websocket is found via DNS, we need to validate cert against domain, *not* target, this is a security problem with XEP-0156, we are doing it the secure but likely unexpected way here for now
XmppConnectionType::WebSocket(ref url, ref origin) => match crate::websocket_connect(to_addr, domain, url, origin, config.clone()).await { XmppConnectionType::WebSocket(ref url, ref origin) => match crate::websocket::outgoing::websocket_connect(to_addr, domain, url, origin, config.clone()).await {
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "websocket-out")), Ok((wr, rd)) => return Ok((wr, rd, to_addr, "websocket-out")),
Err(e) => { Err(e) => {
if self.secure && self.target != orig_domain { if self.secure && self.target != orig_domain {
// https is a special case, as target is sent in the Host: header, so we have to literally try twice in case this is set for the other on the server // https is a special case, as target is sent in the Host: header, so we have to literally try twice in case this is set for the other on the server
match crate::websocket_connect(to_addr, orig_domain, url, origin, config.clone()).await { match crate::websocket::outgoing::websocket_connect(to_addr, orig_domain, url, origin, config.clone()).await {
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "websocket-out")), Ok((wr, rd)) => return Ok((wr, rd, to_addr, "websocket-out")),
Err(e2) => error!("websocket connection failed to IP {} from TXT {}, error try 1: {}, error try 2: {}", to_addr, url, e, e2), Err(e2) => error!("websocket connection failed to IP {} from TXT {}, error try 1: {}, error try 2: {}", to_addr, url, e, e2),
} }
@ -428,7 +439,7 @@ pub async fn srv_connect(
domain: &str, domain: &str,
is_c2s: bool, is_c2s: bool,
stream_open: &[u8], stream_open: &[u8],
in_filter: &mut crate::StanzaFilter, in_filter: &mut StanzaFilter,
client_addr: &mut Context<'_>, client_addr: &mut Context<'_>,
config: OutgoingConfig, config: OutgoingConfig,
) -> Result<(StanzaWrite, StanzaRead, Vec<u8>)> { ) -> Result<(StanzaWrite, StanzaRead, Vec<u8>)> {

View File

@ -1,9 +1,9 @@
#![allow(clippy::upper_case_acronyms)] #![allow(clippy::upper_case_acronyms)]
use crate::common::to_str;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use crate::stanzafilter::StanzaState::*; use StanzaState::*;
use crate::to_str;
#[derive(Debug)] #[derive(Debug)]
enum StanzaState { enum StanzaState {

View File

@ -1,71 +1,30 @@
use crate::*; use crate::common::incoming::{shuffle_rd_wr_filter, CloneableConfig, ServerCerts};
use rustls::ServerConnection;
use std::convert::TryFrom;
use tokio::io::{AsyncBufReadExt, BufStream};
use tokio_rustls::{rustls::ServerName, TlsAcceptor}; use crate::{
common::{first_bytes_match, to_str, IN_BUFFER_SIZE},
context::Context,
in_out::{StanzaRead, StanzaWrite},
slicesubsequence::SliceSubsequence,
stanzafilter::{StanzaFilter, StanzaReader},
*,
};
use anyhow::Result;
use die::Die;
use log::{error, trace};
use rustls::{ServerConfig, ServerConnection};
#[cfg(feature = "outgoing")] use std::sync::Arc;
pub async fn tls_connect(target: SocketAddr, server_name: &str, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> { use tokio::{
let dnsname = ServerName::try_from(server_name)?; io::{AsyncBufReadExt, AsyncWriteExt, BufStream},
let stream = tokio::net::TcpStream::connect(target).await?; net::TcpListener,
let stream = config.connector_alpn.connect(dnsname, stream).await?; task::JoinHandle,
let (rd, wrt) = tokio::io::split(stream); };
Ok((StanzaWrite::new(wrt), StanzaRead::new(rd))) use tokio_rustls::TlsAcceptor;
pub fn tls_acceptor(server_config: ServerConfig) -> TlsAcceptor {
TlsAcceptor::from(Arc::new(server_config))
} }
#[cfg(feature = "outgoing")]
pub async fn starttls_connect(target: SocketAddr, server_name: &str, stream_open: &[u8], in_filter: &mut StanzaFilter, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
let dnsname = ServerName::try_from(server_name)?;
let mut stream = tokio::net::TcpStream::connect(target).await?;
let (in_rd, mut in_wr) = stream.split();
// send the stream_open
trace!("starttls sending: {} '{}'", server_name, to_str(stream_open));
in_wr.write_all(stream_open).await?;
in_wr.flush().await?;
// we naively read 1 byte at a time, which buffering significantly speeds up
let in_rd = tokio::io::BufReader::with_capacity(IN_BUFFER_SIZE, in_rd);
let mut in_rd = StanzaReader(in_rd);
let mut proceed_received = false;
trace!("starttls reading stream open {}", server_name);
while let Ok(Some(buf)) = in_rd.next(in_filter).await {
trace!("received pre-tls stanza: {} '{}'", server_name, to_str(buf));
if buf.starts_with(b"<?xml ") || buf.starts_with(b"<stream:stream ") {
// ignore this
} else if buf.starts_with(b"<stream:features") {
// we send starttls regardless, it could have been stripped out, we don't do plaintext
let buf = br###"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"###;
trace!("> {} '{}'", server_name, to_str(buf));
in_wr.write_all(buf).await?;
in_wr.flush().await?;
} else if buf.starts_with(b"<proceed ") {
proceed_received = true;
break;
} else {
bail!("bad pre-tls stanza: {}", to_str(buf));
}
}
if !proceed_received {
bail!("stream ended before proceed");
}
debug!("starttls starting TLS {}", server_name);
let stream = config.connector.connect(dnsname, stream).await?;
let (rd, wrt) = tokio::io::split(stream);
Ok((StanzaWrite::new(wrt), StanzaRead::new(rd)))
}
#[cfg(feature = "incoming")]
impl Config {
pub fn tls_acceptor(&self, cert_key: Arc<CertsKey>) -> Result<TlsAcceptor> {
Ok(TlsAcceptor::from(Arc::new(self.server_config(cert_key)?)))
}
}
#[cfg(feature = "incoming")]
pub fn spawn_tls_listener(local_addr: SocketAddr, config: CloneableConfig, acceptor: TlsAcceptor) -> JoinHandle<Result<()>> { pub fn spawn_tls_listener(local_addr: SocketAddr, config: CloneableConfig, acceptor: TlsAcceptor) -> JoinHandle<Result<()>> {
tokio::spawn(async move { tokio::spawn(async move {
let listener = TcpListener::bind(&local_addr).await.die("cannot listen on port/interface"); let listener = TcpListener::bind(&local_addr).await.die("cannot listen on port/interface");
@ -83,7 +42,6 @@ pub fn spawn_tls_listener(local_addr: SocketAddr, config: CloneableConfig, accep
}) })
} }
#[cfg(feature = "incoming")]
async fn handle_tls_connection(mut stream: tokio::net::TcpStream, client_addr: &mut Context<'_>, local_addr: SocketAddr, config: CloneableConfig, acceptor: TlsAcceptor) -> Result<()> { async fn handle_tls_connection(mut stream: tokio::net::TcpStream, client_addr: &mut Context<'_>, local_addr: SocketAddr, config: CloneableConfig, acceptor: TlsAcceptor) -> Result<()> {
info!("{} connected", client_addr.log_from()); info!("{} connected", client_addr.log_from());
@ -183,7 +141,7 @@ async fn handle_tls_connection(mut stream: tokio::net::TcpStream, client_addr: &
{ {
let stream: tokio_rustls::TlsStream<tokio::net::TcpStream> = stream.into(); let stream: tokio_rustls::TlsStream<tokio::net::TcpStream> = stream.into();
let mut stream = BufStream::with_capacity(crate::IN_BUFFER_SIZE, 0, stream); let mut stream = BufStream::with_capacity(IN_BUFFER_SIZE, 0, stream);
let websocket = { let websocket = {
// wait up to 10 seconds until 3 bytes have been read // wait up to 10 seconds until 3 bytes have been read
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -207,7 +165,7 @@ async fn handle_tls_connection(mut stream: tokio::net::TcpStream, client_addr: &
}; };
if websocket { if websocket {
handle_websocket_connection(Box::new(stream), config, server_certs, local_addr, client_addr, in_filter).await crate::websocket::incoming::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

5
src/tls/mod.rs Normal file
View File

@ -0,0 +1,5 @@
#[cfg(feature = "incoming")]
pub mod incoming;
#[cfg(feature = "outgoing")]
pub mod outgoing;

61
src/tls/outgoing.rs Normal file
View File

@ -0,0 +1,61 @@
use crate::{
common::{outgoing::OutgoingVerifierConfig, to_str, IN_BUFFER_SIZE},
in_out::{StanzaRead, StanzaWrite},
stanzafilter::{StanzaFilter, StanzaReader},
};
use anyhow::{bail, Result};
use log::{debug, trace};
use rustls::ServerName;
use std::{convert::TryFrom, net::SocketAddr};
use tokio::io::AsyncWriteExt;
pub async fn tls_connect(target: SocketAddr, server_name: &str, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
let dnsname = ServerName::try_from(server_name)?;
let stream = tokio::net::TcpStream::connect(target).await?;
let stream = config.connector_alpn.connect(dnsname, stream).await?;
let (rd, wrt) = tokio::io::split(stream);
Ok((StanzaWrite::new(wrt), StanzaRead::new(rd)))
}
pub async fn starttls_connect(target: SocketAddr, server_name: &str, stream_open: &[u8], in_filter: &mut StanzaFilter, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
let dnsname = ServerName::try_from(server_name)?;
let mut stream = tokio::net::TcpStream::connect(target).await?;
let (in_rd, mut in_wr) = stream.split();
// send the stream_open
trace!("starttls sending: {} '{}'", server_name, to_str(stream_open));
in_wr.write_all(stream_open).await?;
in_wr.flush().await?;
// we naively read 1 byte at a time, which buffering significantly speeds up
let in_rd = tokio::io::BufReader::with_capacity(IN_BUFFER_SIZE, in_rd);
let mut in_rd = StanzaReader(in_rd);
let mut proceed_received = false;
trace!("starttls reading stream open {}", server_name);
while let Ok(Some(buf)) = in_rd.next(in_filter).await {
trace!("received pre-tls stanza: {} '{}'", server_name, to_str(buf));
if buf.starts_with(b"<?xml ") || buf.starts_with(b"<stream:stream ") {
// ignore this
} else if buf.starts_with(b"<stream:features") {
// we send starttls regardless, it could have been stripped out, we don't do plaintext
let buf = br###"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"###;
trace!("> {} '{}'", server_name, to_str(buf));
in_wr.write_all(buf).await?;
in_wr.flush().await?;
} else if buf.starts_with(b"<proceed ") {
proceed_received = true;
break;
} else {
bail!("bad pre-tls stanza: {}", to_str(buf));
}
}
if !proceed_received {
bail!("stream ended before proceed");
}
debug!("starttls starting TLS {}", server_name);
let stream = config.connector.connect(dnsname, stream).await?;
let (rd, wrt) = tokio::io::split(stream);
Ok((StanzaWrite::new(wrt), StanzaRead::new(rd)))
}

View File

@ -1,13 +1,16 @@
use crate::{digest, Posh}; use crate::{
common::ca_roots::TLS_SERVER_ROOTS,
srv::{digest, Posh},
};
use log::debug; use log::debug;
use ring::digest::SHA256; use ring::digest::SHA256;
use rustls::client::{ServerCertVerified, ServerCertVerifier}; use rustls::{
use rustls::server::{ClientCertVerified, ClientCertVerifier}; client::{ServerCertVerified, ServerCertVerifier},
use rustls::{Certificate, DistinguishedNames, Error, ServerName}; server::{ClientCertVerified, ClientCertVerifier},
use std::convert::TryFrom; Certificate, DistinguishedNames, Error, ServerName,
use std::time::SystemTime; };
use tokio_rustls::webpki; use std::{convert::TryFrom, time::SystemTime};
use tokio_rustls::webpki::DnsName; use tokio_rustls::{webpki, webpki::DnsName};
type SignatureAlgorithms = &'static [&'static webpki::SignatureAlgorithm]; type SignatureAlgorithms = &'static [&'static webpki::SignatureAlgorithm];
@ -112,8 +115,7 @@ impl XmppServerCertVerifier {
let (cert, chain) = prepare(end_entity, intermediates)?; let (cert, chain) = prepare(end_entity, intermediates)?;
let webpki_now = webpki::Time::try_from(now).map_err(|_| Error::FailedToGetCurrentTime)?; let webpki_now = webpki::Time::try_from(now).map_err(|_| Error::FailedToGetCurrentTime)?;
cert.verify_is_valid_tls_server_cert(SUPPORTED_SIG_ALGS, &crate::TLS_SERVER_ROOTS, &chain, webpki_now) cert.verify_is_valid_tls_server_cert(SUPPORTED_SIG_ALGS, &TLS_SERVER_ROOTS, &chain, webpki_now).map_err(pki_error)?;
.map_err(pki_error)?;
for name in &self.names { for name in &self.names {
if cert.verify_is_valid_for_dns_name(name.as_ref()).is_ok() { if cert.verify_is_valid_for_dns_name(name.as_ref()).is_ok() {

25
src/websocket/incoming.rs Normal file
View File

@ -0,0 +1,25 @@
use crate::{
common::incoming::{shuffle_rd_wr_filter, CloneableConfig, ServerCerts},
context::Context,
stanzafilter::StanzaFilter,
websocket::{incoming_websocket_connection, AsyncReadAndWrite},
};
use anyhow::Result;
use log::info;
use std::net::SocketAddr;
pub async fn handle_websocket_connection(
stream: Box<dyn AsyncReadAndWrite + Unpin + Send>,
config: CloneableConfig,
server_certs: ServerCerts,
local_addr: SocketAddr,
client_addr: &mut Context<'_>,
in_filter: StanzaFilter,
) -> Result<()> {
client_addr.set_proto("websocket-in");
info!("{} connected", client_addr.log_from());
let (in_rd, in_wr) = incoming_websocket_connection(stream, config.max_stanza_size_bytes).await?;
shuffle_rd_wr_filter(in_rd, in_wr, config, server_certs, local_addr, client_addr, in_filter).await
}

View File

@ -1,9 +1,14 @@
use crate::*;
use anyhow::Result; use anyhow::Result;
use futures::StreamExt; use futures::StreamExt;
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig; use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
#[cfg(feature = "incoming")]
pub mod incoming;
#[cfg(feature = "outgoing")]
pub mod outgoing;
// https://datatracker.ietf.org/doc/html/rfc7395 // https://datatracker.ietf.org/doc/html/rfc7395
fn ws_cfg(max_stanza_size_bytes: usize) -> Option<WebSocketConfig> { fn ws_cfg(max_stanza_size_bytes: usize) -> Option<WebSocketConfig> {
@ -29,23 +34,6 @@ pub async fn incoming_websocket_connection(stream: Box<dyn AsyncReadAndWrite + U
Ok((StanzaRead::WebSocketRead(in_rd), StanzaWrite::WebSocketClientWrite(in_wr))) Ok((StanzaRead::WebSocketRead(in_rd), StanzaWrite::WebSocketClientWrite(in_wr)))
} }
#[cfg(feature = "incoming")]
pub async fn handle_websocket_connection(
stream: Box<dyn AsyncReadAndWrite + Unpin + Send>,
config: CloneableConfig,
server_certs: ServerCerts,
local_addr: SocketAddr,
client_addr: &mut Context<'_>,
in_filter: StanzaFilter,
) -> Result<()> {
client_addr.set_proto("websocket-in");
info!("{} connected", client_addr.log_from());
let (in_rd, in_wr) = incoming_websocket_connection(stream, config.max_stanza_size_bytes).await?;
shuffle_rd_wr_filter(in_rd, 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 {
if stanza.starts_with("<open ") { if stanza.starts_with("<open ") {
let stanza = stanza let stanza = stanza
@ -97,34 +85,10 @@ pub fn to_ws_new(buf: &[u8], mut end_of_first_tag: usize, is_c2s: bool) -> Resul
Ok(ret) Ok(ret)
} }
use rustls::ServerName; use crate::{
use std::convert::TryFrom; in_out::{StanzaRead, StanzaWrite},
slicesubsequence::SliceSubsequence,
use tokio_tungstenite::tungstenite::client::IntoClientRequest; };
use tokio_tungstenite::tungstenite::http::header::{ORIGIN, SEC_WEBSOCKET_PROTOCOL};
use tokio_tungstenite::tungstenite::http::Uri;
#[cfg(feature = "outgoing")]
pub async fn websocket_connect(target: SocketAddr, server_name: &str, url: &Uri, origin: &str, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
let mut request = url.into_client_request()?;
request.headers_mut().append(SEC_WEBSOCKET_PROTOCOL, "xmpp".parse()?);
request.headers_mut().append(ORIGIN, origin.parse()?);
let dnsname = ServerName::try_from(server_name)?;
let stream = tokio::net::TcpStream::connect(target).await?;
let stream = config.connector.connect(dnsname, stream).await?;
//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
//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 (wrt, rd) = stream.split();
Ok((StanzaWrite::WebSocketClientWrite(wrt), StanzaRead::WebSocketRead(rd)))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

37
src/websocket/outgoing.rs Normal file
View File

@ -0,0 +1,37 @@
use crate::{
common::outgoing::OutgoingVerifierConfig,
in_out::{StanzaRead, StanzaWrite},
websocket::{ws_cfg, AsyncReadAndWrite},
};
use anyhow::Result;
use futures_util::StreamExt;
use rustls::ServerName;
use std::{convert::TryFrom, net::SocketAddr};
use tokio_tungstenite::tungstenite::{
client::IntoClientRequest,
http::{
header::{ORIGIN, SEC_WEBSOCKET_PROTOCOL},
Uri,
},
};
pub async fn websocket_connect(target: SocketAddr, server_name: &str, url: &Uri, origin: &str, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
let mut request = url.into_client_request()?;
request.headers_mut().append(SEC_WEBSOCKET_PROTOCOL, "xmpp".parse()?);
request.headers_mut().append(ORIGIN, origin.parse()?);
let dnsname = ServerName::try_from(server_name)?;
let stream = tokio::net::TcpStream::connect(target).await?;
let stream = config.connector.connect(dnsname, stream).await?;
//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
//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 (wrt, rd) = stream.split();
Ok((StanzaWrite::WebSocketClientWrite(wrt), StanzaRead::WebSocketRead(rd)))
}