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>
3536#include < folly/io/async/EventBaseManager.h>
3637#include < folly/io/async/SSLContext.h>
3738
39+ #include < openssl/ssl.h>
40+
3841namespace feedsim {
3942
43+ class ServerConnection ;
44+
4045// ─── RequestContext implementation ──────────────────────────────────────────
4146
4247struct 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
5165RequestContext::RequestContext (
@@ -69,6 +83,17 @@ RequestContext::RequestContext(RequestContext&& other) noexcept
6983
7084RequestContext::~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+
7297void 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
229387struct 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