Skip to content

Commit 664da00

Browse files
jszwedkoclaude
andcommitted
fix(tls): address review feedback on TLS handshake timeout connector
Reject unsupported URI schemes explicitly instead of silently falling back to plaintext, strip IPv6 brackets before constructing the TLS server name, and correct ALPN documentation/test for HTTP/1.1-only mode. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 9dbe3c7 commit 664da00

3 files changed

Lines changed: 87 additions & 5 deletions

File tree

lib/agent-data-plane-config/src/shared.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ pub struct Tls {
122122
pub sslkeylogfile: String,
123123

124124
/// Timeout for completing the TLS handshake after a connection is established.
125+
///
126+
/// Defaults to 10 seconds. Bounds only the handshake step, distinct from the overall request timeout. A value
127+
/// of zero disables the handshake-specific deadline, leaving the overall request timeout as the only bound.
125128
pub handshake_timeout: Duration,
126129
}
127130

lib/saluki-io/src/net/client/http/client.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,8 @@ impl HttpClientBuilder {
316316
///
317317
/// Any ALPN protocols already present in `config` are discarded before the configuration is passed to
318318
/// `hyper-rustls`. [`Self::with_http_protocol`] remains the sole source of HTTP protocol selection:
319-
/// [`HttpProtocol::Auto`] advertises HTTP/2 with HTTP/1.1 fallback, while [`HttpProtocol::Http1`] enables only
320-
/// HTTP/1.1 and does not advertise ALPN.
319+
/// [`HttpProtocol::Auto`] advertises HTTP/2 with HTTP/1.1 fallback, while [`HttpProtocol::Http1`] advertises
320+
/// only HTTP/1.1 via ALPN.
321321
///
322322
/// The supplied configuration is validated for FIPS compliance during [`Self::build`].
323323
pub fn with_client_tls_config(mut self, config: ClientConfig) -> Self {
@@ -469,13 +469,15 @@ mod tests {
469469
let (mut server_config, mut client_config) = mutual_tls_configs();
470470
server_config.alpn_protocols = vec![b"custom".to_vec(), b"http/1.1".to_vec()];
471471
client_config.alpn_protocols = vec![b"custom".to_vec()];
472-
let builder = HttpClient::builder().with_client_tls_config(client_config);
472+
let builder = HttpClient::builder()
473+
.with_http_protocol(HttpProtocol::Http1)
474+
.with_client_tls_config(client_config);
473475

474476
let negotiated_alpn = send_request_to_tls_server(builder, server_config)
475477
.await
476478
.expect("client should normalize ALPN and complete an HTTP/1.1 request");
477479

478-
assert_eq!(negotiated_alpn, None);
480+
assert_eq!(negotiated_alpn, Some(b"http/1.1".to_vec()));
479481
}
480482

481483
fn mutual_tls_configs() -> (ServerConfig, ClientConfig) {

lib/saluki-io/src/net/client/http/conn.rs

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,19 @@ impl Service<Uri> for HttpsCapableConnector {
385385
}
386386

387387
fn call(&mut self, dst: Uri) -> Self::Future {
388-
let is_https = dst.scheme_str() == Some("https");
388+
let is_https = match dst.scheme_str() {
389+
Some("https") => true,
390+
Some("http") => false,
391+
scheme => {
392+
let scheme = scheme.map(str::to_owned);
393+
return Box::pin(async move {
394+
Err(Box::new(io::Error::new(
395+
io::ErrorKind::InvalidInput,
396+
format!("unsupported URI scheme: {scheme:?}"),
397+
)) as BoxError)
398+
});
399+
}
400+
};
389401
let transport_fut = self.inner.call(dst.clone());
390402
let tls_config = Arc::clone(&self.tls_config);
391403
let tls_handshake_timeout = self.tls_handshake_timeout;
@@ -400,6 +412,7 @@ impl Service<Uri> for HttpsCapableConnector {
400412
let host = dst.host().ok_or_else(|| -> BoxError {
401413
Box::new(io::Error::new(io::ErrorKind::InvalidInput, "URI has no host"))
402414
})?;
415+
let host = strip_ipv6_brackets(host);
403416
let server_name = ServerName::try_from(host)
404417
.map_err(|error| -> BoxError { Box::new(error) })?
405418
.to_owned();
@@ -429,6 +442,14 @@ impl Service<Uri> for HttpsCapableConnector {
429442
}
430443
}
431444

445+
/// Strips the surrounding brackets from a bracketed IPv6 host, as found in a URI authority.
446+
///
447+
/// [`rustls::pki_types::ServerName`] accepts unbracketed IPv6 addresses but rejects the bracketed form that
448+
/// [`http::Uri::host`] returns (e.g. `[::1]`), so this normalizes the host before constructing the server name.
449+
fn strip_ipv6_brackets(host: &str) -> &str {
450+
host.strip_prefix('[').and_then(|h| h.strip_suffix(']')).unwrap_or(host)
451+
}
452+
432453
/// Awaits a TLS handshake future, bounding it by `timeout` unless `timeout` is zero.
433454
///
434455
/// A zero duration means the handshake deadline is disabled, matching the core Agent convention for this setting.
@@ -663,6 +684,62 @@ mod tests {
663684
assert_eq!(result.unwrap(), 42);
664685
}
665686

687+
#[test]
688+
fn strip_ipv6_brackets_unwraps_bracketed_addresses() {
689+
use super::strip_ipv6_brackets;
690+
691+
assert_eq!(strip_ipv6_brackets("[::1]"), "::1");
692+
assert_eq!(strip_ipv6_brackets("[2001:db8::1]"), "2001:db8::1");
693+
}
694+
695+
#[test]
696+
fn strip_ipv6_brackets_leaves_unbracketed_hosts_alone() {
697+
use super::strip_ipv6_brackets;
698+
699+
assert_eq!(strip_ipv6_brackets("example.com"), "example.com");
700+
assert_eq!(strip_ipv6_brackets("::1"), "::1");
701+
}
702+
703+
#[cfg(unix)]
704+
#[tokio::test]
705+
async fn call_rejects_unsupported_uri_scheme() {
706+
use std::sync::Arc;
707+
708+
use rustls::{ClientConfig, RootCertStore};
709+
use tower::Service as _;
710+
711+
use super::{HttpsCapableConnector, InnerConnector};
712+
use crate::net::dns::SystemResolver;
713+
714+
let inner = InnerConnector {
715+
http: SystemResolver::new().into_http_connector(),
716+
connect_timeout: Duration::from_secs(1),
717+
error_telemetry: None,
718+
unix_socket_path: None,
719+
#[cfg(target_os = "linux")]
720+
vsock_addr: None,
721+
};
722+
723+
let tls_config = Arc::new(
724+
ClientConfig::builder()
725+
.with_root_certificates(RootCertStore::empty())
726+
.with_no_client_auth(),
727+
);
728+
729+
let mut connector = HttpsCapableConnector {
730+
inner,
731+
tls_config,
732+
tls_handshake_timeout: Duration::from_secs(1),
733+
bytes_sent: None,
734+
error_telemetry: None,
735+
conn_age_limit: None,
736+
};
737+
738+
let uri: http::Uri = "ftp://example.com/".parse().unwrap();
739+
let error = connector.call(uri).await.err().expect("expected scheme to be rejected");
740+
assert!(error.to_string().contains("unsupported URI scheme"));
741+
}
742+
666743
#[test]
667744
fn auto_protocol_advertises_h2_and_http1_alpn() {
668745
let alpn_protocols = configure_alpn_for_http_protocol(HttpProtocol::Auto);

0 commit comments

Comments
 (0)