Skip to content

Commit b02c23e

Browse files
committed
Harden stream proxy timeouts and TLS handling
1 parent 5fa3bfd commit b02c23e

4 files changed

Lines changed: 114 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ behavior when the change improves security or project direction.
2424
PROXY protocol v1 receive from trusted sources.
2525
- Add stream unit coverage for wall-clock lifetime caps and upstream PROXY
2626
protocol v2 framing.
27+
- Bound stream upstream TLS DNS resolution under `connect_timeout_secs`, add
28+
stream write/shutdown deadlines, avoid derived IP-literal SNI, and zeroize
29+
temporary upstream mTLS private-key buffers after parsing.
2730

2831
## 1.4.6 - 2026-05-30
2932

release-notes/RELEASE_NOTES_1.4.7.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ stream proxy foundation.
2020
from trusted sources.
2121
- Stream unit coverage for wall-clock lifetime caps and upstream PROXY
2222
protocol v2 framing.
23+
- Stream hardening for TLS DNS timeout coverage, stalled write/shutdown
24+
deadlines, RFC 6066-safe SNI derivation, and temporary upstream mTLS
25+
private-key zeroization after parsing.
2326

2427
## Planned Scope
2528

src/stream_proxy.rs

Lines changed: 98 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,7 @@ async fn proxy_connected_stream_connection(
555555
options.upstream_proxy_protocol,
556556
source,
557557
destination,
558+
options.idle_timeout,
558559
)
559560
.await?;
560561

@@ -658,15 +659,19 @@ async fn copy_bidirectional_with_limits(
658659
idle_timeout,
659660
).await?;
660661
if bytes == 0 {
661-
upstream_writer.shutdown().await?;
662+
shutdown_with_idle_timeout(&mut upstream_writer, idle_timeout).await?;
662663
Ok::<_, io::Error>(StreamCopyEvent::DownstreamEof)
663664
} else {
664665
let next = checked_stream_byte_count(
665666
downstream_to_upstream,
666667
bytes as u64,
667668
max_connection_bytes,
668669
)?;
669-
upstream_writer.write_all(&downstream_buffer[..bytes]).await?;
670+
write_with_idle_timeout(
671+
&mut upstream_writer,
672+
&downstream_buffer[..bytes],
673+
idle_timeout,
674+
).await?;
670675
Ok::<_, io::Error>(StreamCopyEvent::DownstreamTotal(next))
671676
}
672677
}, if !downstream_eof => result,
@@ -677,15 +682,19 @@ async fn copy_bidirectional_with_limits(
677682
idle_timeout,
678683
).await?;
679684
if bytes == 0 {
680-
downstream_writer.shutdown().await?;
685+
shutdown_with_idle_timeout(&mut downstream_writer, idle_timeout).await?;
681686
Ok::<_, io::Error>(StreamCopyEvent::UpstreamEof)
682687
} else {
683688
let next = checked_stream_byte_count(
684689
upstream_to_downstream,
685690
bytes as u64,
686691
max_connection_bytes,
687692
)?;
688-
downstream_writer.write_all(&upstream_buffer[..bytes]).await?;
693+
write_with_idle_timeout(
694+
&mut downstream_writer,
695+
&upstream_buffer[..bytes],
696+
idle_timeout,
697+
).await?;
689698
Ok::<_, io::Error>(StreamCopyEvent::UpstreamTotal(next))
690699
}
691700
}, if !upstream_eof => result,
@@ -719,6 +728,36 @@ where
719728
}
720729
}
721730

