Skip to content

Commit bfd46bc

Browse files
authored
Merge branch 'master' into pront-changelog-types
2 parents 82fdcee + 8f5e50b commit bfd46bc

16 files changed

Lines changed: 237 additions & 14 deletions

File tree

‎Cargo.lock‎

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Added an optional `tls_handshake_timeout_secs` setting to the `socket` (TCP mode), `syslog` (TCP mode), `logstash`, `fluent`, and `statsd` (TCP mode) sources. When set, a TLS-enabled connection that does not complete its TLS handshake within the configured number of seconds is closed.
2+
3+
Previously, TLS handshakes on these sources had no timeout: a client that opened a TCP connection and never completed (or never started) the TLS handshake would hold its slot against `connection_limit` indefinitely, since neither TCP keepalive nor `max_connection_duration_secs` are evaluated until after the handshake succeeds. This could let misbehaving or unresponsive clients gradually exhaust the connection limit and block legitimate traffic. The new setting is unset by default, preserving prior behavior.
4+
5+
authors: vladimir-dd

‎deny.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ ignore = [
5353
{ id = "RUSTSEC-2026-0195", reason = "quick-xml unbounded namespace-declaration allocation in NsReader - versions needing upgrade are v0.31 (direct dep) and v0.37.4 (via opendal < 0.58)" },
5454
{ id = "RUSTSEC-2026-0235", reason = "rkyv 0.7 is vulnerable but unmaintained; upgrading vector-buffers to rkyv 0.8 requires a breaking serialization migration" },
5555
{ id = "RUSTSEC-2026-0253", reason = "Vector uses lru 0.18.2; aws-sdk-s3 and ratatui still require lru ^0.16.3" },
56+
{ id = "RUSTSEC-2026-0258", reason = "h2 0.3 via aws-smithy-http-client (hyper 0.14) and tonic 0.11 - http 1.0 / AWS SDK upgrade (https://github.com/vectordotdev/vector/issues/19179)" },
5657
]

‎src/internal_events/tcp.rs‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,40 @@ impl InternalEvent for TcpSocketTlsConnectionError {
129129
}
130130
}
131131

132+
/// Emitted when a TLS handshake does not complete within the configured
133+
/// handshake timeout. Without this, a peer that opens a TCP connection but
134+
/// never completes (or never starts) the TLS handshake — a bare TCP
135+
/// healthcheck, a stalled client, or deliberate slow-handshake abuse — would
136+
/// hold its connection-limit slot indefinitely, since neither TCP keepalive
137+
/// nor `max_connection_duration_secs` are wired up until after the handshake
138+
/// succeeds.
139+
#[derive(Debug, NamedInternalEvent)]
140+
pub struct TcpSocketTlsHandshakeTimeout {
141+
pub peer_addr: SocketAddr,
142+
pub timeout: std::time::Duration,
143+
}
144+
145+
impl InternalEvent for TcpSocketTlsHandshakeTimeout {
146+
fn emit(self) {
147+
warn!(
148+
message = "TLS handshake timed out.",
149+
peer_addr = %self.peer_addr,
150+
timeout_secs = self.timeout.as_secs(),
151+
error_code = "tls_handshake_timeout",
152+
error_type = error_type::TIMED_OUT,
153+
stage = error_stage::RECEIVING,
154+
);
155+
counter!(
156+
CounterName::ComponentErrorsTotal,
157+
"error_code" => "tls_handshake_timeout",
158+
"error_type" => error_type::TIMED_OUT,
159+
"stage" => error_stage::RECEIVING,
160+
"mode" => "tcp",
161+
)
162+
.increment(1);
163+
}
164+
}
165+
132166
#[derive(Debug, NamedInternalEvent)]
133167
pub struct TcpSendAckError {
134168
pub error: std::io::Error,

‎src/sources/fluent/mod.rs‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{collections::HashMap, io, net::SocketAddr, time::Duration};
1+
use std::{collections::HashMap, io, net::SocketAddr, num::NonZeroU64, time::Duration};
22

33
use base64::prelude::{BASE64_STANDARD, Engine as _};
44
use bytes::{Buf, Bytes, BytesMut};
@@ -184,6 +184,14 @@ pub struct FluentTcpConfig {
184184
#[configurable(metadata(docs::examples = 65536))]
185185
receive_buffer_bytes: Option<usize>,
186186

187+
/// The timeout, in seconds, before a TLS handshake is aborted if it has not completed.
188+
///
189+
/// This bounds how long a connection can hold its slot against `connection_limit`
190+
/// before the TLS handshake finishes, protecting against clients that open a
191+
/// connection but never complete (or never start) a handshake.
192+
#[configurable(metadata(docs::type_unit = "seconds"))]
193+
tls_handshake_timeout_secs: Option<NonZeroU64>,
194+
187195
#[configurable(derived)]
188196
tls: Option<TlsSourceConfig>,
189197

@@ -216,6 +224,7 @@ impl FluentTcpConfig {
216224
tls_client_metadata_key,
217225
self.receive_buffer_bytes,
218226
None,
227+
self.tls_handshake_timeout_secs,
219228
cx,
220229
self.acknowledgements,
221230
self.connection_limit,
@@ -277,6 +286,7 @@ impl GenerateConfig for FluentConfig {
277286
permit_origin: None,
278287
tls: None,
279288
receive_buffer_bytes: None,
289+
tls_handshake_timeout_secs: None,
280290
acknowledgements: Default::default(),
281291
connection_limit: Some(2),
282292
}),
@@ -1167,6 +1177,7 @@ mod tests {
11671177
keepalive: None,
11681178
permit_origin: None,
11691179
receive_buffer_bytes: None,
1180+
tls_handshake_timeout_secs: None,
11701181
acknowledgements: true.into(),
11711182
connection_limit: None,
11721183
}),
@@ -1240,6 +1251,7 @@ mod tests {
12401251
keepalive: None,
12411252
permit_origin: None,
12421253
receive_buffer_bytes: None,
1254+
tls_handshake_timeout_secs: None,
12431255
acknowledgements: false.into(),
12441256
connection_limit: None,
12451257
}),
@@ -1298,6 +1310,7 @@ mod tests {
12981310
keepalive: None,
12991311
permit_origin: None,
13001312
receive_buffer_bytes: None,
1313+
tls_handshake_timeout_secs: None,
13011314
acknowledgements: false.into(),
13021315
connection_limit: None,
13031316
}),
@@ -1527,6 +1540,7 @@ mod integration_tests {
15271540
keepalive: None,
15281541
permit_origin: None,
15291542
receive_buffer_bytes: None,
1543+
tls_handshake_timeout_secs: None,
15301544
acknowledgements: false.into(),
15311545
connection_limit: None,
15321546
}),

