Skip to content

Commit eccdfd0

Browse files
committed
tls: add support for ALPN fallback when no ALPN protocol matches
1 parent ef17711 commit eccdfd0

File tree

7 files changed

+109
-4
lines changed

7 files changed

+109
-4
lines changed

doc/api/errors.md

+11
Original file line numberDiff line numberDiff line change
@@ -2698,6 +2698,17 @@ This error represents a failed test. Additional information about the failure
26982698
is available via the `cause` property. The `failureType` property specifies
26992699
what the test was doing when the failure occurred.
27002700

2701+
<a id="ERR_TLS_ALPN_FALLBACK_WITHOUT_PROTOCOLS"></a>
2702+
2703+
### `ERR_TLS_ALPN_FALLBACK_WITHOUT_PROTOCOLS`
2704+
2705+
This error is thrown when creating a `TLSServer` if the TLS options sets
2706+
`allowALPNFallback` to `true` without providing an `ALPNProtocols` argument.
2707+
2708+
When `ALPNProtocols` is not provided, ALPN is skipped entirely, so the fallback
2709+
would not be functional. To enable ALPN for all protocols, using the fallback
2710+
in all cases, set `ALPNProtocols` to an empty array instead.
2711+
27012712
<a id="ERR_TLS_CERT_ALTNAME_FORMAT"></a>
27022713

27032714
### `ERR_TLS_CERT_ALTNAME_FORMAT`

doc/api/tls.md

+8
Original file line numberDiff line numberDiff line change
@@ -2012,6 +2012,9 @@ where `secureSocket` has the same API as `pair.cleartext`.
20122012
<!-- YAML
20132013
added: v0.3.2
20142014
changes:
2015+
- version: REPLACEME
2016+
pr-url: https://github.com/nodejs/node/pull/45075
2017+
description: The `options` parameter can now include `allowALPNFallback`.
20152018
- version: v19.0.0
20162019
pr-url: https://github.com/nodejs/node/pull/44031
20172020
description: If `ALPNProtocols` is set, incoming connections that send an
@@ -2042,6 +2045,11 @@ changes:
20422045
e.g. `0x05hello0x05world`, where the first byte is the length of the next
20432046
protocol name. Passing an array is usually much simpler, e.g.
20442047
`['hello', 'world']`. (Protocols should be ordered by their priority.)
2048+
* `allowALPNFallback`: {boolean} If `true`, if the `ALPNProtocols` option was
2049+
set and the client uses ALPN, but requests only protocols that are not do
2050+
not match the server's `ALPNProtocols`, the server will fall back to sending
2051+
an ALPN response accepting the first protocol the client suggested, instead
2052+
of closing the connection immediately with a fatal alert.
20452053
* `clientCertEngine` {string} Name of an OpenSSL engine which can provide the
20462054
client certificate.
20472055
* `enableTrace` {boolean} If `true`, [`tls.TLSSocket.enableTrace()`][] will be

lib/_tls_wrap.js

