Skip to content

feat(tls): add tls_handshake_timeout config option - #2303

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 9 commits into
mainfrom
jszwedko/tls-handshake-timeout
Aug 11, 2026
Merged

feat(tls): add tls_handshake_timeout config option#2303
gh-worker-dd-mergequeue-cf854d[bot] merged 9 commits into
mainfrom
jszwedko/tls-handshake-timeout

Conversation

@jszwedko

@jszwedko jszwedko commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Human Summary

Adds support for tls_handlshake_timeout using newly documented method in tokio-rustls. This is roughly follows #178 but based on latest main.

It doesn't handle configuring the connect timeout when using an HTTPS proxy. That requires changes to hyper-http-proxy so I'll follow up with that.

Closes: #178

AI Summary

TLS handshakes to the Datadog intake have no built-in timeout independent of the overall request timeout: hyper_rustls's connector fuses the transport connect and the TLS handshake into a single opaque future, so a stalled handshake (e.g. a peer that accepts the TCP connection but never completes the TLS negotiation) is only bounded by forwarder_timeout, which is meant to bound the whole request, not just the handshake. This adds a tls_handshake_timeout config option by having the HTTP client connector own the TLS layer directly, so it can time out just the handshake step and still distinguish that failure mode from a slow request. This picks up the intent of #1819, an older PR for the same issue, rewritten against the current typed configuration system rather than resurrected via rebase.

sequenceDiagram
    participant Before as Before (hyper_rustls::HttpsConnector)
    participant After as After (owned TLS layer)

    Note over Before: connect + handshake fused into one future
    Before->>Before: TCP connect
    Before->>Before: TLS handshake
    Note over Before: only forwarder_timeout bounds both steps combined

    Note over After: connect and handshake are separate steps
    After->>After: TCP connect (connect_timeout)
    After->>After: TLS handshake (tls_handshake_timeout, new)
    Note over After: a stalled handshake times out on its own,<br/>without racing the whole request
Loading

Test plan

  • Added tls_handshake_timeout to the Datadog config schema overlay (support: full) and wired it through the typed config system (DatadogTranslator, SalukiConfiguration) and the legacy ForwarderConfiguration facet-based config, both consumed by the HTTP client builder.
  • Added/updated unit tests in saluki-io's conn.rs for the new connector split (ALPN protocol selection, including an explicit http/1.1 ALPN advertisement for HttpProtocol::Http1 to avoid an ALPN regression from the previous implicit behavior).
  • Existing config_smoke::smoke_test in saluki-components (ForwarderConfiguration) exercises the new field's default/deserialization against the config registry.
  • Updated classifier unit tests in datadog-agent-config that previously used tls_handshake_timeout as an example unsupported/incompatible key, substituting other still-unsupported keys since this key is now fully supported.

Known limitation

Connections made through proxy_https bypass this connector and aren't covered by tls_handshake_timeout (flagged on the original PR). Left out of scope here; can be addressed separately if needed.

Owns the TLS handshake step of outbound HTTPS connections directly
instead of delegating to hyper_rustls's fused connect-and-handshake
future, so a timeout can be scoped to just the handshake rather than
the whole connect+handshake duration. Wires this through as a new
tls_handshake_timeout Datadog config option, defaulting to 10s.
@jszwedko
jszwedko requested a review from a team as a code owner August 11, 2026 17:37
@dd-octo-sts dd-octo-sts Bot added area/io General I/O and networking. area/components Sources, transforms, and destinations. area/docs Reference documentation. labels Aug 11, 2026
@pr-commenter

pr-commenter Bot commented Aug 11, 2026

Copy link
Copy Markdown

Binary Size Analysis (Agent Data Plane)

Baseline: 76206c6 · Comparison: b75e898 · diff
Analysis Configuration: stripped binaries · Pass/Fail Threshold: +5%
Sizes: 41.31 MiB (baseline) vs 41.48 MiB (comparison)
Size Change: +170.88 KiB (+0.40%)

✅ Binary size difference within threshold