‎src/sources/logstash.rs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{
33
convert::TryFrom,
44
io,
55
net::SocketAddr,
6-
num::NonZeroUsize,
6+
num::{NonZeroU64, NonZeroUsize},
77
time::Duration,
88
};
99

@@ -62,6 +62,14 @@ pub struct LogstashConfig {
6262
#[configurable(metadata(docs::type_unit = "connections"))]
6363
connection_limit: Option<u32>,
6464

65+
/// The timeout, in seconds, before a TLS handshake is aborted if it has not completed.
66+
///
67+
/// This bounds how long a connection can hold its slot against `connection_limit`
68+
/// before the TLS handshake finishes, protecting against clients that open a
69+
/// connection but never complete (or never start) a handshake.
70+
#[configurable(metadata(docs::type_unit = "seconds"))]
71+
tls_handshake_timeout_secs: Option<NonZeroU64>,
72+
6573
#[configurable(derived)]
6674
#[serde(default, deserialize_with = "bool_or_struct")]
6775
acknowledgements: SourceAcknowledgementsConfig,
@@ -125,6 +133,7 @@ impl Default for LogstashConfig {
125133
receive_buffer_bytes: None,
126134
acknowledgements: Default::default(),
127135
connection_limit: None,
136+
tls_handshake_timeout_secs: None,
128137
log_namespace: None,
129138
}
130139
}
@@ -164,6 +173,7 @@ impl SourceConfig for LogstashConfig {
164173
tls_client_metadata_key,
165174
self.receive_buffer_bytes,
166175
None,
176+
self.tls_handshake_timeout_secs,
167177
cx,
168178
self.acknowledgements,
169179
self.connection_limit,
@@ -902,6 +912,7 @@ mod test {
902912
receive_buffer_bytes: None,
903913
acknowledgements: true.into(),
904914
connection_limit: None,
915+
tls_handshake_timeout_secs: None,
905916
log_namespace: None,
906917
}
907918
.build(SourceContext::new_test(sender, None))
@@ -1667,6 +1678,7 @@ mod integration_tests {
16671678
receive_buffer_bytes: None,
16681679
acknowledgements: false.into(),
16691680
connection_limit: None,
1681+
tls_handshake_timeout_secs: None,
16701682
log_namespace: None,
16711683
}
16721684
.build(SourceContext::new_test(sender, None))

‎src/sources/socket/mod.rs‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ impl SourceConfig for SocketConfig {
143143
tls_client_metadata_key,
144144
config.receive_buffer_bytes(),
145145
config.max_connection_duration_secs(),
146+
config.tls_handshake_timeout_secs(),
146147
cx,
147148
false.into(),
148149
config.connection_limit,
@@ -322,6 +323,7 @@ mod test {
322323
use std::{
323324
collections::HashMap,
324325
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
326+
num::NonZeroU64,
325327
sync::{
326328
Arc,
327329
atomic::{AtomicBool, Ordering},
@@ -930,6 +932,43 @@ mod test {
930932
}
931933
}
932934

935+
#[tokio::test]
936+
async fn tcp_tls_handshake_timeout() {
937+
let (tx, _) = SourceSender::new_test();
938+
let (guard, addr) = next_addr();
939+
940+
let mut config = TcpConfig::from_address(addr.into());
941+
config.set_tls(Some(TlsSourceConfig {
942+
tls_config: TlsEnableableConfig::test_config(),
943+
client_metadata_key: None,
944+
}));
945+
config.set_tls_handshake_timeout_secs(Some(NonZeroU64::new(1).unwrap()));
946+
947+
let source_task = SocketConfig::from(config)
948+
.build(SourceContext::new_test(tx, None))
949+
.await
950+
.unwrap();
951+
952+
// Spawn the source task and wait until we're sure it's listening:
953+
drop(tokio::spawn(source_task));
954+
wait_for_tcp_and_release(guard, addr).await;
955+
956+
// Open a plain TCP connection but deliberately never send a TLS
957+
// ClientHello, so the server's handshake never completes on its own.
958+
let mut stream: TcpStream = TcpStream::connect(addr)
959+
.await
960+
.expect("stream should be able to connect");
961+
let start = Instant::now();
962+
963+
let bytes_read = timeout(Duration::from_secs(2), stream.read(&mut [0]))
964+
.await
965+
.expect("timed out waiting for stream to close")
966+
.expect("failed to read from stream");
967+
968+
assert_eq!(bytes_read, 0, "unexpectedly read data from stream");
969+
assert_relative_eq!(start.elapsed().as_secs_f64(), 1.0, epsilon = 0.5);
970+
}
971+
933972
//////// UDP TESTS ////////
934973
async fn send_lines_udp(to: SocketAddr, lines: impl IntoIterator<Item = String>) -> UdpSocket {
935974
send_lines_udp_from(bind_unused_udp(), to, lines)

‎src/sources/socket/tcp.rs‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::time::Duration;
1+
use std::{num::NonZeroU64, time::Duration};
22

33
use chrono::Utc;
44
use serde_with::serde_as;
@@ -75,6 +75,14 @@ pub struct TcpConfig {
7575
#[configurable(metadata(docs::type_unit = "seconds"))]
7676
max_connection_duration_secs: Option<u64>,
7777

78+
/// The timeout, in seconds, before a TLS handshake is aborted if it has not completed.
79+
///
80+
/// This bounds how long a connection can hold its slot against `connection_limit`
81+
/// before the TLS handshake finishes, protecting against clients that open a
82+
/// connection but never complete (or never start) a handshake.
83+
#[configurable(metadata(docs::type_unit = "seconds"))]
84+
tls_handshake_timeout_secs: Option<NonZeroU64>,
85+
7886
/// The maximum number of TCP connections that are allowed at any given time.
7987
#[configurable(metadata(docs::type_unit = "connections"))]
8088
pub connection_limit: Option<u32>,
@@ -112,6 +120,7 @@ impl TcpConfig {
112120
tls: None,
113121
receive_buffer_bytes: None,
114122
max_connection_duration_secs: None,
123+
tls_handshake_timeout_secs: None,
115124
framing: None,
116125
decoding: default_decoding(),
117126
connection_limit: None,
@@ -164,6 +173,15 @@ impl TcpConfig {
164173
self
165174
}
166175

176+
pub const fn tls_handshake_timeout_secs(&self) -> Option<NonZeroU64> {
177+
self.tls_handshake_timeout_secs
178+
}
179+
180+
pub const fn set_tls_handshake_timeout_secs(&mut self, val: Option<NonZeroU64>) -> &mut Self {
181+
self.tls_handshake_timeout_secs = val;
182+
self
183+
}
184+
167185
pub const fn set_shutdown_timeout_secs(&mut self, val: u64) -> &mut Self {
168186
self.shutdown_timeout_secs = Duration::from_secs(val);
169187
self

‎src/sources/statsd/mod.rs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{
22
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
3+
num::NonZeroU64,
34
time::Duration,
45
};
56

@@ -139,6 +140,14 @@ pub struct TcpConfig {
139140
#[configurable(metadata(docs::type_unit = "connections"))]
140141
connection_limit: Option<u32>,
141142

143+
/// The timeout, in seconds, before a TLS handshake is aborted if it has not completed.
144+
///
145+
/// This bounds how long a connection can hold its slot against `connection_limit`
146+
/// before the TLS handshake finishes, protecting against clients that open a
147+
/// connection but never complete (or never start) a handshake.
148+
#[configurable(metadata(docs::type_unit = "seconds"))]
149+
tls_handshake_timeout_secs: Option<NonZeroU64>,
150+
142151
/// Whether or not to sanitize incoming statsd key names. When "true", keys are sanitized by:
143152
/// - "/" is replaced with "-"
144153
/// - All whitespace is replaced with "_"
@@ -163,6 +172,7 @@ impl TcpConfig {
163172
shutdown_timeout_secs: default_shutdown_timeout_secs(),
164173
receive_buffer_bytes: None,
165174
connection_limit: None,
175+
tls_handshake_timeout_secs: None,
166176
sanitize: default_sanitize(),
167177
convert_to: default_convert_to(),
168178
}
@@ -223,6 +233,7 @@ impl SourceConfig for StatsdConfig {
223233
tls_client_metadata_key,
224234
config.receive_buffer_bytes,
225235
None,
236+
config.tls_handshake_timeout_secs,
226237
cx,
227238
false.into(),
228239
config.connection_limit,

0 commit comments

Comments
 (0)