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_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) Loading test/quic_concurrent_test.cpp 0 → 100644 +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; } 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_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) Loading
test/quic_concurrent_test.cpp 0 → 100644 +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; }