+7
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const {
7171
ERR_INVALID_ARG_VALUE,
7272
ERR_MULTIPLE_CALLBACK,
7373
ERR_SOCKET_CLOSED,
74+
ERR_TLS_ALPN_FALLBACK_WITHOUT_PROTOCOLS,
7475
ERR_TLS_DH_PARAM_SIZE,
7576
ERR_TLS_HANDSHAKE_TIMEOUT,
7677
ERR_TLS_INVALID_CONTEXT,
@@ -716,6 +717,7 @@ TLSSocket.prototype._init = function(socket, wrap) {
716717
ssl.onnewsession = onnewsession;
717718
ssl.lastHandshakeTime = 0;
718719
ssl.handshakes = 0;
720+
ssl.allowALPNFallback = !!options.allowALPNFallback;
719721

720722
if (this.server) {
721723
if (this.server.listenerCount('resumeSession') > 0 ||
@@ -1100,6 +1102,7 @@ function tlsConnectionListener(rawSocket) {
11001102
rejectUnauthorized: this.rejectUnauthorized,
11011103
handshakeTimeout: this[kHandshakeTimeout],
11021104
ALPNProtocols: this.ALPNProtocols,
1105+
allowALPNFallback: this.allowALPNFallback,
11031106
SNICallback: this[kSNICallback] || SNICallback,
11041107
enableTrace: this[kEnableTrace],
11051108
pauseOnConnect: this.pauseOnConnect,
@@ -1198,6 +1201,10 @@ function Server(options, listener) {
11981201
this._contexts = [];
11991202
this.requestCert = options.requestCert === true;
12001203
this.rejectUnauthorized = options.rejectUnauthorized !== false;
1204+
this.allowALPNFallback = options.allowALPNFallback === true;
1205+
if (this.allowALPNFallback && !options.ALPNProtocols) {
1206+
throw new ERR_TLS_ALPN_FALLBACK_WITHOUT_PROTOCOLS();
1207+
}
12011208

12021209
if (options.sessionTimeout)
12031210
this.sessionTimeout = options.sessionTimeout;

lib/internal/errors.js

+3
Original file line numberDiff line numberDiff line change
@@ -1612,6 +1612,9 @@ E('ERR_TEST_FAILURE', function(error, failureType) {
16121612
this.cause = error;
16131613
return msg;
16141614
}, Error);
1615+
E('ERR_TLS_ALPN_FALLBACK_WITHOUT_PROTOCOLS',
1616+
'The allowALPNFallback TLS option cannot be set without ALPNProtocols',
1617+
TypeError);
16151618
E('ERR_TLS_CERT_ALTNAME_FORMAT', 'Invalid subject alternative name string',
16161619
SyntaxError);
16171620
E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) {

src/crypto/crypto_tls.cc

+19-4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ using v8::Array;
3939
using v8::ArrayBuffer;
4040
using v8::ArrayBufferView;
4141
using v8::BackingStore;
42+
using v8::Boolean;
4243
using v8::Context;
4344
using v8::DontDelete;
4445
using v8::Exception;
@@ -237,6 +238,13 @@ int SelectALPNCallback(
237238
if (UNLIKELY(alpn_buffer.IsEmpty()) || !alpn_buffer->IsArrayBufferView())
238239
return SSL_TLSEXT_ERR_NOACK;
239240

241+
bool alpn_fallback_allowed =
242+
w->object()
243+
->Get(env->context(), env->allow_alpn_fallback_string())
244+
.ToLocalChecked()
245+
.As<Boolean>()
246+
->Value();
247+
240248
ArrayBufferViewContents<unsigned char> alpn_protos(alpn_buffer);
241249
int status = SSL_select_next_proto(
242250
const_cast<unsigned char**>(out),
@@ -249,10 +257,17 @@ int SelectALPNCallback(
249257
// Previous versions of Node.js returned SSL_TLSEXT_ERR_NOACK if no protocol
250258
// match was found. This would neither cause a fatal alert nor would it result
251259
// in a useful ALPN response as part of the Server Hello message.
252-
// We now return SSL_TLSEXT_ERR_ALERT_FATAL in that case as per Section 3.2
253-
// of RFC 7301, which causes a fatal no_application_protocol alert.
254-
return status == OPENSSL_NPN_NEGOTIATED ? SSL_TLSEXT_ERR_OK
255-
: SSL_TLSEXT_ERR_ALERT_FATAL;
260+
261+
// By default, we now return SSL_TLSEXT_ERR_ALERT_FATAL in that case as per
262+
// Section 3.2 of RFC 7301, which causes a fatal no_application_protocol
263+
// alert, unless the allowALPNFallback option has been set, which enables
264+
// OpenSSL's default protocol selection behavior, falling back to the client's
265+
// first preference.
266+
if (status == OPENSSL_NPN_NEGOTIATED || alpn_fallback_allowed) {
267+
return SSL_TLSEXT_ERR_OK;
268+
} else {
269+
return SSL_TLSEXT_ERR_ALERT_FATAL;
270+
}
256271
}
257272

258273
int TLSExtStatusCallback(SSL* s, void* arg) {

src/env_properties.h

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
V(ack_string, "ack") \
5151
V(address_string, "address") \
5252
V(aliases_string, "aliases") \
53+
V(allow_alpn_fallback_string, "allowALPNFallback") \
5354
V(args_string, "args") \
5455
V(asn1curve_string, "asn1Curve") \
5556
V(async_ids_stack_string, "async_ids_stack") \

test/parallel/test-tls-alpn-server-client.js

+60
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,66 @@ function TestFatalAlert() {
206206
}
207207
}));
208208
}));
209+
210+
TestALPNFallback();
211+
}
212+
213+
function TestALPNFallback() {
214+
const serverOptions = {
215+
ALPNProtocols: ['b'],
216+
allowALPNFallback: true
217+
};
218+
219+
const clientOptions = [{
220+
ALPNProtocols: ['a', 'b']
221+
}, {
222+
ALPNProtocols: ['a']
223+
}];
224+
225+
runTest(clientOptions, serverOptions, function(results) {
226+
// 'b' is selected by ALPN if available
227+
checkResults(results[0],
228+
{ server: { ALPN: 'b' },
229+
client: { ALPN: 'b' } });
230+
231+
// 'a' is selected by ALPN if not, due to fallback
232+
checkResults(results[1],
233+
{ server: { ALPN: 'a' },
234+
client: { ALPN: 'a' } });
235+
});
236+
237+
TestEmptyALPNFallback();
238+
}
239+
240+
function TestEmptyALPNFallback() {
241+
const serverOptions = {
242+
ALPNProtocols: [],
243+
allowALPNFallback: true
244+
};
245+
246+
const clientOptions = [{
247+
ALPNProtocols: ['a', 'b']
248+
}];
249+
250+
runTest(clientOptions, serverOptions, function(results) {
251+
// 'a' is selected by ALPN, due to fallback with empty array
252+
checkResults(results[0],
253+
{ server: { ALPN: 'a' },
254+
client: { ALPN: 'a' } });
255+
});
256+
257+
TestFallbackWithoutProtocols();
258+
}
259+
260+
function TestFallbackWithoutProtocols() {
261+
assert.throws(() => {
262+
tls.createServer({
263+
allowALPNFallback: true
264+
});
265+
}, {
266+
code: 'ERR_TLS_ALPN_FALLBACK_WITHOUT_PROTOCOLS',
267+
message: 'The allowALPNFallback TLS option cannot be set without ALPNProtocols'
268+
});
209269
}
210270

211271
Test1();

0 commit comments

Comments
 (0)