Skip to content

Commit 90e12f2

Browse files
committed
quic: support multiple ALPN negotiation
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: #62620 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Tim Perry <pimterry@gmail.com>
1 parent 6724cc6 commit 90e12f2

File tree

18 files changed

+285
-217
lines changed

18 files changed

+285
-217
lines changed

doc/api/quic.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,12 +1141,26 @@ added: v23.8.0
11411141
#### `sessionOptions.alpn`
11421142

11431143
<!-- YAML
1144-
added: v23.8.0
1144+
added: REPLACEME
11451145
-->
11461146

1147-
* Type: {string}
1147+
* Type: {string} (client) | {string\[]} (server)
1148+
1149+
The ALPN (Application-Layer Protocol Negotiation) identifier(s).
1150+
1151+
For **client** sessions, this is a single string specifying the protocol
1152+
the client wants to use (e.g. `'h3'`).
1153+
1154+
For **server** sessions, this is an array of protocol names in preference
1155+
order that the server supports (e.g. `['h3', 'h3-29']`). During the TLS
1156+
handshake, the server selects the first protocol from its list that the
1157+
client also supports.
1158+
1159+
The negotiated ALPN determines which Application implementation is used
1160+
for the session. `'h3'` and `'h3-*'` variants select the HTTP/3
1161+
application; all other values select the default application.
11481162

1149-
The ALPN protocol identifier.
1163+
Default: `'h3'`
11501164

11511165
#### `sessionOptions.ca` (client only)
11521166

