Commit 163b8983 authored by jan.koester's avatar jan.koester
Browse files

test

parent eaa67d4f
Loading
Loading
Loading
Loading
+141 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
@@ -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;
}

// ============================================================================
@@ -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;
+42 −0
Original line number Diff line number Diff line
@@ -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);
@@ -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-
+8 −0
Original line number Diff line number Diff line
@@ -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)
+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;
}