Files
node/test/parallel/test-double-tls-client.js
Robert Nagy c7974d9982 tls: coalesce buffered records into one read
SSL_read() returns at most one TLS record, and ClearOut() emitted a
separate read for each one. Peers that use small records, such as
nginx with its default 4 KiB ssl_buffer_size or servers doing dynamic
record sizing, made every record pay for its own buffer allocation
and call into JavaScript.

While the next buffered record is application data, keep reading into
the same 16 KiB buffer before emitting it, so reads never get larger
than one full record already makes them. Batching stops before any
other record type, so that e.g. a TLS 1.2 renegotiation is still
processed after the data in front of it has been emitted. It also
stops before records short enough to be empty, because SSL_read()
skips empty records and carries on with the next one. If SSL_read()
fails in the middle of a batch, the error is captured first and
reported after the data.

test-tls-max-send-fragment relied on 'data' chunks matching records,
so it now checks the record sizes on the wire.

Signed-off-by: Robert Nagy <ronagy@icloud.com>
Assisted-by: claude:opus-5.5
PR-URL: https://github.com/nodejs/node/pull/66698
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
2026-10-21 12:58:11 +00:00

66 lines
2.4 KiB
JavaScript

'use strict';
const common = require('../common');
const assert = require('assert');
if (!common.hasCrypto) common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const tls = require('tls');
// In reality, this can be a HTTP CONNECT message, signaling the incoming
// data is TLS encrypted
const HEAD = 'XXXX';
const subserver = tls.createServer({
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem'),
})
.on('secureConnection', common.mustCall(() => {
process.exit(0);
}));
const server = tls.createServer({
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem'),
})
.listen(client)
.on('secureConnection', common.mustCall((serverTlsSock) => {
serverTlsSock.once('data', common.mustCall((chunk) => {
assert.strictEqual(chunk.toString('latin1', 0, HEAD.length), HEAD);
// The start of the inner handshake can arrive in the same chunk as
// HEAD. Put it back so that the inner TLSSocket, which reads whatever
// is buffered on the socket it wraps, receives it.
if (chunk.length > HEAD.length) {
serverTlsSock.pause();
serverTlsSock.unshift(chunk.subarray(HEAD.length));
}
subserver.emit('connection', serverTlsSock);
}));
}));
function client() {
const down = tls.connect({
host: '127.0.0.1',
port: server.address().port,
rejectUnauthorized: false
}).on('secureConnect', () => {
down.write(HEAD, common.mustSucceed());
// Sending tls data on a client TLSSocket with an active write led to a crash:
//
// node[16862]: ../src/crypto/crypto_tls.cc:963:virtual int node::crypto::TLSWrap::DoWrite(node::WriteWrap*,
// uv_buf_t*, size_t, uv_stream_t*): Assertion `!current_write_' failed.
// 1: 0xb090e0 node::Abort() [node]
// 2: 0xb0915e [node]
// 3: 0xca8413 node::crypto::TLSWrap::DoWrite(node::WriteWrap*, uv_buf_t*, unsigned long, uv_stream_s*) [node]
// 4: 0xcaa549 node::StreamBase::Write(uv_buf_t*, unsigned long, uv_stream_s*, v8::Local<v8::Object>) [node]
// 5: 0xca88d7 node::crypto::TLSWrap::EncOut() [node]
// 6: 0xd3df3e [node]
// 7: 0xd3f35f v8::internal::Builtin_HandleApiCall(int, unsigned long*, v8::internal::Isolate*) [node]
// 8: 0x15d9ef9 [node]
// Aborted
tls.connect({
socket: down,
rejectUnauthorized: false
});
});
}