diff --git a/tokio-xmpp/Cargo.toml b/tokio-xmpp/Cargo.toml index 2a07205b46fd6ed0883af5e8a1e20e820d9bbec1..93c648306efafe8beb2e19ff80215dde6040ae3a 100644 --- a/tokio-xmpp/Cargo.toml +++ b/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"] diff --git a/tokio-xmpp/ChangeLog b/tokio-xmpp/ChangeLog index bf6d77c6c1cf657b3b07e1cc6f8f0e78e6f85632..892096f8064cc4a474cb1c0894fbe2a88771cca5 100644 --- a/tokio-xmpp/ChangeLog +++ b/tokio-xmpp/ChangeLog @@ -35,6 +35,7 @@ XXXX-YY-ZZ RELEASER 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 diff --git a/tokio-xmpp/src/client/mod.rs b/tokio-xmpp/src/client/mod.rs index 763827e4d746d304cc9cf0a6458e1954d83b0399..56c01dab039ee75af6dc99f3d04d185fbc7ff4a0 100644 --- a/tokio-xmpp/src/client/mod.rs +++ b/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, P: Into>(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, P: Into>( + 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, P: Into>(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()) } diff --git a/tokio-xmpp/src/connect/direct_tls.rs b/tokio-xmpp/src/connect/direct_tls.rs new file mode 100644 index 0000000000000000000000000000000000000000..9a0f56621cc4f9e7cf081db03fe3ea814f8c30dd --- /dev/null +++ b/tokio-xmpp/src/connect/direct_tls.rs @@ -0,0 +1,209 @@ +// Copyright (c) 2025 Saarko +// +// 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 = ktls::KtlsStream; + +#[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 for DirectTlsServerConnector { + fn from(dns_config: DnsConfig) -> DirectTlsServerConnector { + Self(dns_config) + } +} + +impl ServerConnector for DirectTlsServerConnector { + type Stream = BufStream>; + + async fn connect( + &self, + jid: &Jid, + ns: &'static str, + timeouts: Timeouts, + ) -> Result<(PendingFeaturesRecv, 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, 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, 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 for DirectTlsError { + fn from(e: TlsError) -> Self { + Self::Tls(e) + } +} + +#[cfg(feature = "rustls-any-backend")] +impl From for DirectTlsError { + fn from(e: InvalidDnsNameError) -> Self { + Self::DnsNameError(e) + } +} diff --git a/tokio-xmpp/src/connect/dns.rs b/tokio-xmpp/src/connect/dns.rs index 29de2f6e6a0cb57cdc30ee4f6e9f16e9b5d65125..e8d06fb38381b68ffb3ae0488c653a648491c46f 100644 --- a/tokio-xmpp/src/connect/dns.rs +++ b/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 { diff --git a/tokio-xmpp/src/connect/mod.rs b/tokio-xmpp/src/connect/mod.rs index 240d915317dea3ee418b10af32269e1333f32411..9bf9990aef0f591592c6e58c32e3b7e2ba1b9e92 100644 --- a/tokio-xmpp/src/connect/mod.rs +++ b/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")]