Loading src/quic.cpp +141 −0 Original line number Diff line number Diff line Loading @@ -496,6 +496,16 @@ void quic::onPacketAckedCC(size_t packet_size, uint64_t pn) { _bytes_in_flight = 0; } if (_cc_algorithm == CongestionControlAlgorithm::BbrLite) { // Entirely separate cwnd-setting strategy — see // setCongestionControlAlgorithm()'s comment. Does not use // _ssthresh/_in_recovery/HyStart++ at all; cwnd is set directly // from the bandwidth-delay-product estimate on every ACK instead // of grown incrementally. bbrLiteUpdateCwnd(packet_size); return; } // If this packet was sent during recovery, don't grow cwnd if (_in_recovery && pn <= _recovery_start_pn) { return; Loading Loading @@ -523,6 +533,43 @@ void quic::onPacketAckedCC(size_t packet_size, uint64_t pn) { } } // BBR-lite bandwidth/RTT estimation and cwnd sizing — see // setCongestionControlAlgorithm()'s comment for scope/limitations. void quic::bbrLiteUpdateCwnd(size_t acked_bytes) { auto now = std::chrono::steady_clock::now(); // Delivery-rate sample: bytes acked since the last sample, divided by // the elapsed wall-clock time. Skipped for the very first sample // (nothing to measure an interval against yet) and for a zero/negative // interval (defensive — steady_clock is monotonic, but back-to-back // ACKs processed in the same batch can legitimately measure ~0s). if (_bbr_last_delivery_time.time_since_epoch().count() != 0) { double dt = std::chrono::duration<double>(now - _bbr_last_delivery_time).count(); if (dt > 0.0) { double bw_sample = static_cast<double>(acked_bytes) / dt; // bytes/sec _bbr_bw_samples[_bbr_bw_sample_idx] = bw_sample; _bbr_bw_sample_idx = (_bbr_bw_sample_idx + 1) % BBR_BW_WINDOW; } } _bbr_last_delivery_time = now; // Running minimum RTT — see the class-level comment on why this never // re-adapts upward (no periodic PROBE_RTT in this simplified mode). if (_latest_rtt > 0.0 && (_bbr_min_rtt < 0.0 || _latest_rtt < _bbr_min_rtt)) { _bbr_min_rtt = _latest_rtt; } double max_bw = 0.0; for (double s : _bbr_bw_samples) max_bw = std::max(max_bw, s); if (max_bw > 0.0 && _bbr_min_rtt > 0.0) { uint64_t bdp = static_cast<uint64_t>(max_bw * _bbr_min_rtt * BBR_CWND_GAIN); _cwnd = std::max(bdp, BBR_MIN_CWND); } // Else: not enough samples yet — leave cwnd at its current value // (the RFC 9002 §7.2 initial window, same starting point as NewReno). } void quic::onPacketLostCC(size_t packet_size, uint64_t pn) { if (_bytes_in_flight >= packet_size) { _bytes_in_flight -= packet_size; Loading @@ -530,6 +577,18 @@ void quic::onPacketLostCC(size_t packet_size, uint64_t pn) { _bytes_in_flight = 0; } if (_cc_algorithm == CongestionControlAlgorithm::BbrLite) { // Deliberately no explicit multiplicative decrease here (unlike // NewReno) — matching real BBR's design of not treating isolated // loss as a direct congestion signal. The bandwidth estimate in // bbrLiteUpdateCwnd() naturally reflects reduced delivery through // fewer/slower subsequent ACKs, which is this simplified mode's // only feedback path for loss; a full BBR2 additionally caps cwnd // explicitly during PROBE_BW on sustained loss, which this mode // does not implement (see setCongestionControlAlgorithm()). return; } // Enter recovery if not already in it if (!_in_recovery || pn > _recovery_start_pn) { _in_recovery = true; Loading @@ -544,7 +603,88 @@ void quic::onPacketLostCC(size_t packet_size, uint64_t pn) { // Multiplicative decrease _ssthresh = std::max(_cwnd / 2, MIN_CWND); _cwnd = _ssthresh; // A loss ends slow start outright — any in-progress HyStart++ // round-tracking (including CSS) is now moot; clear it so stale // pre-loss RTT samples can't influence a later slow-start phase // (e.g. if a future ssthresh increase ever let cwnd re-enter it). hystartReset(); } } void quic::hystartReset() { _hystart_css_active = false; _hystart_css_rounds_remaining = 0; _hystart_window_end = 0; _hystart_curr_round_min_rtt = -1.0; _hystart_last_round_min_rtt = -1.0; _hystart_rtt_sample_count = 0; } // HyStart++ (RFC 9438 §4). Called once per RTT sample (see processAckFrame()) // while cwnd is still growing via slow start or CSS; a no-op once ordinary // congestion avoidance has taken over. Tracks a per-round minimum RTT and, // on a sustained increase versus the previous round, switches from // exponential slow-start growth to Conservative Slow Start (a fraction of // the growth, applied in onPacketAckedCC()) for a bounded number of rounds // before either resuming normal slow start (the increase was transient) or // exiting into congestion avoidance for good (it wasn't). void quic::hystartOnRttSample(double rtt_sample, uint64_t acked_pn) { if (_cwnd >= _ssthresh && !_hystart_css_active) return; if (_hystart_window_end == 0) { // First sample of a fresh round (slow start just began, or this // follows a hystartReset()). _hystart_window_end = _app_pn_send; } if (_hystart_curr_round_min_rtt < 0.0 || rtt_sample < _hystart_curr_round_min_rtt) { _hystart_curr_round_min_rtt = rtt_sample; } ++_hystart_rtt_sample_count; if (acked_pn < _hystart_window_end) { return; // still within the current round } // Round boundary: this ACK covers a packet sent at/after the round's // start — evaluate the round just completed, then start the next one. bool have_enough_samples = _hystart_rtt_sample_count >= HYSTART_N_RTT_SAMPLE; double curr_min = _hystart_curr_round_min_rtt; if (!_hystart_css_active) { if (have_enough_samples && _hystart_last_round_min_rtt >= 0.0) { double thresh = std::max(HYSTART_MIN_RTT_THRESH, std::min(HYSTART_MAX_RTT_THRESH, _hystart_last_round_min_rtt / 8.0)); if (curr_min >= _hystart_last_round_min_rtt + thresh) { // Sustained RTT increase — likely queue buildup ahead of // loss. Enter Conservative Slow Start instead of continuing // to double cwnd every round. _hystart_css_active = true; _hystart_css_rounds_remaining = HYSTART_CSS_ROUNDS; _ssthresh = _cwnd; } } } else { double thresh = std::max(HYSTART_MIN_RTT_THRESH, std::min(HYSTART_MAX_RTT_THRESH, _hystart_last_round_min_rtt / 8.0)); bool still_elevated = have_enough_samples && curr_min >= _hystart_last_round_min_rtt + thresh; if (!still_elevated) { // Resolved (or inconclusive) — treat as transient, resume // ordinary exponential slow start. _hystart_css_active = false; } else if (--_hystart_css_rounds_remaining <= 0) { // Elevated for the whole CSS window — exit slow start for good. _cwnd = _ssthresh; _hystart_css_active = false; } // else: still elevated but CSS rounds remain — keep growing conservatively. } _hystart_last_round_min_rtt = curr_min; _hystart_curr_round_min_rtt = -1.0; _hystart_rtt_sample_count = 0; _hystart_window_end = _app_pn_send; } // ============================================================================ Loading Loading @@ -4806,6 +4946,7 @@ void quic::processAckFrame(const uint8_t* data, size_t len, size_t& offset, bool double adj_delay = static_cast<double>(ack_delay << _peer_ack_delay_exponent) / 1000000.0; if (rtt_sample > adj_delay) rtt_sample -= adj_delay; _latest_rtt = rtt_sample; hystartOnRttSample(rtt_sample, it->first); if (!_rtt_initialized) { _srtt = rtt_sample; _rttvar = rtt_sample / 2.0; Loading src/socket.h +42 −0 Original line number Diff line number Diff line Loading @@ -766,6 +766,27 @@ namespace netplus { // across calls (or opt out and keep today's behavior). void setIncrementalStreamDispatch(bool enable) { _incremental_stream_dispatch = enable; } // Congestion control strategy for this connection. Default // (NewReno) is the existing, unchanged RFC-9002-conformant AIMD // behavior. BbrLite is an experimental, opt-in bandwidth-delay- // product based alternative (state declared further below, // implementation in onPacketAckedCC()/onPacketLostCC()/ // cwndAllowsSend()) — deliberately NOT a full BBR2 implementation: // it has no send pacing (packets are still gated purely by cwnd, // same as NewReno — a real BBR pacer smooths sends across an RTT // instead, which this mode does not do), no gain-cycling PROBE_BW // phase, and no periodic PROBE_RTT re-measurement (min RTT is a // simple running minimum for the connection's lifetime, so it can // only ever go down, never adapt back up if the path's true min // RTT increases). It's a real, working starting point for // experimenting with BDP-based cwnd sizing on high-BDP/lossy // paths, not a drop-in production BBR2 replacement — that needs // dedicated pacing infrastructure and validation against real // variable-bandwidth networks that this project's test suite // (loopback only) cannot exercise. Call before the connection // starts sending data. enum class CongestionControlAlgorithm { NewReno, BbrLite }; void setCongestionControlAlgorithm(CongestionControlAlgorithm algo) { _cc_algorithm = algo; } // Send data on a stream (vector overload for application use) void sendStreamData(uint64_t stream_id, const std::vector<uint8_t>& data, bool fin); Loading Loading @@ -1592,6 +1613,27 @@ namespace netplus { void onPacketAckedCC(size_t packet_size, uint64_t pn); void onPacketLostCC(size_t packet_size, uint64_t pn); // ---- BBR-lite: opt-in BDP-based cwnd sizing ---- // See setCongestionControlAlgorithm()'s comment for what this is // and, importantly, is not. State only updated/consulted when // _cc_algorithm == BbrLite; computing it is cheap enough that // there's no need to gate the bookkeeping itself behind the flag, // only the cwnd-setting decisions in onPacketAckedCC()/ // onPacketLostCC() that act on it. CongestionControlAlgorithm _cc_algorithm = CongestionControlAlgorithm::NewReno; static constexpr size_t BBR_BW_WINDOW = 10; // samples (~RTTs) the max-filter looks back over double _bbr_bw_samples[BBR_BW_WINDOW] = {0}; size_t _bbr_bw_sample_idx = 0; std::chrono::steady_clock::time_point _bbr_last_delivery_time{}; double _bbr_min_rtt = -1.0; // simple running minimum — see class comment on why this never re-adapts upward static constexpr double BBR_CWND_GAIN = 2.0; // headroom over the raw estimated bandwidth-delay product static constexpr uint64_t BBR_MIN_CWND = 2 * 1472; // Recomputes cwnd from the current max-filtered bandwidth estimate // and min RTT; a no-op until both have at least one real sample. // `acked_bytes` is this ACK's contribution to the delivery-rate // sample. void bbrLiteUpdateCwnd(size_t acked_bytes); // ---- HyStart++ (RFC 9438) — slow-start overshoot protection ---- // Plain NewReno slow start only ever exits via an actual loss, // letting cwnd overshoot the path's real capacity on high-BDP/high- Loading test/CMakeLists.txt +8 −0 Original line number Diff line number Diff line Loading @@ -146,6 +146,14 @@ else() endif() add_test(NAME quic_roundtrip_sha256_test COMMAND quic_roundtrip_sha256_test) add_executable(quic_bbrlite_smoke_test quic_bbrlite_smoke_test.cpp) if(WIN32) target_link_libraries(quic_bbrlite_smoke_test netplus-static ws2_32) else() target_link_libraries(quic_bbrlite_smoke_test netplus-static) endif() add_test(NAME quic_bbrlite_smoke_test COMMAND quic_bbrlite_smoke_test) add_executable(quic_incremental_dispatch_test quic_incremental_dispatch_test.cpp) if(WIN32) target_link_libraries(quic_incremental_dispatch_test netplus-static ws2_32) Loading test/quic_bbrlite_smoke_test.cpp 0 → 100644 +169 −0 Original line number Diff line number Diff line // quic_bbrlite_smoke_test.cpp // // Functional smoke test for setCongestionControlAlgorithm(BbrLite) — NOT a // performance/throughput validation (this project's test suite runs over // loopback only, which cannot meaningfully exercise real bandwidth // estimation or path variation; see the CongestionControlAlgorithm comment // in socket.h for why BbrLite is an explicitly experimental, simplified // mode). This test only asserts that opting into it does not break // correctness: a multi-megabyte transfer over a BbrLite-mode connection // still completes and its bytes/SHA-256 still match exactly, exercising the // bandwidth-sample/min-RTT/cwnd-sizing code path many times per transfer // instead of never running it at all. #include <iostream> #include <string> #include <vector> #include <map> #include <thread> #include <atomic> #include <chrono> #include <stdexcept> #include "connection.h" #include "eventapi.h" #include "socket.h" #include "exception.h" #include "random.h" #include "crypto/sha.h" #include "https_certs.h" #include "https_ca_cert.h" using namespace netplus; static int g_passed = 0, g_failed = 0; static void check(bool ok, const std::string& name) { if (ok) { std::cout << " PASS: " << name << std::endl; g_passed++; } else { std::cout << " FAIL: " << name << std::endl; g_failed++; } } class EchoServer : public event { public: EchoServer(std::vector<netplus::socket*> socks, int timeout = 500) : event(socks, timeout) {} void RequestEvent(con& curcon, const int, ULONG_PTR) override { if (!curcon.RecvData.empty()) { curcon.SendData.append(curcon.RecvData.data(), curcon.RecvData.size()); curcon.RecvData.clear(); } } void ResponseEvent(con&, const int, ULONG_PTR) override {} void ConnectEvent(con&, const int, ULONG_PTR) override {} void DisconnectEvent(con&, const int, ULONG_PTR) override {} void CreateConnection(std::shared_ptr<con>& res) override { res = std::make_shared<con>(this); } }; static std::atomic<bool> g_quic_ready(false); static void waitReady(std::atomic<bool>& ready) { while (!ready.load()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(200)); } static void runQuicServer(std::map<std::string, ssl::CertificateBundle>& certs, int port) { try { quic serverSock(certs, "127.0.0.1", port, 64, -1); serverSock.setCongestionControlAlgorithm(quic::CongestionControlAlgorithm::BbrLite); serverSock.setStreamCallback([](netplus::socket* sock, uint64_t stream_id, const std::vector<uint8_t>& data, bool fin) { netplus::quic* q = dynamic_cast<netplus::quic*>(sock); if (!q || data.empty()) return; q->sendStreamData(stream_id, data, fin); }); EchoServer srv({&serverSock}); g_quic_ready.store(true); srv.runEventloop(); } catch (NetException& e) { std::cerr << "[QUIC server] NetException: " << e.what() << std::endl; g_quic_ready.store(true); } catch (std::exception& e) { std::cerr << "[QUIC server] Exception: " << e.what() << std::endl; g_quic_ready.store(true); } } static std::vector<uint8_t> quicRoundTrip(const std::vector<uint8_t>& payload, const std::string& host, int port) { quic client; client.setCongestionControlAlgorithm(quic::CongestionControlAlgorithm::BbrLite); client.connect(host, port); uint64_t sid = client.openStream(true); client.sendStreamData(sid, payload, true); std::vector<uint8_t> out(payload.size()); size_t total = 0; auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60); while (total < out.size() && std::chrono::steady_clock::now() < deadline) { client.pumpNetwork(MSG_DONTWAIT); if (client.hasStreamData(sid)) { size_t got = client.recvStreamData(sid, out.data() + total, out.size() - total); if (got > 0) { total += got; continue; } } std::this_thread::sleep_for(std::chrono::microseconds(200)); } if (total != out.size()) throw std::runtime_error("quicRoundTrip: incomplete echo (" + std::to_string(total) + "/" + std::to_string(out.size()) + " bytes)"); return out; } int main() { std::cout << "=== QUIC BbrLite Congestion Control Smoke Test ===" << std::endl; const size_t kPayloadSize = 4096 * 1024; // 4 MB const int kQuicPort = 19551; int rc = 0; try { std::vector<uint8_t> original(kPayloadSize); fillRandomBytes(original.data(), original.size()); std::vector<uint8_t> originalHash = sha256_hash(original); x509cert cert; if (!cert.loadFromBuffer(test_cert_der)) { std::cerr << "Failed to load certificate" << std::endl; return 1; } std::map<std::string, ssl::CertificateBundle> certs; ssl::CertificateBundle bundle; bundle.cert = cert; bundle.privateKeyDer = std::vector<uint8_t>(test_key_der.begin(), test_key_der.end()); bundle.rsa_key = rsa(bundle.privateKeyDer); bundle.chain.push_back(std::vector<uint8_t>(MKCERT_ROOT_CA_DER, MKCERT_ROOT_CA_DER + MKCERT_ROOT_CA_DER_LEN)); certs["localhost"] = bundle; certs["127.0.0.1"] = bundle; std::thread quicServerThread(runQuicServer, std::ref(certs), kQuicPort); waitReady(g_quic_ready); std::vector<uint8_t> echoed = quicRoundTrip(original, "127.0.0.1", kQuicPort); event::Running = false; quicServerThread.join(); check(echoed == original, "BbrLite-mode echo preserves bytes exactly"); check(sha256_hash(echoed) == originalHash, "BbrLite-mode echo preserves sha256"); } catch (NetException& e) { std::cerr << "NetException: " << e.what() << std::endl; rc = 1; } catch (std::exception& e) { std::cerr << "Exception: " << e.what() << std::endl; rc = 1; } std::cout << "\n==============================" << std::endl; std::cout << "Results: " << g_passed << " passed, " << g_failed << " failed" << std::endl; std::cout << "==============================" << std::endl; return (rc != 0 || g_failed > 0) ? 1 : 0; } Loading
src/quic.cpp +141 −0 Original line number Diff line number Diff line Loading @@ -496,6 +496,16 @@ void quic::onPacketAckedCC(size_t packet_size, uint64_t pn) { _bytes_in_flight = 0; } if (_cc_algorithm == CongestionControlAlgorithm::BbrLite) { // Entirely separate cwnd-setting strategy — see // setCongestionControlAlgorithm()'s comment. Does not use // _ssthresh/_in_recovery/HyStart++ at all; cwnd is set directly // from the bandwidth-delay-product estimate on every ACK instead // of grown incrementally. bbrLiteUpdateCwnd(packet_size); return; } // If this packet was sent during recovery, don't grow cwnd if (_in_recovery && pn <= _recovery_start_pn) { return; Loading Loading @@ -523,6 +533,43 @@ void quic::onPacketAckedCC(size_t packet_size, uint64_t pn) { } } // BBR-lite bandwidth/RTT estimation and cwnd sizing — see // setCongestionControlAlgorithm()'s comment for scope/limitations. void quic::bbrLiteUpdateCwnd(size_t acked_bytes) { auto now = std::chrono::steady_clock::now(); // Delivery-rate sample: bytes acked since the last sample, divided by // the elapsed wall-clock time. Skipped for the very first sample // (nothing to measure an interval against yet) and for a zero/negative // interval (defensive — steady_clock is monotonic, but back-to-back // ACKs processed in the same batch can legitimately measure ~0s). if (_bbr_last_delivery_time.time_since_epoch().count() != 0) { double dt = std::chrono::duration<double>(now - _bbr_last_delivery_time).count(); if (dt > 0.0) { double bw_sample = static_cast<double>(acked_bytes) / dt; // bytes/sec _bbr_bw_samples[_bbr_bw_sample_idx] = bw_sample; _bbr_bw_sample_idx = (_bbr_bw_sample_idx + 1) % BBR_BW_WINDOW; } } _bbr_last_delivery_time = now; // Running minimum RTT — see the class-level comment on why this never // re-adapts upward (no periodic PROBE_RTT in this simplified mode). if (_latest_rtt > 0.0 && (_bbr_min_rtt < 0.0 || _latest_rtt < _bbr_min_rtt)) { _bbr_min_rtt = _latest_rtt; } double max_bw = 0.0; for (double s : _bbr_bw_samples) max_bw = std::max(max_bw, s); if (max_bw > 0.0 && _bbr_min_rtt > 0.0) { uint64_t bdp = static_cast<uint64_t>(max_bw * _bbr_min_rtt * BBR_CWND_GAIN); _cwnd = std::max(bdp, BBR_MIN_CWND); } // Else: not enough samples yet — leave cwnd at its current value // (the RFC 9002 §7.2 initial window, same starting point as NewReno). } void quic::onPacketLostCC(size_t packet_size, uint64_t pn) { if (_bytes_in_flight >= packet_size) { _bytes_in_flight -= packet_size; Loading @@ -530,6 +577,18 @@ void quic::onPacketLostCC(size_t packet_size, uint64_t pn) { _bytes_in_flight = 0; } if (_cc_algorithm == CongestionControlAlgorithm::BbrLite) { // Deliberately no explicit multiplicative decrease here (unlike // NewReno) — matching real BBR's design of not treating isolated // loss as a direct congestion signal. The bandwidth estimate in // bbrLiteUpdateCwnd() naturally reflects reduced delivery through // fewer/slower subsequent ACKs, which is this simplified mode's // only feedback path for loss; a full BBR2 additionally caps cwnd // explicitly during PROBE_BW on sustained loss, which this mode // does not implement (see setCongestionControlAlgorithm()). return; } // Enter recovery if not already in it if (!_in_recovery || pn > _recovery_start_pn) { _in_recovery = true; Loading @@ -544,7 +603,88 @@ void quic::onPacketLostCC(size_t packet_size, uint64_t pn) { // Multiplicative decrease _ssthresh = std::max(_cwnd / 2, MIN_CWND); _cwnd = _ssthresh; // A loss ends slow start outright — any in-progress HyStart++ // round-tracking (including CSS) is now moot; clear it so stale // pre-loss RTT samples can't influence a later slow-start phase // (e.g. if a future ssthresh increase ever let cwnd re-enter it). hystartReset(); } } void quic::hystartReset() { _hystart_css_active = false; _hystart_css_rounds_remaining = 0; _hystart_window_end = 0; _hystart_curr_round_min_rtt = -1.0; _hystart_last_round_min_rtt = -1.0; _hystart_rtt_sample_count = 0; } // HyStart++ (RFC 9438 §4). Called once per RTT sample (see processAckFrame()) // while cwnd is still growing via slow start or CSS; a no-op once ordinary // congestion avoidance has taken over. Tracks a per-round minimum RTT and, // on a sustained increase versus the previous round, switches from // exponential slow-start growth to Conservative Slow Start (a fraction of // the growth, applied in onPacketAckedCC()) for a bounded number of rounds // before either resuming normal slow start (the increase was transient) or // exiting into congestion avoidance for good (it wasn't). void quic::hystartOnRttSample(double rtt_sample, uint64_t acked_pn) { if (_cwnd >= _ssthresh && !_hystart_css_active) return; if (_hystart_window_end == 0) { // First sample of a fresh round (slow start just began, or this // follows a hystartReset()). _hystart_window_end = _app_pn_send; } if (_hystart_curr_round_min_rtt < 0.0 || rtt_sample < _hystart_curr_round_min_rtt) { _hystart_curr_round_min_rtt = rtt_sample; } ++_hystart_rtt_sample_count; if (acked_pn < _hystart_window_end) { return; // still within the current round } // Round boundary: this ACK covers a packet sent at/after the round's // start — evaluate the round just completed, then start the next one. bool have_enough_samples = _hystart_rtt_sample_count >= HYSTART_N_RTT_SAMPLE; double curr_min = _hystart_curr_round_min_rtt; if (!_hystart_css_active) { if (have_enough_samples && _hystart_last_round_min_rtt >= 0.0) { double thresh = std::max(HYSTART_MIN_RTT_THRESH, std::min(HYSTART_MAX_RTT_THRESH, _hystart_last_round_min_rtt / 8.0)); if (curr_min >= _hystart_last_round_min_rtt + thresh) { // Sustained RTT increase — likely queue buildup ahead of // loss. Enter Conservative Slow Start instead of continuing // to double cwnd every round. _hystart_css_active = true; _hystart_css_rounds_remaining = HYSTART_CSS_ROUNDS; _ssthresh = _cwnd; } } } else { double thresh = std::max(HYSTART_MIN_RTT_THRESH, std::min(HYSTART_MAX_RTT_THRESH, _hystart_last_round_min_rtt / 8.0)); bool still_elevated = have_enough_samples && curr_min >= _hystart_last_round_min_rtt + thresh; if (!still_elevated) { // Resolved (or inconclusive) — treat as transient, resume // ordinary exponential slow start. _hystart_css_active = false; } else if (--_hystart_css_rounds_remaining <= 0) { // Elevated for the whole CSS window — exit slow start for good. _cwnd = _ssthresh; _hystart_css_active = false; } // else: still elevated but CSS rounds remain — keep growing conservatively. } _hystart_last_round_min_rtt = curr_min; _hystart_curr_round_min_rtt = -1.0; _hystart_rtt_sample_count = 0; _hystart_window_end = _app_pn_send; } // ============================================================================ Loading Loading @@ -4806,6 +4946,7 @@ void quic::processAckFrame(const uint8_t* data, size_t len, size_t& offset, bool double adj_delay = static_cast<double>(ack_delay << _peer_ack_delay_exponent) / 1000000.0; if (rtt_sample > adj_delay) rtt_sample -= adj_delay; _latest_rtt = rtt_sample; hystartOnRttSample(rtt_sample, it->first); if (!_rtt_initialized) { _srtt = rtt_sample; _rttvar = rtt_sample / 2.0; Loading
src/socket.h +42 −0 Original line number Diff line number Diff line Loading @@ -766,6 +766,27 @@ namespace netplus { // across calls (or opt out and keep today's behavior). void setIncrementalStreamDispatch(bool enable) { _incremental_stream_dispatch = enable; } // Congestion control strategy for this connection. Default // (NewReno) is the existing, unchanged RFC-9002-conformant AIMD // behavior. BbrLite is an experimental, opt-in bandwidth-delay- // product based alternative (state declared further below, // implementation in onPacketAckedCC()/onPacketLostCC()/ // cwndAllowsSend()) — deliberately NOT a full BBR2 implementation: // it has no send pacing (packets are still gated purely by cwnd, // same as NewReno — a real BBR pacer smooths sends across an RTT // instead, which this mode does not do), no gain-cycling PROBE_BW // phase, and no periodic PROBE_RTT re-measurement (min RTT is a // simple running minimum for the connection's lifetime, so it can // only ever go down, never adapt back up if the path's true min // RTT increases). It's a real, working starting point for // experimenting with BDP-based cwnd sizing on high-BDP/lossy // paths, not a drop-in production BBR2 replacement — that needs // dedicated pacing infrastructure and validation against real // variable-bandwidth networks that this project's test suite // (loopback only) cannot exercise. Call before the connection // starts sending data. enum class CongestionControlAlgorithm { NewReno, BbrLite }; void setCongestionControlAlgorithm(CongestionControlAlgorithm algo) { _cc_algorithm = algo; } // Send data on a stream (vector overload for application use) void sendStreamData(uint64_t stream_id, const std::vector<uint8_t>& data, bool fin); Loading Loading @@ -1592,6 +1613,27 @@ namespace netplus { void onPacketAckedCC(size_t packet_size, uint64_t pn); void onPacketLostCC(size_t packet_size, uint64_t pn); // ---- BBR-lite: opt-in BDP-based cwnd sizing ---- // See setCongestionControlAlgorithm()'s comment for what this is // and, importantly, is not. State only updated/consulted when // _cc_algorithm == BbrLite; computing it is cheap enough that // there's no need to gate the bookkeeping itself behind the flag, // only the cwnd-setting decisions in onPacketAckedCC()/ // onPacketLostCC() that act on it. CongestionControlAlgorithm _cc_algorithm = CongestionControlAlgorithm::NewReno; static constexpr size_t BBR_BW_WINDOW = 10; // samples (~RTTs) the max-filter looks back over double _bbr_bw_samples[BBR_BW_WINDOW] = {0}; size_t _bbr_bw_sample_idx = 0; std::chrono::steady_clock::time_point _bbr_last_delivery_time{}; double _bbr_min_rtt = -1.0; // simple running minimum — see class comment on why this never re-adapts upward static constexpr double BBR_CWND_GAIN = 2.0; // headroom over the raw estimated bandwidth-delay product static constexpr uint64_t BBR_MIN_CWND = 2 * 1472; // Recomputes cwnd from the current max-filtered bandwidth estimate // and min RTT; a no-op until both have at least one real sample. // `acked_bytes` is this ACK's contribution to the delivery-rate // sample. void bbrLiteUpdateCwnd(size_t acked_bytes); // ---- HyStart++ (RFC 9438) — slow-start overshoot protection ---- // Plain NewReno slow start only ever exits via an actual loss, // letting cwnd overshoot the path's real capacity on high-BDP/high- Loading
test/CMakeLists.txt +8 −0 Original line number Diff line number Diff line Loading @@ -146,6 +146,14 @@ else() endif() add_test(NAME quic_roundtrip_sha256_test COMMAND quic_roundtrip_sha256_test) add_executable(quic_bbrlite_smoke_test quic_bbrlite_smoke_test.cpp) if(WIN32) target_link_libraries(quic_bbrlite_smoke_test netplus-static ws2_32) else() target_link_libraries(quic_bbrlite_smoke_test netplus-static) endif() add_test(NAME quic_bbrlite_smoke_test COMMAND quic_bbrlite_smoke_test) add_executable(quic_incremental_dispatch_test quic_incremental_dispatch_test.cpp) if(WIN32) target_link_libraries(quic_incremental_dispatch_test netplus-static ws2_32) Loading
test/quic_bbrlite_smoke_test.cpp 0 → 100644 +169 −0 Original line number Diff line number Diff line // quic_bbrlite_smoke_test.cpp // // Functional smoke test for setCongestionControlAlgorithm(BbrLite) — NOT a // performance/throughput validation (this project's test suite runs over // loopback only, which cannot meaningfully exercise real bandwidth // estimation or path variation; see the CongestionControlAlgorithm comment // in socket.h for why BbrLite is an explicitly experimental, simplified // mode). This test only asserts that opting into it does not break // correctness: a multi-megabyte transfer over a BbrLite-mode connection // still completes and its bytes/SHA-256 still match exactly, exercising the // bandwidth-sample/min-RTT/cwnd-sizing code path many times per transfer // instead of never running it at all. #include <iostream> #include <string> #include <vector> #include <map> #include <thread> #include <atomic> #include <chrono> #include <stdexcept> #include "connection.h" #include "eventapi.h" #include "socket.h" #include "exception.h" #include "random.h" #include "crypto/sha.h" #include "https_certs.h" #include "https_ca_cert.h" using namespace netplus; static int g_passed = 0, g_failed = 0; static void check(bool ok, const std::string& name) { if (ok) { std::cout << " PASS: " << name << std::endl; g_passed++; } else { std::cout << " FAIL: " << name << std::endl; g_failed++; } } class EchoServer : public event { public: EchoServer(std::vector<netplus::socket*> socks, int timeout = 500) : event(socks, timeout) {} void RequestEvent(con& curcon, const int, ULONG_PTR) override { if (!curcon.RecvData.empty()) { curcon.SendData.append(curcon.RecvData.data(), curcon.RecvData.size()); curcon.RecvData.clear(); } } void ResponseEvent(con&, const int, ULONG_PTR) override {} void ConnectEvent(con&, const int, ULONG_PTR) override {} void DisconnectEvent(con&, const int, ULONG_PTR) override {} void CreateConnection(std::shared_ptr<con>& res) override { res = std::make_shared<con>(this); } }; static std::atomic<bool> g_quic_ready(false); static void waitReady(std::atomic<bool>& ready) { while (!ready.load()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(200)); } static void runQuicServer(std::map<std::string, ssl::CertificateBundle>& certs, int port) { try { quic serverSock(certs, "127.0.0.1", port, 64, -1); serverSock.setCongestionControlAlgorithm(quic::CongestionControlAlgorithm::BbrLite); serverSock.setStreamCallback([](netplus::socket* sock, uint64_t stream_id, const std::vector<uint8_t>& data, bool fin) { netplus::quic* q = dynamic_cast<netplus::quic*>(sock); if (!q || data.empty()) return; q->sendStreamData(stream_id, data, fin); }); EchoServer srv({&serverSock}); g_quic_ready.store(true); srv.runEventloop(); } catch (NetException& e) { std::cerr << "[QUIC server] NetException: " << e.what() << std::endl; g_quic_ready.store(true); } catch (std::exception& e) { std::cerr << "[QUIC server] Exception: " << e.what() << std::endl; g_quic_ready.store(true); } } static std::vector<uint8_t> quicRoundTrip(const std::vector<uint8_t>& payload, const std::string& host, int port) { quic client; client.setCongestionControlAlgorithm(quic::CongestionControlAlgorithm::BbrLite); client.connect(host, port); uint64_t sid = client.openStream(true); client.sendStreamData(sid, payload, true); std::vector<uint8_t> out(payload.size()); size_t total = 0; auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60); while (total < out.size() && std::chrono::steady_clock::now() < deadline) { client.pumpNetwork(MSG_DONTWAIT); if (client.hasStreamData(sid)) { size_t got = client.recvStreamData(sid, out.data() + total, out.size() - total); if (got > 0) { total += got; continue; } } std::this_thread::sleep_for(std::chrono::microseconds(200)); } if (total != out.size()) throw std::runtime_error("quicRoundTrip: incomplete echo (" + std::to_string(total) + "/" + std::to_string(out.size()) + " bytes)"); return out; } int main() { std::cout << "=== QUIC BbrLite Congestion Control Smoke Test ===" << std::endl; const size_t kPayloadSize = 4096 * 1024; // 4 MB const int kQuicPort = 19551; int rc = 0; try { std::vector<uint8_t> original(kPayloadSize); fillRandomBytes(original.data(), original.size()); std::vector<uint8_t> originalHash = sha256_hash(original); x509cert cert; if (!cert.loadFromBuffer(test_cert_der)) { std::cerr << "Failed to load certificate" << std::endl; return 1; } std::map<std::string, ssl::CertificateBundle> certs; ssl::CertificateBundle bundle; bundle.cert = cert; bundle.privateKeyDer = std::vector<uint8_t>(test_key_der.begin(), test_key_der.end()); bundle.rsa_key = rsa(bundle.privateKeyDer); bundle.chain.push_back(std::vector<uint8_t>(MKCERT_ROOT_CA_DER, MKCERT_ROOT_CA_DER + MKCERT_ROOT_CA_DER_LEN)); certs["localhost"] = bundle; certs["127.0.0.1"] = bundle; std::thread quicServerThread(runQuicServer, std::ref(certs), kQuicPort); waitReady(g_quic_ready); std::vector<uint8_t> echoed = quicRoundTrip(original, "127.0.0.1", kQuicPort); event::Running = false; quicServerThread.join(); check(echoed == original, "BbrLite-mode echo preserves bytes exactly"); check(sha256_hash(echoed) == originalHash, "BbrLite-mode echo preserves sha256"); } catch (NetException& e) { std::cerr << "NetException: " << e.what() << std::endl; rc = 1; } catch (std::exception& e) { std::cerr << "Exception: " << e.what() << std::endl; rc = 1; } std::cout << "\n==============================" << std::endl; std::cout << "Results: " << g_passed << " passed, " << g_failed << " failed" << std::endl; std::cout << "==============================" << std::endl; return (rc != 0 || g_failed > 0) ? 1 : 0; }