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

rtt implemeted

parent 356e4eaf
Loading
Loading
Loading
Loading
+922 −37

File changed.

Preview size limit exceeded, changes collapsed.

+162 −2
Original line number Diff line number Diff line
@@ -575,6 +575,11 @@ namespace netplus {
			bool     fin;            // carried FIN flag
			bool     ack_eliciting;  // true for STREAM frames
			std::vector<uint8_t> stream_data; // copy of payload for retransmit
			// True if this packet was originally sent as 0-RTT (long-header,
			// _early_keys) rather than 1-RTT (_app_keys) — see
			// checkLossAndRetransmit()'s branch on this for why a retransmit
			// must not just always re-encrypt under _app_keys.send.
			bool     sent_as_early_data = false;
		};

		// Stream state
@@ -744,6 +749,22 @@ namespace netplus {
		// Retry case — only for reusing a NEW_TOKEN across connections).
		void setToken(const std::vector<uint8_t>& token) { _pending_token = token; }

		// RFC 8446 §4.6.1 session ticket, issued by the server after a
		// successful handshake (via NewSessionTicket), carrying a
		// resumption PSK. Save it with getSessionTicket() and hand it to
		// setSessionTicket() on a later, separate connection attempt
		// (before calling connect()) to request 0-RTT (early data) on that
		// attempt. An opaque blob to the application, like getStoredToken()/
		// setToken() above — combine both on the same reconnect to skip
		// Retry too. Empty if no NewSessionTicket has arrived yet.
		const std::vector<uint8_t>& getSessionTicket() const { return _received_session_ticket; }
		// Must be called before connect(). 0-RTT is only actually attempted
		// when connect() is used in nonblock mode — see sendStreamData()'s
		// early-data gate — since the default blocking connect() only
		// returns once the full handshake (and thus any early-data window)
		// has already completed.
		void setSessionTicket(const std::vector<uint8_t>& ticket) { _pending_session_ticket = ticket; }


		// Stream data callback (for HTTP/3 handling in application layer)
		// Override base class method - internally wraps to quic-specific callback
@@ -829,7 +850,11 @@ namespace netplus {
		// parameter type for protectPacket()/unprotectPacket() below, and a
		// nested type must be declared before it's named in a member
		// function's parameter list.
		enum class EncryptionLevel { Initial, Handshake, Application };
		// EarlyData (0-RTT) is asymmetric, unlike the other three levels:
		// only one direction is ever populated (client fills
		// _early_keys.send, server fills _early_keys.recv) — see
		// deriveEarlyDataKeys().
		enum class EncryptionLevel { Initial, Handshake, Application, EarlyData };

		// Key material + resolved ciphers for one direction (send or recv)
		// at one encryption level. Filled once, in deriveInitialKeys()/
@@ -879,6 +904,13 @@ namespace netplus {
		void processShortHeaderPacket(const uint8_t* data, size_t len);
		void processInitialPacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX);
		void processHandshakePacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX);
		// Server-only: decrypt and process an incoming 0-RTT (early data)
		// packet using _early_keys.recv. Shares the Application
		// packet-number space with 1-RTT (RFC 9000 §12.3) — tracked via
		// _app_pn_recv/_app_pn_recv_ranges, not a space of its own. Only
		// ever reached when _early_keys.recv is populated (see the dispatch
		// guard in processLongHeaderPacket()).
		void processZeroRTTPacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX);
		void processApplicationPacket(const uint8_t* data, size_t len);
		// Client-side: handle an incoming Retry packet (RFC 9000 §8.1.2) —
		// verify its RFC 9001 §5.8 Integrity Tag, capture the token + new
@@ -931,11 +963,32 @@ namespace netplus {
		// Loss detection and retransmission
		void checkLossAndRetransmit();
		void recordSentPacket(uint64_t pn, uint64_t stream_id, uint64_t stream_offset,
		                      const uint8_t* data, size_t len, bool fin, size_t wire_size = 0);
		                      const uint8_t* data, size_t len, bool fin, size_t wire_size = 0,
		                      bool sent_as_early_data = false);

		// Client-only 0-RTT send path, used by sendStreamData() in place of
		// its normal (heavily optimized, batched, congestion-window-gated)
		// logic whenever the handshake hasn't completed yet but early data
		// keys are ready. Deliberately simple/unbatched/uncongestion-gated:
		// 0-RTT only ever runs once, for a modest amount of data sent
		// optimistically before any handshake response, so keeping the
		// well-tested 1-RTT hot path untouched matters more here than raw
		// throughput. Packets are recorded via recordSentPacket(...,
		// sent_as_early_data=true) so checkLossAndRetransmit() retransmits
		// correctly (as 0-RTT while still possible, else falling back to
		// 1-RTT) if the server never acknowledges them — including the
		// case where the server declines 0-RTT entirely.
		size_t sendStreamDataEarly(uint64_t stream_id, const uint8_t* data, size_t len, bool fin);

		// Packet building
		std::vector<uint8_t> buildInitialPacket(const std::vector<uint8_t>& payload);
		std::vector<uint8_t> buildHandshakePacket(const std::vector<uint8_t>& payload);
		// Client-only: builds a 0-RTT (early data) long-header packet —
		// same layout as buildHandshakePacket() (no token field) but with
		// packet-type bits 0x01 and protected under _early_keys.send. Packet
		// number is drawn from the shared _app_pn_send counter (RFC 9000
		// §12.3 — 0-RTT and 1-RTT share the Application packet-number space).
		std::vector<uint8_t> buildZeroRTTPacket(const std::vector<uint8_t>& payload);
		std::vector<uint8_t> buildShortHeaderPacket(const std::vector<uint8_t>& payload);
		// Server-side: build and send a Retry packet (RFC 9000 §17.2.5) in
		// response to an Initial that arrived without a valid
