Skip to content

Commit ce000d6

Browse files
excelle08facebook-github-bot
authored andcommitted
Add working driver↔leaf TLS (hardware AES-GCM) to match prod encryption
Reviewed By: YifanYuan3 Differential Revision: D112488664
1 parent ba695b9 commit ce000d6

3 files changed

Lines changed: 233 additions & 23 deletions

File tree

packages/feedsim/run.sh

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,30 @@ main() {
823823
if [ "$mock_tls" = "1" ]; then
824824
export MOCK_TLS=1
825825
fi
826+
# Driver↔Leaf TLS. When FEEDSIM_DRIVER_TLS=1 is passed in the environment,
827+
# the DriverNodeRank client (reads FEEDSIM_DRIVER_TLS) wraps its
828+
# bufferevents in OpenSSL, and the LeafNodeRank server needs a cert so
829+
# FeedSimServer's accept path enables AsyncSSLSocket — wire the shared
830+
# example cert/key into FEEDSIM_TLS_CERT/FEEDSIM_TLS_KEY. Both ends pin
831+
# AES-GCM so the encrypted driver↔leaf volume registers as hardware crypto
832+
# (matches prod's Rocket-over-TLS driver path). Default off. The LeafNodeRank
833+
# server reads FEEDSIM_TLS_CERT/KEY from the exported env; the DriverNodeRank
834+
# client is launched through search_qps.sh, which does not reliably forward
835+
# the parent's exported env to the driver process, so FEEDSIM_DRIVER_TLS is
836+
# also injected directly on the driver command line via `driver_bin`.
837+
driver_bin="build/workloads/ranking/DriverNodeRank"
838+
if [ "${FEEDSIM_DRIVER_TLS:-0}" = "1" ]; then
839+
driver_cert_dir="${FEEDSIM_ROOT}/certs"
840+
if [ ! -r "${driver_cert_dir}/example.crt" ] || [ ! -r "${driver_cert_dir}/example.key" ]; then
841+
echo "ERROR: FEEDSIM_DRIVER_TLS=1 but ${driver_cert_dir}/example.{crt,key} not found" >&2
842+
exit 1
843+
fi
844+
export FEEDSIM_TLS_CERT="${driver_cert_dir}/example.crt"
845+
export FEEDSIM_TLS_KEY="${driver_cert_dir}/example.key"
846+
export FEEDSIM_DRIVER_TLS=1
847+
driver_bin="env FEEDSIM_DRIVER_TLS=1 build/workloads/ranking/DriverNodeRank"
848+
echo "Driver↔Leaf TLS: ENABLED (cert=${driver_cert_dir}/example.crt, AES-GCM)"
849+
fi
826850
# MOCK_ZSTD_FRAC env consumed by MockServicesClient::resolveZstdFraction.
827851
# Always export so the t43 c7 default 0.75 reaches the binary.
828852
export MOCK_ZSTD_FRAC="$mock_zstd_frac"
@@ -960,7 +984,7 @@ main() {
960984
echo "search_qps SLA: ${sla_arg}"
961985
# shellcheck disable=SC2086
962986
scripts/search_qps.sh -w 15 -f 300 -s "$sla_arg" -P "$LEAF_PID" -B "$BREAKDOWN_FOLDER" $qps_threshold_args $no_retry_args -o "${FEEDSIM_ROOT}/${result_filename}" -- \
963-
build/workloads/ranking/DriverNodeRank \
987+
$driver_bin \
964988
--server "0.0.0.0:$port" \
965989
--monitor_port "$client_monitor_port" \
966990
--threads="${driver_threads}" \
@@ -974,7 +998,7 @@ main() {
974998
echo "search_qps SLA: ${sla_arg}"
975999
# shellcheck disable=SC2086
9761000
scripts/search_qps.sh -a -w 15 -f 300 -s "$sla_arg" -P "$LEAF_PID" -B "$BREAKDOWN_FOLDER" $qps_threshold_args $no_retry_args -o "${FEEDSIM_ROOT}/${result_filename}" -- \
977-
build/workloads/ranking/DriverNodeRank \
1001+
$driver_bin \
9781002
--monitor_port "$client_monitor_port" \
9791003
--server "0.0.0.0:$port" \
9801004
$client_feature_opts \
@@ -1000,7 +1024,7 @@ main() {
10001024
-P "$LEAF_PID" -B "$BREAKDOWN_FOLDER" \
10011025
$qps_threshold_args $no_retry_args \
10021026
-o "${FEEDSIM_ROOT}/${result_filename}" \
1003-
-- build/workloads/ranking/DriverNodeRank \
1027+
-- $driver_bin \
10041028
--server "0.0.0.0:$port" \
10051029
--monitor_port "$client_monitor_port" \
10061030
--threads="${num_workers}" \

packages/feedsim/third_party/src/workloads/ranking/FeedSimDriver.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,18 @@ SSL_CTX* getDriverSslCtxOrNull() {
246246
return nullptr;
247247
}
248248
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr);
249+
// Restrict the offered ciphers to AES-GCM so the connection negotiates
250+
// hardware AES (AES-NI on x86, ARMv8 crypto extensions on aarch64) via
251+
// libcrypto, matching prod's cipher. Without this, OpenSSL may pick
252+
// ChaCha20-Poly1305 (a NEON/integer cipher with no AES instructions),
253+
// which reads as ~0% in the crypto instruction mix and runs the AEAD
254+
// un-accelerated. Mirrors MockServicesClient's cipher pinning.
255+
SSL_CTX_set_cipher_list(
256+
ctx,
257+
"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:"
258+
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"); // TLS 1.2
259+
SSL_CTX_set_ciphersuites(
260+
ctx, "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"); // TLS 1.3
249261
std::cout << "FeedSimDriver: TLS enabled via FEEDSIM_DRIVER_TLS=1"
250262
<< std::endl;
251263
return ctx;

packages/feedsim/third_party/src/workloads/ranking/FeedSimServer.cc

Lines changed: 194 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
#include <folly/MPMCQueue.h>
2929
#include <folly/container/F14Map.h>
30+
#include <folly/io/IOBuf.h>
3031
#include <folly/io/async/AsyncSSLSocket.h>
3132
#include <folly/io/async/AsyncServerSocket.h>
3233
#include <folly/io/async/AsyncSocket.h>
@@ -35,17 +36,30 @@
3536
#include <folly/io/async/EventBaseManager.h>
3637
#include <folly/io/async/SSLContext.h>
3738

39+
#include <openssl/ssl.h>
40+
3841
namespace feedsim {
3942

43+
class ServerConnection;
44+
4045
// ─── RequestContext implementation ──────────────────────────────────────────
4146

4247
struct RequestContext::Impl {
43-
// The socket fd to write the response back on.
44-
// We use raw fd + write() because the response is a single small write
45-
// and we want to avoid the complexity of AsyncSocket write callbacks.
48+
// Plaintext path: the socket fd to write the response back on. We use raw
49+
// fd + writev() because the response is a single small write and we want to
50+
// avoid the complexity of AsyncSocket write callbacks.
4651
int fd;
4752
uint64_t received_time;
4853
bool response_sent;
54+
// TLS path: raw fd writes bypass the TLS layer (they hit the TCP socket
55+
// underneath AsyncSSLSocket, so the peer receives plaintext on an encrypted
56+
// connection). When tls is true, the response must be written THROUGH the
57+
// AsyncSSLSocket on its owning EventBase instead. sendResponse marshals the
58+
// write onto evb and targets the connection via a weak_ptr so a response
59+
// that completes after the connection closed is dropped safely.
60+
bool tls = false;
61+
folly::EventBase* evb = nullptr;
62+
std::weak_ptr<ServerConnection> conn;
4963
};
5064

5165
RequestContext::RequestContext(
@@ -69,6 +83,17 @@ RequestContext::RequestContext(RequestContext&& other) noexcept
6983

7084
RequestContext::~RequestContext() = default;
7185

86+
// Marshals a TLS response (header+payload already framed in buf) onto the
87+
// connection's EventBase and writes it through the AsyncSSLSocket. Defined
88+
// after ServerConnection (needs its full type); declared here so sendResponse
89+
// can call it. Safe if the connection has already closed.
90+
namespace {
91+
void enqueueTlsResponse(
92+
std::weak_ptr<ServerConnection> conn,
93+
folly::EventBase* evb,
94+
std::unique_ptr<folly::IOBuf> buf);
95+
} // namespace
96+
7297
void RequestContext::sendResponse(const void* data, uint32_t data_length) {
7398
if (!impl_ || impl_->response_sent) return;
7499
impl_->response_sent = true;
@@ -85,7 +110,21 @@ void RequestContext::sendResponse(const void* data, uint32_t data_length) {
85110

86111
ResponsePacketHeader net = responseToNetwork(hdr);
87112

88-
// Use writev to send header + payload atomically
113+
if (impl_->tls) {
114+
// Raw fd writes bypass TLS, so build one contiguous frame (copying the
115+
// payload synchronously — the caller may free `data` after we return) and
116+
// hand it to the connection's EventBase to write through AsyncSSLSocket.
117+
auto buf = folly::IOBuf::create(sizeof(net) + data_length);
118+
memcpy(buf->writableData(), &net, sizeof(net));
119+
if (data_length > 0) {
120+
memcpy(buf->writableData() + sizeof(net), data, data_length);
121+
}
122+
buf->append(sizeof(net) + data_length);
123+
enqueueTlsResponse(impl_->conn, impl_->evb, std::move(buf));
124+
return;
125+
}
126+
127+
// Plaintext path (unchanged): use writev to send header + payload atomically
89128
struct iovec iov[2];
90129
iov[0].iov_base = &net;
91130
iov[0].iov_len = sizeof(net);
@@ -122,15 +161,19 @@ void RequestContext::sendResponse(const void* data, uint32_t data_length) {
122161

123162
// ─── ServerConnection: handles framing for one client connection ────────────
124163

125-
class ServerConnection : public folly::AsyncTransport::ReadCallback {
164+
class ServerConnection
165+
: public folly::AsyncTransport::ReadCallback,
166+
public std::enable_shared_from_this<ServerConnection> {
126167
public:
127168
ServerConnection(
128169
folly::AsyncSocket::UniquePtr socket,
129170
int thread_id,
130-
const folly::F14FastMap<uint32_t, QueryCallback>& callbacks)
171+
const folly::F14FastMap<uint32_t, QueryCallback>& callbacks,
172+
bool tls)
131173
: socket_(std::move(socket)),
132174
thread_id_(thread_id),
133175
callbacks_(callbacks),
176+
tls_(tls),
134177
read_buf_(nullptr),
135178
read_buf_size_(0),
136179
data_offset_(0) {
@@ -144,6 +187,17 @@ class ServerConnection : public folly::AsyncTransport::ReadCallback {
144187
delete[] read_buf_;
145188
}
146189

190+
// Called once right after construction so the connection keeps itself alive
191+
// while registered as the socket's read callback (broken on EOF/error).
192+
void attachSelf(std::shared_ptr<ServerConnection> self) {
193+
self_ = std::move(self);
194+
}
195+
196+
// Writes a fully-framed TLS response through the AsyncSSLSocket. MUST be
197+
// invoked on the socket's EventBase thread. Defined out-of-line below
198+
// (needs TlsWriteCallback's full definition).
199+
void writeResponse(std::unique_ptr<folly::IOBuf> buf);
200+
147201
// AsyncTransport::ReadCallback
148202
void getReadBuffer(void** bufReturn, size_t* lenReturn) override {
149203
// Grow buffer if needed
@@ -165,15 +219,14 @@ class ServerConnection : public folly::AsyncTransport::ReadCallback {
165219
}
166220

167221
void readEOF() noexcept override {
168-
// Client disconnected
169-
socket_->close();
170-
// Self-delete via destroy callback (see below)
171-
delete this;
222+
// Client disconnected. Break the self-reference so the object is destroyed
223+
// once any in-flight TLS write callbacks release their refs. Hold a local
224+
// ref so `this` stays valid until we return from the callback.
225+
close();
172226
}
173227

174228
void readErr(const folly::AsyncSocketException& ex) noexcept override {
175-
socket_->close();
176-
delete this;
229+
close();
177230
}
178231

179232
private:
@@ -199,6 +252,13 @@ class ServerConnection : public folly::AsyncTransport::ReadCallback {
199252
impl->fd = socket_->getNetworkSocket().toFd();
200253
impl->received_time = getTimeNano();
201254
impl->response_sent = false;
255+
impl->tls = tls_;
256+
if (tls_) {
257+
// The response may be produced asynchronously on a pool thread; it
258+
// must be written back through the AsyncSSLSocket on this EventBase.
259+
impl->evb = socket_->getEventBase();
260+
impl->conn = weak_from_this();
261+
}
202262

203263
RequestContext ctx(
204264
hdr.type, hdr.request_id, hdr.start_time,
@@ -216,14 +276,112 @@ class ServerConnection : public folly::AsyncTransport::ReadCallback {
216276
}
217277
}
218278

279+
void close() {
280+
socket_->close();
281+
// Hold a local ref so `this` survives until we return, then drop the
282+
// self-reference. If TLS write callbacks are still outstanding they hold
283+
// their own refs and the object lives until they complete.
284+
auto keepalive = shared_from_this();
285+
self_.reset();
286+
}
287+
219288
folly::AsyncSocket::UniquePtr socket_;
220289
int thread_id_;
221290
const folly::F14FastMap<uint32_t, QueryCallback>& callbacks_;
291+
bool tls_;
292+
std::shared_ptr<ServerConnection> self_;
222293
uint8_t* read_buf_;
223294
size_t read_buf_size_;
224295
size_t data_offset_;
225296
};
226297

298+
// ─── TLS response write path ────────────────────────────────────────────────
299+
300+
namespace {
301+
// Keeps the connection alive (via shared_ptr) until the AsyncSSLSocket finishes
302+
// encrypting and writing the response, then deletes itself.
303+
class TlsWriteCallback : public folly::AsyncWriter::WriteCallback {
304+
public:
305+
explicit TlsWriteCallback(std::shared_ptr<ServerConnection> conn)
306+
: conn_(std::move(conn)) {}
307+
void writeSuccess() noexcept override { delete this; }
308+
void writeErr(
309+
size_t /*bytesWritten*/,
310+
const folly::AsyncSocketException& /*ex*/) noexcept override {
311+
delete this;
312+
}
313+
314+
private:
315+
std::shared_ptr<ServerConnection> conn_;
316+
};
317+
318+
void enqueueTlsResponse(
319+
std::weak_ptr<ServerConnection> weak,
320+
folly::EventBase* evb,
321+
std::unique_ptr<folly::IOBuf> buf) {
322+
if (evb == nullptr) {
323+
return;
324+
}
325+
evb->runInEventBaseThread(
326+
[weak = std::move(weak), buf = std::move(buf)]() mutable {
327+
auto conn = weak.lock();
328+
if (!conn) {
329+
return; // connection closed before the response was ready
330+
}
331+
conn->writeResponse(std::move(buf));
332+
});
333+
}
334+
} // namespace
335+
336+
void ServerConnection::writeResponse(std::unique_ptr<folly::IOBuf> buf) {
337+
// Runs on the socket's EventBase thread. The callback holds a ref that keeps
338+
// this connection alive until the encrypted write completes.
339+
auto* cb = new TlsWriteCallback(shared_from_this());
340+
socket_->writeChain(cb, std::move(buf));
341+
}
342+
343+
// ─── SslAcceptor: drives the server-side TLS handshake before wire reads ─────
344+
345+
// A folly server-side AsyncSSLSocket does NOT auto-handshake when you merely
346+
// setReadCB — sslAccept() must be called to run the handshake. Until it
347+
// completes the socket can neither decrypt requests nor send responses. This
348+
// helper owns the socket during the handshake and, on success, hands it to a
349+
// ServerConnection (which then starts reading). It self-deletes either way.
350+
class SslAcceptor : public folly::AsyncSSLSocket::HandshakeCB {
351+
public:
352+
SslAcceptor(
353+
folly::AsyncSSLSocket::UniquePtr socket,
354+
int thread_id,
355+
const folly::F14FastMap<uint32_t, QueryCallback>& callbacks)
356+
: socket_(std::move(socket)),
357+
thread_id_(thread_id),
358+
callbacks_(callbacks) {}
359+
360+
void start() {
361+
auto* raw = socket_.get();
362+
raw->sslAccept(this);
363+
}
364+
365+
void handshakeSuc(folly::AsyncSSLSocket* /*sock*/) noexcept override {
366+
folly::AsyncSocket::UniquePtr base(socket_.release());
367+
auto conn = std::make_shared<ServerConnection>(
368+
std::move(base), thread_id_, callbacks_, /*tls=*/true);
369+
conn->attachSelf(conn);
370+
delete this;
371+
}
372+
373+
void handshakeErr(
374+
folly::AsyncSSLSocket* /*sock*/,
375+
const folly::AsyncSocketException& /*ex*/) noexcept override {
376+
delete this; // socket_ (and the fd) torn down with it
377+
}
378+
379+
private:
380+
folly::AsyncSSLSocket::UniquePtr socket_;
381+
int thread_id_;
382+
const folly::F14FastMap<uint32_t, QueryCallback>& callbacks_;
383+
};
384+
227385
// ─── WorkerThread ───────────────────────────────────────────────────────────
228386

229387
struct WorkerThread {
@@ -275,20 +433,25 @@ class AcceptCallback : public folly::AsyncServerSocket::AcceptCallback {
275433
worker->evb->runInEventBaseThread(
276434
[fd, thread_id = worker->thread_id, &callbacks = callbacks_,
277435
evb = worker->evb.get(), ssl_ctx]() {
278-
folly::AsyncSocket::UniquePtr socket;
279436
if (ssl_ctx) {
280-
// AsyncSSLSocket server-side: pass true for the server flag.
281-
// The handshake is initiated lazily on first read/write,
282-
// matching the existing client's connect-then-write pattern.
437+
// AsyncSSLSocket server-side: pass true for the server flag, then
438+
// drive the handshake via sslAccept (SslAcceptor). The
439+
// ServerConnection is created only after the handshake succeeds —
440+
// reading/writing before that would see undecrypted bytes.
283441
folly::AsyncSSLSocket::UniquePtr ssl_sock(new folly::AsyncSSLSocket(
284442
ssl_ctx, evb, folly::NetworkSocket::fromFd(fd), true));
285-
socket.reset(ssl_sock.release());
443+
auto* acceptor =
444+
new SslAcceptor(std::move(ssl_sock), thread_id, callbacks);
445+
acceptor->start();
286446
} else {
287-
socket = folly::AsyncSocket::newSocket(
447+
folly::AsyncSocket::UniquePtr socket = folly::AsyncSocket::newSocket(
288448
evb, folly::NetworkSocket::fromFd(fd));
449+
// ServerConnection keeps itself alive via a self-reference (set by
450+
// attachSelf) until EOF/error.
451+
auto conn = std::make_shared<ServerConnection>(
452+
std::move(socket), thread_id, callbacks, /*tls=*/false);
453+
conn->attachSelf(conn);
289454
}
290-
// ServerConnection self-manages its lifetime
291-
new ServerConnection(std::move(socket), thread_id, callbacks);
292455
});
293456
}
294457

@@ -433,6 +596,17 @@ void FeedSimServer::run() {
433596
auto ctx = std::make_shared<folly::SSLContext>();
434597
ctx->loadCertificate(cert_env);
435598
ctx->loadPrivateKey(key_env);
599+
// Restrict to AES-GCM so the negotiated cipher uses hardware AES
600+
// (AES-NI / ARMv8 crypto extensions) via libcrypto, matching prod
601+
// and the driver's pinned ciphers. Without this the server may accept
602+
// ChaCha20-Poly1305, which has no AES instructions and reads as ~0%
603+
// crypto in the instruction mix.
604+
ctx->setCiphersOrThrow(
605+
"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:"
606+
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"); // TLS 1.2
607+
SSL_CTX_set_ciphersuites(
608+
ctx->getSSLCtx(),
609+
"TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"); // TLS 1.3
436610
// No ALPN — FeedSim uses its own custom binary protocol over the
437611
// TLS-wrapped socket, not Rocket. The client similarly does not
438612
// advertise ALPN.

0 commit comments

Comments
 (0)