Repository navigation
quic: defer server session emit until TLS ClientHello is processed #64132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -184,6 +184,8 @@ uint64_t MaxDatagramPayload(uint64_t max_frame_size) { | |
| #define SESSION_JS_METHODS(V) \ | ||
| V(Destroy, destroy, SIDE_EFFECT) \ | ||
| V(GetRemoteAddress, getRemoteAddress, NO_SIDE_EFFECT) \ | ||
| V(GetServername, getServername, NO_SIDE_EFFECT) \ | ||
| V(GetAlpnProtocol, getAlpnProtocol, NO_SIDE_EFFECT) \ | ||
| V(GetLocalAddress, getLocalAddress, NO_SIDE_EFFECT) \ | ||
| V(GetCertificate, getCertificate, NO_SIDE_EFFECT) \ | ||
| V(GetEphemeralKeyInfo, getEphemeralKey, NO_SIDE_EFFECT) \ | ||
|
|
@@ -781,6 +783,8 @@ struct Session::Impl final : public MemoryRetainer { | |
| SocketAddress remote_address_; | ||
| std::unique_ptr<Application> application_; | ||
| StreamsMap streams_; | ||
| // Emits deferred until after session setup is completed | ||
| std::vector<std::function<void()>> deferred_emits_; | ||
| TimerWrapHandle timer_; | ||
| size_t send_scope_depth_ = 0; | ||
| QuicError last_error_; | ||
|
|
@@ -1001,6 +1005,36 @@ struct Session::Impl final : public MemoryRetainer { | |
| session->Destroy(); | ||
| } | ||
|
|
||
| // The SNI servername from the TLS handshake; empty (-> undefined) only if | ||
| // none was sent. | ||
| JS_METHOD(GetServername) { | ||
| auto env = Environment::GetCurrent(args); | ||
| Session* session; | ||
| ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); | ||
| if (session->is_destroyed()) return; | ||
| auto sn = session->tls_session().servername(); | ||
| if (sn.empty()) return; | ||
| Local<Value> ret; | ||
| if (ToV8Value(env->context(), sn).ToLocal(&ret)) { | ||
| args.GetReturnValue().Set(ret); | ||
| } | ||
| } | ||
|
|
||
| // The negotiated ALPN protocol. Undefined only for clients before the | ||
| // handshake is completed, as ALPN is mandatory for QUIC. | ||
| JS_METHOD(GetAlpnProtocol) { | ||
| auto env = Environment::GetCurrent(args); | ||
| Session* session; | ||
| ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); | ||
| if (session->is_destroyed()) return; | ||
| auto proto = session->tls_session().protocol(); | ||
| if (proto.empty()) return; | ||
| Local<Value> ret; | ||
| if (ToV8Value(env->context(), proto).ToLocal(&ret)) { | ||
| args.GetReturnValue().Set(ret); | ||
| } | ||
| } | ||
|
|
||
| JS_METHOD(GetRemoteAddress) { | ||
| auto env = Environment::GetCurrent(args); | ||
| Session* session; | ||
|
|
@@ -2190,6 +2224,12 @@ const Session::Options& Session::options() const { | |
| void Session::EmitQlog(uint32_t flags, std::string_view data) { | ||
| if (!env()->can_call_into_js()) return; | ||
|
|
||
| if (!is_destroyed() && must_defer_emits()) { | ||
| QueueDeferredEmit( | ||
| [this, flags, held = std::string(data)]() { EmitQlog(flags, held); }); | ||
| return; | ||
| } | ||
|
|
||
| bool fin = (flags & NGTCP2_QLOG_WRITE_FLAG_FIN) != 0; | ||
|
|
||
| // Fun fact... ngtcp2 does not emit the final qlog statement until the | ||
|
|
@@ -2312,6 +2352,16 @@ bool Session::ReadPacket(const uint8_t* data, | |
| // Process deferred operations that couldn't run inside callback | ||
| // scopes (e.g., HTTP/3 GOAWAY handling that calls into JS). | ||
| application().PostReceive(); | ||
| // Surface a server session to JS once its ClientHello has been | ||
| // processed (OnSelectAlpn fired: SNI + ALPN are known and reliable). | ||
| // Held first-flight events - including 0-RTT request streams - replay | ||
| // at emit. The !wrapped guard makes this fire exactly once, on | ||
| // whichever packet completes the ClientHello (so a multi-datagram | ||
| // ClientHello is handled correctly). | ||
| if (is_server() && hello_processed_ && !impl_->state()->wrapped && | ||
| !is_destroyed()) { | ||
| endpoint().EmitNewSession(BaseObjectPtr<Session>(this)); | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
@@ -2975,6 +3025,30 @@ void Session::set_wrapped() { | |
| impl_->state()->wrapped = 1; | ||
| } | ||
|
|
||
| bool Session::must_defer_emits() const { | ||
| // Server sessions are surfaced to JS (via the deferred new-session emit) | ||
| // only after the ClientHello has been processed and wrapped; anything | ||
| // emitted before then has no JS wrapper to receive it and must be held | ||
| // for replay. | ||
| return is_server() && !impl_->state()->wrapped; | ||
| } | ||
|
|
||
| void Session::QueueDeferredEmit(std::function<void()> fn) { | ||
| impl_->deferred_emits_.emplace_back(std::move(fn)); | ||
| } | ||
|
|
||
| void Session::ReplayDeferredEmits() { | ||
| if (is_destroyed()) return; | ||
| DCHECK(impl_->state()->wrapped); | ||
| // Runs synchronously immediately after the new-session callback | ||
| // returns (still within first-flight processing). | ||
| auto emits = std::move(impl_->deferred_emits_); | ||
| for (auto& emit : emits) { | ||
| if (is_destroyed()) return; | ||
| emit(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since each of these is a C++-to-JavaScript call, I wonder if it would be possible to batch them to a new callback. Essentially, rather than enqueuing a function, enqueue the arguments and type of emit, send them all to JavaScript at once and process each one there instead. Calls from C++-to-JavaScript can be a bit expensive to set up.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be looked at later tho.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've left this for now. It looks likely to be a little fiddly, and I think it rarely comes up (just a couple of 0RTT events, or keylog/qlog when they're enabled) but we can explore as an optimization later. |
||
| } | ||
| } | ||
|
|
||
| void Session::set_priority_supported(bool on) { | ||
| DCHECK(!is_destroyed()); | ||
| impl_->state()->priority_supported = on ? 1 : 0; | ||
|
|
@@ -3284,6 +3358,10 @@ bool Session::HandshakeCompleted() { | |
|
|
||
| Debug(this, "Session handshake completed"); | ||
| impl_->state()->handshake_completed = 1; | ||
| // This implies fully completing a handshake without setting hello_processed | ||
| // (set during ALPN negotiation). Should be impossible unless ALPN flow is | ||
| // changed drastically, but good to check as it'd lose sessions. | ||
| DCHECK(!is_server() || hello_processed_); | ||
|
|
||
| STAT_RECORD_TIMESTAMP(Stats, handshake_completed_at); | ||
| SetStreamOpenAllowed(); | ||
|
|
@@ -3482,6 +3560,7 @@ void Session::set_max_datagram_size(uint16_t size) { | |
|
|
||
| void Session::EmitGoaway(stream_id last_stream_id) { | ||
| if (is_destroyed()) return; | ||
| if (DeferEmit([this, last_stream_id] { EmitGoaway(last_stream_id); })) return; | ||
| if (!env()->can_call_into_js()) return; | ||
|
|
||
| CallbackScope<Session> cb_scope(this); | ||
|
|
@@ -3496,6 +3575,14 @@ void Session::EmitGoaway(stream_id last_stream_id) { | |
|
|
||
| void Session::EmitDatagram(Store&& datagram, DatagramReceivedFlags flag) { | ||
| DCHECK(!is_destroyed()); | ||
|
|
||
| if (must_defer_emits()) { | ||
| QueueDeferredEmit([this, datagram = std::move(datagram), flag]() mutable { | ||
| EmitDatagram(std::move(datagram), flag); | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| if (!env()->can_call_into_js()) return; | ||
|
|
||
| CallbackScope<Session> cbv_scope(this); | ||
|
|
@@ -3511,6 +3598,8 @@ void Session::EmitDatagram(Store&& datagram, DatagramReceivedFlags flag) { | |
| void Session::EmitDatagramStatus(datagram_id id, quic::DatagramStatus status) { | ||
| DCHECK(!is_destroyed()); | ||
|
|
||
| if (DeferEmit([this, id, status] { EmitDatagramStatus(id, status); })) return; | ||
|
|
||
| if (!env()->can_call_into_js()) return; | ||
|
|
||
| CallbackScope<Session> cb_scope(this); | ||
|
|
@@ -3672,6 +3761,7 @@ void Session::EmitSessionTicket(Store&& ticket) { | |
|
|
||
| void Session::EmitApplication() { | ||
| if (is_destroyed()) return; | ||
| if (DeferEmit([this] { EmitApplication(); })) return; | ||
| if (!env()->can_call_into_js()) return; | ||
|
|
||
| if (!has_application()) { | ||
|
|
@@ -3742,6 +3832,10 @@ void Session::EmitNewToken(const uint8_t* token, size_t len) { | |
| void Session::EmitStream(const BaseObjectWeakPtr<Stream>& stream) { | ||
| DCHECK(!is_destroyed()); | ||
|
|
||
| if (DeferEmit([this, stream] { EmitStream(stream); })) return; | ||
|
|
||
| if (!stream) return; | ||
|
|
||
| if (!env()->can_call_into_js()) return; | ||
| CallbackScope<Session> cb_scope(this); | ||
|
|
||
|
|
@@ -3797,6 +3891,14 @@ void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, | |
|
|
||
| void Session::EmitOrigins(std::vector<std::string>&& origins) { | ||
| DCHECK(!is_destroyed()); | ||
|
|
||
| if (must_defer_emits()) { | ||
| QueueDeferredEmit([this, origins = std::move(origins)]() mutable { | ||
| EmitOrigins(std::move(origins)); | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| if (!HasListenerFlag(impl_->state()->listener_flags, | ||
| SessionListenerFlags::ORIGIN)) | ||
| return; | ||
|
|
@@ -3822,11 +3924,17 @@ void Session::EmitOrigins(std::vector<std::string>&& origins) { | |
|
|
||
| void Session::EmitKeylog(const char* line) { | ||
| DCHECK(!is_destroyed()); | ||
|
|
||
| if (must_defer_emits()) { | ||
| QueueDeferredEmit( | ||
| [this, str = std::string(line)]() { EmitKeylog(str.c_str()); }); | ||
| return; | ||
| } | ||
|
|
||
| if (!env()->can_call_into_js()) return; | ||
|
|
||
| auto str = std::string(line); | ||
| Local<Value> argv[] = {Undefined(env()->isolate())}; | ||
| if (!ToV8Value(env()->context(), str).ToLocal(&argv[0])) { | ||
| if (!ToV8Value(env()->context(), std::string(line)).ToLocal(&argv[0])) { | ||
| Debug(this, "Failed to convert keylog line to V8 string"); | ||
| return; | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.