@@ -971,6 +1024,17 @@ namespace netplus {
		void deriveInitialKeys(const std::vector<uint8_t>& dcid, bool is_server);
		void deriveHandshakeKeys(const std::vector<uint8_t>& shared_secret);
		void deriveApplicationKeys();
		// RFC 8446 §7.1 / RFC 9001 §4.6.1 early (0-RTT) traffic secret,
		// derived from a resumption PSK rather than the (EC)DHE shared
		// secret. Unlike the three functions above, only one direction is
		// ever populated per call (`is_send`: true fills _early_keys.send —
		// client, offering the ticket; false fills _early_keys.recv —
		// server, having just accepted it) — see EncryptionLevel's comment.
		// `psk` is the ticket's resumption PSK (_early_data_psk); the
		// transcript used is whatever `_tls_transcript` holds at the call
		// site, which callers must ensure is exactly the ClientHello bytes
		// (RFC 8446: Derive-Secret(early_secret, "c e traffic", ClientHello)).
		void deriveEarlyDataKeys(const std::vector<uint8_t>& psk, bool is_send);

		// `mat` is the already-role-resolved key material for this call —
		// the send-direction struct (e.g. _app_keys.send) to protect/apply
@@ -1062,6 +1126,28 @@ namespace netplus {
		ParsedToken validateToken(const std::vector<uint8_t>& token,
		                          const sockaddr_storage& peer_addr, socklen_t peer_len) const;

		// Session tickets (RFC 8446 §4.6.1): a self-encrypted, AEAD-sealed
		// opaque blob carrying a resumption PSK, mirroring generateToken()/
		// validateToken()'s wire format exactly (12-byte nonce + AES-128-GCM
		// ciphertext + 16-byte tag) but sealed under _ticket_secret instead
		// of _token_secret, since this payload (a real key) is more
		// sensitive than an address-validation token. `psk`/`cipher_suite`
		// are the already-derived per-ticket PSK (see sendNewSessionTicket())
		// and the cipher suite it was issued under.
		struct ParsedTicket {
			bool valid = false;
			std::vector<uint8_t> ticket_id;
			uint16_t cipher_suite = 0;
			std::vector<uint8_t> psk;
			uint32_t max_early_data_size = 0;
		};
		std::vector<uint8_t> generateSessionTicket(const std::vector<uint8_t>& ticket_id,
		                                            uint16_t cipher_suite,
		                                            const std::vector<uint8_t>& psk,
		                                            uint32_t lifetime_secs,
		                                            uint32_t max_early_data_size) const;
		ParsedTicket validateSessionTicket(const std::vector<uint8_t>& ticket) const;

		// RFC 9001 §5.8 Retry Integrity Tag: fixed, RFC-specified key/nonce
		// (public, not secret — authenticates packet *format*, not origin;
		// real protection comes from the token above plus the transport
@@ -1092,6 +1178,16 @@ namespace netplus {
		// after HANDSHAKE_DONE, letting this client skip Retry on a future
		// reconnect by presenting the token via setToken().
		void sendNewTokenFrame();
		// Server-only, RFC 8446 §4.6.1: issued once per connection right
		// after HANDSHAKE_DONE (alongside sendNewTokenFrame()), carrying a
		// resumption PSK the client can present on a later connection to
		// request 0-RTT. Sent as a CRYPTO frame at the Application level.
		void sendNewSessionTicket();
		// Client-only: handles the post-handshake NewSessionTicket message
		// (handshake type 0x04) arriving over the Application-level CRYPTO
		// stream — independently rederives the same resumption PSK the
		// server used and stores the ticket for getSessionTicket().
		void processNewSessionTicket(const std::vector<uint8_t>& msg);
		void sendHttp3ControlStreams();
		void completeHandshake();

@@ -1143,6 +1239,38 @@ namespace netplus {
		// computed over.
		std::vector<uint8_t> _initial_crypto_frame;

		// Client-only 0-RTT/session-ticket state (RFC 8446 4.6.1,
		// RFC 9001 4.6.1). _pending_session_ticket is offered in the next
		// ClientHello's pre_shared_key extension (set via
		// setSessionTicket(), mirroring _pending_token/setToken());
		// _received_session_ticket is the last NewSessionTicket this
		// connection processed, for getSessionTicket(). Opaque to the
		// application, same as the NEW_TOKEN pattern, but NOT simply the
		// server's wire ticket bytes internally: unlike the server (whose
		// ticket is self-encrypted, so any server instance can recover the
		// PSK from the ticket alone), a client presenting this on a later,
		// separate quic object has no shared state with the connection that
		// received it, so the PSK has to travel with the blob — the actual
		// format (built in processNewSessionTicket(), consumed in
		// buildClientHello()) is 2-byte cipher_suite + 2-byte psk length +
		// psk + the server's own ticket bytes.
		std::vector<uint8_t> _pending_session_ticket;
		std::vector<uint8_t> _received_session_ticket;
		// Resumption PSK recovered from _pending_session_ticket (client) or
		// from the validated ticket offered by the peer (server) — set
		// right before deriveEarlyDataKeys() is called. Cleared once no
		// longer needed (it is sensitive key material).
		std::vector<uint8_t> _early_data_psk;
		// Cipher suite the offered/accepted ticket was issued under —
		// determines which hash (SHA-256/SHA-384) the PSK binder and early
		// traffic secret use, independent of _selected_cipher (RFC 8446
		// 4.2.11: a PSK's hash must match the negotiated cipher's hash,
		// checked before accepting it — see processClientHello()).
		uint16_t _early_data_cipher_suite = 0x1301;
		bool _early_data_offered = false;   // client sent pre_shared_key + early_data
		bool _early_data_accepted = false;  // peer confirmed early_data (ServerHello PSK / EncryptedExtensions)
		bool _early_data_rejected = false;  // handshake completed without confirmation — stop using 0-RTT, retransmit unacked bytes at 1-RTT

		// Anti-amplification limit (RFC 9000 §8.1): until the peer's
		// address is validated, this endpoint must never send more than
		// 3x the bytes it has received from that address — otherwise it
@@ -1197,10 +1325,24 @@ namespace netplus {
		LevelKeys _initial_keys;  // Initial: derived from connection ID, always AES-128
		LevelKeys _hs_keys;       // Handshake: size depends on negotiated cipher
		LevelKeys _app_keys;      // Application: size depends on negotiated cipher
		// 0-RTT (RFC 9001 §4.6.1): asymmetric — client only ever fills
		// .send, server only ever fills .recv (see EncryptionLevel's
		// comment). Populated by deriveEarlyDataKeys() when a session
		// ticket's PSK is offered (client) or accepted (server).
		LevelKeys _early_keys;

		// TLS handshake traffic secrets (for computing Finished)
		std::vector<uint8_t> _c_hs_traffic_secret;
		std::vector<uint8_t> _s_hs_traffic_secret;
		// Master secret from the main (EC)DHE key schedule (RFC 8446 §7.1) —
		// needed after the handshake completes to derive
		// resumption_master_secret for session tickets. Set at the end of
		// deriveApplicationKeys(); was previously just a local temporary.
		std::vector<uint8_t> _master_secret;
		// Derive-Secret(_master_secret, "res master", transcript through the
		// client's Finished) — set (server-side only) in processFinished(),
		// consumed by sendNewSessionTicket() to derive each ticket's PSK.
		std::vector<uint8_t> _resumption_master_secret;

		// TLS handshake state.
		//
@@ -1231,6 +1373,9 @@ namespace netplus {
		std::vector<uint8_t> _crypto_recv_app;
		std::vector<uint8_t> _crypto_send_initial;
		std::vector<uint8_t> _crypto_send_handshake;
		// Server-only: post-handshake CRYPTO stream (NewSessionTicket),
		// sent at the Application level — see sendNewSessionTicket().
		std::vector<uint8_t> _crypto_send_app;

		// CRYPTO stream reassembly - stores received ranges to handle out-of-order delivery
		struct CryptoRange {
@@ -1417,6 +1562,21 @@ namespace netplus {
		// of persisting/rotating a secret across restarts.
		std::vector<uint8_t> _token_secret;

		// Secret backing session tickets (RFC 8446 4.6.1), same
		// generated-once-per-listener/reached-via-_parent lifetime as
		// _token_secret above, but a distinct secret — a ticket AEAD-seals
		// the actual resumption PSK, a more sensitive payload than an
		// address-validation token, so it gets its own key rather than
		// reusing _token_secret under a different label.
		std::vector<uint8_t> _ticket_secret;

		// Anti-replay for session-ticket PSKs (RFC 8446 8.1): a ticket's
		// resumption PSK is single-use for early data. Keyed by ticket_id
		// (the same 16 random bytes doubling as the wire ticket_nonce),
		// value is the ticket's expiry — swept lazily on lookup, same
		// pattern as _recent_retries below. Guarded by _registry_mutex.
		std::unordered_map<std::string, std::chrono::steady_clock::time_point> _used_ticket_ids;

		// Mutex for this connection's own state (recursive: musl returns
		// EDEADLK on double-lock by same thread, which happens when
		// processFrame callbacks re-enter locking functions like