Skip to content

Commit ef81dfe

Browse files
HTHoucodex
andcommitted
THRIFT-6097: Add rustls client and server support
Client: rs Co-Authored-By: OpenAI Codex (GPT-5) <noreply@openai.com>
1 parent 09cd5f2 commit ef81dfe

14 files changed

Lines changed: 867 additions & 16 deletions

File tree

doc/thrift-threat-model.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,10 +1308,13 @@ The claim is correct for Go: validation is on by default, and the system store i
13081308
InsecureSkipVerify: true in their *tls.Config, which is outside Thrift's control.
13091309

13101310
---
1311-
Rust — No TLS support
1311+
Rust — Validation is controlled by the supplied rustls configuration
13121312

1313-
The Rust library (lib/rs/src/transport/socket.rs) implements only plain TCP via TcpStream. There is no TSSLSocket type, no TLS transport, and no SSL dependency. The claim is
1314-
inapplicable.
1313+
When the optional `rustls` feature is enabled, `TTlsClientChannel` requires the caller to supply an `Arc<ClientConfig>` and a `ServerName`. `connect()` completes the TLS
1314+
handshake according to that configuration before returning. Chain validation and server-name verification are controlled by the configuration's verifier. `TTlsServerChannel`
1315+
and `TServer::listen_tls()` similarly require an `Arc<ServerConfig>`; client-certificate authentication is whatever that configuration selects. Thrift does not construct a
1316+
default configuration or automatically load system roots, so there is no implicit trust-store fallback. Applications may also supply rustls custom verifiers; their behavior is
1317+
outside Thrift's control.
13151318

