quic: add Http3Session, so you can explicitly pick the app protocol - #65993
quic: add Http3Session, so you can explicitly pick the app protocol#65993pimterry wants to merge 1 commit into
Conversation
|
Review requested:
|
Previously, ALPN decided automatically which application protocol implementation was used. QuicSession was used everywhere, but its actual behaviour and API changes implicitly based on the wire traffic involved. Now, QuicSession is used for pure QUIC only, and Http3Session is used for HTTP/3 sessions only. To do HTTP/3 on a connection, you enable it explicitly by wrapping a QUIC session in Http3Session. Doing so attaches the internal protocol application handling so everything is HTTP/3 on that session from that point onwards. For now, this only changes the application selection process and the top-level APIs involved, but none of the details. In a future PR, we can introduce Http3Stream and migrate other HTTP/3 specific functionality (e.g. SETTINGS & GOAWAY handling) out of the QUIC API. Signed-off-by: Tim Perry <pimterry@gmail.com>
da14bdc to
77cdc45
Compare
|
Ok, so it's good to get this isolated. Trust me, I'm not trying to be difficult! :-) ... Before going through the code let's see if we can settle on this. And yes! I can be convinced but it might take some doing. Let's take your points:
Useful in our tests does not mean useful in the general API That We Have To Support Indefinitely sense. Also, HTTP/2 is easier to do over raw TCP since it's just framing with no TLS, flow control, retransmission framing, etc. That said, we can do raw QUIC with a HTTP/3 ALPN without this kind of architectural change. It could be as simple as a boolean configuration option that says "use the default application with h3" in which case we simply don't install the Http3Application. Essentially, this point alone does not justify the re-architecture.
This is also something that could just be a config option. "Treat ALPN {FOO} like H3" in which case we install the Http3Application. Or if someone really wanted to implement all the H3 semantics themselves, see point one above... it could be as simple as a boolean configuration option. Still not a strong motivation for the re-architecture.
We can split QuicStream and Http3Stream purely at the JS level without any more C++ side redesign AND make it possible for applications that want to just handle raw QUIC but implement their own HTTP3 semantics to use Http3Stream without going with the dynamic attach path. I'm fine with splitting things at the JS level. You'll not get much argument from me there, but I strongly prefer the existing So my ask would be this: please take a moment to explain why the current architecture needs to be changed to meet the goals. What goals simply cannot be met with the current architecture or why does it make it harder? etc. What I'm not saying is that your suggested approach is wrong, not by any means... I just think there's more than one correct approach to achieve the goal and I haven't yet seen why the current approach isn't workable. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #65993 +/- ##
==========================================
- Coverage 89.98% 89.97% -0.01%
==========================================
Files 784 785 +1
Lines 268410 268751 +341
Branches 51123 51135 +12
==========================================
+ Hits 241520 241815 +295
- Misses 17433 17484 +51
+ Partials 9457 9452 -5
🚀 New features to boost your workflow:
|
Starting here because I think it's the core point. I think you're mostly referring to the previous PR here. In this PR, there's no real architectural change. It doesn't change the overall Application structure. The only code change in static constexpr uint8_t kTypePending = 0x80The changes in C++ here are effectively moving the timing around (moving the code that triggers Application attach from earliest possible moment to first-use) and changing the details of that attach step internally (reading a flag and optionally SETTINGS config stored by JS, instead of just reading ALPN). There are architectural changes I think are worth exploring later, but this PR doesn't do any of that.
Our tests are one example close to home, but there's plenty of other ecosystem test suites that do the same. Undici has a nice one here: https://github.com/nodejs/undici/blob/main/test/http2-goaway-refusal-storm.js. That's a security bug fix, which does this specifically because it requires invalid HTTP/2 behaviour we don't support. This will apply similarly for roughly any repro of a security bug involving a badly behaved HTTP/3 peer, and any test covering those cases. Supporting this is useful.
TLS, flow control & retransmission are all QUIC, so would still be handled by Node even in the "custom HTTP/3 implementation" case. Some of this is actually easier than HTTP/2 - for H2 you do need flow control on top of TCP. In practice I think it's a wash. Implementing HTTP/3 alone, you get to skip flow control and you can disable QPACK by sending capacity 0 (or alternatively, I built a working QPACK implementation in pure JS 😆 - works, but very slowly), but framing & encoding is more complicated. Not suggesting this is a good idea for anything serious, it is very hard to do 100% correctly or make it fast, but it's very achievable for test cases.
In short, I think:
In the implementation here, the runtime flow for the dynamic attach specifically is setting one flag in the shared state set from JS, and then doing one check against that in C++ on first use. There's other changes to split logic into Http3Session, pass across (optionally) any custom SETTINGS configuration, etc, but the actual attach flow is just writing a flag and reading it later (instead of reading ALPN). For an alternative design, for any non-trivial use cases, you need at least a mapping of ALPN to implementation (this is basically what we have today, but non-configurable) and probably an SNI + ALPN mapping, so this can vary by host. Something like: listen(() => { ... }, {
sessionTypeMapping: {
'raw.example.com': {
'*': 'quic'
},
'h3.example.com': {
'h3': 'h3',
'funky-h3-alpn': 'h3'
}
}
})Implementing that mapping logic is going to be more complex than the single state flag set & read. In effect this PR outsources "map connections to implementations" logic to user JS. If we want to take responsibility for that, we end up pulling more logic into core, not less. Making a static API like the above would make the mapping faster (C++ checking of ALPN & SNI will be faster than JS doing it). This is only relevant to the unusual cases that do need this though (test suites, CVE repros, networking tools) who generally aren't very perf sensitive and would (imo) prefer API flexibility over a small speed boosts. It doesn't make the very common cases that do care about performance (always use Http3/always use QUIC) any faster. More generally, we do support the "control it from JS" approach in basically every other API:
|
This extracts dynamic application attach from #63995. This is still very much under debate, so I'll try to explain thoroughly:
Terminology
Sessionmakes calls into its Application any time it needs anything protocol-specific.Current state
Right now, we have fixed rules that lock which Application is used according to the ALPN negotation on the socket. We use ALPN to make this decision and attach the Application at the earliest possible moment.
For clients that means in the Session constructor (clients currently only support one fixed ALPN) and for servers it means in
OnClientHello, which is the moment we know which ALPN protocol we'll select.Why add dynamic attach
h3-*. It's perfectly valid to do this - if you agree onh3via ALPN you should speak HTTP/3, but the inverse is not required.How this works
This change drops the fixed Application selection & default ALPN configuration, and provides a new
Http3SessionAPI which lets JS control which Application is used directly. You wrap aQuicSessionin anHttp3Sessionand open your streams through the latter if you want to speak HTTP/3.It does this without changing the fundamental steps involved, just by reordering the work within. There's no additional callbacks, and no extra boundary crossing anywhere. Almost everything here is deferring work we're already doing (by moving application selection to the last minute) and offering JS APIs to preconfigure the attach logic.
What happens is:
session.opened).new Http3Session(quicSession)to use HTTP/3. This class:Sessionreads later when it needs an Application:session->EnsureApplication(). If no application has been attached yet, that checksstate.applicationType. If there's a pending HTTP/3 flag, it attaches the HTTP/3 application. If not, it attaches the default raw QUIC application. Attachment itself is the same as before, it just happens later, and checks the state flags instead of ALPN.The actual timing of the calls to
EnsureApplication()are what make this practical: it runs immediately afterMakeCallbackfires thelisten(cb)callback for server sessions, or afterclient.openedresolves. That guarantees a synchronous+microtasks window where the handshake information is available to JS but the application hasn't attached yet. So you can do this:This is possible strictly until the MakeCallback call for this existing event returns - there's no extra tick or delay introduced.
Performance
Benchmarks on my machine show no measurable impact on either the handshake (raw or H3) or H3 request (1RTT or 0RTT) benchmarks. Varying positive/negative on different runs, but always <0.5% and no significant results.
As noted above, this doesn't change when or how we cross between JS & C++. It primarily changes when the Application attach is done, not how. The implementation isn't literally free, but doesn't do anything substantial and I don't see any real-world impact in testing.
Other related changes
node:tls), so HTTP/3 is fully opt-in.Http3Session(this is slightly contrived now, since it could work, but sets a clear model and becomes more useful later).applicationoption for QuicSessions is moved to Http3Session assettings. This exclusively supports HTTP/3 settings, and makes no sense in its current form for anything else (QUIC can't use or validate it by itself even if it wanted to).What this PR does not do
There's many things from #63995 not included here, which I would like to do later, including:
Session. That means the session has to look up and verify the supported features before every HTTP call, store bits of HTTP-only state, and general be a bit inelegant in thinking about both protocols at the same time.connectHttp3orlistenHttp3. We could create API methods like this which preconfigure the ALPN & automatically attach the HTTP/3 session, as a more convenient API sugar. I think the conclusion was we'd rather not and focus on primitives instead, but personally I don't feel strongly either way.