Commit d82c1069 authored by jan.koester's avatar jan.koester
Browse files

new test

parent 21e655fe
Loading
Loading
Loading
Loading
+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_concurrent_test quic_concurrent_test.cpp)
if(WIN32)
    target_link_libraries(quic_concurrent_test netplus-static ws2_32)
else()
    target_link_libraries(quic_concurrent_test netplus-static)
endif()
add_test(NAME quic_concurrent_test COMMAND quic_concurrent_test)

add_executable(tls_roundtrip_sha256_test tls_roundtrip_sha256_test.cpp)
if(WIN32)
    target_link_libraries(tls_roundtrip_sha256_test netplus-static ws2_32)
+268 −0
Original line number Diff line number Diff line
// quic_concurrent_test.cpp
//
// Stress test for the per-connection locking model: N client threads open
// independent QUIC connections to the same server *simultaneously* (a
// barrier holds them all until every connection has handshaked, so their
// steady-state traffic genuinely overlaps), each sends a distinct
// SHA-256-tagged payload on its own stream and verifies its echo comes back
// byte-for-byte identical with no cross-connection mixing, then closes.
//
// This exercises the concurrent-accept()/pumpIncoming() path that
// quic_test.cpp / quic_rfc9000_test.cpp / quic_roundtrip_sha256_test.cpp
// cannot: those all drive at most one live connection at a time. A data
// race in the registry (_registry_mutex) or per-connection locking
// (quic_mtx()) would show up here as cross-connection data corruption, a
// crash, or a hang -- not as a throughput number -- so this is run for
// pass/fail, not performance.

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <thread>
#include <atomic>
#include <chrono>
#include <mutex>
#include <sstream>
#include <random>

#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 std::mutex g_report_mtx;

static void check(bool ok, const std::string& name) {
    std::lock_guard<std::mutex> lk(g_report_mtx);
    if (ok) { std::cout << "  PASS: " << name << std::endl; g_passed++; }
    else    { std::cout << "  FAIL: " << name << std::endl; g_failed++; }
}

static std::string toHex(const std::vector<uint8_t>& v) {
    static const char* hexd = "0123456789abcdef";
    std::string s;
    s.reserve(v.size() * 2);
    for (uint8_t b : v) {
        s.push_back(hexd[b >> 4]);
        s.push_back(hexd[b & 0x0F]);
    }
    return s;
}

// ============================================================================
// QUIC echo server
// ============================================================================

static std::atomic<bool> g_quic_ready(false);

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 void runQuicServer(std::map<std::string, ssl::CertificateBundle>& certs, int port) {
    try {
        quic serverSock(certs, "127.0.0.1", port, 256, -1);
        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});
        std::cout << "Server worker threads: " << srv.threads << std::endl;
        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);
    }
}

// ============================================================================
// Client worker: connect, wait at the barrier, then send/receive/verify
// ============================================================================

struct ClientResult {
    bool connected = false;
    bool complete = false;
    bool bytes_match = false;
    bool hash_match = false;
};

static void clientWorker(int idx, const std::string& host, int port,
                          std::atomic<int>& ready_count, int total_clients,
                          std::atomic<bool>& release, ClientResult& result) {
    try {
        quic client;
        client.connect(host, port);
        result.connected = true;

        // Barrier: every client waits until all have handshaked, so their
        // steady-state traffic genuinely overlaps on the server instead of
        // trickling in one at a time.
        ready_count.fetch_add(1);
        while (!release.load()) std::this_thread::sleep_for(std::chrono::microseconds(200));

        // Distinct, per-client payload: index-tagged pseudo-random bytes,
        // large enough to span multiple packets (several KB), so a mixup
        // between connections would show up as corrupted bytes, not just a
        // wrong total length.
        const size_t payload_size = 64 * 1024;
        std::vector<uint8_t> payload(payload_size);
        std::mt19937 rng(0xC0FFEE ^ static_cast<unsigned>(idx));
        std::uniform_int_distribution<int> dist(0, 255);
        for (auto& b : payload) b = static_cast<uint8_t>(dist(rng));
        // Stamp the client index into the first bytes so a cross-connection
        // mixup is trivially visible in the failure message.
        std::ostringstream tag;
        tag << "CLIENT-" << idx << "-";
        std::string tag_str = tag.str();
        for (size_t i = 0; i < tag_str.size() && i < payload.size(); ++i)
            payload[i] = static_cast<uint8_t>(tag_str[i]);

        std::vector<uint8_t> originalHash = sha256_hash(payload);

        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(30);
        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));
        }

        result.complete = (total == out.size());
        result.bytes_match = result.complete && (out == payload);
        result.hash_match = result.complete && (sha256_hash(out) == originalHash);

        client.close();
    } catch (NetException& e) {
        std::cerr << "[client " << idx << "] NetException: " << e.what() << std::endl;
    } catch (std::exception& e) {
        std::cerr << "[client " << idx << "] Exception: " << e.what() << std::endl;
    }
}

// ============================================================================
// One full round: N concurrent clients against one server
// ============================================================================

static bool runConcurrentRound(int num_clients, const std::string& host, int port) {
    std::vector<ClientResult> results(num_clients);
    std::vector<std::thread> clients;
    std::atomic<int> ready_count{0};
    std::atomic<bool> release{false};

    for (int i = 0; i < num_clients; ++i) {
        clients.emplace_back(clientWorker, i, host, port,
                              std::ref(ready_count), num_clients,
                              std::ref(release), std::ref(results[i]));
    }

    // Release the barrier once everyone has connected (or after a timeout,
    // so a single stuck connect() can't hang the whole round forever).
    auto barrier_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(15);
    while (ready_count.load() < num_clients &&
           std::chrono::steady_clock::now() < barrier_deadline) {
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
    }
    release.store(true);

    for (auto& t : clients) t.join();

    bool round_ok = true;
    for (int i = 0; i < num_clients; ++i) {
        const auto& r = results[i];
        bool ok = r.connected && r.complete && r.bytes_match && r.hash_match;
        if (!ok) {
            std::cout << "  client " << i << ": connected=" << r.connected
                       << " complete=" << r.complete
                       << " bytes_match=" << r.bytes_match
                       << " hash_match=" << r.hash_match << std::endl;
            round_ok = false;
        }
    }
    return round_ok;
}

// ============================================================================
// main
// ============================================================================

int main() {
    std::cout << "=== QUIC Concurrent Connections Stress Test ===" << std::endl;

    const int kQuicPort = 19611;
    const int kNumClients = 24;
    const int kRounds = 3;

    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);
    while (!g_quic_ready.load()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::this_thread::sleep_for(std::chrono::milliseconds(200));

    for (int round = 1; round <= kRounds; ++round) {
        std::cout << "\n--- Round " << round << "/" << kRounds
                  << ": " << kNumClients << " concurrent connections ---" << std::endl;
        bool ok = runConcurrentRound(kNumClients, "127.0.0.1", kQuicPort);
        check(ok, "round " + std::to_string(round) + ": all " +
                  std::to_string(kNumClients) + " connections completed with no cross-talk");
    }

    event::Running = false;
    quicServerThread.join();

    std::cout << "\n==============================" << std::endl;
    std::cout << "Results: " << g_passed << " passed, " << g_failed << " failed" << std::endl;
    std::cout << "==============================" << std::endl;

    return (g_failed > 0) ? 1 : 0;
}