731+
async fn write_with_idle_timeout<W>(
732+
writer: &mut W,
733+
buffer: &[u8],
734+
idle_timeout: Duration,
735+
) -> io::Result<()>
736+
where
737+
W: tokio::io::AsyncWrite + Unpin,
738+
{
739+
match tokio::time::timeout(idle_timeout, writer.write_all(buffer)).await {
740+
Ok(result) => result,
741+
Err(_) => Err(io::Error::new(
742+
io::ErrorKind::TimedOut,
743+
"stream write timeout elapsed",
744+
)),
745+
}
746+
}
747+
748+
async fn shutdown_with_idle_timeout<W>(writer: &mut W, idle_timeout: Duration) -> io::Result<()>
749+
where
750+
W: tokio::io::AsyncWrite + Unpin,
751+
{
752+
match tokio::time::timeout(idle_timeout, writer.shutdown()).await {
753+
Ok(result) => result,
754+
Err(_) => Err(io::Error::new(
755+
io::ErrorKind::TimedOut,
756+
"stream shutdown timeout elapsed",
757+
)),
758+
}
759+
}
760+
722761
fn checked_stream_byte_count(
723762
current: u64,
724763
additional: u64,
@@ -793,42 +832,44 @@ async fn connect_tls_upstream(
793832
feature = "tls-s2n"
794833
))]
795834
{
796-
let socket_addr = resolve_upstream_socket_addr(upstream_authority).await?;
797-
let sni = options
798-
.upstream_sni
799-
.as_deref()
800-
.map(str::to_owned)
801-
.or_else(|| upstream_host(upstream_authority))
802-
.unwrap_or_default();
803-
let mut peer = HttpPeer::new(socket_addr, true, sni);
804-
peer.options.connection_timeout = Some(options.connect_timeout);
805-
peer.options.total_connection_timeout = Some(options.connect_timeout);
806-
peer.options.verify_cert = options.upstream_verify_cert;
807-
peer.options.verify_hostname = options.upstream_verify_hostname;
808-
peer.options.alternative_cn = options
809-
.upstream_alternative_cn
810-
.as_deref()
811-
.map(str::to_owned);
812-
#[cfg(any(
813-
feature = "tls-rustls-backend",
814-
feature = "tls-openssl",
815-
feature = "tls-boringssl"
816-
))]
817-
{
818-
peer.options.ca = options.upstream_tls_material.ca.clone();
819-
peer.client_cert_key = options.upstream_tls_material.client_cert_key.clone();
820-
}
821-
let Some(connector) = &options.connector else {
822-
return Err(io::Error::new(
823-
io::ErrorKind::InvalidInput,
824-
"stream upstream TLS connector is not initialized",
825-
));
835+
let connect = async {
836+
let socket_addr = resolve_upstream_socket_addr(upstream_authority).await?;
837+
let sni = stream_upstream_tls_sni(options.upstream_sni.as_deref(), upstream_authority);
838+
let mut peer = HttpPeer::new(socket_addr, true, sni);
839+
peer.options.connection_timeout = Some(options.connect_timeout);
840+
peer.options.total_connection_timeout = Some(options.connect_timeout);
841+
peer.options.verify_cert = options.upstream_verify_cert;
842+
peer.options.verify_hostname = options.upstream_verify_hostname;
843+
peer.options.alternative_cn = options
844+
.upstream_alternative_cn
845+
.as_deref()
846+
.map(str::to_owned);
847+
#[cfg(any(
848+
feature = "tls-rustls-backend",
849+
feature = "tls-openssl",
850+
feature = "tls-boringssl"
851+
))]
852+
{
853+
peer.options.ca = options.upstream_tls_material.ca.clone();
854+
peer.client_cert_key = options.upstream_tls_material.client_cert_key.clone();
855+
}
856+
let Some(connector) = &options.connector else {
857+
return Err(io::Error::new(
858+
io::ErrorKind::InvalidInput,
859+
"stream upstream TLS connector is not initialized",
860+
));
861+
};
862+
connector
863+
.get_stream(&peer)
864+
.await
865+
.map(|(stream, _reused)| stream)
866+
.map_err(|error| {
867+
io::Error::other(format!("stream upstream TLS connect failed: {error}"))
868+
})
826869
};
827-
match tokio::time::timeout(options.connect_timeout, connector.get_stream(&peer)).await {
828-
Ok(Ok((stream, _reused))) => Ok(stream),
829-
Ok(Err(error)) => Err(io::Error::other(format!(
830-
"stream upstream TLS connect failed: {error}"
831-
))),
870+
871+
match tokio::time::timeout(options.connect_timeout, connect).await {
872+
Ok(result) => result,
832873
Err(_) => Err(io::Error::new(
833874
io::ErrorKind::TimedOut,
834875
"stream upstream TLS connect timeout elapsed",
@@ -849,6 +890,22 @@ async fn connect_tls_upstream(
849890
}
850891
}
851892

893+
#[cfg(any(
894+
feature = "tls-rustls-backend",
895+
feature = "tls-openssl",
896+
feature = "tls-boringssl",
897+
feature = "tls-s2n"
898+
))]
899+
fn stream_upstream_tls_sni(configured: Option<&str>, upstream_authority: &str) -> String {
900+
configured
901+
.map(str::to_owned)
902+
.or_else(|| {
903+
let host = upstream_host(upstream_authority)?;
904+
host.parse::<IpAddr>().is_err().then_some(host)
905+
})
906+
.unwrap_or_default()
907+
}
908+
852909
async fn connect_upstream_inner(upstream_authority: &str) -> io::Result<tokio::net::TcpStream> {
853910
let socket_addr = resolve_upstream_socket_addr(upstream_authority).await?;
854911
tokio::net::TcpStream::connect(socket_addr).await
@@ -873,6 +930,7 @@ async fn write_upstream_proxy_protocol(
873930
protocol: UpstreamProxyProtocol,
874931
source: Option<SocketAddr>,
875932
destination: Option<SocketAddr>,
933+
idle_timeout: Duration,
876934
) -> io::Result<()> {
877935
let header = match protocol {
878936
UpstreamProxyProtocol::Off => return Ok(()),
@@ -883,7 +941,7 @@ async fn write_upstream_proxy_protocol(
883941
crate::proxy_protocol::proxy_protocol_v2_header(source, destination)
884942
}
885943
};
886-
upstream.write_all(&header).await
944+
write_with_idle_timeout(upstream, &header, idle_timeout).await
887945
}
888946

889947
fn downstream_peer_addr(downstream: &Stream) -> Option<SocketAddr> {

src/upstream_tls.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ use std::sync::Arc;
1717
feature = "tls-boringssl"
1818
))]
1919
use crate::config::ProxyConfig;
20+
#[cfg(any(
21+
feature = "tls-rustls-backend",
22+
feature = "tls-openssl",
23+
feature = "tls-boringssl"
24+
))]
25+
use zeroize::Zeroizing;
2026

