-module(nquic_protocol_recv). -moduledoc """ Inbound side of the QUIC protocol state. Pure functions over `#conn_state{}` covering datagram parsing (`process_datagram/3`), per-packet dispatch (`handle_single_packet/4`, `decrypt_and_process/4`), Version Negotiation and Retry handling (RFC 9000 §6, §17.2.5), stateless-reset detection (RFC 9000 §10.3.1), the per-frame handler dispatch table (`handle_frame/3` for every RFC 9000 / 9221 frame type), and CRYPTO fragment reassembly (RFC 9001 §4.1.3). Extracted from `nquic_protocol` as part of REVIEW_PLAN.md Phase 4.4. The trunk's public dispatchers (`handle_packet/3,4`, `handle_packet_notimers/3,4`) call into `process_datagram/3` here; stream-frame and stream-cleanup helpers live in `nquic_protocol` until slice 7 moves them out. """. -include("nquic_conn.hrl"). -include("nquic_frame.hrl"). -include("nquic_packet.hrl"). -include("nquic_transport.hrl"). -export([ process_datagram/3 ]). -export([ check_stateless_reset/2, handle_single_packet/4 ]). -export([ handle_retry_packet/3, handle_version_negotiation/2 ]). -export([ handle_frame/3, handle_frames/3, maybe_update_peer_spin/4 ]). -export([ crypto_buffer_add/3, crypto_buffer_data/1, crypto_buffer_merge/3 ]). -export_type([crypto_buffer_entry/0]). -type crypto_buffer_entry() :: {non_neg_integer(), iodata(), [{non_neg_integer(), binary()}]}. %%%----------------------------------------------------------------------------- %% DATAGRAM PARSING %%%----------------------------------------------------------------------------- -spec process_datagram(binary(), nquic_protocol:state(), [nquic_protocol:event()]) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. process_datagram(<<>>, State, Events) -> {ok, lists:reverse(Events), State}; process_datagram(Bin, State, EventsAcc) -> SCIDLen = byte_size(State#conn_state.scid), case nquic_packet:parse_header(Bin, SCIDLen) of {ok, #long_header{type = version_negotiation} = Header, _} -> case handle_version_negotiation(Header, State) of {ok, _Events, NewState} -> {ok, lists:reverse(EventsAcc), NewState}; {error, _, _} = Error -> Error end; {ok, #short_header{} = Header, Rest} -> case handle_single_packet(Bin, Rest, Header, State) of {ok, NewEvents, NewState} -> {ok, lists:reverse(EventsAcc, NewEvents), NewState}; {error, Reason, NewState} -> {error, Reason, NewState} end; {ok, Header, Rest} -> HeaderLen = byte_size(Bin) - byte_size(Rest), {ok, PayloadLen} = nquic_protocol_send:get_packet_len(Header, byte_size(Rest)), case byte_size(Rest) >= PayloadLen of true -> PacketLen = HeaderLen + PayloadLen, <> = Bin, SlicedRest = binary:part(Rest, 0, PayloadLen), case handle_single_packet(CurrentPacket, SlicedRest, Header, State) of {ok, [], NewState} -> process_datagram(NextPackets, NewState, EventsAcc); {ok, NewEvents, NewState} -> process_datagram( NextPackets, NewState, lists:reverse(NewEvents, EventsAcc) ); {error, Reason, NewState} -> {error, Reason, NewState} end; false -> {ok, lists:reverse(EventsAcc), State} end; {error, _} -> {ok, lists:reverse(EventsAcc), State} end. %%%----------------------------------------------------------------------------- %% VERSION NEGOTIATION (RFC 9000 6) %%%----------------------------------------------------------------------------- -spec choose_negotiated_version([non_neg_integer()], nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. choose_negotiated_version(Offered, State) -> Supported = nquic_packet:supported_versions(), Common = [V || V <- Supported, lists:member(V, Offered)], case Common of [NewVersion | _] -> retry_with_version(NewVersion, State); [] -> {error, {transport_error, version_negotiation_error}, State} end. -doc """ Handle a Version Negotiation packet. RFC 9000 §6 governs acceptance: * a client MUST discard VN packets that arrive after any other server packet has been successfully decrypted (the `server_packet_processed` flag latches once that happens), * VN must echo the client's chosen connection IDs (DCID = our SCID, SCID = the DCID we used on the Initial that triggered the VN); mismatched echoes look like injected VN and are dropped, * a VN that lists the version the client was attempting MUST be discarded (RFC 9000 §6.2), * servers MUST NOT receive VN; drop for robustness. When negotiation succeeds the function picks the highest-priority version that both peers support and re-runs the client handshake on that version (resetting Initial-space keys and the Initial PN space, queueing a fresh ClientHello via `start_client_handshake/1`). When no common version exists the function returns `{error, {transport_error, version_negotiation_error}, State}` and the wrapper drives the connection into the draining state. """. -spec handle_version_negotiation(#long_header{}, nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_version_negotiation(_Header, #conn_state{role = server} = State) -> {ok, [], State}; handle_version_negotiation( _Header, #conn_state{role = client, server_packet_processed = true} = State ) -> {ok, [], State}; handle_version_negotiation(#long_header{dcid = VNDCID, scid = VNSCID}, State) when VNDCID =/= State#conn_state.scid orelse VNSCID =/= State#conn_state.dcid -> {ok, [], State}; handle_version_negotiation(#long_header{token = VersionsBin}, State) -> CurrentVersion = State#conn_state.version, case byte_size(VersionsBin) rem 4 of 0 -> Versions = [V || <> <= VersionsBin], case lists:member(CurrentVersion, Versions) of true -> {ok, [], State}; false -> choose_negotiated_version(Versions, State) end; _ -> {ok, [], State} end. -spec retry_with_version(non_neg_integer(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()}. retry_with_version(NewVersion, State) -> Crypto0 = State#conn_state.crypto, NewCrypto = Crypto0#conn_crypto{keys = #{}, tls_state = undefined}, State1 = State#conn_state{ version = NewVersion, crypto = NewCrypto, pn_spaces = #{}, app_next_pn = 0, app_largest_received = -1 }, {ok, State2} = nquic_protocol_handshake:start_client_handshake(State1), {ok, [], State2}. %%%----------------------------------------------------------------------------- %% RETRY PACKET HANDLING (RFC 9000 17 2 5) %%%----------------------------------------------------------------------------- -doc """ Handle a Retry packet (RFC 9000 §17.2.5). A client that has not yet processed a Retry verifies the integrity tag, adopts the server's SCID as the new DCID, stashes the original DCID as `odcid` and the Retry token on `#conn_state.retry_token`, re-derives Initial-space keys against the new DCID, resets the Initial PN space and loss detector, and queues a fresh ClientHello via `queue_initial_frame/2`. Subsequent Initial packets (queued retransmits, PTO probes) carry the token through `build_initial_packet/2`. A Retry that fails any check (server role, second Retry, parse error, integrity-tag mismatch) is silently dropped per RFC 9000 §17.2.5.1. """. -spec handle_retry_packet(binary(), #long_header{}, nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()}. handle_retry_packet(_Bin, _Header, #conn_state{role = server} = State) -> {ok, [], State}; handle_retry_packet(_Bin, _Header, #conn_state{odcid = ODCID} = State) when ODCID =/= undefined -> {ok, [], State}; handle_retry_packet(Bin, #long_header{scid = ServerSCID, token = RawToken}, State) -> ODCID = State#conn_state.dcid, Version = State#conn_state.version, case nquic_packet:parse_retry(RawToken, Bin) of {ok, RetryToken, PacketNoTag, IntegrityTag} -> case nquic_retry:verify_integrity_tag(ODCID, PacketNoTag, IntegrityTag, Version) of ok -> resend_initial_after_retry(ServerSCID, ODCID, RetryToken, State); {error, _} -> {ok, [], State} end; {error, _} -> {ok, [], State} end. -spec resend_initial_after_retry( nquic:connection_id(), nquic:connection_id(), binary(), nquic_protocol:state() ) -> {ok, [nquic_protocol:event()], nquic_protocol:state()}. resend_initial_after_retry(NewDCID, ODCID, RetryToken, State) -> Version = State#conn_state.version, {ClientSecret, ServerSecret} = nquic_keys:initial_secrets(NewDCID, Version), {CKey, CIV, CHP} = nquic_keys:derive_packet_protection(ClientSecret, aes_128_gcm, Version), {SKey, SIV, SHP} = nquic_keys:derive_packet_protection(ServerSecret, aes_128_gcm, Version), ClientRoleKeys = nquic_keys:make_role_keys(aes_128_gcm, CKey, CIV, CHP), ServerRoleKeys = nquic_keys:make_role_keys(aes_128_gcm, SKey, SIV, SHP), NewKeys = #{ initial => #{ client => ClientRoleKeys, server => ServerRoleKeys } }, CHBin = maps:get(client_hello, (State#conn_state.crypto)#conn_crypto.tls_state), Crypto0 = State#conn_state.crypto, State1 = State#conn_state{ dcid = NewDCID, odcid = ODCID, retry_scid = NewDCID, retry_token = RetryToken, crypto = Crypto0#conn_crypto{keys = NewKeys}, pn_spaces = #{initial => #{next_pn => 0}}, app_next_pn = 0, app_largest_received = -1, loss_state = nquic_loss:init( nquic_loss:get_cc_algorithm(State#conn_state.loss_state), nquic_loss:pacer_config(State#conn_state.loss_state) ) }, {ok, State2} = nquic_protocol_send_queues:queue_initial_frame( #crypto{offset = 0, data = CHBin}, State1 ), {ok, [], State2}. %%%----------------------------------------------------------------------------- %% PER-PACKET HANDLER (DECRYPT FRAME DISPATCH ACK QUEUING) %%%----------------------------------------------------------------------------- -spec check_stateless_reset(binary(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()}. check_stateless_reset(Packet, #conn_state{path = #conn_path_mgmt{peer_cids = PeerCids}} = State) -> case byte_size(Packet) >= 21 of false -> {ok, [], State}; true -> Tokens = [ maps:get(token, Entry, <<>>) || Entry <- maps:values(PeerCids), is_map(Entry) ], case lists:any(fun(T) -> nquic_stateless_reset:detect(Packet, T) end, Tokens) of true -> {ok, [connection_closed], State}; false -> {ok, [], State} end end. -spec decrypt_and_process(binary(), binary(), nquic_packet:header(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. decrypt_and_process(Packet, Rest, #short_header{} = Header, State) -> LargestRecv = max(0, State#conn_state.app_largest_received), case decrypt_short_with_key_update(Packet, Rest, Header, LargestRecv, State) of {ok, DecHeader, Frames, State1} -> handle_decrypt_result({ok, DecHeader, Frames}, Header, Packet, State1); {error, _} = Err -> handle_decrypt_result(Err, Header, Packet, State) end; decrypt_and_process(Packet, Rest, Header, State) -> Space = nquic_protocol_send:packet_space_from_header(Header), LargestRecv = largest_received_for(Space, State), handle_decrypt_result( decrypt_packet(Header, Packet, Rest, LargestRecv, State), Header, Packet, State ). -spec decrypt_packet( #long_header{}, binary(), binary(), non_neg_integer(), nquic_protocol:state() ) -> {ok, nquic_packet:header(), [nquic_frame:t()]} | {error, term()}. decrypt_packet(#long_header{type = initial} = Header, Packet, Rest, LargestRecv, State) -> #conn_state{role = Role, crypto = #conn_crypto{keys = #{initial := InitKeys}}} = State, nquic_packet:unmask_and_decrypt( Packet, Rest, Header, aes_128_gcm, nquic_keys:peer_keys(Role, InitKeys), LargestRecv ); decrypt_packet(#long_header{type = handshake} = Header, Packet, Rest, LargestRecv, State) -> #conn_state{role = Role, crypto = #conn_crypto{cipher = Cipher, keys = Keys}} = State, decrypt_with_keys( maps:get(handshake, Keys, undefined), no_handshake_keys, Packet, Rest, Header, Cipher, Role, LargestRecv ); decrypt_packet(#long_header{type = rtt0} = Header, Packet, Rest, LargestRecv, State) -> #conn_state{crypto = #conn_crypto{cipher = Cipher, keys = Keys}} = State, decrypt_zero_rtt(maps:get(rtt0, Keys, undefined), Packet, Rest, Header, Cipher, LargestRecv). -spec decrypt_short_phase_mismatch( #short_header{}, binary(), binary(), non_neg_integer(), aes_128_gcm | aes_256_gcm | chacha20_poly1305, nquic_protocol:state() ) -> {ok, nquic_packet:header(), [nquic_frame:t()], nquic_protocol:state()} | {error, term()}. decrypt_short_phase_mismatch(DecHeader, AAD, CT, PN, Cipher, State) -> #conn_crypto{old_read_keys = OldReadKeys} = State#conn_state.crypto, case try_old_read_keys(OldReadKeys, Cipher, PN, AAD, CT) of {ok, Frames} -> {ok, DecHeader, Frames, State}; none -> try_rotated_read_keys(DecHeader, AAD, CT, PN, Cipher, State) end. -spec decrypt_short_with_key_update( binary(), binary(), #short_header{}, non_neg_integer(), nquic_protocol:state() ) -> {ok, nquic_packet:header(), [nquic_frame:t()], nquic_protocol:state()} | {error, term()}. decrypt_short_with_key_update(Packet, Rest, Header, LargestRecv, State) -> #conn_state{crypto = Crypto} = State, #conn_crypto{cipher = Cipher, key_phase = CurrentKP, app_recv_keys = RecvKeys} = Crypto, case RecvKeys of undefined -> {error, no_app_keys}; CurrentPeerKeys -> case nquic_packet:unmask_header( Packet, Rest, Header, Cipher, CurrentPeerKeys, LargestRecv ) of {error, _} = Err -> Err; {ok, #short_header{key_phase = KP} = DecHeader, PN, AAD, CT} when KP =:= CurrentKP -> case nquic_packet:decrypt_unmasked(Cipher, CurrentPeerKeys, PN, AAD, CT) of {ok, Frames} -> {ok, DecHeader, Frames, State}; {error, _} = Err -> Err end; {ok, #short_header{} = DecHeader, PN, AAD, CT} -> decrypt_short_phase_mismatch(DecHeader, AAD, CT, PN, Cipher, State) end end. -spec decrypt_with_keys( map() | undefined, atom(), binary(), binary(), nquic_packet:header(), atom(), client | server, non_neg_integer() ) -> {ok, nquic_packet:header(), [nquic_frame:t()]} | {error, term()}. decrypt_with_keys(undefined, MissingTag, _Packet, _Rest, _Header, _Cipher, _Role, _LargestRecv) -> {error, MissingTag}; decrypt_with_keys(Keys, _MissingTag, Packet, Rest, Header, Cipher, Role, LargestRecv) -> nquic_packet:unmask_and_decrypt( Packet, Rest, Header, Cipher, nquic_keys:peer_keys(Role, Keys), LargestRecv ). -spec decrypt_zero_rtt( map() | undefined, binary(), binary(), nquic_packet:header(), atom(), non_neg_integer() ) -> {ok, nquic_packet:header(), [nquic_frame:t()]} | {error, term()}. decrypt_zero_rtt(undefined, _Packet, _Rest, _Header, _Cipher, _LargestRecv) -> {error, no_zero_rtt_keys}; decrypt_zero_rtt(#{client := CKeys}, Packet, Rest, Header, Cipher, LargestRecv) -> nquic_packet:unmask_and_decrypt(Packet, Rest, Header, Cipher, CKeys, LargestRecv). -spec filter_zero_rtt_frames(nquic_packet:header(), nquic_protocol:state(), [nquic_frame:t()]) -> [nquic_frame:t()]. filter_zero_rtt_frames( #long_header{type = rtt0}, #conn_state{crypto = #conn_crypto{zero_rtt_accepted = false}}, Frames ) -> [F || F <- Frames, is_record(F, crypto)]; filter_zero_rtt_frames(_Header, _State, Frames) -> Frames. -spec finalize_frames( {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}, nquic_packet:header(), [nquic_frame:t()] ) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. finalize_frames({ok, Events, State}, DecHeader, Frames) -> {ok, Events, nquic_protocol_ack:maybe_queue_ack(DecHeader, Frames, State)}; finalize_frames({error, _, _} = Error, _DecHeader, _Frames) -> Error. -spec handle_decrypt_result( {ok, nquic_packet:header(), [nquic_frame:t()]} | {error, term()}, nquic_packet:header(), binary(), nquic_protocol:state() ) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_decrypt_result({ok, DecHeader, Frames}, _Header, _Packet, State) -> process_decrypted(DecHeader, Frames, State); handle_decrypt_result({error, frame_encoding_error}, _Header, _Packet, State) -> {error, {transport_error, frame_encoding_error}, State}; handle_decrypt_result({error, protocol_violation}, _Header, _Packet, State) -> {error, {transport_error, protocol_violation}, State}; handle_decrypt_result(_Error, #short_header{}, Packet, State) -> check_stateless_reset(Packet, State); handle_decrypt_result(_Error, _Header, _Packet, State) -> {ok, [], State}. -spec handle_single_packet(binary(), binary(), nquic_packet:header(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_single_packet(Packet, Rest, Header, State) -> case maybe_compatible_version_negotiation(Header, State) of {ok, State0} -> case nquic_protocol_send:ensure_initial_keys(Header, State0) of {ok, State1} -> State2 = nquic_protocol_send:maybe_update_dcid(Header, State1), decrypt_and_process(Packet, Rest, Header, State2); _Error -> {ok, [], State0} end; {error, _Reason} -> {ok, [], State} end. -spec largest_received_for(nquic_packet:space(), nquic_protocol:state()) -> non_neg_integer(). largest_received_for(application, #conn_state{app_largest_received = LR}) -> max(0, LR); largest_received_for(Space, #conn_state{pn_spaces = PnSpaces}) -> SpaceMap = maps:get(Space, PnSpaces, #{next_pn => 0}), maps:get(largest_received, SpaceMap, 0). -spec mark_server_packet_processed(nquic_protocol:state()) -> nquic_protocol:state(). mark_server_packet_processed(#conn_state{server_packet_processed = true} = State) -> State; mark_server_packet_processed(#conn_state{role = client} = State) -> State#conn_state{server_packet_processed = true}; mark_server_packet_processed(State) -> State. -doc """ RFC 9368 Compatible Version Negotiation (client side). When the client receives the first Initial packet from the server with a QUIC version different from the one originally sent, and that version is listed in the client's advertised `other_versions`, switch to it: rederive Initial keys with the new version's salt and HKDF labels, and update the connection's negotiated version. Subsequent packets, including the just- arrived Initial, are then processed with the chosen version's keys. The check is gated on `server_packet_processed = false` so a peer cannot flip the version after the handshake has begun making progress on the original version. """. -spec maybe_compatible_version_negotiation(nquic_packet:header(), nquic_protocol:state()) -> {ok, nquic_protocol:state()} | {error, term()}. maybe_compatible_version_negotiation( #long_header{type = initial, version = HeaderVer}, #conn_state{ role = client, version = CurVer, server_packet_processed = false, local_params = #transport_params{ version_information = #{other_versions := Others} } } = State ) when HeaderVer =/= CurVer, HeaderVer =/= 0 -> case lists:member(HeaderVer, Others) andalso nquic_packet:is_supported_version(HeaderVer) of true -> {ok, switch_compat_version(HeaderVer, State)}; false -> {error, version_negotiation_error} end; maybe_compatible_version_negotiation(_Header, State) -> {ok, State}. -spec maybe_discard_initial_on_server_handshake_received( nquic_packet:space(), nquic_protocol:state() ) -> nquic_protocol:state(). maybe_discard_initial_on_server_handshake_received( handshake, #conn_state{role = server, crypto = #conn_crypto{keys = Keys}} = State ) -> case maps:is_key(initial, Keys) of true -> nquic_protocol_handshake:discard_initial_keys(State); false -> State end; maybe_discard_initial_on_server_handshake_received(_Space, State) -> State. -spec maybe_update_peer_spin( nquic_packet:header(), nquic_packet:space(), nquic_packet_number:t(), nquic_protocol:state() ) -> nquic_protocol:state(). maybe_update_peer_spin( #short_header{spin = Spin}, application, PN, #conn_state{spin_enabled = true, app_largest_received = Prev} = State ) when PN > Prev -> State#conn_state{peer_spin = Spin}; maybe_update_peer_spin(_Header, _Space, _PN, State) -> State. -spec process_decrypted(nquic_packet:header(), [nquic_frame:t()], nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. process_decrypted(DecHeader, Frames, State) -> State1 = nquic_protocol_key_update:maybe_handle_key_update(DecHeader, State), State2 = mark_server_packet_processed(State1), Space = nquic_protocol_send:packet_space_from_header(DecHeader), PN = nquic_protocol_send:packet_number_from_header(DecHeader), State2a = maybe_update_peer_spin(DecHeader, Space, PN, State2), State2b = qlog_packet_received(State2a, Space, PN), State3 = nquic_protocol_ack:track_received_pn_and_ecn( Space, PN, State2b#conn_state.recv_ecn, State2b ), State4 = maybe_discard_initial_on_server_handshake_received(Space, State3), Filtered = filter_zero_rtt_frames(DecHeader, State4, Frames), finalize_frames(handle_frames(Filtered, DecHeader, State4), DecHeader, Filtered). -spec qlog_packet_received(nquic_protocol:state(), nquic_packet:space(), nquic_packet_number:t()) -> nquic_protocol:state(). qlog_packet_received(#conn_state{qlog = undefined} = State, _Space, _PN) -> State; qlog_packet_received(#conn_state{qlog = QLog} = State, Space, PN) -> Data = #{packet_type => Space, packet_number => PN}, State#conn_state{qlog = nquic_qlog:event(QLog, transport_packet_received, Data)}. -spec switch_compat_version(non_neg_integer(), nquic_protocol:state()) -> nquic_protocol:state(). switch_compat_version(NewVersion, State) -> DCID = State#conn_state.dcid, {ClientSecret, ServerSecret} = nquic_keys:initial_secrets(DCID, NewVersion), {CKey, CIV, CHP} = nquic_keys:derive_packet_protection( ClientSecret, aes_128_gcm, NewVersion ), {SKey, SIV, SHP} = nquic_keys:derive_packet_protection( ServerSecret, aes_128_gcm, NewVersion ), NewInitial = #{ client => nquic_keys:make_role_keys(aes_128_gcm, CKey, CIV, CHP), server => nquic_keys:make_role_keys(aes_128_gcm, SKey, SIV, SHP) }, Crypto0 = State#conn_state.crypto, NewKeys = (Crypto0#conn_crypto.keys)#{initial => NewInitial}, NewTLSState = case Crypto0#conn_crypto.tls_state of undefined -> undefined; TS -> TS#{quic_version => NewVersion} end, State#conn_state{ version = NewVersion, crypto = Crypto0#conn_crypto{keys = NewKeys, tls_state = NewTLSState} }. -spec try_old_read_keys( map() | undefined, aes_128_gcm | aes_256_gcm | chacha20_poly1305, non_neg_integer(), binary(), binary() ) -> {ok, [nquic_frame:t()]} | none. try_old_read_keys(undefined, _Cipher, _PN, _AAD, _CT) -> none; try_old_read_keys(OldReadKeys, Cipher, PN, AAD, CT) -> case nquic_packet:decrypt_unmasked(Cipher, OldReadKeys, PN, AAD, CT) of {ok, _} = Ok -> Ok; {error, _} -> none end. -spec try_rotated_read_keys( #short_header{}, binary(), binary(), non_neg_integer(), aes_128_gcm | aes_256_gcm | chacha20_poly1305, nquic_protocol:state() ) -> {ok, nquic_packet:header(), [nquic_frame:t()], nquic_protocol:state()} | {error, term()}. try_rotated_read_keys(DecHeader, AAD, CT, PN, Cipher, State) -> Rotated = nquic_protocol_key_update:perform_key_update(State), NewPeerKeys = (Rotated#conn_state.crypto)#conn_crypto.app_recv_keys, case nquic_packet:decrypt_unmasked(Cipher, NewPeerKeys, PN, AAD, CT) of {ok, Frames} -> {ok, DecHeader, Frames, Rotated}; {error, _} = Err -> Err end. %%%----------------------------------------------------------------------------- %% FRAME HANDLING %%%----------------------------------------------------------------------------- -spec apply_max_stream_data( nquic:stream_id(), non_neg_integer(), map(), #conn_streams{}, nquic_protocol:state() ) -> {ok, [nquic_protocol:event()], nquic_protocol:state()}. apply_max_stream_data(StreamID, MaxData, Streams, SS, State) -> case maps:find(StreamID, Streams) of {ok, S} -> CurrentMax = S#stream_state.send_max_data, NewMax = max(CurrentMax, MaxData), S1 = S#stream_state{send_max_data = NewMax}, SS1 = SS#conn_streams{streams = Streams#{StreamID => S1}}, State1 = State#conn_state{streams_state = SS1}, case NewMax > CurrentMax of true -> {Events, State2} = nquic_protocol_streams_send:scan_blocked_stream( StreamID, State1 ), {ok, Events, State2}; false -> {ok, [], State1} end; error -> {ok, [], State} end. -spec crypto_buffer_add( non_neg_integer(), binary(), crypto_buffer_entry() ) -> crypto_buffer_entry(). crypto_buffer_add(Offset, FragData, {NextOffset, Buf, Pending}) -> DataEnd = Offset + byte_size(FragData), if Offset =:= NextOffset -> crypto_buffer_merge(DataEnd, [Buf, FragData], Pending); Offset < NextOffset, DataEnd > NextOffset -> Skip = NextOffset - Offset, <<_:Skip/binary, Useful/binary>> = FragData, crypto_buffer_merge(DataEnd, [Buf, Useful], Pending); Offset =< NextOffset -> {NextOffset, Buf, Pending}; true -> NewPending = lists:sort( fun({A, _}, {B, _}) -> A =< B end, [{Offset, FragData} | Pending] ), {NextOffset, Buf, NewPending} end. -spec crypto_buffer_data(crypto_buffer_entry()) -> binary(). crypto_buffer_data({_NextOffset, Data, _Pending}) -> iolist_to_binary(Data). -spec crypto_buffer_merge( non_neg_integer(), iodata(), [{non_neg_integer(), binary()}] ) -> crypto_buffer_entry(). crypto_buffer_merge(NextOffset, Buf, []) -> {NextOffset, Buf, []}; crypto_buffer_merge(NextOffset, Buf, [{Offset, FragData} | Rest]) -> DataEnd = Offset + byte_size(FragData), if Offset =< NextOffset, DataEnd > NextOffset -> Skip = NextOffset - Offset, <<_:Skip/binary, Useful/binary>> = FragData, crypto_buffer_merge(DataEnd, [Buf, Useful], Rest); Offset =< NextOffset -> crypto_buffer_merge(NextOffset, Buf, Rest); true -> {NextOffset, Buf, [{Offset, FragData} | Rest]} end. -spec handle_ack_frame(#ack{}, nquic_packet:header(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()}. handle_ack_frame( #ack{ largest_acknowledged = LargestAcked, delay = Delay, first_ack_range = FirstRange, ack_ranges = Ranges, ecn_counts = ECNCounts }, Header, State ) -> #conn_state{loss_state = LossState, remote_params = RemoteParams} = State, Space = nquic_protocol_send:packet_space_from_header(Header), AckedRanges = nquic_frame_handler:decode_ack_ranges(LargestAcked, FirstRange, Ranges), Now = erlang:monotonic_time(microsecond), AckDelay = nquic_protocol:scale_ack_delay(Delay, RemoteParams), MaxAckDelayUs = case RemoteParams of #transport_params{max_ack_delay = MAD} -> MAD * 1000; undefined -> 25_000 end, InFlightBefore = nquic_loss:get_bytes_in_flight(LossState), {ok, NewLossState, AckedFrames, LostFrames} = nquic_loss:on_ack_received( LossState, Space, AckedRanges, AckDelay, Now, MaxAckDelayUs ), NewLossState2 = nquic_loss:process_ecn_counts(NewLossState, Space, ECNCounts), InFlightAfter = nquic_loss:get_bytes_in_flight(NewLossState2), State1 = State#conn_state{loss_state = NewLossState2}, State1a = nquic_protocol_ack:apply_received_ranges_prune(Space, AckedFrames, State1), State2 = nquic_protocol_send:handle_lost_frames(LostFrames, Space, State1a), case InFlightAfter < InFlightBefore of true -> {WritableEvents, State3} = nquic_protocol_streams_send:scan_blocked_streams(State2), {ok, WritableEvents, State3}; false -> {ok, [], State2} end. -spec handle_cid_frame(nquic_frame:t(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()}. handle_cid_frame( #new_connection_id{ seq_num = SeqNum, retire_prior_to = RetirePriorTo, cid = CID, stateless_reset_token = Token }, State ) -> {ok, State1} = nquic_protocol_cid:handle_new_connection_id( SeqNum, RetirePriorTo, CID, Token, State ), {ok, [], State1}; handle_cid_frame(#retire_connection_id{seq_num = SeqNum}, State) -> {ok, State1} = nquic_protocol_cid:handle_retire_connection_id(SeqNum, State), {ok, [], State1}. -spec handle_crypto_frame(#crypto{}, nquic_packet:header(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_crypto_frame(#crypto{}, #long_header{type = rtt0}, State) -> {error, {transport_error, protocol_violation}, State}; handle_crypto_frame( #crypto{offset = Offset, data = CryptoData}, #long_header{type = initial}, #conn_state{role = Role} = State ) -> {NewBuf, State1} = nquic_protocol_handshake:buffer_crypto( initial, Offset, CryptoData, State ), case Role of client -> nquic_protocol_handshake:process_initial_crypto_client(NewBuf, State1); server -> nquic_protocol_handshake:process_initial_crypto_server(NewBuf, State1) end; handle_crypto_frame( #crypto{offset = Offset, data = CryptoData}, #long_header{type = handshake}, #conn_state{role = Role} = State ) -> {NewBuf, State1} = nquic_protocol_handshake:buffer_crypto( handshake, Offset, CryptoData, State ), case Role of client -> nquic_protocol_handshake:process_handshake_crypto_client(NewBuf, State1); server -> nquic_protocol_handshake:process_handshake_crypto_server(NewBuf, State1) end; handle_crypto_frame(#crypto{data = CryptoData}, _Header, State) -> case nquic_frame_handler:check_post_handshake_crypto(CryptoData) of ok -> Events = case CryptoData of <<4:8, _/binary>> -> [{new_session_ticket, CryptoData}]; _ -> [] end, {ok, Events, State}; {error, TLSError} -> {error, {transport_error, TLSError}, State} end. -spec handle_flow_control_frame(nquic_frame:t(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_flow_control_frame(#max_data{max_data = MaxData}, State) -> Flow0 = State#conn_state.flow, CurrentMax = Flow0#conn_flow.remote_max_data, NewMax = max(CurrentMax, MaxData), State1 = State#conn_state{flow = Flow0#conn_flow{remote_max_data = NewMax}}, case NewMax > CurrentMax of true -> {Events, State2} = nquic_protocol_streams_send:scan_blocked_streams(State1), {ok, Events, State2}; false -> {ok, [], State1} end; handle_flow_control_frame( #max_stream_data{stream_id = StreamID, max_stream_data = MaxData}, State ) -> #conn_state{streams_state = SS, role = Role} = State, #conn_streams{streams = Streams} = SS, case nquic_frame_handler:validate_stream_for_max_stream_data(StreamID, Role, Streams) of ok -> apply_max_stream_data(StreamID, MaxData, Streams, SS, State); {error, stream_state_error} -> case nquic_protocol_streams_lifecycle:is_closed_stream(StreamID, State) of true -> {ok, [], State}; false -> {error, {transport_error, stream_state_error}, State} end end; handle_flow_control_frame(#max_streams{max_streams = Max, is_uni = false}, State) -> SS0 = State#conn_state.streams_state, NewSS = SS0#conn_streams{ peer_max_streams_bidi = max(SS0#conn_streams.peer_max_streams_bidi, Max) }, {ok, [], State#conn_state{streams_state = NewSS}}; handle_flow_control_frame(#max_streams{max_streams = Max, is_uni = true}, State) -> SS0 = State#conn_state.streams_state, NewSS = SS0#conn_streams{ peer_max_streams_uni = max(SS0#conn_streams.peer_max_streams_uni, Max) }, {ok, [], State#conn_state{streams_state = NewSS}}; handle_flow_control_frame(#streams_blocked{is_uni = false}, State) -> Current = (State#conn_state.streams_state)#conn_streams.local_max_streams_bidi, Frame = #max_streams{max_streams = Current, is_uni = false}, {ok, State1} = nquic_protocol_send_queues:queue_app_frame(Frame, State), SS1 = (State1#conn_state.streams_state)#conn_streams{last_sent_max_streams_bidi = Current}, {ok, [], State1#conn_state{streams_state = SS1}}; handle_flow_control_frame(#streams_blocked{is_uni = true}, State) -> Current = (State#conn_state.streams_state)#conn_streams.local_max_streams_uni, Frame = #max_streams{max_streams = Current, is_uni = true}, {ok, State1} = nquic_protocol_send_queues:queue_app_frame(Frame, State), SS1 = (State1#conn_state.streams_state)#conn_streams{last_sent_max_streams_uni = Current}, {ok, [], State1#conn_state{streams_state = SS1}}; handle_flow_control_frame(#data_blocked{limit = Limit}, State) -> nquic_protocol_streams:respond_to_data_blocked(Limit, State); handle_flow_control_frame(#stream_data_blocked{stream_id = StreamID, limit = Limit}, State) -> nquic_protocol_streams:respond_to_stream_data_blocked(StreamID, Limit, State). -spec handle_frame(nquic_frame:t(), nquic_packet:header(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_frame(#crypto{} = F, H, S) -> handle_crypto_frame(F, H, S); handle_frame(#stream{} = F, _H, S) -> handle_stream_data_frame(F, S); handle_frame(#reset_stream{} = F, _H, S) -> handle_reset_stream_frame(F, S); handle_frame(#stop_sending{} = F, _H, S) -> handle_stop_sending_frame(F, S); handle_frame(#ack{} = F, H, S) -> handle_ack_frame(F, H, S); handle_frame(#max_data{} = F, _H, S) -> handle_flow_control_frame(F, S); handle_frame(#max_stream_data{} = F, _H, S) -> handle_flow_control_frame(F, S); handle_frame(#max_streams{} = F, _H, S) -> handle_flow_control_frame(F, S); handle_frame(#streams_blocked{} = F, _H, S) -> handle_flow_control_frame(F, S); handle_frame(#data_blocked{} = F, _H, S) -> handle_flow_control_frame(F, S); handle_frame(#stream_data_blocked{} = F, _H, S) -> handle_flow_control_frame(F, S); handle_frame(#path_challenge{} = F, H, S) -> handle_path_frame(F, H, S); handle_frame(#path_response{} = F, H, S) -> handle_path_frame(F, H, S); handle_frame(#new_connection_id{} = F, _H, S) -> handle_cid_frame(F, S); handle_frame(#retire_connection_id{} = F, _H, S) -> handle_cid_frame(F, S); handle_frame(F, _H, S) -> handle_misc_frame(F, S). -spec handle_frames([nquic_frame:t()], nquic_packet:header(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_frames(Frames, Header, State) -> handle_frames_acc(Frames, Header, State, []). -spec handle_frames_acc([nquic_frame:t()], nquic_packet:header(), nquic_protocol:state(), [ nquic_protocol:event() ]) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_frames_acc([], _Header, State, EventsAcc) -> {ok, lists:reverse(EventsAcc), State}; handle_frames_acc([Frame | Rest], Header, State, EventsAcc) -> case handle_frame(Frame, Header, State) of {ok, [], NewState} -> handle_frames_acc(Rest, Header, NewState, EventsAcc); {ok, Events, NewState} -> handle_frames_acc(Rest, Header, NewState, lists:reverse(Events, EventsAcc)); {error, _, _} = Error -> Error end. -spec handle_misc_frame(nquic_frame:t(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_misc_frame(#new_token{}, #conn_state{role = server} = State) -> {error, {transport_error, protocol_violation}, State}; handle_misc_frame(#new_token{token = Token}, #conn_state{role = client} = State) -> {ok, [{new_token_received, Token}], State}; handle_misc_frame(#handshake_done{}, #conn_state{role = server} = State) -> {error, {transport_error, protocol_violation}, State}; handle_misc_frame(#handshake_done{}, State) -> {ok, [], nquic_protocol_handshake:discard_handshake_keys(State)}; handle_misc_frame(#connection_close{}, State) -> {ok, [connection_closed], State}; handle_misc_frame(#ping{}, State) -> {ok, [], State}; handle_misc_frame(#datagram{data = Data}, State) -> {ok, [{datagram_received, Data}], State}; handle_misc_frame(_Frame, State) -> {ok, [], State}. -spec handle_path_frame(nquic_frame:t(), nquic_packet:header(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_path_frame(#path_challenge{}, #long_header{type = handshake}, State) -> {error, {transport_error, protocol_violation}, State}; handle_path_frame(#path_challenge{data = ChallengeData}, _Header, State) -> Frame = #path_response{data = ChallengeData}, {ok, NewState} = nquic_protocol_send_queues:queue_app_frame(Frame, State), {ok, [], NewState}; handle_path_frame(#path_response{data = ResponseData}, _Header, State) -> Path0 = State#conn_state.path, case nquic_path:on_response(Path0#conn_path_mgmt.path_state, ResponseData) of {validated, NewPS} -> State1 = State#conn_state{path = Path0#conn_path_mgmt{path_state = NewPS}}, on_path_validated(State1); {mismatch, _} -> {ok, [], State} end. -spec handle_reset_existing( nquic:stream_id(), non_neg_integer(), non_neg_integer(), #stream_state{}, nquic_protocol:state() ) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_reset_existing(StreamID, FinalSize, AppErrCode, Stream, State) -> case nquic_protocol_streams:handle_reset_stream( StreamID, FinalSize, AppErrCode, Stream, State ) of {ok, Events, State1} -> State2 = nquic_protocol_streams_lifecycle:maybe_cleanup_stream(StreamID, State1), {ok, Events, State2}; Error -> Error end. -spec handle_reset_stream_frame(#reset_stream{}, nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_reset_stream_frame( #reset_stream{stream_id = StreamID, app_error_code = AppErrCode, final_size = FinalSize}, State ) -> #conn_state{role = Role, streams_state = #conn_streams{streams = Streams}} = State, case nquic_frame_handler:validate_stream_for_reset(StreamID, Role) of {error, stream_state_error} -> {error, {transport_error, stream_state_error}, State}; ok -> case maps:find(StreamID, Streams) of {ok, Stream} -> handle_reset_existing(StreamID, FinalSize, AppErrCode, Stream, State); error -> case nquic_protocol_streams_lifecycle:is_closed_stream(StreamID, State) of true -> {ok, [], State}; false -> nquic_protocol_streams:handle_reset_stream_new(FinalSize, State) end end end. -spec handle_stop_sending_frame(#stop_sending{}, nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_stop_sending_frame( #stop_sending{stream_id = StreamID, app_error_code = ErrCode}, State ) -> #conn_state{role = Role, streams_state = #conn_streams{streams = Streams}} = State, case nquic_frame_handler:validate_stream_for_stop_sending(StreamID, Role, Streams) of {error, stream_state_error} -> case nquic_protocol_streams_lifecycle:is_closed_stream(StreamID, State) of true -> {ok, [], State}; false -> {error, {transport_error, stream_state_error}, State} end; ok -> nquic_protocol_streams:handle_stop_sending(StreamID, ErrCode, State) end. -spec handle_stream_data_dispatch( nquic:stream_id(), non_neg_integer(), binary(), nquic_frame:t(), map(), nquic_protocol:state() ) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_stream_data_dispatch(StreamID, Offset, StreamData, Frame, Streams, State) -> Existing = maps:find(StreamID, Streams), case Existing =:= error andalso nquic_protocol_streams_lifecycle:is_closed_stream(StreamID, State) of true -> {ok, [], State}; false -> Limits = #{ max_bidi => (State#conn_state.streams_state)#conn_streams.local_max_streams_bidi, max_uni => (State#conn_state.streams_state)#conn_streams.local_max_streams_uni }, nquic_protocol_streams:handle_stream_frame( StreamID, Offset, StreamData, Frame, Existing, Limits, State ) end. -spec handle_stream_data_frame(#stream{}, nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}. handle_stream_data_frame( #stream{stream_id = StreamID, offset = Offset, data = StreamData} = Frame, State ) -> #conn_state{streams_state = #conn_streams{streams = Streams}, role = Role} = State, case nquic_frame_handler:validate_stream_for_recv(StreamID, Role, Streams) of {error, stream_state_error} -> case nquic_protocol_streams_lifecycle:is_closed_stream(StreamID, State) of true -> {ok, [], State}; false -> {error, {transport_error, stream_state_error}, State} end; ok -> handle_stream_data_dispatch(StreamID, Offset, StreamData, Frame, Streams, State) end. -spec on_path_validated(nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()}. on_path_validated(#conn_state{self_migration_pending = true} = State) -> State1 = State#conn_state{self_migration_pending = false}, {ok, [local_migration_validated], State1}; on_path_validated(State) -> case nquic_protocol_migration:complete_migration(State) of {ok, State1} -> {ok, [], State1}; {error, no_available_cids} -> {ok, [], nquic_protocol_migration:revert_migration(State)} end.