13161319
---
13171320
Summary table
@@ -1333,7 +1336,7 @@ Summary table
13331336
│ Go │ crypto/tls enforces (default │ System root CA pool if RootCAs=nil │ Yes (system pool) │ Caller can bypass by setting InsecureSkipVerify: │
13341337
│ │ InsecureSkipVerify=false) │ │ │ true │
13351338
├─────────┼─────────────────────────────────────────┼───────────────────────────────────────────┼────────────────────┼─────────────────────────────────────────────────────┤
1336-
│ Rust │ N/A │ N/A │ N/A │ No TLS transport exists
1339+
│ Rust │ Caller-configured rustls verifier │ Caller-supplied ClientConfig │ No No default config or implicit trust-store loading
13371340
└─────────┴─────────────────────────────────────────┴───────────────────────────────────────────┴────────────────────┴─────────────────────────────────────────────────────┘
13381341
```
13391342

@@ -1503,7 +1506,7 @@ not recommended. THRIFT-5926 (crash on None initial DIGEST-MD5 response) is a co
15031506
mechanism removal. If the crash is reachable pre-authentication, it qualifies as a remotely-triggerable DoS and will be treated accordingly.
15041507

15051508
**Q42.**
1506-
Thrift sets no project-wide TLS version floor. Each binding delegates version negotiation to its underlying TLS library (OpenSSL, JSSE, crypto/tls, etc.).
1509+
Thrift sets no project-wide TLS version floor. Each binding delegates version negotiation to its underlying TLS library (OpenSSL, JSSE, crypto/tls, rustls, etc.).
15071510
THRIFT-5743/5876 added TLS 1.3 capability where it was absent. Operators are responsible for configuring a version floor in the underlying library; TLS 1.2
15081511
minimum is recommended. A finding that TLS 1.0 or 1.1 is negotiable is a deployment or library-configuration concern, not a Thrift vulnerability. (Note: verify
15091512
whether the C++ binding explicitly disables SSLv2/v3 via SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3; if so, state that floor explicitly for the C++ binding.)

lib/rs/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ uuid = "1"
1919
log = {version = "0.4", optional = true}
2020
ordered-float = "3.0"
2121
threadpool = {version = "1.7", optional = true}
22+
rustls = { version = "0.23.42", default-features = false, features = ["std", "tls12"], optional = true }
2223

2324
[features]
2425
default = ["server"]
2526
server = ["threadpool", "log"]
27+
rustls = ["dep:rustls"]
2628

2729
[dev-dependencies]
2830
uuid = { version = "1", features = ["v4"] }
31+
rustls = { version = "0.23.42", default-features = false, features = ["ring", "std", "tls12"] }

lib/rs/Makefile.am

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,17 @@ install:
3333

3434
check-local:
3535
$(CARGO) fmt --all -- --check
36-
$(CARGO) clippy --all -- -D warnings
36+
$(CARGO) clippy --all --all-features -- -D warnings
37+
$(CARGO) check --no-default-features --features rustls
3738
$(CARGO) test
39+
$(CARGO) test --all-features
3840

3941
all-local:
4042
$(CARGO) fmt --all -- --check
41-
$(CARGO) clippy --all -- -D warnings
43+
$(CARGO) clippy --all --all-features -- -D warnings
44+
$(CARGO) check --no-default-features --features rustls
4245
$(CARGO) build
46+
$(CARGO) build --all-features
4347

4448
clean-local:
4549
$(CARGO) clean
@@ -50,6 +54,7 @@ distdir:
5054

5155
EXTRA_DIST = \
5256
src \
57+
tests \
5358
Cargo.toml \
5459
README.md \
5560
release.sh \

lib/rs/README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,33 @@ types and services by writing their own code on top.
3333
Add `thrift = "x.y.z"` to your `Cargo.toml`, where `x.y.z` is the version of the
3434
Thrift compiler you're using.
3535

36+
### TLS
37+
38+
TLS client and server channels are available through the optional `rustls`
39+
feature:
40+
41+
```toml
42+
[dependencies]
43+
thrift = { version = "x.y.z", features = ["rustls"] }
44+
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
45+
```
46+
47+
Applications construct a rustls `ClientConfig` or `ServerConfig` and pass it to
48+
`TTlsClientChannel::connect` or `TServer::listen_tls`. Thrift does not choose a
49+
crypto provider, load a system trust store, or read certificate files. This
50+
keeps trust anchors, client authentication, protocol versions, and certificate
51+
selection under application control.
52+
3653
## API Documentation
3754

3855
Full [Rustdoc](https://docs.rs/thrift/)
3956

4057
## Compatibility
4158

42-
The Rust library and auto-generated code targets Rust versions 1.28+.
43-
It does not currently use any Rust 2021 features.
59+
The Rust library and auto-generated code target Rust versions 1.65+. The
60+
minimum was raised from Rust 1.28 by THRIFT-5158, when the library and generator
61+
moved to Rust 2021-only output. Enabling the optional `rustls` feature requires
62+
Rust 1.71+.
4463

4564
### Breaking Changes
4665

lib/rs/src/server/threaded.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ use std::net::{TcpListener, ToSocketAddrs};
2121
use std::sync::Arc;
2222
use threadpool::ThreadPool;
2323

24+
#[cfg(feature = "rustls")]
25+
use rustls::ServerConfig;
26+
2427
#[cfg(unix)]
2528
use std::os::unix::net::UnixListener;
2629
#[cfg(unix)]
@@ -29,6 +32,8 @@ use std::path::Path;
2932
use crate::protocol::{
3033
TInputProtocol, TInputProtocolFactory, TOutputProtocol, TOutputProtocolFactory,
3134
};
35+
#[cfg(feature = "rustls")]
36+
use crate::transport::TTlsServerChannel;
3237
use crate::transport::{TIoChannel, TReadTransportFactory, TTcpChannel, TWriteTransportFactory};
3338
use crate::{ApplicationError, ApplicationErrorKind};
3439

@@ -199,6 +204,41 @@ where
199204
}))
200205
}
201206

207+
/// Listen for incoming TLS connections on `listen_address`.
208+
///
209+
/// `config` controls certificate selection, client authentication, crypto
210+
/// provider, and protocol policy. Accepted connections are handed to a
211+
/// worker before the TLS handshake begins, so the accept loop does not
212+
/// block on a peer that connects without completing a handshake.
213+
#[cfg(feature = "rustls")]
214+
pub fn listen_tls<A: ToSocketAddrs>(
215+
&mut self,
216+
listen_address: A,
217+
config: Arc<ServerConfig>,
218+
) -> crate::Result<()> {
219+
let listener = TcpListener::bind(listen_address)?;
220+
for stream in listener.incoming() {
221+
match stream {
222+
Ok(stream) => {
223+
stream.set_nodelay(true).ok();
224+
let channel = TTlsServerChannel::with_stream(stream, Arc::clone(&config))?;
225+
self.handle_stream(channel)?;
226+
}
227+
Err(error) => {
228+
warn!(
229+
"failed to accept remote TLS connection with error {:?}",
230+
error
231+
);
232+
}
233+
}
234+
}
235+
236+
Err(crate::Error::Application(ApplicationError {
237+
kind: ApplicationErrorKind::Unknown,
238+
message: "aborted TLS listen loop".into(),
239+
}))
240+
}
241+
202242
/// Listen for incoming connections on `listen_path`.
203243
///
204244
/// `listen_path` should implement `AsRef<Path>` trait.

lib/rs/src/transport/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ macro_rules! assert_eq_transport_written_bytes {
4444
mod buffered;
4545
mod framed;
4646
mod mem;
47+
mod shared;
4748
mod socket;
49+
#[cfg(feature = "rustls")]
50+
mod tls;
4851

4952
pub use self::buffered::{
5053
TBufferedReadTransport, TBufferedReadTransportFactory, TBufferedWriteTransport,
@@ -55,7 +58,10 @@ pub use self::framed::{
5558
TFramedWriteTransportFactory,
5659
};
5760
pub use self::mem::TBufferChannel;
61+
pub use self::shared::TSharedChannel;
5862
pub use self::socket::TTcpChannel;
63+
#[cfg(feature = "rustls")]
64+
pub use self::tls::{TTlsClientChannel, TTlsServerChannel};
5965

6066
/// Identifies a transport used by a `TInputProtocol` to receive bytes.
6167
pub trait TReadTransport: Read {}

lib/rs/src/transport/shared.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use std::io::{self, Read, Write};
19+
use std::sync::{Arc, Mutex, MutexGuard};
20+
21+
use super::{ReadHalf, TIoChannel, WriteHalf};
22+
23+
/// A cloneable channel that serializes access to an underlying I/O stream.
24+
///
25+
/// This adapter allows a bidirectional stream that cannot be cloned, such as a
26+
/// TLS session, to implement [`TIoChannel`]. Every read, write, and flush holds
27+
/// the same lock for the duration of that operation. It is intended for
28+
/// synchronous request-response traffic, where a caller writes and flushes a
29+
/// complete request before reading its response.
30+
///
31+
/// The shared lock makes access memory-safe across threads, but it does not
32+
/// provide full-duplex progress: a blocking read holds the lock and prevents a
33+
/// concurrent write until that read finishes.
34+
#[derive(Debug)]
35+
pub struct TSharedChannel<C> {
36+
inner: Arc<Mutex<C>>,
37+
}
38+
39+
impl<C> TSharedChannel<C> {
40+
/// Wrap `inner` in a shared channel.
41+
pub fn new(inner: C) -> Self {
42+
Self {
43+
inner: Arc::new(Mutex::new(inner)),
44+
}
45+
}
46+
47+
// io::Error::other requires Rust 1.74.
48+
#[allow(unknown_lints)]
49+
#[allow(clippy::io_other_error)]
50+
pub(crate) fn lock(&self) -> io::Result<MutexGuard<'_, C>> {
51+
self.inner
52+
.lock()
53+
.map_err(|_| io::Error::new(io::ErrorKind::Other, "shared channel lock is poisoned"))
54+
}
55+
}
56+
57+
impl<C> Clone for TSharedChannel<C> {
58+
fn clone(&self) -> Self {
59+
Self {
60+
inner: Arc::clone(&self.inner),
61+
}
62+
}
63+
}
64+
65+
impl<C: Read> Read for TSharedChannel<C> {
66+
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
67+
self.lock()?.read(buffer)
68+
}
69+
}
70+
71+
impl<C: Write> Write for TSharedChannel<C> {
72+
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
73+
self.lock()?.write(buffer)
74+
}
75+
76+
fn flush(&mut self) -> io::Result<()> {
77+
self.lock()?.flush()
78+
}
79+
}
80+
81+
impl<C: Read + Write> TIoChannel for TSharedChannel<C> {
82+
fn split(self) -> crate::Result<(ReadHalf<Self>, WriteHalf<Self>)>
83+
where
84+
Self: Sized,
85+
{
86+
Ok((ReadHalf::new(self.clone()), WriteHalf::new(self)))
87+
}
88+
}

0 commit comments

Comments
 (0)