Changes by Module
Module File Size Symbols
core +79.11 KiB 3064
figment +23.68 KiB 111
hyper_util +19.21 KiB 76
anon.5ffef35c7768023811ac5ab5fd4f00ff.896.llvm.14492608351097168908 +17.71 KiB 1
anon.5ffef35c7768023811ac5ab5fd4f00ff.893.llvm.29477675648423616 -17.62 KiB 1
anon.9f7fcabd42bdb20c281b9db2ecef43c6.574.llvm.17066318201269722550 -17.62 KiB 1
anon.602358bcc0b10a82cfb0c6a39880d07f.789.llvm.4308290865566122582 +17.27 KiB 1
anon.9f7fcabd42bdb20c281b9db2ecef43c6.687.llvm.17066318201269722550 -12.37 KiB 1
anon.602358bcc0b10a82cfb0c6a39880d07f.923.llvm.4308290865566122582 +12.29 KiB 1
saluki_components::common::datadog -12.25 KiB 70
alloc -12.23 KiB 717
agent_data_plane_config::shared::_ +12.01 KiB 16
serde_core +11.31 KiB 175
agent_data_plane_config::domains::dogstatsd -10.49 KiB 13
anon.602358bcc0b10a82cfb0c6a39880d07f.156.llvm.4308290865566122582 +9.63 KiB 1
anon.9f7fcabd42bdb20c281b9db2ecef43c6.145.llvm.17066318201269722550 -9.62 KiB 1
agent_data_plane::internal::env -9.41 KiB 24
agent_data_plane_config::_::_<impl serde_core -9.18 KiB 1
anon.602358bcc0b10a82cfb0c6a39880d07f.773.llvm.4308290865566122582 +9.07 KiB 1
anon.9f7fcabd42bdb20c281b9db2ecef43c6.571.llvm.17066318201269722550 -9.06 KiB 1
Detailed Symbol Changes
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.2%  +180Ki  +1.3%  +153Ki    [18672 Others]
  [NEW] +59.6Ki  [NEW] +59.4Ki    saluki_components::common::datadog::io::run_endpoint_io_loop::_{{closure}}::h39d334283f104a13
  [NEW] +18.4Ki  [NEW] +18.1Ki    hyper_util::client::legacy::client::Client<C,B>::connect_to::_{{closure}}::_{{closure}}::_{{closure}}::hcf33c57005f2c572
  [NEW] +18.0Ki  [NEW] +17.9Ki    hyper_util::client::legacy::client::Client<C,B>::send_request::_{{closure}}::hbc5aa5001c795f4e
  [NEW] +17.7Ki  [NEW] +17.6Ki    anon.5ffef35c7768023811ac5ab5fd4f00ff.896.llvm.14492608351097168908
  [NEW] +17.3Ki  [NEW]     +76    anon.602358bcc0b10a82cfb0c6a39880d07f.789.llvm.4308290865566122582
  [NEW] +15.8Ki  [NEW] +15.3Ki    core::ptr::drop_in_place<datadog_agent_config::generated::datadog_configuration::DatadogConfiguration>::h4cfece785c53d821
  [NEW] +15.6Ki  [NEW] +15.5Ki    _<tracing::instrument::Instrumented<T> as core::future::future::Future>::poll::h2b4944a60d3397f4
  [NEW] +14.8Ki  [NEW] +14.7Ki    agent_data_plane::state::metrics::rules::compat::get_compat_remappings::h5bb445b7815c4600
  [NEW] +14.1Ki  [NEW] +14.0Ki    datadog_agent_config::generated::witness::drive::hd9cd6e33cbbafae0
  [NEW] +12.4Ki  [NEW] +12.3Ki    agent_data_plane::state::metrics::rules::dogstatsd::get_dogstatsd_remappings::ha71516d8223bc1b6
  [DEL] -14.2Ki  [DEL] -14.1Ki    agent_data_plane::state::metrics::rules::get_compat_remappings::hecb92936be43ea62
  [DEL] -14.5Ki  [DEL] -14.4Ki    datadog_agent_config::generated::witness::drive::h1f4ddadd82aeab06
  [DEL] -15.7Ki  [DEL] -15.5Ki    _<tracing::instrument::Instrumented<T> as core::future::future::Future>::poll::hfb787573ae58fc8b
  [DEL] -15.8Ki  [DEL] -15.3Ki    core::ptr::drop_in_place<datadog_agent_config::generated::datadog_configuration::DatadogConfiguration>::h12005a1fdeb0ffb6
  [DEL] -16.3Ki  [DEL] -16.2Ki    hyper_util::client::legacy::client::Client<C,B>::send_request::_{{closure}}::h8d3663f94bd4ee59
  [DEL] -17.6Ki  [DEL] -17.5Ki    anon.5ffef35c7768023811ac5ab5fd4f00ff.893.llvm.29477675648423616
  [DEL] -17.6Ki  [DEL]     -76    anon.9f7fcabd42bdb20c281b9db2ecef43c6.574.llvm.17066318201269722550
  [DEL] -18.0Ki  [DEL] -17.9Ki    _<core::pin::Pin<P> as core::future::future::Future>::poll::hc31c20a361c2347a
 -98.0% -23.7Ki -98.5% -23.7Ki    agent_data_plane::state::metrics::rules::get_datadog_agent_remappings::hdf20e2a8dcc983e4
  [DEL] -59.5Ki  [DEL] -59.4Ki    saluki_components::common::datadog::io::run_endpoint_io_loop::_{{closure}}::h9245c5be895cc3bb
  +0.4%  +170Ki  +0.4%  +143Ki    TOTAL