2127
#[cfg(any(
2228
feature = "tls-rustls-backend",
@@ -267,7 +273,7 @@ fn load_upstream_client_cert_key(
267273
key_path: &std::path::Path,
268274
) -> io::Result<Arc<pingora::utils::tls::CertKey>> {
269275
let cert_contents = read_upstream_tls_file(cert_path)?;
270-
let key_contents = read_upstream_tls_file(key_path)?;
276+
let key_contents = Zeroizing::new(read_upstream_tls_file(key_path)?);
271277

272278
use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
273279

@@ -302,12 +308,13 @@ fn load_upstream_client_cert_key(
302308
)
303309
})?;
304310

311+
let key_der = Zeroizing::new(key.secret_der().to_vec());
305312
let cert_key = pingora::utils::tls::CertKey::try_new(
306313
certs
307314
.into_iter()
308315
.map(|cert| cert.as_ref().to_vec())
309316
.collect(),
310-
key.secret_der().to_vec(),
317+
key_der.to_vec(),
311318
)
312319
.map_err(|error| {
313320
io::Error::new(
@@ -330,7 +337,7 @@ fn load_upstream_client_cert_key(
330337
key_path: &std::path::Path,
331338
) -> io::Result<Arc<pingora::utils::tls::CertKey>> {
332339
let cert_contents = read_upstream_tls_file(cert_path)?;
333-
let key_contents = read_upstream_tls_file(key_path)?;
340+
let key_contents = Zeroizing::new(read_upstream_tls_file(key_path)?);
334341
let certs = pingora::tls::x509::X509::stack_from_pem(&cert_contents).map_err(|error| {
335342
io::Error::new(
336343
io::ErrorKind::InvalidData,

0 commit comments

Comments
 (0)