tokio-xmpp/client: add directTLS connection method

This commit is contained in:
Saarko 2025-05-30 14:05:28 +02:00
commit c674c14597
6 changed files with 266 additions and 5 deletions

View file

@ -67,8 +67,9 @@ name = "keep_connection"
required-features = ["starttls"]
[features]
default = ["starttls", "aws_lc_rs", "rustls-native-certs"]
default = ["direct-tls", "starttls", "aws_lc_rs", "rustls-native-certs"]
starttls = ["dns"]
direct-tls = ["dns"]
aws_lc_rs = ["rustls-any-backend", "tokio-rustls/aws_lc_rs"]
ring = ["rustls-any-backend", "tokio-rustls/ring"]

View file

@ -35,6 +35,7 @@ XXXX-YY-ZZ RELEASER <admin@example.com>
Please refer to the crate docs for details. (!581)
* Added:
- Add new directTLS connection method to the `Client`. (Placeholder for PR number)
- Support for sending IQ requests while tracking their responses in a
Future.
- `rustls` is now re-exported if it is enabled, to allow applications to

View file

@ -16,7 +16,9 @@ use crate::{
Stanza,
};
#[cfg(any(feature = "starttls", feature = "insecure-tcp"))]
#[cfg(feature = "direct-tls")]
use crate::connect::DirectTlsServerConnector;
#[cfg(any(feature = "direct-tls", feature = "starttls", feature = "insecure-tcp"))]
use crate::connect::DnsConfig;
#[cfg(feature = "starttls")]
use crate::connect::StartTlsServerConnector;
@ -130,6 +132,39 @@ impl Client {
}
}
#[cfg(feature = "direct-tls")]
impl Client {
/// Start a new XMPP client using DirectTLS transport and autoreconnect
///
/// It use RFC 7590 _xmpps-client._tcp loopup for connector details.
pub fn new_direct_tls<J: Into<Jid>, P: Into<String>>(jid: J, password: P) -> Self {
let jid_ref = jid.into();
let dns_config = DnsConfig::srv_xmpps(jid_ref.domain().as_ref());
Self::new_with_connector(
jid_ref,
password,
DirectTlsServerConnector::from(dns_config),
Timeouts::default(),
)
}
/// Start a new XMPP client with direct TLS transport, useful for testing or
/// when one does not want to rely on dns lookups
pub fn new_direct_tls_with_config<J: Into<Jid>, P: Into<String>>(
jid: J,
password: P,
dns_config: DnsConfig,
timeouts: Timeouts,
) -> Self {
Self::new_with_connector(
jid,
password,
DirectTlsServerConnector::from(dns_config),
timeouts,
)
}
}
#[cfg(feature = "starttls")]
impl Client {
/// Start a new XMPP client using StartTLS transport and autoreconnect
@ -138,7 +173,7 @@ impl Client {
/// and yield events.
pub fn new<J: Into<Jid>, P: Into<String>>(jid: J, password: P) -> Self {
let jid = jid.into();
let dns_config = DnsConfig::srv(jid.domain().as_ref(), "_xmpp-client._tcp", 5222);
let dns_config = DnsConfig::srv_default_client(jid.domain().as_ref());
Self::new_starttls(jid, password, dns_config, Timeouts::default())
}

View file

@ -0,0 +1,209 @@
// Copyright (c) 2025 Saarko <saarko@tutanota.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//! `direct_tls::ServerConfig` provides a `ServerConnector` for direct TLS connections
use alloc::borrow::Cow;
use core::{error::Error as StdError, fmt};
#[cfg(feature = "native-tls")]
use native_tls::Error as TlsError;
#[cfg(feature = "rustls-any-backend")]
use tokio_rustls::rustls::pki_types::InvalidDnsNameError;
// Note: feature = "rustls-any-backend" and feature = "native-tls" are
// mutually exclusive during normal compiles, but we allow it for rustdoc
// builds. Thus, we have to make sure that the compilation still succeeds in
// such a case.
#[cfg(all(feature = "rustls-any-backend", not(feature = "native-tls")))]
use tokio_rustls::rustls::Error as TlsError;
#[cfg(all(feature = "rustls-any-backend", not(feature = "native-tls")))]
use {
alloc::sync::Arc,
tokio_rustls::{
rustls::pki_types::ServerName,
rustls::{ClientConfig, RootCertStore},
TlsConnector,
},
};
#[cfg(all(
feature = "rustls-any-backend",
not(feature = "ktls"),
not(feature = "native-tls")
))]
use tokio_rustls::client::TlsStream;
#[cfg(all(feature = "ktls", not(feature = "native-tls")))]
type TlsStream<S> = ktls::KtlsStream<S>;
#[cfg(feature = "native-tls")]
use {
native_tls::TlsConnector as NativeTlsConnector,
tokio_native_tls::{TlsConnector, TlsStream},
};
use sasl::common::ChannelBinding;
use tokio::{io::BufStream, net::TcpStream};
use xmpp_parsers::jid::Jid;
use crate::{
connect::{DnsConfig, ServerConnector, ServerConnectorError},
error::Error,
xmlstream::{initiate_stream, PendingFeaturesRecv, StreamHeader, Timeouts},
};
/// Connect via direct TLS to an XMPP server
#[derive(Debug, Clone)]
pub struct DirectTlsServerConnector(pub DnsConfig);
impl From<DnsConfig> for DirectTlsServerConnector {
fn from(dns_config: DnsConfig) -> DirectTlsServerConnector {
Self(dns_config)
}
}
impl ServerConnector for DirectTlsServerConnector {
type Stream = BufStream<TlsStream<TcpStream>>;
async fn connect(
&self,
jid: &Jid,
ns: &'static str,
timeouts: Timeouts,
) -> Result<(PendingFeaturesRecv<Self::Stream>, ChannelBinding), Error> {
let tcp_stream = self.0.resolve().await?;
// Immediately establish TLS connection
let (tls_stream, channel_binding) =
establish_tls(tcp_stream, jid.domain().as_str()).await?;
// Establish XMPP stream over TLS
Ok((
initiate_stream(
tokio::io::BufStream::new(tls_stream),
ns,
StreamHeader {
to: Some(Cow::Borrowed(jid.domain().as_str())),
// Setting explicitly `from` here, because server require it
// in order to advertise i.e. SASL2 (XEP-0388).
from: Some(Cow::Borrowed(jid.to_bare().as_str())),
id: None,
},
timeouts,
)
.await?,
channel_binding,
))
}
}
#[cfg(feature = "native-tls")]
async fn establish_tls(
tcp_stream: TcpStream,
domain: &str,
) -> Result<(TlsStream<TcpStream>, ChannelBinding), Error> {
let domain = domain.to_owned();
let tls_stream = TlsConnector::from(NativeTlsConnector::builder().build().unwrap())
.connect(&domain, tcp_stream)
.await
.map_err(|e| DirectTlsError::Tls(e))?;
log::warn!(
"tls-native doesn't support channel binding, please use tls-rust if you want this feature!"
);
Ok((tls_stream, ChannelBinding::None))
}
#[cfg(all(feature = "rustls-any-backend", not(feature = "native-tls")))]
async fn establish_tls(
tcp_stream: TcpStream,
domain: &str,
) -> Result<(TlsStream<TcpStream>, ChannelBinding), Error> {
let domain = ServerName::try_from(domain.to_owned()).map_err(DirectTlsError::DnsNameError)?;
let mut root_store = RootCertStore::empty();
#[cfg(feature = "webpki-roots")]
{
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
#[cfg(feature = "rustls-native-certs")]
{
root_store.add_parsable_certificates(rustls_native_certs::load_native_certs()?);
}
#[allow(unused_mut, reason = "This config is mutable when using ktls")]
let mut config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
#[cfg(feature = "ktls")]
let tcp_stream = {
config.enable_secret_extraction = true;
ktls::CorkStream::new(tcp_stream)
};
let tls_stream = TlsConnector::from(Arc::new(config))
.connect(domain, tcp_stream)
.await
.map_err(crate::Error::Io)?;
// Extract the channel-binding information before we hand the stream over to ktls.
let (_, connection) = tls_stream.get_ref();
let channel_binding = match connection.protocol_version() {
// TODO: Add support for TLS 1.2 and earlier.
Some(tokio_rustls::rustls::ProtocolVersion::TLSv1_3) => {
let data = vec![0u8; 32];
let data = connection
.export_keying_material(data, b"EXPORTER-Channel-Binding", None)
.map_err(DirectTlsError::Tls)?;
ChannelBinding::TlsExporter(data)
}
_ => ChannelBinding::None,
};
#[cfg(feature = "ktls")]
let tls_stream = ktls::config_ktls_client(tls_stream)
.await
.map_err(DirectTlsError::KtlsError)?;
Ok((tls_stream, channel_binding))
}
/// Direct TLS ServerConnector Error
#[derive(Debug)]
pub enum DirectTlsError {
/// TLS error
Tls(TlsError),
#[cfg(feature = "rustls-any-backend")]
/// DNS name parsing error
DnsNameError(InvalidDnsNameError),
#[cfg(feature = "ktls")]
/// Error while setting up kernel TLS
KtlsError(ktls::Error),
}
impl ServerConnectorError for DirectTlsError {}
impl fmt::Display for DirectTlsError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Tls(e) => write!(fmt, "TLS error: {}", e),
#[cfg(feature = "rustls-any-backend")]
Self::DnsNameError(e) => write!(fmt, "DNS name error: {}", e),
#[cfg(feature = "ktls")]
Self::KtlsError(e) => write!(fmt, "Kernel TLS error: {}", e),
}
}
}
impl StdError for DirectTlsError {}
impl From<TlsError> for DirectTlsError {
fn from(e: TlsError) -> Self {
Self::Tls(e)
}
}
#[cfg(feature = "rustls-any-backend")]
impl From<InvalidDnsNameError> for DirectTlsError {
fn from(e: InvalidDnsNameError) -> Self {
Self::DnsNameError(e)
}
}

