Skip to content

Commit 30a72e8

Browse files
sam-githubtargos
authored andcommitted
tls: allow enabling the TLS debug trace
Enable the same trace output that the OpenSSL s_client and s_server support with their `-trace` option. This is invaluable when debugging reports of TLS bugs as well as when debugging the internal TLS implementation. See: - #25383 - #17936 - postmanlabs/postman-app-support#5918 (comment) PR-URL: #27376 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Richard Lau <[email protected]>
1 parent 230a773 commit 30a72e8

File tree

5 files changed

+116
-0
lines changed

5 files changed

+116
-0
lines changed

doc/api/tls.md

+18
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,19 @@ added: v8.4.0
725725
Disables TLS renegotiation for this `TLSSocket` instance. Once called, attempts
726726
to renegotiate will trigger an `'error'` event on the `TLSSocket`.
727727

728+
### tlsSocket.enableTrace()
729+
<!-- YAML
730+
added: REPLACEME
731+
-->
732+
733+
When enabled, TLS packet trace information is written to `stderr`. This can be
734+
used to debug TLS connection problems.
735+
736+
Note: The format of the output is identical to the output of `openssl s_client
737+
-trace` or `openssl s_server -trace`. While it is produced by OpenSSL's
738+
`SSL_trace()` function, the format is undocumented, can change without notice,
739+
and should not be relied on.
740+
728741
### tlsSocket.encrypted
729742
<!-- YAML
730743
added: v0.11.4
@@ -1438,6 +1451,10 @@ changes:
14381451
`['hello', 'world']`. (Protocols should be ordered by their priority.)
14391452
* `clientCertEngine` {string} Name of an OpenSSL engine which can provide the
14401453
client certificate.
1454+
* `enableTrace` {boolean} If `true`, [`tls.TLSSocket.enableTrace()`][] will be
1455+
called on new connections. Tracing can be enabled after the secure
1456+
connection is established, but this option must be used to trace the secure
1457+
connection setup. **Default:** `false`.
14411458
* `handshakeTimeout` {number} Abort the connection if the SSL/TLS handshake
14421459
does not finish in the specified number of milliseconds.
14431460
A `'tlsClientError'` is emitted on the `tls.Server` object whenever
@@ -1693,6 +1710,7 @@ where `secureSocket` has the same API as `pair.cleartext`.
16931710
[`tls.DEFAULT_MAX_VERSION`]: #tls_tls_default_max_version
16941711
[`tls.DEFAULT_MIN_VERSION`]: #tls_tls_default_min_version
16951712
[`tls.Server`]: #tls_class_tls_server
1713+
[`tls.TLSSocket.enableTrace()`]: #tls_tlssocket_enabletrace
16961714
[`tls.TLSSocket.getPeerCertificate()`]: #tls_tlssocket_getpeercertificate_detailed
16971715
[`tls.TLSSocket.getSession()`]: #tls_tlssocket_getsession
16981716
[`tls.TLSSocket.getTLSTicket()`]: #tls_tlssocket_gettlsticket

lib/_tls_wrap.js

+13
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const kErrorEmitted = Symbol('error-emitted');
6363
const kHandshakeTimeout = Symbol('handshake-timeout');
6464
const kRes = Symbol('res');
6565
const kSNICallback = Symbol('snicallback');
66+
const kEnableTrace = Symbol('enableTrace');
6667

6768
const noop = () => {};
6869

@@ -811,6 +812,7 @@ function makeSocketMethodProxy(name) {
811812
'getSession',
812813
'getTLSTicket',
813814
'isSessionReused',
815+
'enableTrace',
814816
].forEach((method) => {
815817
TLSSocket.prototype[method] = makeSocketMethodProxy(method);
816818
});
@@ -872,6 +874,8 @@ function tlsConnectionListener(rawSocket) {
872874
ALPNProtocols: this.ALPNProtocols,
873875
SNICallback: this[kSNICallback] || SNICallback
874876
});
877+
if (this[kEnableTrace] && socket._handle)
878+
socket._handle.enableTrace();
875879

876880
socket.on('secure', onServerSocketSecure);
877881

@@ -992,6 +996,15 @@ function Server(options, listener) {
992996
if (listener) {
993997
this.on('secureConnection', listener);
994998
}
999+
1000+
const enableTrace = options.enableTrace;
1001+
if (enableTrace === true)
1002+
this[kEnableTrace] = true;
1003+
else if (enableTrace === false || enableTrace == null)
1004+
; // Tracing explicitly disabled, or defaulting to disabled.
1005+
else
1006+
throw new ERR_INVALID_ARG_TYPE(
1007+
'options.enableTrace', 'boolean', enableTrace);
9951008
}
9961009

9971010
Object.setPrototypeOf(Server.prototype, net.Server.prototype);

src/tls_wrap.cc

+26
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,29 @@ void TLSWrap::EnableSessionCallbacks(
912912
wrap);
913913
}
914914

