Skip to content

Commit fe58bca

Browse files
mildsunriseBethGriggs
authored andcommitted
tls: group chunks into TLS segments
TLSWrap::DoWrite() now concatenates data chunks and makes a single call to SSL_write(). Grouping data into a single segment: - reduces network overhead: by factors of even 2 or 3 in usages like `http2` or `form-data` - improves security: segment lengths can reveal lots of info, i.e. with `form-data`, how many fields are sent and the approximate length of every individual field and its headers - reduces encryption overhead: a quick benchmark showed a ~30% CPU time decrease for an extreme case, see #27573 (comment) Fixes: #27573 Backport-PR-URL: #28904 PR-URL: #27861 Reviewed-By: Fedor Indutny <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Ujjwal Sharma <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent f78ecc3 commit fe58bca

File tree

2 files changed

+43
-45
lines changed

2 files changed

+43
-45
lines changed

src/tls_wrap.cc

+42-44
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ void TLSWrap::EncOut() {
291291
// No encrypted output ready to write to the underlying stream.
292292
if (BIO_pending(enc_out_) == 0) {
293293
Debug(this, "No pending encrypted output");
294-
if (pending_cleartext_input_.empty())
294+
if (pending_cleartext_input_.size() == 0)
295295
InvokeQueued(0);
296296
return;
297297
}
@@ -509,28 +509,21 @@ void TLSWrap::ClearIn() {
509509
return;
510510
}
511511

512-
std::vector<uv_buf_t> buffers;
513-
buffers.swap(pending_cleartext_input_);
512+
if (pending_cleartext_input_.size() == 0) {
513+
Debug(this, "Returning from ClearIn(), no pending data");
514+
return;
515+
}
514516

517+
std::vector<char> data = std::move(pending_cleartext_input_);
515518
crypto::MarkPopErrorOnReturn mark_pop_error_on_return;
516519

517-
size_t i;
518-
int written = 0;
519-
for (i = 0; i < buffers.size(); ++i) {
520-
size_t avail = buffers[i].len;
521-
char* data = buffers[i].base;
522-
written = SSL_write(ssl_.get(), data, avail);
523-
Debug(this, "Writing %zu bytes, written = %d", avail, written);
524-
CHECK(written == -1 || written == static_cast<int>(avail));
525-
if (written == -1)
526-
break;
527-
}
520+
int written = SSL_write(ssl_.get(), data.data(), data.size());
521+
Debug(this, "Writing %zu bytes, written = %d", data.size(), written);
522+
CHECK(written == -1 || written == static_cast<int>(data.size()));
528523

529524
// All written
530-
if (i == buffers.size()) {
525+
if (written != -1) {
531526
Debug(this, "Successfully wrote all data to SSL");
532-
// We wrote all the buffers, so no writes failed (written < 0 on failure).
533-
CHECK_GE(written, 0);
534527
return;
535528
}
536529

@@ -548,13 +541,10 @@ void TLSWrap::ClearIn() {
548541
// possible.
549542
InvokeQueued(UV_EPROTO, error_str.c_str());
550543
} else {
551-
Debug(this, "Pushing back %zu buffers", buffers.size() - i);
552-
// Push back the not-yet-written pending buffers into their queue.
553-
// This can be skipped in the error case because no further writes
554-
// would succeed anyway.
555-
pending_cleartext_input_.insert(pending_cleartext_input_.end(),
556-
buffers.begin() + i,
557-
buffers.end());
544+
Debug(this, "Pushing data back");
545+
// Push back the not-yet-written data. This can be skipped in the error
546+
// case because no further writes would succeed anyway.
547+
pending_cleartext_input_ = std::move(data);
558548
}
559549

560550
return;
@@ -640,14 +630,10 @@ int TLSWrap::DoWrite(WriteWrap* w,
640630
return UV_EPROTO;
641631
}
642632

643-
bool empty = true;
633+
size_t length = 0;
644634
size_t i;
645-
for (i = 0; i < count; i++) {
646-
if (bufs[i].len > 0) {
647-
empty = false;
648-
break;
649-
}
650-
}
635+
for (i = 0; i < count; i++)
636+
length += bufs[i].len;
651637

652638
// We want to trigger a Write() on the underlying stream to drive the stream
653639
// system, but don't want to encrypt empty buffers into a TLS frame, so see
@@ -659,7 +645,7 @@ int TLSWrap::DoWrite(WriteWrap* w,
659645
// stream. Since the bufs are empty, it won't actually write non-TLS data
660646
// onto the socket, we just want the side-effects. After, make sure the
661647
// WriteWrap was accepted by the stream, or that we call Done() on it.
662-
if (empty) {
648+
if (length == 0) {
663649
Debug(this, "Empty write");
664650
ClearOut();
665651
if (BIO_pending(enc_out_) == 0) {
@@ -683,23 +669,36 @@ int TLSWrap::DoWrite(WriteWrap* w,
683669
current_write_ = w;
684670

685671
// Write encrypted data to underlying stream and call Done().
686-
if (empty) {
672+
if (length == 0) {
687673
EncOut();
688674
return 0;
689675
}
690676

677+
std::vector<char> data;
691678
crypto::MarkPopErrorOnReturn mark_pop_error_on_return;
692679

693680
int written = 0;
694-
for (i = 0; i < count; i++) {
695-
written = SSL_write(ssl_.get(), bufs[i].base, bufs[i].len);
696-
CHECK(written == -1 || written == static_cast<int>(bufs[i].len));
697-
Debug(this, "Writing %zu bytes, written = %d", bufs[i].len, written);
698-
if (written == -1)
699-
break;
681+
if (count != 1) {
682+
data = std::vector<char>(length);
683+
size_t offset = 0;
684+
for (i = 0; i < count; i++) {
685+
memcpy(data.data() + offset, bufs[i].base, bufs[i].len);
686+
offset += bufs[i].len;
687+
}
688+
written = SSL_write(ssl_.get(), data.data(), length);
689+
} else {
690+
// Only one buffer: try to write directly, only store if it fails
691+
written = SSL_write(ssl_.get(), bufs[0].base, bufs[0].len);
692+
if (written == -1) {
693+
data = std::vector<char>(length);
694+
memcpy(data.data(), bufs[0].base, bufs[0].len);
695+
}
700696
}
701697

702-
if (i != count) {
698+
CHECK(written == -1 || written == static_cast<int>(length));
699+
Debug(this, "Writing %zu bytes, written = %d", length, written);
700+
701+
if (written == -1) {
703702
int err;
704703
Local<Value> arg = GetSSLError(written, &err, &error_);
705704

@@ -710,11 +709,10 @@ int TLSWrap::DoWrite(WriteWrap* w,
710709
return UV_EPROTO;
711710
}
712711

713-
Debug(this, "Saving %zu buffers for later write", count - i);
712+
Debug(this, "Saving data for later write");
714713
// Otherwise, save unwritten data so it can be written later by ClearIn().
715-
pending_cleartext_input_.insert(pending_cleartext_input_.end(),
716-
&bufs[i],
717-
&bufs[count]);
714+
CHECK_EQ(pending_cleartext_input_.size(), 0);
715+
pending_cleartext_input_ = std::move(data);
718716
}
719717

720718
// Write any encrypted/handshake output that may be ready.

src/tls_wrap.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ class TLSWrap : public AsyncWrap,
166166
BIO* enc_in_ = nullptr; // StreamListener fills this for SSL_read().
167167
BIO* enc_out_ = nullptr; // SSL_write()/handshake fills this for EncOut().
168168
// Waiting for ClearIn() to pass to SSL_write().
169-
std::vector<uv_buf_t> pending_cleartext_input_;
169+
std::vector<char> pending_cleartext_input_;
170170
size_t write_size_ = 0;
171171
WriteWrap* current_write_ = nullptr;
172172
WriteWrap* current_empty_write_ = nullptr;

0 commit comments

Comments
 (0)