lib/internal/quic/quic.js

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ let debug = require('internal/util/debuglog').debuglog('quic', (fn) => {
3333

3434
const {
3535
Endpoint: Endpoint_,
36-
Http3Application: Http3,
3736
setCallbacks,
3837

3938
// The constants to be exposed to end users for various options.
@@ -118,7 +117,6 @@ const {
118117
const kEmptyObject = { __proto__: null };
119118

120119
const {
121-
kApplicationProvider,
122120
kBlocked,
123121
kConnect,
124122
kDatagram,
@@ -2222,7 +2220,7 @@ function processIdentityOptions(identity, label) {
22222220
function processTlsOptions(tls, forServer) {
22232221
const {
22242222
servername,
2225-
protocol,
2223+
alpn,
22262224
ciphers = DEFAULT_CIPHERS,
22272225
groups = DEFAULT_GROUPS,
22282226
keylog = false,
@@ -2240,9 +2238,6 @@ function processTlsOptions(tls, forServer) {
22402238
if (servername !== undefined) {
22412239
validateString(servername, 'options.servername');
22422240
}
2243-
if (protocol !== undefined) {
2244-
validateString(protocol, 'options.protocol');
2245-
}
22462241
if (ciphers !== undefined) {
22472242
validateString(ciphers, 'options.ciphers');
22482243
}
@@ -2253,11 +2248,42 @@ function processTlsOptions(tls, forServer) {
22532248
validateBoolean(verifyClient, 'options.verifyClient');
22542249
validateBoolean(tlsTrace, 'options.tlsTrace');
22552250

2251+
// Encode the ALPN option to wire format (length-prefixed protocol names).
2252+
// Server: array of protocol names. Client: single protocol name.
2253+
// If not specified, the C++ default (h3) is used.
2254+
let encodedAlpn;
2255+
if (alpn !== undefined) {
2256+
const protocols = forServer ?
2257+
(ArrayIsArray(alpn) ? alpn : [alpn]) :
2258+
[alpn];
2259+
if (!forServer) {
2260+
validateString(alpn, 'options.alpn');
2261+
}
2262+
let totalLen = 0;
2263+
for (let i = 0; i < protocols.length; i++) {
2264+
validateString(protocols[i], `options.alpn[${i}]`);
2265+
if (protocols[i].length === 0 || protocols[i].length > 255) {
2266+
throw new ERR_INVALID_ARG_VALUE(`options.alpn[${i}]`, protocols[i],
2267+
'must be between 1 and 255 characters');
2268+
}
2269+
totalLen += 1 + protocols[i].length;
2270+
}
2271+
// Build wire format: [len1][name1][len2][name2]...
2272+
const buf = Buffer.allocUnsafe(totalLen);
2273+
let offset = 0;
2274+
for (let i = 0; i < protocols.length; i++) {
2275+
buf[offset++] = protocols[i].length;
2276+
buf.write(protocols[i], offset, 'ascii');
2277+
offset += protocols[i].length;
2278+
}
2279+
encodedAlpn = buf.toString('latin1');
2280+
}
2281+
22562282
// Shared TLS options (same for all identities on the endpoint).
22572283
const shared = {
22582284
__proto__: null,
22592285
servername,
2260-
protocol,
2286+
alpn: encodedAlpn,
22612287
ciphers,
22622288
groups,
22632289
keylog,
@@ -2360,17 +2386,12 @@ function processSessionOptions(options, config = {}) {
23602386
maxStreamWindow,
23612387
maxWindow,
23622388
cc,
2363-
[kApplicationProvider]: provider,
23642389
} = options;
23652390

23662391
const {
23672392
forServer = false,
23682393
} = config;
23692394

2370-
if (provider !== undefined) {
2371-
validateObject(provider, 'options[kApplicationProvider]');
2372-
}
2373-
23742395
if (cc !== undefined) {
23752396
validateOneOf(cc, 'options.cc', [CC_ALGO_RENO, CC_ALGO_BBR, CC_ALGO_CUBIC]);
23762397
}
@@ -2392,7 +2413,6 @@ function processSessionOptions(options, config = {}) {
23922413
maxStreamWindow,
23932414
maxWindow,
23942415
sessionTicket,
2395-
provider,
23962416
cc,
23972417
};
23982418
}
@@ -2494,7 +2514,6 @@ module.exports = {
24942514
QuicEndpoint,
24952515
QuicSession,
24962516
QuicStream,
2497-
Http3,
24982517
CC_ALGO_RENO,
24992518
CC_ALGO_CUBIC,
25002519
CC_ALGO_BBR,

lib/internal/quic/state.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const {
6262
IDX_STATE_SESSION_STREAM_OPEN_ALLOWED,
6363
IDX_STATE_SESSION_PRIORITY_SUPPORTED,
6464
IDX_STATE_SESSION_WRAPPED,
65+
IDX_STATE_SESSION_APPLICATION_TYPE,
6566
IDX_STATE_SESSION_LAST_DATAGRAM_ID,
6667

6768
IDX_STATE_ENDPOINT_BOUND,
@@ -99,6 +100,7 @@ assert(IDX_STATE_SESSION_HANDSHAKE_CONFIRMED !== undefined);
99100
assert(IDX_STATE_SESSION_STREAM_OPEN_ALLOWED !== undefined);
100101
assert(IDX_STATE_SESSION_PRIORITY_SUPPORTED !== undefined);
101102
assert(IDX_STATE_SESSION_WRAPPED !== undefined);
103+
assert(IDX_STATE_SESSION_APPLICATION_TYPE !== undefined);
102104
assert(IDX_STATE_SESSION_LAST_DATAGRAM_ID !== undefined);
103105
assert(IDX_STATE_ENDPOINT_BOUND !== undefined);
104106
assert(IDX_STATE_ENDPOINT_RECEIVING !== undefined);
@@ -347,6 +349,12 @@ class QuicSessionState {
347349
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_WRAPPED);
348350
}
349351

352+
/** @type {number} */
353+
get applicationType() {
354+
if (this.#handle.byteLength === 0) return undefined;
355+
return DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_APPLICATION_TYPE);
356+
}
357+
350358
/** @type {bigint} */
351359
get lastDatagramId() {
352360
if (this.#handle.byteLength === 0) return undefined;
@@ -407,6 +415,7 @@ class QuicSessionState {
407415
isStreamOpenAllowed: this.isStreamOpenAllowed,
408416
isPrioritySupported: this.isPrioritySupported,
409417
isWrapped: this.isWrapped,
418+
applicationType: this.applicationType,
410419
lastDatagramId: this.lastDatagramId,
411420
}, opts)}`;
412421
}

lib/internal/quic/symbols.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ const {
2323
// Symbols used to hide various private properties and methods from the
2424
// public API.
2525

26-
const kApplicationProvider = Symbol('kApplicationProvider');
2726
const kBlocked = Symbol('kBlocked');
2827
const kConnect = Symbol('kConnect');
2928
const kDatagram = Symbol('kDatagram');
@@ -50,7 +49,6 @@ const kWantsHeaders = Symbol('kWantsHeaders');
5049
const kWantsTrailers = Symbol('kWantsTrailers');
5150

5251
module.exports = {
53-
kApplicationProvider,
5452
kBlocked,
5553
kConnect,
5654
kDatagram,

src/quic/application.cc

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,10 @@ class DefaultApplication final : public Session::Application {
464464
// of the namespace.
465465
using Application::Application; // NOLINT
466466

467+
Session::Application::Type type() const override {
468+
return Session::Application::Type::DEFAULT;
469+
}
470+
467471
error_code GetNoErrorCode() const override { return 0; }
468472

469473
bool ReceiveStreamData(int64_t stream_id,
@@ -601,14 +605,9 @@ class DefaultApplication final : public Session::Application {
601605
Stream::Queue stream_queue_;
602606
};
603607

604-
std::unique_ptr<Session::Application> Session::SelectApplication(
605-
Session* session, const Config& config) {
606-
if (config.options.application_provider) {
607-
return config.options.application_provider->Create(session);
608-
}
609-
610-
return std::make_unique<DefaultApplication>(session,
611-
Application_Options::kDefault);
608+
std::unique_ptr<Session::Application> CreateDefaultApplication(
609+
Session* session, const Session::Application_Options& options) {
610+
return std::make_unique<DefaultApplication>(session, options);
612611
}
613612

614613
} // namespace quic

src/quic/application.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,19 @@ class Session::Application : public MemoryRetainer {
1717
public:
1818
using Options = Session::Application_Options;
1919

20+
// The type of Application, exposed via the session state so JS
21+
// can observe which Application was selected after ALPN negotiation.
22+
enum class Type : uint8_t {
23+
NONE = 0, // Not yet selected (server pre-negotiation)
24+
DEFAULT = 1, // DefaultApplication (non-h3 ALPN)
25+
HTTP3 = 2, // Http3ApplicationImpl (h3 / h3-XX ALPN)
26+
};
27+
2028
Application(Session* session, const Options& options);
2129
DISALLOW_COPY_AND_MOVE(Application)
2230

31+
virtual Type type() const = 0;
32+
2333
virtual bool Start();
2434

2535
virtual error_code GetNoErrorCode() const = 0;
@@ -169,6 +179,10 @@ struct Session::Application::StreamData final {
169179
std::string ToString() const;
170180
};
171181

182+
// Create a DefaultApplication for the given session.
183+
std::unique_ptr<Session::Application> CreateDefaultApplication(
184+
Session* session, const Session::Application_Options& options);
185+
172186
} // namespace node::quic
173187

174188
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

src/quic/bindingdata.h

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ class Packet;
2424
// The FunctionTemplates the BindingData will store for us.
2525
#define QUIC_CONSTRUCTORS(V) \
2626
V(endpoint) \
27-
V(http3application) \
2827
V(logstream) \
2928
V(session) \
3029
V(stream) \
@@ -59,7 +58,7 @@ class Packet;
5958
V(ack_delay_exponent, "ackDelayExponent") \
6059
V(active_connection_id_limit, "activeConnectionIDLimit") \
6160
V(address_lru_size, "addressLRUSize") \
62-
V(application_provider, "provider") \
61+
V(application, "application") \
6362
V(bbr, "bbr") \
6463
V(ca, "ca") \
6564
V(cc_algorithm, "cc") \
@@ -78,7 +77,6 @@ class Packet;
7877
V(groups, "groups") \
7978
V(handshake_timeout, "handshakeTimeout") \
8079
V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \
81-
V(http3application, "Http3Application") \
8280
V(initial_max_data, "initialMaxData") \
8381
V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \
8482
V(initial_max_stream_data_bidi_remote, "initialMaxStreamDataBidiRemote") \
@@ -105,7 +103,7 @@ class Packet;
105103
V(max_window, "maxWindow") \
106104
V(min_version, "minVersion") \
107105
V(preferred_address_strategy, "preferredAddressPolicy") \
108-
V(protocol, "protocol") \
106+
V(alpn, "alpn") \
109107
V(qlog, "qlog") \
110108
V(qpack_blocked_streams, "qpackBlockedStreams") \
111109
V(qpack_encoder_max_dtable_capacity, "qpackEncoderMaxDTableCapacity") \

src/quic/endpoint.cc

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ bool is_diagnostic_packet_loss(double probability) {
9292
return (static_cast<double>(c) / 255) < probability;
9393
}
9494

95-
template <typename Opt, double Opt::*member>
95+
template <typename Opt, double Opt::* member>
9696
bool SetOption(Environment* env,
9797
Opt* options,
9898
const Local<Object>& object,
@@ -113,7 +113,7 @@ bool SetOption(Environment* env,
113113
}
114114
#endif // DEBUG
115115

116-
template <typename Opt, uint8_t Opt::*member>
116+
template <typename Opt, uint8_t Opt::* member>
117117
bool SetOption(Environment* env,
118118
Opt* options,
119119
const Local<Object>& object,
@@ -140,7 +140,7 @@ bool SetOption(Environment* env,
140140
return true;
141141
}
142142

143-
template <typename Opt, TokenSecret Opt::*member>
143+
template <typename Opt, TokenSecret Opt::* member>
144144
bool SetOption(Environment* env,
145145
Opt* options,
146146
const Local<Object>& object,
@@ -511,7 +511,6 @@ JS_CONSTRUCTOR_IMPL(Endpoint, endpoint_constructor_template, {
511511

512512
void Endpoint::InitPerIsolate(IsolateData* data, Local<ObjectTemplate> target) {
513513
// TODO(@jasnell): Implement the per-isolate state
514-
Http3Application::InitPerIsolate(data, target);
515514
}
516515

517516
void Endpoint::InitPerContext(Realm* realm, Local<Object> target) {
@@ -567,8 +566,6 @@ void Endpoint::InitPerContext(Realm* realm, Local<Object> target) {
567566
NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_SEND_FAILURE);
568567
NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_START_FAILURE);
569568

570-
Http3Application::InitPerContext(realm, target);
571-
572569
SetConstructorFunction(realm->context(),
573570
target,
574571
"Endpoint",

0 commit comments

Comments
 (0)