Repository navigation
Conversation
|
Review requested:
|
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 90.69% 90.67% -0.02%
==========================================
Files 801 802 +1
Lines 281875 282179 +304
Branches 54317 54322 +5
==========================================
+ Hits 255652 255879 +227
- Misses 16348 16417 +69
- Partials 9875 9883 +8
🚀 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:
|
f5437de to
d5c2d52
Compare
| `closed`, `destroy()`, `destroyed`, `ephemeralKeyInfo`, `onerror`, `opened`, | ||
| `peerCertificate`, `servername`, and `stats`. | ||
|
|
||
| ### `http3session.createBidirectionalStream([options])` |
There was a problem hiding this comment.
Wonder if we should go ahead and just call this request?
There was a problem hiding this comment.
Yes, definitely, but in a later PR.
I'm trying to break up the work, and one of the next steps (see #63995 (comment) for the outline) is changing the stream side to Http3Stream, and I was going to touch the stream APIs like this as part of that. I think the current API is a reasonable intermediate state and it'll make more sense in that later PR, so I'd rather defer this.
There was a problem hiding this comment.
No, for WebTransport it is not an request, but a bidirectional stream. But may be an alias request for it just for the http3 interface.
| [`session.createBidirectionalStream()`][] on the underlying session. | ||
|
|
||
| HTTP/3 has no server-initiated request streams, so calling this on a server | ||
| session throws `ERR_INVALID_STATE`. |
There was a problem hiding this comment.
This begs the question about whether we should consider a Http3ServerSession and Http3ClientSession split where this method just doesn't exist on the server. Given the idea that this is a low-level toolkit API for library authors, that might be going too far tho. Non-blocking.
There was a problem hiding this comment.
I think this could be quite useful, but I think it'll be easier to review separately in the H3 streams PR. The main (only?) real difference between H3 servers & clients is how they handle streams: clients don't need a stream callback at all, and servers don't need to open streams.
To make this possible though, and because it's a nicer API, I've changed the dynamic attach API from new Http3Session(quicSession) to Http3Session.from(quicSession) which gives us a bit more leeway for interesting games like this later where we return different types. Probably a better match for things we might do elsewhere for WebTransport too.
| * Type: {Function} | ||
|
|
||
| Called with each request stream the peer opens, as a {quic.QuicStream}. See | ||
| [`session.onstream`][]. |
There was a problem hiding this comment.
Same as for request(): I'd prefer to defer this to the stream work.
| QuicError::ForApplication(GetInternalErrorCode())); | ||
| if (!session().is_destroyed()) { | ||
| session().EmitEarlyDataRejected(); | ||
| } |
There was a problem hiding this comment.
It's not immediately obvious why this is removed here.
There was a problem hiding this comment.
Fair point. The original motivation was that in a previous design, EnsureApplication() was called for streams but not datagrams (as applications aren't involved in sending datagrams, for now) which meant you could get a 0RTT rejection with no application, which would break this behaviour as it's defined inside the application.
To fix that, I moved the logic into Session (session.cc:1647) so it doesn't need an application at all to fire this. It was duplicated in both two applications, and it's just generic QUIC logic: if early data is rejected, we should fire the session event. Doesn't matter which application is installed.
Then, before I opened the PR, I added EnsureApplication() to sending datagrams as well to match streaming. Even if not strictly necessary, it seems more consistent and prepares for H3 datagram support later, and I can't imagine a case where you want to start sending datagrams on a session before you know what protocol it's using.
That means this change isn't strictly required any more - the motivating bug no longer applies. Could revert it, but tbh I think it's an improvement anyway - it's just a couple of lines and deduplicates code from both applications that should really live in the Session imo. Can revert instead if you disagree.
| // Not installed yet. If an attach has been scheduled, its settings are | ||
| // already known and can be reported before the install happens. | ||
| if ((session->application_type() & ~Application::kTypePending) != | ||
| static_cast<uint8_t>(Application::Type::HTTP3)) { |
There was a problem hiding this comment.
We should add a TODO in here to see if we can generalize this a bit more for when/if we have additional built-in applications beyond http3. Not critical to do now tho.
There was a problem hiding this comment.
Definitely agree there's more to do here. There's a later step in the plan to clean up settings API, aiming to migrate those into the HTTP/3 session API so this goes away. It's very awkward today as a generic API (hence the per-type if we need here) because it's fundamentally not something that's defined in QUIC at all.
Initially I did that inline in this PR but it's a bit fiddly, so I've avoided making settings API changes here (it changes settings internals, but just enough to the minimum required to make the session creation API functional) and we can work on h3 settings in parallel with the h3 streams API, once we have the session structure pinned down.
|
I am finished with the review. |
|
Thank @martenrichter! I've answered all the ones where I have answers, most of the rest are indeed things to fix, I'll do that shortly. Yes, sorry for all the delay here, between the security carnage this week and a conference I had to prep for I've been off QUIC all week, but I'm on a plane back to normality as we speak (hopefully) so I'll resolve the other points & rebase this this week. |
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>
…tocol First pass of the simple mechanical changes from review
Signed-off-by: Tim Perry <pimterry@gmail.com>
And a little related cleanup en route Signed-off-by: Tim Perry <pimterry@gmail.com>
Signed-off-by: Tim Perry <pimterry@gmail.com>
Use static functions & private methods instead of symbol keys, brand-check consistently throughout, ensure Http3Session callbacks aren't affected by user-patchable accessors, cache lazily loaded http3 module, and make connection.applicationOptions consistently null when unset (was previously null/undefined, and undocumented as such). Signed-off-by: Tim Perry <pimterry@gmail.com>
Signed-off-by: Tim Perry <pimterry@gmail.com>
Signed-off-by: Tim Perry <pimterry@gmail.com>
e24abc9 to
3165b8c
Compare
Signed-off-by: Tim Perry <pimterry@gmail.com>
|
I think I've handled all the outstanding comments here, and this is now rebased on top of the latest changes and ready to review again. I've resolved every comment that's either handled or now irrelevant (due to the connection+session redesign) so the open comments mostly cover work I think we should punt to a future PR, and the open session/connection naming question. There may be other small bits, but hopefully we've resolved most of the practical details. I think the main remaining question is whether @jasnell (and everybody else) is happy with the overall API design of connection + h3/quic session + PR description is up to date if you want a fresh summary of everything. |
This extracts dynamic application attach from #63995. This is still very much under debate, so I'll try to explain thoroughly:
(EDIT: updated to the connection/session distinction and updated
autoStartdesign)The visible API changes
QuicConnection: this represents the core QUIC transport state and lifecycle - but not the application protocol communication on top.QuicSessionandHttp3Session) which each represent the application protocol on top of the transport, and expose the protocol-relevant stream/datagram APIs for that.autoStart: true, the default, same asmaintoday) or it's started manually after settingautoStart: false.Http3Session.start(conn)orQuicSession.start(conn). You must do this before the end of the tick when the connection is exposed (the tick when the server onsession callback fires, or the clientopenedpromise resolves). If you don't, the connection is closed automatically.session.connection(but my expectation with the design here is that you mostly don't need to).autoStartchanges the type of the clientconnect()call/server onsession callback: if you autoStart, it always returns/passes the started session instance - in practice, this behaves very similar tomain. If you setautoStart: false, it always returns/passes aQuicConnectioninstead.alpnis now a required option. Even withautoStart, I really don't think we should default to HTTP/3 in an API that isn't callednode:http3. In practice with autoStartconnect({ alpn: 'h3' })is now all you need to specify to get an Http3Session back, and that seems reasonable to me.Changes
This implement this, this PR splits
QuicSessionintoQuicConnection(lifecycle APIs, TLS & transport fields), an internalQuicSessionBasebase class (the common parts of the app session APIs: mirroring some very common lifecycle APIs from the connection, some callback handling, mirroring datagram APIs for now) andQuicSession/Http3Session(inheriting fromQuicSessionBase, exposing the protocol-specific APIs).This doesn't change the C++ internal structure: most notably it doesn't refactor them to match the changed JS API, so C++ still just has
Session+Application, which JS exposes asQuicConnection(part of the native session API) +Quic/Http3Session(the rest of the native session API, tightly integrated with the application behaviour). I've left that as is for now since it works and it keeps the diff manageable, but we could clean up to clarify this later.There are some other smaller C++ changes though to support the actual change: exposing a manual application start API (
StartApplication), making various APIs work if no session has been started yet during manual start (meaning no application is present), and shutting down the connection if there's no session started by the deadline (RequireApplication).There was a complicated optimization here before, which is now gone: deferring manual session start so that it just set a state field from JS, and then the start happened later on when the application was actually required. This avoided the JS->native call, but added lots of complexity. Now that this is just opt-in for specialist use cases I don't think it's worth it - better to skip it & simplify.
Why add
dynamic attachmanual session starth3-*. It's perfectly valid to do this - if you agree onh3via ALPN you should speak HTTP/3, but the inverse is not required.Demo
In both cases, manually starting the session is possible strictly until the MakeCallback call for this existing event returns - there's no extra tick or delay introduced. If you don't do it by the, the connection is closed automatically and later calls fail.
Performance
Still no measureable impact I can see in either
autoStart: trueorautoStart: falsecases, even despite removing the optimization.What this PR does not do
There's many things not included here:
quic.session.*). Debatable, happy to change more..