nquic_protocol_recv (nquic v1.0.0)

View Source

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.

Summary

Functions

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.

Handle a Version Negotiation packet. RFC 9000 §6 governs acceptance

Types

crypto_buffer_entry()

-type crypto_buffer_entry() :: {non_neg_integer(), iodata(), [{non_neg_integer(), binary()}]}.

Functions

check_stateless_reset/2

-spec check_stateless_reset(binary(), nquic_protocol:state()) ->
                               {ok, [nquic_protocol:event()], nquic_protocol:state()}.

crypto_buffer_add/3

-spec crypto_buffer_add(non_neg_integer(), binary(), crypto_buffer_entry()) -> crypto_buffer_entry().

crypto_buffer_data/1

-spec crypto_buffer_data(crypto_buffer_entry()) -> binary().

crypto_buffer_merge/3

-spec crypto_buffer_merge(non_neg_integer(), iodata(), [{non_neg_integer(), binary()}]) ->
                             crypto_buffer_entry().

handle_frame/3

handle_frames(Frames, Header, State)

handle_retry_packet/3

-spec handle_retry_packet(binary(),
                          #long_header{type :: initial | handshake | rtt0 | retry | version_negotiation,
                                       version :: 0..4294967295,
                                       dcid :: nquic:connection_id(),
                                       scid :: nquic:connection_id(),
                                       token :: binary() | undefined,
                                       payload_len :: non_neg_integer() | undefined,
                                       packet_number :: nquic_packet_number:t() | undefined,
                                       pn_len :: 1..4 | undefined},
                          nquic_protocol:state()) ->
                             {ok, [nquic_protocol:event()], nquic_protocol:state()}.

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.

handle_single_packet(Packet, Rest, Header, 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_version_negotiation/2

-spec handle_version_negotiation(#long_header{type ::
                                                  initial | handshake | rtt0 | retry |
                                                  version_negotiation,
                                              version :: 0..4294967295,
                                              dcid :: nquic:connection_id(),
                                              scid :: nquic:connection_id(),
                                              token :: binary() | undefined,
                                              payload_len :: non_neg_integer() | undefined,
                                              packet_number :: nquic_packet_number:t() | undefined,
                                              pn_len :: 1..4 | undefined},
                                 nquic_protocol:state()) ->
                                    {ok, [nquic_protocol:event()], nquic_protocol:state()} |
                                    {error, term(), nquic_protocol:state()}.

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.

maybe_update_peer_spin/4

process_datagram/3

-spec process_datagram(binary(), nquic_protocol:state(), [nquic_protocol:event()]) ->
                          {ok, [nquic_protocol:event()], nquic_protocol:state()} |
                          {error, term(), nquic_protocol:state()}.