Skip to content

Commit 5b2ead9

Browse files
committed
quic: use 'kApplication' not ALPN to explicitly connect QUIC to H3
1 parent 9a22647 commit 5b2ead9

16 files changed

Lines changed: 154 additions & 57 deletions

lib/internal/quic/http3.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const {
1414
listen: quicListen,
1515
QuicSession,
1616
QuicStream,
17+
kApplication,
1718
} = require('internal/quic/quic');
1819

1920
const {
@@ -224,6 +225,7 @@ async function connect(address, options = kEmptyObject) {
224225
const session = await quicConnect(address, {
225226
...options,
226227
alpn: kHttp3Alpn,
228+
[kApplication]: 'http3',
227229
});
228230
return new Http3Session(session);
229231
}
@@ -240,7 +242,7 @@ async function listen(onsession, options = kEmptyObject) {
240242
validateObject(options, 'options');
241243
return quicListen((session) => {
242244
onsession(new Http3Session(session));
243-
}, { ...options, alpn: kHttp3Alpn });
245+
}, { ...options, alpn: kHttp3Alpn, [kApplication]: 'http3' });
244246
}
245247

246248
module.exports = {

lib/internal/quic/quic.js

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4974,35 +4974,35 @@ function processTlsOptions(tls, forServer) {
49744974
validateBoolean(tlsTrace, 'options.tlsTrace');
49754975

49764976
// Encode the ALPN option to wire format (length-prefixed protocol names).
4977-
// Server: array of protocol names. Client: single protocol name.
4978-
// If not specified, the C++ default (h3) is used.
4979-
let encodedAlpn;
4980-
if (alpn !== undefined) {
4981-
const protocols = forServer ?
4982-
(ArrayIsArray(alpn) ? alpn : [alpn]) :
4983-
[alpn];
4984-
if (!forServer) {
4985-
validateString(alpn, 'options.alpn');
4986-
}
4987-
let totalLen = 0;
4988-
for (let i = 0; i < protocols.length; i++) {
4989-
validateString(protocols[i], `options.alpn[${i}]`);
4990-
if (protocols[i].length === 0 || protocols[i].length > 255) {
4991-
throw new ERR_INVALID_ARG_VALUE(`options.alpn[${i}]`, protocols[i],
4992-
'must be between 1 and 255 characters');
4993-
}
4994-
totalLen += 1 + protocols[i].length;
4995-
}
4996-
// Build wire format: [len1][name1][len2][name2]...
4997-
const buf = Buffer.allocUnsafe(totalLen);
4998-
let offset = 0;
4999-
for (let i = 0; i < protocols.length; i++) {
5000-
buf[offset++] = protocols[i].length;
5001-
buf.write(protocols[i], offset, 'ascii');
5002-
offset += protocols[i].length;
5003-
}
5004-
encodedAlpn = buf.toString('latin1');
5005-
}
4977+
if (alpn === undefined) {
4978+
throw new ERR_INVALID_ARG_VALUE(
4979+
'options.alpn', alpn,
4980+
'is required');
4981+
}
4982+
const protocols = forServer ?
4983+
(ArrayIsArray(alpn) ? alpn : [alpn]) :
4984+
[alpn];
4985+
if (!forServer) {
4986+
validateString(alpn, 'options.alpn');
4987+
}
4988+
let totalLen = 0;
4989+
for (let i = 0; i < protocols.length; i++) {
4990+
validateString(protocols[i], `options.alpn[${i}]`);
4991+
if (protocols[i].length === 0 || protocols[i].length > 255) {
4992+
throw new ERR_INVALID_ARG_VALUE(`options.alpn[${i}]`, protocols[i],
4993+
'must be between 1 and 255 characters');
4994+
}
4995+
totalLen += 1 + protocols[i].length;
4996+
}
4997+
// Build wire format: [len1][name1][len2][name2]...
4998+
const buf = Buffer.allocUnsafe(totalLen);
4999+
let offset = 0;
5000+
for (let i = 0; i < protocols.length; i++) {
5001+
buf[offset++] = protocols[i].length;
5002+
buf.write(protocols[i], offset, 'ascii');
5003+
offset += protocols[i].length;
5004+
}
5005+
const encodedAlpn = buf.toString('latin1');
50065006

50075007
if (ca !== undefined) {
50085008
const caInputs = ArrayIsArray(ca) ? ca : [ca];
@@ -5195,6 +5195,7 @@ function processSessionOptions(options, config = kEmptyObject) {
51955195
// HTTP/3 application-specific options. Nested under `application`
51965196
// to separate protocol-specific settings from transport-level ones.
51975197
application = kEmptyObject,
5198+
51985199
// Session callbacks that can be set at construction time to avoid
51995200
// race conditions with events that fire during or immediately
52005201
// after the handshake.
@@ -5226,6 +5227,10 @@ function processSessionOptions(options, config = kEmptyObject) {
52265227
targetAddress,
52275228
} = config;
52285229

5230+
const applicationName = options[kApplication];
5231+
assert(applicationName === undefined || typeof applicationName === 'string',
5232+
'options[kApplication] must be a registered application name');
5233+
52295234
if (token !== undefined) {
52305235
if (!isArrayBufferView(token)) {
52315236
throw new ERR_INVALID_ARG_TYPE('options.token',
@@ -5329,6 +5334,7 @@ function processSessionOptions(options, config = kEmptyObject) {
53295334
maxDatagramSendAttempts,
53305335
streamIdleTimeout,
53315336
application,
5337+
applicationName,
53325338
onerror,
53335339
onstream,
53345340
ondatagram,
@@ -5465,6 +5471,8 @@ module.exports = {
54655471
CC_ALGO_BBR,
54665472
DEFAULT_CIPHERS,
54675473
DEFAULT_GROUPS,
5474+
// Internal only for http3+quic integration
5475+
kApplication,
54685476
// These are exported only for internal testing purposes.
54695477
getQuicStreamState,
54705478
getQuicSessionState,

src/quic/application.cc

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
#include <node_sockaddr-inl.h>
1111
#include <uv.h>
1212
#include <v8.h>
13+
#include <mutex>
14+
#include <string>
15+
#include <vector>
1316
#include "application.h"
1417
#include "defs.h"
1518
#include "endpoint.h"
@@ -165,6 +168,39 @@ MaybeLocal<Object> Session::Application_Options::ToObject(
165168

166169
// ============================================================================
167170

171+
namespace {
172+
struct ApplicationFactoryEntry {
173+
std::string name;
174+
ApplicationFactory factory;
175+
};
176+
// Process-wide registry. Registered once per process at binding
177+
// initialization (registration is idempotent for repeated binding inits,
178+
// e.g. workers); intentionally leaked, process lifetime.
179+
std::mutex application_factories_mutex;
180+
std::vector<ApplicationFactoryEntry>* application_factories = nullptr;
181+
} // namespace
182+
183+
void RegisterApplicationFactory(std::string_view name,
184+
ApplicationFactory factory) {
185+
std::lock_guard<std::mutex> lock(application_factories_mutex);
186+
if (application_factories == nullptr) {
187+
application_factories = new std::vector<ApplicationFactoryEntry>();
188+
}
189+
for (const auto& entry : *application_factories) {
190+
if (entry.factory == factory) return;
191+
}
192+
application_factories->push_back({std::string(name), factory});
193+
}
194+
195+
ApplicationFactory FindApplicationFactory(std::string_view name) {
196+
std::lock_guard<std::mutex> lock(application_factories_mutex);
197+
if (application_factories == nullptr) return nullptr;
198+
for (const auto& entry : *application_factories) {
199+
if (entry.name == name) return entry.factory;
200+
}
201+
return nullptr;
202+
}
203+
168204
Session::Application::Application(Session* session, const Options& options)
169205
: session_(session) {}
170206

src/quic/application.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
44

55
#include <optional>
6+
#include <string_view>
67
#include <variant>
78
#include <vector>
89

@@ -266,6 +267,17 @@ class Session::Application : public MemoryRetainer {
266267
std::unique_ptr<Session::Application> CreateDefaultApplication(
267268
Session* session, const Session::Application_Options& options);
268269

270+
// A factory for protocol-specific Session::Application implementations.
271+
// Protocols register themselves under a name at binding initialization
272+
// (e.g. "http3"); a session installs one only when its options request
273+
// that name explicitly.
274+
using ApplicationFactory = std::unique_ptr<Session::Application> (*)(
275+
Session* session, const Session::Application_Options& options);
276+
void RegisterApplicationFactory(std::string_view name,
277+
ApplicationFactory factory);
278+
// Returns the factory registered under name, or nullptr.
279+
ApplicationFactory FindApplicationFactory(std::string_view name);
280+
269281
} // namespace node::quic
270282

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

src/quic/bindingdata.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <nghttp3/nghttp3.h>
88
#include <ngtcp2/ngtcp2.h>
99
#include <node.h>
10+
#include "http3.h"
1011
#include <node_errors.h>
1112
#include <node_external_reference.h>
1213
#include <node_mem-inl.h>
@@ -303,6 +304,7 @@ BindingData::BindingData(Realm* realm, Local<Object> object)
303304
MakeWeak();
304305
// Unref so the check handle doesn't keep the event loop alive on its own.
305306
flush_check_.Unref();
307+
RegisterHttp3Application();
306308
}
307309

308310
SessionManager& BindingData::session_manager() {

src/quic/bindingdata.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class SessionManager;
8888
V(disable_stateless_reset, "disableStatelessReset") \
8989
V(draining_period_multiplier, "drainingPeriodMultiplier") \
9090
V(enable_connect_protocol, "enableConnectProtocol") \
91+
V(applicationName, "applicationName") \
9192
V(enable_early_data, "enableEarlyData") \
9293
V(enable_datagrams, "enableDatagrams") \
9394
V(enable_tls_trace, "tlsTrace") \

src/quic/http3.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,10 @@ std::unique_ptr<Session::Application> CreateHttp3Application(
14261426
return std::make_unique<Http3ApplicationImpl>(session, options);
14271427
}
14281428

1429+
void RegisterHttp3Application() {
1430+
RegisterApplicationFactory("http3", CreateHttp3Application);
1431+
}
1432+
14291433
} // namespace quic
14301434
} // namespace node
14311435
#endif // OPENSSL_NO_QUIC

src/quic/http3.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ namespace node::quic {
1515
std::unique_ptr<Session::Application> CreateHttp3Application(
1616
Session* session, const Session::Application_Options& options);
1717

18+
void RegisterHttp3Application();
19+
1820
// Parse HTTP/3 specific session ticket app data. Called from
1921
// Application::ParseTicketData() when the type byte is HTTP3.
2022
// The data includes the type byte prefix.

src/quic/session.cc

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
#include "data.h"
2424
#include "defs.h"
2525
#include "endpoint.h"
26-
#include "http3.h"
2726
#include "ncrypto.h"
2827
#include "packet.h"
2928
#include "preferredaddress.h"
@@ -249,8 +248,6 @@ SessionStatsArena& GetSessionStatsArena(BindingData& binding) {
249248

250249
// ============================================================================
251250

252-
class Http3Application;
253-
254251
namespace {
255252
constexpr std::string to_string(PreferredAddress::Policy policy) {
256253
switch (policy) {
@@ -617,6 +614,7 @@ Maybe<Session::Options> Session::Options::From(Environment* env,
617614

618615
if (!SET(version) || !SET(min_version) || !SET(preferred_address_strategy) ||
619616
!SET(transport_params) || !SET(tls_options) || !SET(qlog) ||
617+
!SET(applicationName) ||
620618
!SET(handshake_timeout) || !SET(initial_rtt) ||
621619
!SET(keep_alive_timeout) || !SET(max_stream_window) || !SET(max_window) ||
622620
!SET(max_payload_size) || !SET(unacknowledged_packet_threshold) ||
@@ -2279,8 +2277,7 @@ Session::Session(Endpoint* endpoint,
22792277
// known upfront from the options. For servers, application_ stays
22802278
// null until OnSelectAlpn fires during the TLS handshake.
22812279
if (config.side == Side::CLIENT) {
2282-
auto app =
2283-
SelectApplicationFromAlpn(DecodeAlpn(config.options.tls_options.alpn));
2280+
auto app = SelectApplication();
22842281
if (app) SetApplication(std::move(app));
22852282
}
22862283

@@ -2632,12 +2629,12 @@ std::string_view Session::DecodeAlpn(std::string_view wire) {
26322629
return {};
26332630
}
26342631

2635-
std::unique_ptr<Session::Application> Session::SelectApplicationFromAlpn(
2636-
std::string_view alpn) {
2637-
// h3 and h3-XX variants use Http3ApplicationImpl.
2638-
// Everything else uses DefaultApplication.
2639-
if (alpn == "h3" || (alpn.size() > 3 && alpn.substr(0, 3) == "h3-")) {
2640-
return CreateHttp3Application(this, config().options.application_options);
2632+
std::unique_ptr<Session::Application> Session::SelectApplication() {
2633+
const auto& name = config().options.applicationName;
2634+
if (!name.empty()) {
2635+
auto factory = FindApplicationFactory(name);
2636+
CHECK_NOT_NULL(factory);
2637+
return factory(this, config().options.application_options);
26412638
}
26422639
return CreateDefaultApplication(this, config().options.application_options);
26432640
}

src/quic/session.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,10 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
126126
// Decode the first ALPN protocol name from wire format (length-prefixed).
127127
static std::string_view DecodeAlpn(std::string_view wire);
128128

129-
// Select the Application implementation based on the negotiated ALPN.
130-
// h3 (and h3-XX variants) map to Http3ApplicationImpl; all others map
131-
// to DefaultApplication. Sets the application_type state field.
132-
std::unique_ptr<Application> SelectApplicationFromAlpn(std::string_view alpn);
129+
// Select the Application implementation: the factory registered under
130+
// options.applicationName when set (see application.h), otherwise the default
131+
// raw-stream application.
132+
std::unique_ptr<Application> SelectApplication();
133133

134134
// Install the Application on the session. Called at construction for
135135
// clients (ALPN known upfront) or from OnSelectAlpn for servers
@@ -174,6 +174,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
174174
// ALPN selects Http3ApplicationImpl).
175175
Application_Options application_options = Application_Options::kDefault;
176176

177+
std::string applicationName;
178+
177179
// When true, QLog output will be enabled for the session.
178180
bool qlog = false;
179181

0 commit comments

Comments
 (0)