915+
// Check required capabilities were not excluded from the OpenSSL build:
916+
// - OPENSSL_NO_SSL_TRACE excludes SSL_trace()
917+
// - OPENSSL_NO_STDIO excludes BIO_new_fp()
918+
// HAVE_SSL_TRACE is available on the internal tcp_wrap binding for the tests.
919+
#if defined(OPENSSL_NO_SSL_TRACE) || defined(OPENSSL_NO_STDIO)
920+
# define HAVE_SSL_TRACE 0
921+
#else
922+
# define HAVE_SSL_TRACE 1
923+
#endif
924+
925+
void TLSWrap::EnableTrace(
926+
const FunctionCallbackInfo<Value>& args) {
927+
TLSWrap* wrap;
928+
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
929+
930+
#if HAVE_SSL_TRACE
931+
if (wrap->ssl_) {
932+
BIO* b = BIO_new_fp(stderr, BIO_NOCLOSE | BIO_FP_TEXT);
933+
SSL_set_msg_callback(wrap->ssl_.get(), SSL_trace);
934+
SSL_set_msg_callback_arg(wrap->ssl_.get(), b);
935+
}
936+
#endif
937+
}
915938

916939
void TLSWrap::DestroySSL(const FunctionCallbackInfo<Value>& args) {
917940
TLSWrap* wrap;
@@ -1057,6 +1080,8 @@ void TLSWrap::Initialize(Local<Object> target,
10571080

10581081
env->SetMethod(target, "wrap", TLSWrap::Wrap);
10591082

1083+
NODE_DEFINE_CONSTANT(target, HAVE_SSL_TRACE);
1084+
10601085
Local<FunctionTemplate> t = BaseObject::MakeLazilyInitializedJSTemplate(env);
10611086
Local<String> tlsWrapString =
10621087
FIXED_ONE_BYTE_STRING(env->isolate(), "TLSWrap");
@@ -1080,6 +1105,7 @@ void TLSWrap::Initialize(Local<Object> target,
10801105
env->SetProtoMethod(t, "start", Start);
10811106
env->SetProtoMethod(t, "setVerifyMode", SetVerifyMode);
10821107
env->SetProtoMethod(t, "enableSessionCallbacks", EnableSessionCallbacks);
1108+
env->SetProtoMethod(t, "enableTrace", EnableTrace);
10831109
env->SetProtoMethod(t, "destroySSL", DestroySSL);
10841110
env->SetProtoMethod(t, "enableCertCb", EnableCertCb);
10851111

src/tls_wrap.h

+1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ class TLSWrap : public AsyncWrap,
160160
static void SetVerifyMode(const v8::FunctionCallbackInfo<v8::Value>& args);
161161
static void EnableSessionCallbacks(
162162
const v8::FunctionCallbackInfo<v8::Value>& args);
163+
static void EnableTrace(const v8::FunctionCallbackInfo<v8::Value>& args);
163164
static void EnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args);
164165
static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
165166
static void GetServername(const v8::FunctionCallbackInfo<v8::Value>& args);
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
const common = require('../common');
4+
if (!common.hasCrypto) common.skip('missing crypto');
5+
const fixtures = require('../common/fixtures');
6+
7+
// Test enableTrace: option for TLS.
8+
9+
const assert = require('assert');
10+
const { fork } = require('child_process');
11+
12+
if (process.argv[2] === 'test')
13+
return test();
14+
15+
const binding = require('internal/test/binding').internalBinding;
16+
17+
if (!binding('tls_wrap').HAVE_SSL_TRACE)
18+
return common.skip('no SSL_trace() compiled into openssl');
19+
20+
const child = fork(__filename, ['test'], { silent: true });
21+
22+
let stderr = '';
23+
child.stderr.setEncoding('utf8');
24+
child.stderr.on('data', (data) => stderr += data);
25+
child.on('close', common.mustCall(() => {
26+
assert(/Received Record/.test(stderr));
27+
assert(/ClientHello/.test(stderr));
28+
}));
29+
30+
// For debugging and observation of actual trace output.
31+
child.stderr.pipe(process.stderr);
32+
child.stdout.pipe(process.stdout);
33+
34+
child.on('exit', common.mustCall((code) => {
35+
assert.strictEqual(code, 0);
36+
}));
37+
38+
function test() {
39+
const {
40+
connect, keys
41+
} = require(fixtures.path('tls-connect'));
42+
43+
connect({
44+
client: {
45+
checkServerIdentity: (servername, cert) => { },
46+
ca: `${keys.agent1.cert}\n${keys.agent6.ca}`,
47+
},
48+
server: {
49+
cert: keys.agent6.cert,
50+
key: keys.agent6.key,
51+
enableTrace: true,
52+
},
53+
}, common.mustCall((err, pair, cleanup) => {
54+
pair.client.conn.enableTrace();
55+
56+
return cleanup();
57+
}));
58+
}

0 commit comments

Comments
 (0)