-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,
<<CurrentPacket:PacketLen/binary, NextPackets/binary>> = 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 || <<V:32>> <= 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.