View file

@ -11,7 +11,7 @@ use tokio::net::TcpStream;
use crate::Error;
/// StartTLS XMPP server connection configuration
/// XMPP server connection configuration
#[derive(Clone, Debug)]
pub enum DnsConfig {
/// Use SRV record to find server host
@ -66,7 +66,7 @@ impl DnsConfig {
}
}
/// Constructor for the default SRV resolution strategy for clients
/// Constructor for the default SRV resolution strategy for clients (StartTLS)
#[cfg(feature = "dns")]
pub fn srv_default_client(host: &str) -> Self {
Self::UseSrv {
@ -76,6 +76,16 @@ impl DnsConfig {
}
}
/// Constructor for direct TLS connections using RFC 7590 _xmpps-client._tcp
#[cfg(feature = "dns")]
pub fn srv_xmpps(host: &str) -> Self {
Self::UseSrv {
host: host.to_string(),
srv: "_xmpps-client._tcp".to_string(),
fallback_port: 5223,
}
}
/// Constructor for DnsConfig::NoSrv variant
#[cfg(feature = "dns")]
pub fn no_srv(host: &str, port: u16) -> Self {

View file

@ -12,6 +12,11 @@ pub mod starttls;
#[cfg(feature = "starttls")]
pub use starttls::StartTlsServerConnector;
#[cfg(feature = "direct-tls")]
pub mod direct_tls;
#[cfg(feature = "direct-tls")]
pub use direct_tls::DirectTlsServerConnector;
#[cfg(feature = "insecure-tcp")]
pub mod tcp;
#[cfg(feature = "insecure-tcp")]