@datadog-prod-us1-4 datadog-prod-us1-4 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datadog Autotest: FAIL

The shared Agent key accepts 0 to disable the TLS handshake deadline, but the new connector turns it into an immediate Tokio timeout. Deployments carrying that valid override would lose all new direct HTTPS intake connections.

📊 Validated against 4 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit cc34cf1 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cc34cf17fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/saluki-io/src/net/client/http/conn.rs Outdated
.with_request_timeout(config.request_timeout())
.with_max_idle_conns_per_host(config.max_idle_connections_per_host())
.with_min_tls_version(config.min_tls_version())
.with_tls_handshake_timeout(config.tls_handshake_timeout())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the handshake timeout after proxy CONNECT

When an HTTPS intake is reached through a configured proxy, hyper_http_proxy::ProxyConnector calls this connector only for the proxy transport and performs the destination TLS handshake itself after CONNECT. Consequently, the configured timeout here never bounds the intake handshake (for an HTTPS proxy it only bounds the proxy-side handshake), so a stalled destination handshake lasts until the overall request timeout instead of tls_handshake_timeout; the timeout must also wrap the proxy connector's post-CONNECT TLS step.

Useful? React with 👍 / 👎.

@jszwedko jszwedko Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Sonnet 5] This is a known, pre-existing limitation, not something introduced by this PR — hyper_http_proxy::ProxyConnector performs its own TLS handshake after CONNECT for HTTPS-through-proxy, independent of this connector. tls_handshake_timeout can't bound that step today. Leaving this out of scope for this PR; it's already called out as a known limitation in the PR description.

I'll follow up to add support there.

Comment thread lib/saluki-io/src/net/client/http/conn.rs
Comment thread lib/agent-data-plane-config/src/shared.rs
@pr-commenter

pr-commenter Bot commented Aug 11, 2026

Copy link
Copy Markdown

Regression Detector (Agent Data Plane)

Run ID: b30a4cbd-f61e-4685-abdf-289f2268dcee
Baseline: 76206c6b · Comparison: b75e8984 · diff

Optimization Goals: ✅ No significant changes detected

Fine details of change detection per experiment (5)

Experiments configured erratic: true are tagged (ignored) and skipped when determining which experiments regressed or improved. Experiments which are detected as erratic at runtime are tagged (erratic) to flag that the run's sample dispersion was high, but their regression / improvement signal still counts.

experiment goal Δ mean % links
quality_gates_rss_dsd_heavy memory ⚪ +0.83 metrics profiles logs
quality_gates_rss_idle memory ⚪ +0.48 metrics profiles logs
quality_gates_rss_dsd_low memory ⚪ +0.39 metrics profiles logs
quality_gates_rss_dsd_medium memory ⚪ +0.20 metrics profiles logs
quality_gates_rss_dsd_ultraheavy memory ⚪ -0.03 metrics profiles logs
Bounds Checks: ✅ Passed (5)
experiment check replicates observed links
quality_gates_rss_dsd_heavy memory_usage 10/10 ✅ 227 MiB ≤ 250 MiB metrics profiles logs
quality_gates_rss_dsd_low memory_usage 10/10 ✅ 51.1 MiB ≤ 60 MiB metrics profiles logs
quality_gates_rss_dsd_medium memory_usage 10/10 ✅ 92.3 MiB ≤ 100 MiB metrics profiles logs
quality_gates_rss_dsd_ultraheavy memory_usage 10/10 ✅ 379 MiB ≤ 420 MiB metrics profiles logs
quality_gates_rss_idle memory_usage 10/10 ✅ 32.1 MiB ≤ 40 MiB metrics profiles logs
Explanation

A change is flagged as a regression when |Δ mean %| > 5.00% in the regressing direction for its optimization goal AND SMP marks the experiment as a regression (is_regression: true). Improvements use the matching criteria for the improving direction. Experiments configured erratic: true (tagged (ignored)) are skipped outright; experiments detected as erratic at runtime (tagged (erratic)) still count, since that flag describes sample dispersion rather than directional certainty. The Δ mean % cell is colored accordingly: 🟢 = improvement, 🔴 = regression, ⚪ = neutral. Reduction in CPU or memory is an improvement; reduction in ingress throughput is a regression.

