Commit 215f3cb4 authored by jan.koester's avatar jan.koester
Browse files

test

parent e873698e
Loading
Loading
Loading
Loading
+56 −109
Original line number Diff line number Diff line
@@ -3359,6 +3359,7 @@ namespace netplus {

        rx_handshake_buf.clear();
        rx_tcp_buf.clear();
        rx_tcp_off = 0;

        // -------------------------
        // Send/receive buffers (IOCP)
@@ -3538,7 +3539,13 @@ namespace netplus {
            return false;
        };

        while (rx_tcp_buf.size() < 5) {
        // Bytes not yet consumed sit at rx_tcp_buf[rx_tcp_off .. size()); this
        // avoids an erase()/memmove of the whole remaining buffer on every
        // record (see the compaction step at the end of this function,
        // which reclaims that space only occasionally instead).
        auto available = [this]() { return rx_tcp_buf.size() - rx_tcp_off; };

        while (available() < 5) {
            if (is_client) {
                if (!tryReadFromSocket()) {
                    NetException n;
@@ -3552,10 +3559,10 @@ namespace netplus {
            }
        }

        uint16_t recLen = (uint16_t(rx_tcp_buf[3]) << 8) | uint16_t(rx_tcp_buf[4]);
        uint16_t recLen = (uint16_t(rx_tcp_buf[rx_tcp_off + 3]) << 8) | uint16_t(rx_tcp_buf[rx_tcp_off + 4]);
        size_t total = 5 + recLen;

        while (rx_tcp_buf.size() < total) {
        while (available() < total) {
            if (is_client) {
                if (!tryReadFromSocket()) {
                    NetException n;
@@ -3569,8 +3576,21 @@ namespace netplus {
            }
        }

        std::vector<uint8_t> rec(rx_tcp_buf.begin(), rx_tcp_buf.begin() + total);
        rx_tcp_buf.erase(rx_tcp_buf.begin(), rx_tcp_buf.begin() + total);
        std::vector<uint8_t> rec(rx_tcp_buf.begin() + rx_tcp_off, rx_tcp_buf.begin() + rx_tcp_off + total);
        rx_tcp_off += total;

        // Compact only occasionally: reclaim the consumed prefix's space
        // with a single erase() once it's grown large and consumed at least
        // half the buffer, rather than memmove-ing the remainder on every
        // single record. Cheapest case first: buffer fully drained needs no
        // memmove at all.
        if (rx_tcp_off == rx_tcp_buf.size()) {
            rx_tcp_buf.clear();
            rx_tcp_off = 0;
        } else if (rx_tcp_off >= 4096 && rx_tcp_off * 2 >= rx_tcp_buf.size()) {
            rx_tcp_buf.erase(rx_tcp_buf.begin(), rx_tcp_buf.begin() + rx_tcp_off);
            rx_tcp_off = 0;
        }

        return rec;
    }
@@ -4385,88 +4405,10 @@ namespace netplus {
                                const std::vector<uint8_t>& plaintext,
                                bool handshake_keys)
    {
        auto throwSSL = [&](int type, const std::string& msg) {
            netplus::NetException e;
            e[type] << "tls::tls13_send_record: " << msg;
            throw e;
        };

        // Choose correct key/iv + seq depending on phase (HS or APP)
        netplus::aes* aead = nullptr;
        const uint8_t* base_iv = nullptr;
        uint64_t* seqp = nullptr;

        if (handshake_keys) {
            aead = aes13_hs_send.get();
            base_iv = is_client ? tls13_hs_iv_c2s : tls13_hs_iv_s2c;
            seqp = &tls13_hs_send_seq;
        } else {
            aead = aes13_app_send.get();
            base_iv = is_client ? tls13_app_iv_c2s : tls13_app_iv_s2c;
            seqp = &tls13_app_send_seq;
        }

        if (!aead || !base_iv || !seqp)
            throwSSL(NetException::Error, "TLS1.3 send_record: missing AEAD state");

        uint64_t seq = *seqp;


        // Build TLSInnerPlaintext = plaintext || inner_type
        std::vector<uint8_t> inner = plaintext;
        inner.push_back(inner_type);   // MUST include inner content type byte

        // Compute nonce = base_iv XOR seq (seq big-endian into last 8 bytes)
        uint8_t nonce[12];
        std::memcpy(nonce, base_iv, 12);

        uint64_t s = seq;
        for (int i = 0; i < 8; i++) {
            nonce[11 - i] ^= uint8_t(s & 0xFF);
            s >>= 8;
        }


        // Outer TLSCiphertext header (this is the AEAD AAD)
        constexpr size_t TAG_LEN = 16;

        const size_t ct_len = inner.size();
        const size_t rec_payload_len = ct_len + TAG_LEN;

        uint8_t hdr[5];
        hdr[0] = 0x17; // application_data (always, for protected TLS1.3 records)
        hdr[1] = 0x03;
        hdr[2] = 0x03;
        hdr[3] = uint8_t(rec_payload_len >> 8);
        hdr[4] = uint8_t(rec_payload_len & 0xFF);

        // AEAD encrypt
        std::vector<uint8_t> ct(ct_len);
        uint8_t tag16[16];

        bool ok = aead->aes_gcm_encrypt(
            nonce,
            hdr, sizeof(hdr),
            inner.data(), inner.size(),
            ct.data(),
            tag16
        );

        if (!ok)
            throwSSL(NetException::Error, "TLS1.3 send_record: AES-GCM encrypt failed");

        // Assemble final TLSCiphertext record: hdr || ct || tag
        std::vector<uint8_t> rec;
        rec.reserve(5 + rec_payload_len);

        rec.insert(rec.end(), hdr, hdr + 5);
        rec.insert(rec.end(), ct.begin(), ct.end());
        rec.insert(rec.end(), tag16, tag16 + 16);

        queueRaw(rec);

        // Increment sequence number
        (*seqp)++;
        // Delegates to the pointer/length overload, which builds and queues
        // the entire TLSCiphertext record (header || ciphertext || tag) in a
        // single allocation — see that overload for the encrypt logic.
        tls13_send_record(inner_type, plaintext.data(), plaintext.size(), handshake_keys);
    }

    void tls::tls13_send_record(uint8_t inner_type,
@@ -4498,12 +4440,6 @@ namespace netplus {

        uint64_t seq = *seqp;

        // Build TLSInnerPlaintext = data || inner_type
        std::vector<uint8_t> inner;
        inner.reserve(plain_len + 1);
        inner.insert(inner.end(), plain_data, plain_data + plain_len);
        inner.push_back(inner_type);

        uint8_t nonce[12];
        std::memcpy(nonce, base_iv, 12);
        uint64_t s = seq;
@@ -4512,36 +4448,47 @@ namespace netplus {
            s >>= 8;
        }

        // TLSInnerPlaintext = plain_data || inner_type (RFC 8446 5.2)
        constexpr size_t TAG_LEN = 16;
        const size_t ct_len = inner.size();
        const size_t rec_payload_len = ct_len + TAG_LEN;
        const size_t inner_len = plain_len + 1;
        const size_t rec_payload_len = inner_len + TAG_LEN;

        // Single allocation for the whole TLSCiphertext record:
        // [5-byte header][ciphertext, same length as TLSInnerPlaintext][16-byte tag].
        // The plaintext is copied in once (unavoidable: it must be paired
        // with the trailing inner_type byte before it can be encrypted) and
        // then AES-GCM-encrypted in place, so no separate `inner`/`ct`/`rec`
        // buffers or extra copies are needed.
        std::vector<uint8_t> rec(5 + rec_payload_len);
        uint8_t* hdr     = rec.data();
        uint8_t* payload = rec.data() + 5;       // TLSInnerPlaintext, then overwritten in place with ciphertext
        uint8_t* tag16   = payload + inner_len;

        uint8_t hdr[5];
        hdr[0] = 0x17;
        hdr[0] = 0x17; // application_data (always, for protected TLS1.3 records)
        hdr[1] = 0x03;
        hdr[2] = 0x03;
        hdr[3] = uint8_t(rec_payload_len >> 8);
        hdr[4] = uint8_t(rec_payload_len & 0xFF);

        std::vector<uint8_t> ct(ct_len);
        uint8_t tag16[16];
        std::memcpy(payload, plain_data, plain_len);
        payload[plain_len] = inner_type;

        // In-place AEAD: aes::aes_gcm_encrypt documents that ct may alias pt
        // (each output byte only depends on the corresponding input byte and
        // the CTR keystream), so encrypting `payload` into itself is safe.
        bool ok = aead->aes_gcm_encrypt(
            nonce, hdr, sizeof(hdr),
            inner.data(), inner.size(),
            ct.data(), tag16
            nonce,
            hdr, 5,
            payload, inner_len,
            payload,
            tag16
        );

        if (!ok)
            throwSSL(NetException::Error, "TLS1.3 send_record: AES-GCM encrypt failed");

        std::vector<uint8_t> rec;
        rec.reserve(5 + rec_payload_len);
        rec.insert(rec.end(), hdr, hdr + 5);
        rec.insert(rec.end(), ct.begin(), ct.end());
        rec.insert(rec.end(), tag16, tag16 + 16);
        queueRaw(std::move(rec));

        queueRaw(rec);
        (*seqp)++;
    }

+7 −6
Original line number Diff line number Diff line
@@ -381,17 +381,17 @@ namespace netplus {
        /**
         * @brief Check whether there is buffered data still waiting to be processed.
         *
         * True if there are unconsumed raw TCP bytes (@ref rx_tcp_buf) or
         * unread decrypted application data (@ref rx_record_buf, from
         * @ref recv_off onward). Used by the event loop to decide whether
         * @ref recvData can make progress without waiting for more socket
         * I/O.
         * True if there are unconsumed raw TCP bytes (@ref rx_tcp_buf, from
         * @ref rx_tcp_off onward) or unread decrypted application data
         * (@ref rx_record_buf, from @ref recv_off onward). Used by the event
         * loop to decide whether @ref recvData can make progress without
         * waiting for more socket I/O.
         * @return true if buffered data is available to process.
         * @note Thread-safe; locks @ref mutex_.
         */
        bool hasBufferedData() const {
            std::lock_guard<std::recursive_mutex> lk(mutex_);
            return !rx_tcp_buf.empty() || recv_off < rx_record_buf.size();
            return rx_tcp_off < rx_tcp_buf.size() || recv_off < rx_record_buf.size();
        }

        /**
@@ -1447,6 +1447,7 @@ namespace netplus {
        std::vector<uint8_t> rx_record_buf;   /**< Decrypted application-data plaintext not yet fully delivered to the caller of @ref recvData; paired with @ref recv_off. */
        std::vector<uint8_t> rx_handshake_buf;/**< Reassembled plaintext handshake-message bytes, consumed by @ref popHandshakeMsg / @ref tls13_pop_hs_from_buf. */
        std::vector<uint8_t> rx_tcp_buf;      /**< Raw, not-yet-framed-into-records TCP bytes received from the socket; consumed by @ref readTlsRecordAsync. */
        size_t rx_tcp_off = 0;                /**< Offset of the next unconsumed byte within @ref rx_tcp_buf (avoids an erase()/memmove of the buffer front on every record; see @ref readTlsRecordAsync). */
        size_t rx_handshake_off = 0;          /**< Reserved/auxiliary offset bookkeeping for handshake buffer consumption. */
        size_t recv_off = 0;                  /**< Offset of the next unread byte within @ref rx_record_buf. */

+7 −0
Original line number Diff line number Diff line
@@ -164,6 +164,13 @@ else()
    target_link_libraries(benchmark_quic_bulk netplus-static)
endif()

add_executable(benchmark_tls_tcp_bulk benchmark_tls_tcp_bulk.cpp)
if(WIN32)
    target_link_libraries(benchmark_tls_tcp_bulk netplus-static ws2_32)
else()
    target_link_libraries(benchmark_tls_tcp_bulk netplus-static)
endif()

add_executable(quic_rfc9000_test quic_rfc9000_test.cpp)
if(WIN32)
    target_link_libraries(quic_rfc9000_test netplus-static ws2_32)
+185 −0
Original line number Diff line number Diff line
// One-way bulk transfer throughput benchmark for TCP+TLS 1.3, loopback.
//
// Companion to benchmark_quic_bulk.cpp: sends one large payload over a
// blocking TLS 1.3 connection in 16 KB application writes (the TLS
// max-plaintext-per-record size) and measures sustained sender-side
// throughput on 127.0.0.1. This exercises exactly the tls::sendData() /
// tls13_send_record() / queueRaw() / flush_out() / tls13_recv_record()
// hot path the send/recv allocation-reduction optimizations target, so the
// same benchmark can be re-run after each optimization step to see its
// effect in isolation.
//
// Usage: benchmark_tls_tcp_bulk [total_MB] [chunk_KB]

#include <iostream>
#include <iomanip>
#include <string>
#include <vector>
#include <thread>
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cstring>

#include "connection.h"
#include "eventapi.h"
#include "socket.h"
#include "exception.h"

#include "https_certs.h"
#include "https_ca_cert.h"

using namespace netplus;

static std::atomic<bool> g_server_ready(false);
static std::atomic<uint64_t> g_sink_bytes_received{0};

class SinkServer : public event {
public:
    SinkServer(std::vector<netplus::socket*> socks, int timeout = 500)
        : event(socks, timeout) {}

    // Pure sink: count and discard whatever arrived, never write back.
    // Stresses the receive hot path at full sender-side throughput instead
    // of being paced by an echo round trip.
    void RequestEvent(con& curcon, const int, ULONG_PTR) override {
        if (!curcon.RecvData.empty()) {
            g_sink_bytes_received.fetch_add(curcon.RecvData.size(), std::memory_order_relaxed);
            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 run_sink_server(std::map<std::string, ssl::CertificateBundle>& certs, int port) {
    try {
        ssl serverSock(certs, "127.0.0.1", port, 64, -1);
        SinkServer srv({&serverSock});
        g_server_ready.store(true);
        srv.runEventloop();
    } catch (std::exception& e) {
        std::cerr << "[SinkServer] Error: " << e.what() << std::endl;
        g_server_ready.store(true);
    }
}

using hrc = std::chrono::high_resolution_clock;

static double elapsed_s(hrc::time_point start, hrc::time_point end) {
    return std::chrono::duration<double>(end - start).count();
}

static void benchmark_bulk_transfer(std::map<std::string, ssl::CertificateBundle>& certs,
                                     const std::string& host, int port,
                                     size_t total_bytes, size_t chunk_size) {
    ssl client(certs);
    client.getTls().trust_policy.verifyPeer = false; // test-only self-signed cert
    client.connect(host, port);

    std::vector<uint8_t> chunk(chunk_size, 0xAB);

    g_sink_bytes_received.store(0, std::memory_order_relaxed);

    auto t0 = hrc::now();
    size_t sent_total = 0;
    while (sent_total < total_bytes) {
        size_t remaining = total_bytes - sent_total;
        size_t this_chunk = std::min(remaining, chunk_size);
        buffer snd(reinterpret_cast<const char*>(chunk.data()), this_chunk);
        size_t sent;
        try {
            sent = client.sendData(snd, 0);
        } catch (NetException& e) {
            if (e.getErrorType() == NetException::Note) {
                std::this_thread::sleep_for(std::chrono::milliseconds(1));
                continue;
            }
            throw;
        }
        if (sent == 0) throw std::runtime_error("benchmark: sendData stalled");
        sent_total += sent;
    }
    auto t1 = hrc::now();

    // Wait for the server's event-loop thread to actually finish draining
    // (it processes asynchronously), bounded so a real failure doesn't hang.
    auto deadline = hrc::now() + std::chrono::seconds(10);
    uint64_t last_seen = 0;
    while (hrc::now() < deadline) {
        uint64_t now_seen = g_sink_bytes_received.load(std::memory_order_relaxed);
        if (now_seen >= total_bytes) break;
        if (now_seen != last_seen) {
            last_seen = now_seen;
            deadline = hrc::now() + std::chrono::seconds(2); // still making progress
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(2));
    }
    auto t2 = hrc::now();

    double send_elapsed = elapsed_s(t0, t1);
    double total_elapsed = elapsed_s(t0, t2);
    double send_mb_s = (double(sent_total) / (1024.0 * 1024.0)) / send_elapsed;
    uint64_t recv_bytes = g_sink_bytes_received.load(std::memory_order_relaxed);
    double recv_mb_s = (double(recv_bytes) / (1024.0 * 1024.0)) / total_elapsed;

    std::cout << std::fixed << std::setprecision(2);
    std::cout << "TCP+TLS1.3 bulk transfer | " << (total_bytes / (1024 * 1024)) << " MB, "
              << (chunk_size / 1024) << " KB chunks" << std::endl;
    std::cout << "  Sent:     " << sent_total / (1024 * 1024) << " MB in "
              << send_elapsed * 1000.0 << " ms  => " << send_mb_s << " MB/s (sender-side)" << std::endl;
    std::cout << "  Received: " << recv_bytes / (1024 * 1024) << " MB in "
              << total_elapsed * 1000.0 << " ms => " << recv_mb_s << " MB/s (receiver-confirmed)"
              << (recv_bytes >= total_bytes ? "" : "  [TIMED OUT waiting for all bytes]") << std::endl;

    client.close();
}

int main(int argc, char** argv) {
    try {
        x509cert cert;
        if (!cert.loadFromBuffer(test_cert_der)) {
            std::cerr << "Failed to load certificate" << std::endl;
            return 1;
        }

        std::map<std::string, netplus::ssl::CertificateBundle> certs;
        netplus::ssl::CertificateBundle bundle;
        bundle.cert = cert;
        bundle.privateKeyDer = std::vector<uint8_t>(test_key_der.begin(), test_key_der.end());
        bundle.rsa_key = netplus::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;

        const int port = 19546;
        std::thread server_thread(run_sink_server, std::ref(certs), port);
        server_thread.detach();
        while (!g_server_ready.load())
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        std::this_thread::sleep_for(std::chrono::milliseconds(100));

        // Default: 512 MB in 16 KB application writes (TLS_MAX_PLAINTEXT,
        // one write == one TLS record). Override via argv for quick smoke
        // runs, e.g. `benchmark_tls_tcp_bulk 64 16`.
        size_t total_mb = argc > 1 ? std::stoul(argv[1]) : 512;
        size_t chunk_kb  = argc > 2 ? std::stoul(argv[2]) : 16;

        std::cout << "TCP+TLS1.3 one-way bulk transfer benchmark (loopback, 127.0.0.1)" << std::endl;
        benchmark_bulk_transfer(certs, "127.0.0.1", port, total_mb * 1024 * 1024, chunk_kb * 1024);

        return 0;
    } catch (netplus::NetException& e) {
        std::cerr << "NetException: " << e.what() << std::endl;
        return 1;
    } catch (std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
        return 1;
    }
}