Skip to content

Commit 9560084

Browse files
jasnelladuh95
authored andcommitted
quic: add support for future ECN marking
Set up for when libuv eventually supports ECN marking. Pass the ECN marking stuff into ngtcp2. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: #63267 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 2b3ff8a commit 9560084

6 files changed

Lines changed: 77 additions & 24 deletions

File tree

src/quic/application.cc

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,12 @@ ssize_t Session::Application::TryWritePendingDatagram(PathStorage* path,
262262
int accepted = 0;
263263
int dg_flags = NGTCP2_WRITE_DATAGRAM_FLAG_MORE;
264264

265+
// PacketInfo for the datagram path. When libuv gains per-socket ECN
266+
// marking, the value from ngtcp2 should be forwarded to the send path.
267+
PacketInfo dg_pi;
265268
ssize_t dg_nwrite = ngtcp2_conn_writev_datagram(*session_,
266269
&path->path,
267-
nullptr,
270+
dg_pi,
268271
dest,
269272
destlen,
270273
&accepted,
@@ -390,12 +393,14 @@ void Session::Application::SendPendingData() {
390393
};
391394

392395
// Accumulate a completed packet into the batch.
393-
auto enqueue_packet = [&](Packet::Ptr& pkt, size_t len) {
394-
Debug(session_, "Enqueuing packet with %zu bytes into batch", len);
395-
pkt->Truncate(len);
396-
path.CopyTo(&batch_paths[batch_count]);
397-
batch[batch_count++] = std::move(pkt);
398-
};
396+
auto enqueue_packet =
397+
[&](Packet::Ptr& pkt, size_t len, const PacketInfo& pi) {
398+
Debug(session_, "Enqueuing packet with %zu bytes into batch", len);
399+
pkt->Truncate(len);
400+
pkt->set_pkt_info(pi);
401+
path.CopyTo(&batch_paths[batch_count]);
402+
batch[batch_count++] = std::move(pkt);
403+
};
399404

400405
// We're going to enter a loop here to prepare and send no more than
401406
// max_packet_count packets.
@@ -434,8 +439,9 @@ void Session::Application::SendPendingData() {
434439
}
435440

436441
// Awesome, let's write our packet!
442+
PacketInfo pi;
437443
ssize_t nwrite = WriteVStream(
438-
&path, packet->data(), &ndatalen, packet->length(), stream_data);
444+
&path, &pi, packet->data(), &ndatalen, packet->length(), stream_data);
439445

440446
// When ndatalen is > 0, that's our indication that stream data was accepted
441447
// in to the packet. Yay!
@@ -531,7 +537,7 @@ void Session::Application::SendPendingData() {
531537
if (result > 0) {
532538
size_t len = result;
533539
Debug(session_, "Sending packet with %zu bytes", len);
534-
enqueue_packet(packet, len);
540+
enqueue_packet(packet, len, pi);
535541
if (++packet_send_count == max_packet_count) return;
536542
} else if (result < 0) {
537543
// Any negative result other than NGTCP2_ERR_WRITE_MORE
@@ -568,7 +574,7 @@ void Session::Application::SendPendingData() {
568574
// is the size of the packet we are sending.
569575
size_t len = nwrite;
570576
Debug(session_, "Sending packet with %zu bytes", len);
571-
enqueue_packet(packet, len);
577+
enqueue_packet(packet, len, pi);
572578
if (++packet_send_count == max_packet_count) return;
573579

574580
// If there are pending datagrams, try sending them in a fresh packet.
@@ -587,7 +593,7 @@ void Session::Application::SendPendingData() {
587593
TryWritePendingDatagram(&path, packet->data(), packet->length());
588594
if (result > 0) {
589595
Debug(session_, "Sending datagram packet with %zd bytes", result);
590-
enqueue_packet(packet, static_cast<size_t>(result));
596+
enqueue_packet(packet, static_cast<size_t>(result), PacketInfo());
591597
if (++packet_send_count == max_packet_count) return;
592598
} else if (result < 0 && result != NGTCP2_ERR_WRITE_MORE) {
593599
// Fatal error — session already closed by TryWritePendingDatagram.
@@ -600,17 +606,20 @@ void Session::Application::SendPendingData() {
600606
}
601607

602608
ssize_t Session::Application::WriteVStream(PathStorage* path,
609+
PacketInfo* pi,
603610
uint8_t* dest,
604611
ssize_t* ndatalen,
605612
size_t max_packet_size,
606613
const StreamData& stream_data) {
607614
DCHECK_LE(stream_data.count, kMaxVectorCount);
608615
uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE;
609616
if (stream_data.fin) flags |= NGTCP2_WRITE_STREAM_FLAG_FIN;
617+
// The PacketInfo out-param is populated by ngtcp2 with the ECN codepoint
618+
// to apply when sending this packet. When libuv gains per-socket ECN
619+
// marking, the value should be forwarded to the send path.
610620
return ngtcp2_conn_writev_stream(*session_,
611621
&path->path,
612-
// TODO(@jasnell): ECN blocked on libuv
613-
nullptr,
622+
*pi,
614623
dest,
615624
max_packet_size,
616625
ndatalen,

src/quic/application.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,11 @@ class Session::Application : public MemoryRetainer {
269269
uint8_t* dest,
270270
size_t destlen);
271271

272-
// Write the given stream_data into the buffer.
272+
// Write the given stream_data into the buffer. The PacketInfo out-param
273+
// is populated by ngtcp2 with per-packet metadata (e.g., ECN codepoint)
274+
// that should be applied when sending the packet.
273275
ssize_t WriteVStream(PathStorage* path,
276+
PacketInfo* pi,
274277
uint8_t* buf,
275278
ssize_t* ndatalen,
276279
size_t max_packet_size,

src/quic/data.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,40 @@ namespace node::quic {
1919
template <typename T>
2020
concept OneByteType = sizeof(T) == 1;
2121

22+
// Lightweight wrapper around ngtcp2_pkt_info. Insulates the Node.js QUIC
23+
// code from the ngtcp2 struct layout and provides a clean API boundary
24+
// for per-packet metadata (currently ECN codepoint; may grow as ngtcp2
25+
// and libuv evolve).
26+
//
27+
// Default-constructed PacketInfo is zero-initialized, which ngtcp2 treats
28+
// as ECN Not-ECT — identical to passing nullptr for the pkt_info parameter.
29+
class PacketInfo final {
30+
public:
31+
// ECN codepoints as defined by RFC 3168.
32+
enum class Ecn : uint32_t {
33+
NOT_ECT = 0, // Not ECN-Capable Transport
34+
ECT_1 = 1, // ECN-Capable Transport(1)
35+
ECT_0 = 2, // ECN-Capable Transport(0)
36+
CE = 3, // Congestion Experienced
37+
};
38+
39+
PacketInfo() : info_{} {}
40+
explicit PacketInfo(const ngtcp2_pkt_info& info) : info_(info) {}
41+
42+
// ECN codepoint for this packet. When libuv gains per-packet ECN
43+
// reporting, populate via set_ecn() from the receive metadata
44+
// before passing to ReadPacket().
45+
Ecn ecn() const { return static_cast<Ecn>(info_.ecn); }
46+
void set_ecn(Ecn ecn) { info_.ecn = static_cast<uint32_t>(ecn); }
47+
48+
// Conversion operators for ngtcp2 API calls.
49+
operator const ngtcp2_pkt_info*() const { return &info_; }
50+
operator ngtcp2_pkt_info*() { return &info_; }
51+
52+
private:
53+
ngtcp2_pkt_info info_;
54+
};
55+
2256
struct Path final : public ngtcp2_path {
2357
explicit Path(const SocketAddress& local, const SocketAddress& remote);
2458
Path(Path&& other) noexcept = default;

src/quic/packet.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ class Packet final {
6868
size_t length() const { return length_; }
6969
size_t capacity() const { return capacity_; }
7070
const SocketAddress& destination() const { return destination_; }
71+
const PacketInfo& pkt_info() const { return pkt_info_; }
72+
void set_pkt_info(const PacketInfo& pi) { pkt_info_ = pi; }
7173
Listener* listener() const { return listener_; }
7274

7375
// Redirect the packet to a different endpoint for cross-endpoint sends
@@ -148,6 +150,7 @@ class Packet final {
148150
Listener* listener_;
149151

150152
// Touched at send time.
153+
PacketInfo pkt_info_;
151154
SocketAddress destination_;
152155

153156
// Only touched by libuv during uv_udp_send and in the send callback.

src/quic/session.cc

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2103,19 +2103,21 @@ void Session::SetLastError(QuicError&& error) {
21032103

21042104
bool Session::Receive(Store&& store,
21052105
const SocketAddress& local_address,
2106-
const SocketAddress& remote_address) {
2106+
const SocketAddress& remote_address,
2107+
const PacketInfo& pkt_info) {
21072108
// Convenience wrapper: reads the packet and immediately triggers
21082109
// SendPendingData. Used by paths that need an immediate response
21092110
// (e.g., Endpoint::Connect for client Initial packets).
21102111
// The hot receive path uses ReadPacket() directly with deferred
21112112
// flush via BindingData's uv_check callback.
21122113
SendPendingDataScope send_scope(this);
2113-
return ReadPacket(std::move(store), local_address, remote_address);
2114+
return ReadPacket(std::move(store), local_address, remote_address, pkt_info);
21142115
}
21152116

21162117
bool Session::ReadPacket(Store&& store,
21172118
const SocketAddress& local_address,
2118-
const SocketAddress& remote_address) {
2119+
const SocketAddress& remote_address,
2120+
const PacketInfo& pkt_info) {
21192121
DCHECK(!is_destroyed());
21202122
impl_->remote_address_ = remote_address;
21212123

@@ -2137,12 +2139,12 @@ bool Session::ReadPacket(Store&& store,
21372139
int err;
21382140
{
21392141
NgTcp2CallbackScope callback_scope(this);
2140-
// ECN codepoint (ngtcp2_pkt_info.ecn) is not yet populated because
2141-
// libuv does not currently deliver per-packet ECN metadata. When
2142-
// libuv gains ECN receive reporting, the pkt_info should be
2143-
// populated from the per-packet metadata and passed through here.
2142+
// The PacketInfo carries per-packet metadata (currently ECN codepoint).
2143+
// When libuv gains per-packet ECN reporting, the caller should
2144+
// populate pkt_info from the receive metadata before calling
2145+
// ReadPacket().
21442146
err = ngtcp2_conn_read_pkt(
2145-
*this, &path, nullptr, vec.base, vec.len, uv_hrtime());
2147+
*this, &path, pkt_info, vec.base, vec.len, uv_hrtime());
21462148
}
21472149
if (is_destroyed()) return false;
21482150

src/quic/session.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
355355

356356
bool Receive(Store&& store,
357357
const SocketAddress& local_address,
358-
const SocketAddress& remote_address);
358+
const SocketAddress& remote_address,
359+
const PacketInfo& pkt_info = PacketInfo());
359360

360361
// ReadPacket processes a single inbound packet through ngtcp2 without
361362
// triggering SendPendingData. This is the building block for batched
@@ -367,7 +368,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
367368
// immediate response).
368369
bool ReadPacket(Store&& store,
369370
const SocketAddress& local_address,
370-
const SocketAddress& remote_address);
371+
const SocketAddress& remote_address,
372+
const PacketInfo& pkt_info = PacketInfo());
371373

372374
// Called by BindingData's flush callback to trigger SendPendingData
373375
// on this session. Encapsulates the application() access so that

0 commit comments

Comments
 (0)