jszwedko and others added 2 commits August 11, 2026 11:09
The shared Agent key allows 0 to disable the TLS handshake deadline, but
tokio::time::timeout fires immediately on a zero duration, so a valid
override to disable the deadline caused every direct HTTPS intake
connection to fail its handshake.
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>
@@ -115,7 +115,9 @@ mod tests {
#[test]
fn incompatible_non_default() {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests in this file needed to be updated now that tls_handshake_timeout is a supported option.

HttpProtocol::Http1 previously advertised "http/1.1" via ALPN, a
behavior change from main's hyper-rustls-based connector, which leaves
ALPN unset in HTTP/1.1-only mode. Restore that behavior since it
wasn't an intentional part of this PR.

#[test]
fn auto_protocol_advertises_h2_and_http1_alpn() {
let tls_config = configure_tls_alpn_for_http_protocol(empty_tls_config(), HttpProtocol::Auto);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously these tests, strangely, seemed to actually only be testing test code (the previously existing configure_tls_alpn_for_http_protocol was gated with cfg(test)).

Exercises HttpClient end-to-end against a real TCP+TLS server that
accepts the connection but never completes the handshake, following
the existing send_request_to_tls_server pattern in this file.
check-docs rejects "e.g." per our style guide (Google.Latin).

@lucastemb lucastemb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking PR comment. Looks good 👍

Comment thread lib/saluki-io/src/net/client/http/conn.rs Outdated
saluki-io is a source-agnostic library and shouldn't reference the Datadog Agent.
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 4d40f35 into main Aug 11, 2026
93 of 94 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the jszwedko/tls-handshake-timeout branch August 11, 2026 20:40
dd-octo-sts Bot pushed a commit that referenced this pull request Aug 11, 2026
## Human Summary

Adds support for `tls_handlshake_timeout` using [newly documented method in tokio-rustls](rustls/tokio-rustls#187). This is roughly follows #178 but based on latest main.

It doesn't handle configuring the connect timeout when using an HTTPS proxy. That requires changes to hyper-http-proxy so I'll follow up with that.

Closes: #178

## AI Summary
TLS handshakes to the Datadog intake have no built-in timeout independent of the overall request timeout: `hyper_rustls`'s connector fuses the transport connect and the TLS handshake into a single opaque future, so a stalled handshake (e.g. a peer that accepts the TCP connection but never completes the TLS negotiation) is only bounded by `forwarder_timeout`, which is meant to bound the whole request, not just the handshake. This adds a `tls_handshake_timeout` config option by having the HTTP client connector own the TLS layer directly, so it can time out just the handshake step and still distinguish that failure mode from a slow request. This picks up the intent of #1819, an older PR for the same issue, rewritten against the current typed configuration system rather than resurrected via rebase.

```mermaid
sequenceDiagram
    participant Before as Before (hyper_rustls::HttpsConnector)
    participant After as After (owned TLS layer)

    Note over Before: connect + handshake fused into one future
    Before->>Before: TCP connect
    Before->>Before: TLS handshake
    Note over Before: only forwarder_timeout bounds both steps combined

    Note over After: connect and handshake are separate steps
    After->>After: TCP connect (connect_timeout)
    After->>After: TLS handshake (tls_handshake_timeout, new)
    Note over After: a stalled handshake times out on its own,<br/>without racing the whole request
```

## Test plan

- [x] Added `tls_handshake_timeout` to the Datadog config schema overlay (`support: full`) and wired it through the typed config system (`DatadogTranslator`, `SalukiConfiguration`) and the legacy `ForwarderConfiguration` facet-based config, both consumed by the HTTP client builder.
- [x] Added/updated unit tests in `saluki-io`'s `conn.rs` for the new connector split (ALPN protocol selection, including an explicit `http/1.1` ALPN advertisement for `HttpProtocol::Http1` to avoid an ALPN regression from the previous implicit behavior).
- [x] Existing `config_smoke::smoke_test` in `saluki-components` (`ForwarderConfiguration`) exercises the new field's default/deserialization against the config registry.
- [x] Updated classifier unit tests in `datadog-agent-config` that previously used `tls_handshake_timeout` as an example unsupported/incompatible key, substituting other still-unsupported keys since this key is now fully supported.

## Known limitation

Connections made through `proxy_https` bypass this connector and aren't covered by `tls_handshake_timeout` (flagged on the original PR). Left out of scope here; can be addressed separately if needed.

Co-authored-by: jesse.szwedko <jesse.szwedko@datadoghq.com> 4d40f35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/components Sources, transforms, and destinations. area/docs Reference documentation. area/io General I/O and networking. mergequeue-status: done

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow configuring the TLS handshake timeout for HTTP clients.

2 participants