Loading src/crypto/aes.cpp +10 −0 Original line number Diff line number Diff line Loading @@ -1483,6 +1483,16 @@ uint32_t aes256::subWord_ct(uint32_t w) { (uint32_t(b3)); } // Deliberately always the constant-time software S-box (subWord_ct), even // when AES-NI is available: key expansion runs once per encryption // level/direction/connection (see deriveInitialKeys()/deriveHandshakeKeys()/ // deriveApplicationKeys() in quic.cpp), never in the per-packet hot path — // AES-NI's _mm_aeskeygenassist_si128 keygen path would only shorten an // already-sub-microsecond, one-time-per-key operation, and AES-256's // alternating (SubWord only on every other word) key schedule makes that // path meaningfully more intricate to get right than AES-128's. Not worth // the correctness risk for a speedup that never shows up in steady-state // throughput. void aes256::keyExpansion(const std::vector<uint8_t>& key) { if (key.size() != 32) throw std::invalid_argument("aes256::keyExpansion: key must be 32 bytes"); Loading src/posix/udp.cpp +11 −0 Original line number Diff line number Diff line Loading @@ -530,6 +530,17 @@ size_t udp::recvBatchAddr(std::vector<std::vector<uint8_t>>& out, #else // Use thread_local static buffers to avoid 64×65KB heap allocation per call. // These are reused across calls on the same thread. // // Per-slot size is 65535, not the ~1472-byte PMTU ceiling a QUIC // connection actually negotiates: this buffer is shared (thread_local) // across every udp/quic instance serviced by this thread, and when // _gro_enabled is set, a single recvmmsg slot can hold a UDP_GRO // coalesced batch of multiple datagrams — up to the kernel's ~64KB GRO // limit, not one MTU-sized packet. Sizing slots down to PMTU+margin // would silently truncate/corrupt any GRO-coalesced receive on a thread // that also happens to service a GRO-enabled socket. Safe to shrink only // if GRO support were removed, or reworked to size per-socket instead of // per-thread. static constexpr int MAX_BATCH = 64; static thread_local std::vector<uint8_t> flat_buf(MAX_BATCH * 65535); static thread_local struct iovec iovecs[MAX_BATCH]; Loading src/quic.cpp +8 −2 Original line number Diff line number Diff line Loading @@ -502,9 +502,15 @@ void quic::onPacketAckedCC(size_t packet_size, uint64_t pn) { } _in_recovery = false; // Slow start: cwnd grows by bytes acked // Slow start: cwnd grows by bytes acked (or, under HyStart++ // Conservative Slow Start, by a fraction of that — see // hystartOnRttSample()). if (_cwnd < _ssthresh) { if (_hystart_css_active) { _cwnd += packet_size / HYSTART_CSS_GROWTH_DIVISOR; } else { _cwnd += packet_size; } } else { // Congestion avoidance: cwnd grows by ~1 MTU per RTT (RFC 9002 // Appendix B.5). Uses the actual confirmed usable datagram size Loading src/socket.h +48 −1 Original line number Diff line number Diff line Loading @@ -1142,7 +1142,21 @@ namespace netplus { std::vector<uint8_t> _c_hs_traffic_secret; std::vector<uint8_t> _s_hs_traffic_secret; // TLS handshake state // TLS handshake state. // // _tls_transcript grows across the handshake (ClientHello, // ServerHello, ... Finished) and is re-hashed from byte 0 on each of // the ~8-10 sha256_hash()/sha384_hash() calls over it, rather than // fed into an incremental/streaming hash state that only processes // the newly-appended bytes each time. This is O(n^2) in transcript // hash calls rather than O(n), but n is a handshake's worth of // messages (typically low single-digit KB), the whole thing happens // once per connection (never in the per-packet hot path), and // sha256_hash()/sha384_hash() (crypto/sha.h) only expose a one-shot // API — adding and validating a new incremental variant carries real // correctness risk (this feeds Finished/CertificateVerify; a subtly // wrong incremental state would silently break every handshake) for // a gain that only shows up with unusually large certificate chains. std::vector<uint8_t> _tls_transcript; std::vector<uint8_t> _client_random; std::vector<uint8_t> _server_random; Loading Loading @@ -1578,6 +1592,39 @@ namespace netplus { void onPacketAckedCC(size_t packet_size, uint64_t pn); void onPacketLostCC(size_t packet_size, uint64_t pn); // ---- 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- // latency paths before the first loss event corrects it. HyStart++ // watches for a sustained per-round RTT increase (a sign of queue // buildup ahead of loss) and reacts by capping growth (Conservative // Slow Start) instead of continuing to double cwnd every RTT. // MIN_RTT_THRESH guards against false triggers from ordinary // scheduling jitter on very-low-RTT paths (loopback, LAN) — the // same class of problem the loss-detection 5ms floor elsewhere in // this file was added for. static constexpr double HYSTART_MIN_RTT_THRESH = 0.004; // 4ms static constexpr double HYSTART_MAX_RTT_THRESH = 0.016; // 16ms static constexpr int HYSTART_N_RTT_SAMPLE = 8; static constexpr int HYSTART_CSS_ROUNDS = 5; static constexpr uint64_t HYSTART_CSS_GROWTH_DIVISOR = 4; bool _hystart_css_active = false; int _hystart_css_rounds_remaining = 0; uint64_t _hystart_window_end = 0; // PN marking current round's end double _hystart_curr_round_min_rtt = -1.0; double _hystart_last_round_min_rtt = -1.0; int _hystart_rtt_sample_count = 0; // Called from processAckFrame() for every RTT sample while in slow // start or CSS (a no-op once in ordinary congestion avoidance). void hystartOnRttSample(double rtt_sample, uint64_t acked_pn); // Resets all HyStart++ round-tracking state — called whenever a // loss/recovery event redefines cwnd growth from scratch, so stale // round data from before the loss can't influence the decision // after it. void hystartReset(); // Reused across the congestion-window-blocked wait retries in // sendStreamData() instead of constructing a fresh socketwait (and // its underlying epoll fd) on every ~1ms poll attempt. Profiling a Loading Loading
src/crypto/aes.cpp +10 −0 Original line number Diff line number Diff line Loading @@ -1483,6 +1483,16 @@ uint32_t aes256::subWord_ct(uint32_t w) { (uint32_t(b3)); } // Deliberately always the constant-time software S-box (subWord_ct), even // when AES-NI is available: key expansion runs once per encryption // level/direction/connection (see deriveInitialKeys()/deriveHandshakeKeys()/ // deriveApplicationKeys() in quic.cpp), never in the per-packet hot path — // AES-NI's _mm_aeskeygenassist_si128 keygen path would only shorten an // already-sub-microsecond, one-time-per-key operation, and AES-256's // alternating (SubWord only on every other word) key schedule makes that // path meaningfully more intricate to get right than AES-128's. Not worth // the correctness risk for a speedup that never shows up in steady-state // throughput. void aes256::keyExpansion(const std::vector<uint8_t>& key) { if (key.size() != 32) throw std::invalid_argument("aes256::keyExpansion: key must be 32 bytes"); Loading
src/posix/udp.cpp +11 −0 Original line number Diff line number Diff line Loading @@ -530,6 +530,17 @@ size_t udp::recvBatchAddr(std::vector<std::vector<uint8_t>>& out, #else // Use thread_local static buffers to avoid 64×65KB heap allocation per call. // These are reused across calls on the same thread. // // Per-slot size is 65535, not the ~1472-byte PMTU ceiling a QUIC // connection actually negotiates: this buffer is shared (thread_local) // across every udp/quic instance serviced by this thread, and when // _gro_enabled is set, a single recvmmsg slot can hold a UDP_GRO // coalesced batch of multiple datagrams — up to the kernel's ~64KB GRO // limit, not one MTU-sized packet. Sizing slots down to PMTU+margin // would silently truncate/corrupt any GRO-coalesced receive on a thread // that also happens to service a GRO-enabled socket. Safe to shrink only // if GRO support were removed, or reworked to size per-socket instead of // per-thread. static constexpr int MAX_BATCH = 64; static thread_local std::vector<uint8_t> flat_buf(MAX_BATCH * 65535); static thread_local struct iovec iovecs[MAX_BATCH]; Loading
src/quic.cpp +8 −2 Original line number Diff line number Diff line Loading @@ -502,9 +502,15 @@ void quic::onPacketAckedCC(size_t packet_size, uint64_t pn) { } _in_recovery = false; // Slow start: cwnd grows by bytes acked // Slow start: cwnd grows by bytes acked (or, under HyStart++ // Conservative Slow Start, by a fraction of that — see // hystartOnRttSample()). if (_cwnd < _ssthresh) { if (_hystart_css_active) { _cwnd += packet_size / HYSTART_CSS_GROWTH_DIVISOR; } else { _cwnd += packet_size; } } else { // Congestion avoidance: cwnd grows by ~1 MTU per RTT (RFC 9002 // Appendix B.5). Uses the actual confirmed usable datagram size Loading
src/socket.h +48 −1 Original line number Diff line number Diff line Loading @@ -1142,7 +1142,21 @@ namespace netplus { std::vector<uint8_t> _c_hs_traffic_secret; std::vector<uint8_t> _s_hs_traffic_secret; // TLS handshake state // TLS handshake state. // // _tls_transcript grows across the handshake (ClientHello, // ServerHello, ... Finished) and is re-hashed from byte 0 on each of // the ~8-10 sha256_hash()/sha384_hash() calls over it, rather than // fed into an incremental/streaming hash state that only processes // the newly-appended bytes each time. This is O(n^2) in transcript // hash calls rather than O(n), but n is a handshake's worth of // messages (typically low single-digit KB), the whole thing happens // once per connection (never in the per-packet hot path), and // sha256_hash()/sha384_hash() (crypto/sha.h) only expose a one-shot // API — adding and validating a new incremental variant carries real // correctness risk (this feeds Finished/CertificateVerify; a subtly // wrong incremental state would silently break every handshake) for // a gain that only shows up with unusually large certificate chains. std::vector<uint8_t> _tls_transcript; std::vector<uint8_t> _client_random; std::vector<uint8_t> _server_random; Loading Loading @@ -1578,6 +1592,39 @@ namespace netplus { void onPacketAckedCC(size_t packet_size, uint64_t pn); void onPacketLostCC(size_t packet_size, uint64_t pn); // ---- 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- // latency paths before the first loss event corrects it. HyStart++ // watches for a sustained per-round RTT increase (a sign of queue // buildup ahead of loss) and reacts by capping growth (Conservative // Slow Start) instead of continuing to double cwnd every RTT. // MIN_RTT_THRESH guards against false triggers from ordinary // scheduling jitter on very-low-RTT paths (loopback, LAN) — the // same class of problem the loss-detection 5ms floor elsewhere in // this file was added for. static constexpr double HYSTART_MIN_RTT_THRESH = 0.004; // 4ms static constexpr double HYSTART_MAX_RTT_THRESH = 0.016; // 16ms static constexpr int HYSTART_N_RTT_SAMPLE = 8; static constexpr int HYSTART_CSS_ROUNDS = 5; static constexpr uint64_t HYSTART_CSS_GROWTH_DIVISOR = 4; bool _hystart_css_active = false; int _hystart_css_rounds_remaining = 0; uint64_t _hystart_window_end = 0; // PN marking current round's end double _hystart_curr_round_min_rtt = -1.0; double _hystart_last_round_min_rtt = -1.0; int _hystart_rtt_sample_count = 0; // Called from processAckFrame() for every RTT sample while in slow // start or CSS (a no-op once in ordinary congestion avoidance). void hystartOnRttSample(double rtt_sample, uint64_t acked_pn); // Resets all HyStart++ round-tracking state — called whenever a // loss/recovery event redefines cwnd growth from scratch, so stale // round data from before the loss can't influence the decision // after it. void hystartReset(); // Reused across the congestion-window-blocked wait retries in // sendStreamData() instead of constructing a fresh socketwait (and // its underlying epoll fd) on every ~1ms poll attempt. Profiling a Loading