Loading debian/changelog +19 −0 Original line number Diff line number Diff line libnetplus (20260505+10) unstable; urgency=medium * QUIC: fix FIN-only STREAM frame never sent on the wire — when sendStreamData(sid, nullptr, 0, true) was called after prior data sends, the main send loop did not execute (send_len == 0) and no packet carrying the FIN bit was ever emitted. The local stream state was marked send_fin=true but the peer never received the FIN, causing the remote side to wait indefinitely for stream completion. Fix: emit an empty STREAM frame with FIN+OFF when send_len == 0. * test: add RFC 9000 QUIC transport compliance test suite (quic_rfc9000_test) — 43 tests covering connection state machine, version negotiation, connection IDs, cipher selection, bidirectional and unidirectional stream types, FIN handling (inline, separate, empty), data integrity (1 B to 1 MB echo), connection-level and stream-level flow control, concurrent streams, rapid stream creation (MAX_STREAMS), and idle survival -- Jan Koester <jan.koester@tuxist.de> Mon, 05 May 2026 14:00:00 +0200 libnetplus (20260505+9) unstable; urgency=medium * QUIC: fix connection-level flow control double-counting — _data_recv Loading src/quic.cpp +22 −27 Original line number Diff line number Diff line Loading @@ -4724,33 +4724,6 @@ size_t quic::sendData(buffer& data, int flags) { sent_total += chunk; } // FIN-only: send an empty STREAM frame with FIN when there's no data // (e.g. sendStreamData(sid, nullptr, 0, true) after prior data sends). // The main loop doesn't execute when send_len == 0, so handle it here. if (send_len == 0 && fin && !stream.send_fin) { // Build STREAM frame: type=0x09 (FIN + no LEN), OFF bit if offset > 0 std::vector<uint8_t> stream_frame; uint8_t frame_type = 0x08 | 0x01; // FIN bit if (stream.send_offset > 0) frame_type |= 0x04; // OFF bit stream_frame.push_back(frame_type); uint8_t vbuf[8]; size_t vlen = encodeVarInt(stream_id, vbuf); stream_frame.insert(stream_frame.end(), vbuf, vbuf + vlen); if (stream.send_offset > 0) { vlen = encodeVarInt(stream.send_offset, vbuf); stream_frame.insert(stream_frame.end(), vbuf, vbuf + vlen); } // No data, no LEN — just the FIN flag uint64_t pn_before = _app_pn_send; std::vector<uint8_t> packet = buildShortHeaderPacket(stream_frame); const uint8_t* hp_key = _is_server ? _app_hp_server : _app_hp_client; applyHeaderProtection(packet, hp_key); sendPacket(packet.data(), packet.size()); recordSentPacket(pn_before, stream_id, stream.send_offset, nullptr, 0, true); stream.send_fin = true; } return sent_total; } Loading Loading @@ -5026,6 +4999,28 @@ size_t quic::sendStreamData(uint64_t stream_id, const uint8_t* data, size_t len, // Only mark FIN if we actually sent all requested data (FIN is on the last chunk). // If CC broke the loop early, the FIN was never put on the wire. if (fin && sent_total >= send_len) { // FIN-only: when send_len == 0, the main loop never ran, so we must // send an empty STREAM frame with the FIN bit on the wire. if (send_len == 0 && !stream.send_fin) { uint8_t frame_type = 0x08 | 0x01; // STREAM + FIN if (stream.send_offset > 0) frame_type |= 0x04; // OFF bit std::vector<uint8_t> fin_frame; fin_frame.push_back(frame_type); uint8_t vbuf[8]; size_t vlen = encodeVarInt(stream_id, vbuf); fin_frame.insert(fin_frame.end(), vbuf, vbuf + vlen); if (stream.send_offset > 0) { vlen = encodeVarInt(stream.send_offset, vbuf); fin_frame.insert(fin_frame.end(), vbuf, vbuf + vlen); } uint64_t pn = _app_pn_send; std::vector<uint8_t> packet = buildShortHeaderPacket(fin_frame); const uint8_t* hp_key = _is_server ? _app_hp_server : _app_hp_client; applyHeaderProtection(packet, hp_key); sendPacket(packet.data(), packet.size()); recordSentPacket(pn, stream_id, stream.send_offset, nullptr, 0, true); } stream.send_fin = true; // Auto-cleanup: both sides FIN'd → erase stream & replenish peer's stream budget if (stream.recv_fin) { Loading Loading
debian/changelog +19 −0 Original line number Diff line number Diff line libnetplus (20260505+10) unstable; urgency=medium * QUIC: fix FIN-only STREAM frame never sent on the wire — when sendStreamData(sid, nullptr, 0, true) was called after prior data sends, the main send loop did not execute (send_len == 0) and no packet carrying the FIN bit was ever emitted. The local stream state was marked send_fin=true but the peer never received the FIN, causing the remote side to wait indefinitely for stream completion. Fix: emit an empty STREAM frame with FIN+OFF when send_len == 0. * test: add RFC 9000 QUIC transport compliance test suite (quic_rfc9000_test) — 43 tests covering connection state machine, version negotiation, connection IDs, cipher selection, bidirectional and unidirectional stream types, FIN handling (inline, separate, empty), data integrity (1 B to 1 MB echo), connection-level and stream-level flow control, concurrent streams, rapid stream creation (MAX_STREAMS), and idle survival -- Jan Koester <jan.koester@tuxist.de> Mon, 05 May 2026 14:00:00 +0200 libnetplus (20260505+9) unstable; urgency=medium * QUIC: fix connection-level flow control double-counting — _data_recv Loading
src/quic.cpp +22 −27 Original line number Diff line number Diff line Loading @@ -4724,33 +4724,6 @@ size_t quic::sendData(buffer& data, int flags) { sent_total += chunk; } // FIN-only: send an empty STREAM frame with FIN when there's no data // (e.g. sendStreamData(sid, nullptr, 0, true) after prior data sends). // The main loop doesn't execute when send_len == 0, so handle it here. if (send_len == 0 && fin && !stream.send_fin) { // Build STREAM frame: type=0x09 (FIN + no LEN), OFF bit if offset > 0 std::vector<uint8_t> stream_frame; uint8_t frame_type = 0x08 | 0x01; // FIN bit if (stream.send_offset > 0) frame_type |= 0x04; // OFF bit stream_frame.push_back(frame_type); uint8_t vbuf[8]; size_t vlen = encodeVarInt(stream_id, vbuf); stream_frame.insert(stream_frame.end(), vbuf, vbuf + vlen); if (stream.send_offset > 0) { vlen = encodeVarInt(stream.send_offset, vbuf); stream_frame.insert(stream_frame.end(), vbuf, vbuf + vlen); } // No data, no LEN — just the FIN flag uint64_t pn_before = _app_pn_send; std::vector<uint8_t> packet = buildShortHeaderPacket(stream_frame); const uint8_t* hp_key = _is_server ? _app_hp_server : _app_hp_client; applyHeaderProtection(packet, hp_key); sendPacket(packet.data(), packet.size()); recordSentPacket(pn_before, stream_id, stream.send_offset, nullptr, 0, true); stream.send_fin = true; } return sent_total; } Loading Loading @@ -5026,6 +4999,28 @@ size_t quic::sendStreamData(uint64_t stream_id, const uint8_t* data, size_t len, // Only mark FIN if we actually sent all requested data (FIN is on the last chunk). // If CC broke the loop early, the FIN was never put on the wire. if (fin && sent_total >= send_len) { // FIN-only: when send_len == 0, the main loop never ran, so we must // send an empty STREAM frame with the FIN bit on the wire. if (send_len == 0 && !stream.send_fin) { uint8_t frame_type = 0x08 | 0x01; // STREAM + FIN if (stream.send_offset > 0) frame_type |= 0x04; // OFF bit std::vector<uint8_t> fin_frame; fin_frame.push_back(frame_type); uint8_t vbuf[8]; size_t vlen = encodeVarInt(stream_id, vbuf); fin_frame.insert(fin_frame.end(), vbuf, vbuf + vlen); if (stream.send_offset > 0) { vlen = encodeVarInt(stream.send_offset, vbuf); fin_frame.insert(fin_frame.end(), vbuf, vbuf + vlen); } uint64_t pn = _app_pn_send; std::vector<uint8_t> packet = buildShortHeaderPacket(fin_frame); const uint8_t* hp_key = _is_server ? _app_hp_server : _app_hp_client; applyHeaderProtection(packet, hp_key); sendPacket(packet.data(), packet.size()); recordSentPacket(pn, stream_id, stream.send_offset, nullptr, 0, true); } stream.send_fin = true; // Auto-cleanup: both sides FIN'd → erase stream & replenish peer's stream budget if (stream.recv_fin) { Loading