Skip to content

Commit 5c59acf

Browse files
committed
tls: add ALPNCallback server option for dynamic ALPN negotiation
1 parent c127e4e commit 5c59acf

File tree

8 files changed

+210
-3
lines changed

8 files changed

+210
-3
lines changed

doc/api/errors.md

+14
Original file line numberDiff line numberDiff line change
@@ -2698,6 +2698,20 @@ 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_CALLBACK_INVALID_RESULT"></a>
2702+
2703+
### `ERR_TLS_ALPN_CALLBACK_INVALID_RESULT`
2704+
2705+
This error is thrown when an `ALPNCallback` returns a value that is not in the
2706+
list of ALPN protocols offered by the client.
2707+
2708+
<a id="ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS"></a>
2709+
2710+
### `ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS`
2711+
2712+
This error is thrown when creating a `TLSServer` if the TLS options include
2713+
both `ALPNProtocols` and `ALPNCallback`. These options are mutually exclusive.
2714+
27012715
<a id="ERR_TLS_CERT_ALTNAME_FORMAT"></a>
27022716

27032717
### `ERR_TLS_CERT_ALTNAME_FORMAT`

doc/api/tls.md

+14
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/45190
2017+
description: The `options` parameter can now include `ALPNCallback`.
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,17 @@ 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+
* `ALPNCallback(params)`: {Function} If set, this will be called when a
2049+
client opens a connection using the ALPN extension. One argument will
2050+
be passed to the callback: an object containing `serverName` and
2051+
`clientALPNProtocols` fields, respectively containing the server name from
2052+
the SNI extension (if any) and an array of ALPN protocol name strings. The
2053+
callback must return either one of the strings listed in
2054+
`clientALPNProtocols`, which will be returned to the client as the selected
2055+
ALPN protocol, or `undefined`, to reject the connection with a fatal alert.
2056+
If a string is returned that does not match one of the client's ALPN
2057+
protocols, an error will be thrown. This option cannot be used with the
2058+
`ALPNProtocols` option, and setting both options will throw an error.
20452059
* `clientCertEngine` {string} Name of an OpenSSL engine which can provide the
20462060
client certificate.
20472061
* `enableTrace` {boolean} If `true`, [`tls.TLSSocket.enableTrace()`][] will be

lib/_tls_wrap.js

