Commit 135b70e6707 for nodejs
commit 135b70e6707fe2eef715bc56c7a338edc621d24e
Author: Tim Perry <pimterry@gmail.com>
Date: Mon Oct 19 14:35:59 2026 +0200
tls: add tlsSocket.socket, to access the raw stream beneath TLS
Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66598
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
diff --git a/doc/api/tls.md b/doc/api/tls.md
index 7e4f77f3ced..678513846ba 100644
--- a/doc/api/tls.md
+++ b/doc/api/tls.md
@@ -1670,6 +1670,27 @@ and their processing can be delayed due to packet loss or reordering. However,
smaller fragments add extra TLS framing bytes and CPU overhead, which may
decrease overall server throughput.
+### `tlsSocket.socket`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type: {net.Socket|stream.Duplex|null}
+
+The underlying socket or stream that this socket reads and writes TLS data
+through. This is the `socket` passed to [`new tls.TLSSocket()`][] or
+[`tls.connect()`][], or the connection given to a [`tls.Server`][] by its
+`'connection'` event.
+
+This is only `null` for client sockets that manage their own connection,
+created by [`tls.connect()`][] with a `port` or `path` rather than a `socket`.
+Connection details such as `remoteAddress` are then available directly on this
+socket. Server sockets always have an underlying socket.
+
+This remains available after the socket has been destroyed, for example to
+identify the underlying connection from a [`'tlsClientError'`][] listener.
+
## `tls.checkServerIdentity(hostname, cert)`
<!-- YAML
@@ -2596,6 +2617,7 @@ added: v0.11.3
[`'secureConnect'`]: #event-secureconnect
[`'secureConnection'`]: #event-secureconnection
[`'session'`]: #event-session
+[`'tlsClientError'`]: #event-tlsclienterror
[`--tls-cipher-list`]: cli.md#--tls-cipher-listlist
[`--use-bundled-ca`]: cli.md#--use-bundled-ca---use-openssl-ca
[`--use-openssl-ca`]: cli.md#--use-bundled-ca---use-openssl-ca
@@ -2611,6 +2633,7 @@ added: v0.11.3
[`net.Server`]: net.md#class-netserver
[`net.Socket`]: net.md#class-netsocket
[`net.createServer()`]: net.md#netcreateserveroptions-connectionlistener
+[`new tls.TLSSocket()`]: #new-tlstlssocketsocket-options
[`server.addContext()`]: #serveraddcontexthostname-context
[`server.getTicketKeys()`]: #servergetticketkeys
[`server.listen()`]: net.md#serverlisten
diff --git a/lib/internal/tls/wrap.js b/lib/internal/tls/wrap.js
index 7a47f10bb04..65611de32d8 100644
--- a/lib/internal/tls/wrap.js
+++ b/lib/internal/tls/wrap.js
@@ -700,6 +700,8 @@ function TLSSocket(socket, opts) {
// Proxy for API compatibility
this.ssl = this._handle; // C++ TLSWrap object
+ this.socket = socket ?? null;
+
this.on('error', this._tlsError);
this._init(socket, wrap);
diff --git a/test/parallel/test-tls-socket-underlying-socket.js b/test/parallel/test-tls-socket-underlying-socket.js
new file mode 100644
index 00000000000..de689443914
--- /dev/null
+++ b/test/parallel/test-tls-socket-underlying-socket.js
@@ -0,0 +1,57 @@
+'use strict';
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const assert = require('assert');
+const fixtures = require('../common/fixtures');
+const net = require('net');
+const tls = require('tls');
+const { duplexPair } = require('stream');
+
+const key = fixtures.readKey('agent1-key.pem');
+const cert = fixtures.readKey('agent1-cert.pem');
+
+// Server sockets over a plain connection expose that net.Socket.
+{
+ const server = tls.createServer({ key, cert });
+ let rawSocket;
+ server.prependListener('connection', common.mustCall((socket) => {
+ rawSocket = socket;
+ }));
+ server.on('secureConnection', common.mustCall((tlsSocket) => {
+ assert.ok(rawSocket instanceof net.Socket);
+ assert.strictEqual(tlsSocket.socket, rawSocket);
+ tlsSocket.end();
+ server.close();
+ }));
+ server.listen(0, common.mustCall(() => {
+ const client = tls.connect({
+ port: server.address().port,
+ rejectUnauthorized: false,
+ }, common.mustCall(() => {
+ assert.strictEqual(client.socket, null);
+ client.end();
+ }));
+ }));
+}
+
+// Server sockets over any other stream expose that stream, and still do after
+// a failed handshake has destroyed them.
+{
+ const server = tls.createServer({ key, cert, minVersion: 'TLSv1.3' });
+ const [serverSide, clientSide] = duplexPair();
+ server.on('secureConnection', common.mustNotCall());
+ server.on('tlsClientError', common.mustCall((err, tlsSocket) => {
+ assert.strictEqual(tlsSocket.socket, serverSide);
+ setImmediate(common.mustCall(() => {
+ assert.strictEqual(tlsSocket.destroyed, true);
+ assert.strictEqual(tlsSocket.socket, serverSide);
+ }));
+ }));
+ server.emit('connection', serverSide);
+
+ const client = tls.connect({ socket: clientSide, maxVersion: 'TLSv1.2' });
+ assert.strictEqual(client.socket, clientSide);
+ client.on('error', common.mustCall());
+}