tokio-xmpp/client: add directTLS connection method

Saarko created

Change summary

tokio-xmpp/Cargo.toml                |   3 
tokio-xmpp/ChangeLog                 |   1 
tokio-xmpp/src/client/mod.rs         |  39 +++++
tokio-xmpp/src/connect/direct_tls.rs | 209 ++++++++++++++++++++++++++++++
tokio-xmpp/src/connect/dns.rs        |  14 +
tokio-xmpp/src/connect/mod.rs        |   5 
6 files changed, 266 insertions(+), 5 deletions(-)

Detailed changes

tokio-xmpp/Cargo.toml 🔗

@@ -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"]

tokio-xmpp/ChangeLog 🔗

@@ -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

tokio-xmpp/src/client/mod.rs 🔗

@@ -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())
     }
 

tokio-xmpp/src/connect/direct_tls.rs 🔗

@@ -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)
+    }
+}

tokio-xmpp/src/connect/dns.rs 🔗

@@ -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 {

tokio-xmpp/src/connect/mod.rs 🔗

@@ -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")]