+56
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ const {
7171
ERR_INVALID_ARG_VALUE,
7272
ERR_MULTIPLE_CALLBACK,
7373
ERR_SOCKET_CLOSED,
74+
ERR_TLS_ALPN_CALLBACK_INVALID_RESULT,
75+
ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS,
7476
ERR_TLS_DH_PARAM_SIZE,
7577
ERR_TLS_HANDSHAKE_TIMEOUT,
7678
ERR_TLS_INVALID_CONTEXT,
@@ -233,6 +235,46 @@ function loadSNI(info) {
233235
}
234236

235237

238+
function callALPNCallback(protocolsBuffer) {
239+
const handle = this;
240+
const socket = handle[owner_symbol];
241+
242+
const serverName = handle.getServername();
243+
244+
// Collect all the protocols from the given buffer:
245+
const protocols = [];
246+
let offset = 0;
247+
while (offset < protocolsBuffer.length) {
248+
const protocolLen = protocolsBuffer[offset];
249+
offset += 1;
250+
251+
const protocol = protocolsBuffer.slice(offset, offset + protocolLen);
252+
offset += protocolLen;
253+
254+
protocols.push(protocol.toString('ascii'));
255+
}
256+
257+
const selectedProtocol = socket._ALPNCallback({
258+
serverName: serverName,
259+
clientALPNProtocols: protocols
260+
});
261+
262+
// Undefined -> all proposed protocols rejected
263+
if (selectedProtocol === undefined) return undefined;
264+
265+
const protocolIndex = protocols.indexOf(selectedProtocol);
266+
if (protocolIndex === -1) {
267+
throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, protocols);
268+
}
269+
let protocolOffset = 0;
270+
for (let i = 0; i < protocolIndex; i++) {
271+
protocolOffset += 1 + protocols[i].length;
272+
}
273+
274+
return protocolOffset;
275+
}
276+
277+
236278
function requestOCSP(socket, info) {
237279
if (!info.OCSPRequest || !socket.server)
238280
return requestOCSPDone(socket);
@@ -492,6 +534,7 @@ function TLSSocket(socket, opts) {
492534
this._controlReleased = false;
493535
this.secureConnecting = true;
494536
this._SNICallback = null;
537+
this._ALPNCallback = null;
495538
this.servername = null;
496539
this.alpnProtocol = null;
497540
this.authorized = false;
@@ -717,6 +760,13 @@ TLSSocket.prototype._init = function(socket, wrap) {
717760
ssl.lastHandshakeTime = 0;
718761
ssl.handshakes = 0;
719762

763+
if (options.ALPNCallback) {
764+
assert(typeof options.ALPNCallback === 'function');
765+
this._ALPNCallback = options.ALPNCallback;
766+
ssl.ALPNCallback = callALPNCallback;
767+
ssl.enableALPNCb();
768+
}
769+
720770
if (this.server) {
721771
if (this.server.listenerCount('resumeSession') > 0 ||
722772
this.server.listenerCount('newSession') > 0) {
@@ -1097,6 +1147,7 @@ function tlsConnectionListener(rawSocket) {
10971147
rejectUnauthorized: this.rejectUnauthorized,
10981148
handshakeTimeout: this[kHandshakeTimeout],
10991149
ALPNProtocols: this.ALPNProtocols,
1150+
ALPNCallback: this.ALPNCallback,
11001151
SNICallback: this[kSNICallback] || SNICallback,
11011152
enableTrace: this[kEnableTrace],
11021153
pauseOnConnect: this.pauseOnConnect,
@@ -1196,6 +1247,11 @@ function Server(options, listener) {
11961247
this.requestCert = options.requestCert === true;
11971248
this.rejectUnauthorized = options.rejectUnauthorized !== false;
11981249

1250+
this.ALPNCallback = options.ALPNCallback;
1251+
if (this.ALPNCallback && options.ALPNProtocols) {
1252+
throw new ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS();
1253+
}
1254+
11991255
if (options.sessionTimeout)
12001256
this.sessionTimeout = options.sessionTimeout;
12011257

lib/internal/errors.js

+10
Original file line numberDiff line numberDiff line change
@@ -1612,6 +1612,16 @@ E('ERR_TEST_FAILURE', function(error, failureType) {
16121612
this.cause = error;
16131613
return msg;
16141614
}, Error);
1615+
E('ERR_TLS_ALPN_CALLBACK_INVALID_RESULT', (value, protocols) => {
1616+
return `ALPN callback returned a value (${
1617+
value
1618+
}) that did not match any of the client's offered protocols (${
1619+
protocols.join(', ')
1620+
})`;
1621+
}, TypeError);
1622+
E('ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS',
1623+
'The ALPNCallback and ALPNProtocols TLS options are mutually exclusive',
1624+
TypeError);
16151625
E('ERR_TLS_CERT_ALTNAME_FORMAT', 'Invalid subject alternative name string',
16161626
SyntaxError);
16171627
E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) {

src/crypto/crypto_tls.cc

+51
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,46 @@ int SelectALPNCallback(
226226
unsigned int inlen,
227227
void* arg) {
228228
TLSWrap* w = static_cast<TLSWrap*>(arg);
229+
if (w->alpn_callback_enabled_) {
230+
Environment* env = w->env();
231+
232+
Local<Value> callback_arg = Buffer::Copy(
233+
env,
234+
reinterpret_cast<const char*>(in),
235+
inlen).ToLocalChecked();
236+
237+
MaybeLocal<Value> maybe_callback_result = w->MakeCallback(
238+
env->alpn_callback_string(),
239+
1,
240+
&callback_arg);
241+
242+
if (UNLIKELY(maybe_callback_result.IsEmpty())) {
243+
// Implies the callback didn't return, because some exception was thrown
244+
// during processing, e.g. if callback returned an invalid ALPN value.
245+
return SSL_TLSEXT_ERR_ALERT_FATAL;
246+
}
247+
248+
Local<Value> callback_result = maybe_callback_result.ToLocalChecked();
249+
250+
if (callback_result->IsUndefined()) {
251+
// If you set an ALPN callback, but you return undefined for an ALPN
252+
// request, you're rejecting all proposed ALPN protocols, and so we send
253+
// a fatal alert:
254+
return SSL_TLSEXT_ERR_ALERT_FATAL;
255+
}
256+
257+
CHECK(callback_result->IsNumber());
258+
unsigned int result_int = callback_result.As<v8::Number>()->Value();
259+
260+
// The callback returns an offset into the given buffer, for the selected
261+
// protocol that should be returned. We then set outlen & out to point
262+
// to the selected input length & value directly:
263+
*outlen = *(in + result_int);
264+
*out = (in + result_int + 1);
265+
266+
return SSL_TLSEXT_ERR_OK;
267+
}
268+
229269
const std::vector<unsigned char>& alpn_protos = w->alpn_protos_;
230270

231271
if (alpn_protos.empty()) return SSL_TLSEXT_ERR_NOACK;
@@ -1233,6 +1273,15 @@ void TLSWrap::OnClientHelloParseEnd(void* arg) {
12331273
c->Cycle();
12341274
}
12351275

1276+
void TLSWrap::EnableALPNCb(const FunctionCallbackInfo<Value>& args) {
1277+
TLSWrap* wrap;
1278+
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
1279+
wrap->alpn_callback_enabled_ = true;
1280+
1281+
SSL* ssl = wrap->ssl_.get();
1282+
SSL_CTX_set_alpn_select_cb(SSL_get_SSL_CTX(ssl), SelectALPNCallback, wrap);
1283+
}
1284+
12361285
void TLSWrap::GetServername(const FunctionCallbackInfo<Value>& args) {
12371286
Environment* env = Environment::GetCurrent(args);
12381287

@@ -2044,6 +2093,7 @@ void TLSWrap::Initialize(
20442093
SetProtoMethod(isolate, t, "certCbDone", CertCbDone);
20452094
SetProtoMethod(isolate, t, "destroySSL", DestroySSL);
20462095
SetProtoMethod(isolate, t, "enableCertCb", EnableCertCb);
2096+
SetProtoMethod(isolate, t, "enableALPNCb", EnableALPNCb);
20472097
SetProtoMethod(isolate, t, "endParser", EndParser);
20482098
SetProtoMethod(isolate, t, "enableKeylogCallback", EnableKeylogCallback);
20492099
SetProtoMethod(isolate, t, "enableSessionCallbacks", EnableSessionCallbacks);
@@ -2109,6 +2159,7 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
21092159
registry->Register(CertCbDone);
21102160
registry->Register(DestroySSL);
21112161
registry->Register(EnableCertCb);
2162+
registry->Register(EnableALPNCb);
21122163
registry->Register(EndParser);
21132164
registry->Register(EnableKeylogCallback);
21142165
registry->Register(EnableSessionCallbacks);

src/crypto/crypto_tls.h

+2
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ class TLSWrap : public AsyncWrap,
174174
static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
175175
static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
176176
static void EnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args);
177+
static void EnableALPNCb(const v8::FunctionCallbackInfo<v8::Value>& args);
177178
static void EnableKeylogCallback(
178179
const v8::FunctionCallbackInfo<v8::Value>& args);
179180
static void EnableSessionCallbacks(
@@ -287,6 +288,7 @@ class TLSWrap : public AsyncWrap,
287288

288289
public:
289290
std::vector<unsigned char> alpn_protos_; // Accessed by SelectALPNCallback.
291+
bool alpn_callback_enabled_ = false; // Accessed by SelectALPNCallback.
290292
};
291293

292294
} // namespace crypto

src/env_properties.h

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
V(ack_string, "ack") \
5050
V(address_string, "address") \
5151
V(aliases_string, "aliases") \
52+
V(alpn_callback_string, "ALPNCallback") \
5253
V(args_string, "args") \
5354
V(asn1curve_string, "asn1Curve") \
5455
V(async_ids_stack_string, "async_ids_stack") \

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

+62-3
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ function runTest(clientsOptions, serverOptions, cb) {
4141
opt.rejectUnauthorized = false;
4242

4343
results[clientIndex] = {};
44-
const client = tls.connect(opt, function() {
45-
results[clientIndex].client = { ALPN: client.alpnProtocol };
46-
client.end();
44+
45+
function startNextClient() {
4746
if (options.length) {
4847
clientIndex++;
4948
connectClient(options);
@@ -53,6 +52,15 @@ function runTest(clientsOptions, serverOptions, cb) {
5352
cb(results);
5453
});
5554
}
55+
}
56+
57+
const client = tls.connect(opt, function() {
58+
results[clientIndex].client = { ALPN: client.alpnProtocol };
59+
client.end();
60+
startNextClient();
61+
}).on('error', function(err) {
62+
results[clientIndex].client = { error: err };
63+
startNextClient();
5664
});
5765
}
5866

@@ -200,12 +208,63 @@ function TestFatalAlert() {
200208
.on('close', common.mustCall(() => {
201209
assert.match(stderr, /SSL alert number 120/);
202210
server.close();
211+
TestALPNCallback();
203212
}));
204213
} else {
205214
server.close();
215+
TestALPNCallback();
206216
}
207217
}));
208218
}));
209219
}
210220

221+
function TestALPNCallback() {
222+
// Server always selects the client's 2nd preference:
223+
const serverOptions = {
224+
ALPNCallback: ({ clientALPNProtocols }) => {
225+
return clientALPNProtocols[1];
226+
}
227+
};
228+
229+
const clientsOptions = [{
230+
ALPNProtocols: ['a', 'b', 'c'],
231+
}, {
232+
ALPNProtocols: ['a'],
233+
}];
234+
235+
runTest(clientsOptions, serverOptions, function(results) {
236+
// Callback picks 2nd preference => picks 'b'
237+
checkResults(results[0],
238+
{ server: { ALPN: 'b' },
239+
client: { ALPN: 'b' } });
240+
241+
// Callback picks 2nd preference => undefined => ALPN rejected:
242+
assert.strictEqual(results[1].server, undefined);
243+
assert.strictEqual(results[1].client.error.code, 'ECONNRESET');
244+
245+
TestBadALPNCallback();
246+
});
247+
}
248+
249+
function TestBadALPNCallback() {
250+
// Server always returns a fixed invalid value:
251+
const serverOptions = {
252+
ALPNCallback: () => 'http/5'
253+
};
254+
255+
const clientsOptions = [{
256+
ALPNProtocols: ['http/1', 'h2'],
257+
}];
258+
259+
process.once('uncaughtException', common.mustCall((error) => {
260+
assert.strictEqual(error.code, 'ERR_TLS_ALPN_CALLBACK_INVALID_RESULT');
261+
}));
262+
263+
runTest(clientsOptions, serverOptions, function(results) {
264+
// Callback returns 'http/5' => doesn't match client ALPN => error & reset
265+
assert.strictEqual(results[0].server, undefined);
266+
assert.strictEqual(results[0].client.error.code, 'ECONNRESET');
267+
});
268+
}
269+
211270
Test1();

0 commit comments

Comments
 (0)