nquic_protocol_send (nquic v1.2.0)
View SourceOutbound side of the QUIC protocol state.
Pure functions over #conn_state{} covering encrypted packet
construction (Initial / Handshake / 1-RTT / 0-RTT), MTU-aware
splitting and batched send, the per-flush send context,
anti-amplification gating (RFC 9000 §8.1), congestion-control
admission (RFC 9002), header protection sample extraction
(RFC 9001 §5.4), Initial-key derivation, and the loss-detection
retransmission glue.
ACK generation lives in nquic_protocol_ack; the per-encryption-level
pending-frame queues and their flush drains live in
nquic_protocol_send_queues. Both call down into this module's packet
builders module-qualified; the dependency is one-way.
Summary
Functions
Build an encrypted Handshake-space packet from a list of frames.
Same return shape as build_initial_packet/2. Returns
{error, no_handshake_keys, nquic_protocol:state()} before Handshake keys are
installed.
Build one or more Handshake-space packets from Frames, fragmenting the
CRYPTO flight so that no datagram exceeds the path/peer UDP payload limit
(min(max_payload_size, peer max_udp_payload_size), RFC 9000 §14.1 and
§18.2). Each batch becomes its own Handshake packet/datagram; oversized
CRYPTO frames are split at their offset boundary (RFC 9000 §19.6) and
reassemble on the peer. Without this split a large server flight (cert
chain) is sent as one >1200-byte datagram, which Chromium/quiche drops on
receipt (ERR_MSG_TOO_BIG) since it exceeds the peer's advertised
max_udp_payload_size, stalling the handshake.
Build an encrypted Initial-space packet from a list of frames.
Returns {ok, iodata(), nquic_protocol:state()} with the masked packet and the state
updated for loss detection, anti-amplification, and packet-number space.
The wire datagram is padded to RFC 9000 §14.1's 1200-byte minimum.
Returns {ok, <<>>, nquic_protocol:state()} if the connection is anti-amplification
limited (server has not yet validated the client). The caller should
not send anything in that case.
Returns {error, no_initial_keys, nquic_protocol:state()} if Initial keys have not
been derived yet (caller bug).
Used by flush/1 to drain pending_initial_frames and by callers that
need to send a single Initial packet directly (e.g. client first flight
once the handshake API exposes it).
Build an encrypted Initial-space packet with a Retry token.
Same shape as build_initial_packet/2. The token is included in the
long-header packet (see RFC 9000 §17.2.5, Retry); pass <<>> for
no-token Initials.
Service the loss detection timer before the handshake completes
(RFC 9002 Appendix A.10). A pending loss time runs
handle_loss_timeout/2. Otherwise the timer is a PTO, and
queue_handshake_probes/2 queues the probes.
Service the loss time of Space (RFC 9002 Appendix A.10). The frames
of the packets that the time threshold declares lost are queued again
by handle_lost_frames/3. When the congestion window decreases, one
packet can exceed it (RFC 9002 §7.3.2).
Largest UDP payload this endpoint sends on the path:
min(max_payload_size, peer max_udp_payload_size), never below 1200
bytes. This is the maximum datagram size of the congestion controller
(RFC 9002 §7.2). The peer max_udp_payload_size is a receive limit,
not a path MTU (RFC 9000 §18.2), so it only bounds the value.
Outgoing latency spin bit for 1-RTT packets (RFC 9000 §17.4). Returns 0 when the spin bit is disabled or no peer packet has been seen yet. When enabled, the server inverts the peer's last spin sample and the client mirrors it.
Upper bound of the bytes that a 1-RTT packet adds to its payload: the short header with a 4-byte packet number and the AEAD tag.
Get the max_ack_delay of the peer in microseconds. Before the transport
parameters of the peer arrive, the value is the 25 ms default of
RFC 9000 §18.2.
Queue the content of a 1-RTT PTO probe (RFC 9002 §6.2.4). Queued STREAM
or DATAGRAM data is new data, and the probe carries it. Otherwise the
frames of the oldest ack-eliciting packet in flight that still carries
retransmittable data are queued again, as handle_lost_frames/3 does.
When no packet in flight carries such data, the probe is a PING.
Count a handshake PTO and queue its probes (RFC 9002 §6.2.4).
Types
-type pre_encoded() :: {non_neg_integer(), iodata(), nquic_frame:t()}.
Functions
-spec build_app_packet([nquic_frame:t()], nquic_protocol:state()) -> {ok, iodata(), nquic_protocol:state()} | {error, term(), nquic_protocol:state()}.
-spec build_app_packet_pre([pre_encoded()], integer(), nquic_protocol:state()) -> {ok, iodata(), nquic_protocol:state()} | {error, term(), nquic_protocol:state()}.
-spec build_app_packet_pre_ctx([pre_encoded()], #app_send_ctx{cipher :: aes_128_gcm | aes_256_gcm | chacha20_poly1305, role_keys :: map(), key :: binary(), iv :: binary(), hp_source :: {ctx, crypto:crypto_state()} | {key, binary()}, dcid :: nquic:connection_id(), dcid_prefix_size :: pos_integer(), gso_size :: undefined | pos_integer(), pad_bin :: undefined | binary(), key_phase :: boolean(), largest_acked :: non_neg_integer(), time :: integer(), track_anti_amp :: boolean()}, nquic_protocol:state()) -> {ok, iodata(), nquic_protocol:state()} | {error, term(), nquic_protocol:state()}.
-spec build_handshake_packet([nquic_frame:t()], nquic_protocol:state()) -> {ok, iodata(), nquic_protocol:state()} | {error, term(), nquic_protocol:state()}.
Build an encrypted Handshake-space packet from a list of frames.
Same return shape as build_initial_packet/2. Returns
{error, no_handshake_keys, nquic_protocol:state()} before Handshake keys are
installed.
-spec build_handshake_packets([nquic_frame:t()], nquic_protocol:state()) -> {ok, [iodata()], nquic_protocol:state()}.
Build one or more Handshake-space packets from Frames, fragmenting the
CRYPTO flight so that no datagram exceeds the path/peer UDP payload limit
(min(max_payload_size, peer max_udp_payload_size), RFC 9000 §14.1 and
§18.2). Each batch becomes its own Handshake packet/datagram; oversized
CRYPTO frames are split at their offset boundary (RFC 9000 §19.6) and
reassemble on the peer. Without this split a large server flight (cert
chain) is sent as one >1200-byte datagram, which Chromium/quiche drops on
receipt (ERR_MSG_TOO_BIG) since it exceeds the peer's advertised
max_udp_payload_size, stalling the handshake.
Stops early and re-queues the unsent remainder if anti-amplification blocks a packet, so the rest is resent on the next flush / PTO.
-spec build_initial_packet([nquic_frame:t()], nquic_protocol:state()) -> {ok, iodata(), nquic_protocol:state()} | {error, term(), nquic_protocol:state()}.
Build an encrypted Initial-space packet from a list of frames.
Returns {ok, iodata(), nquic_protocol:state()} with the masked packet and the state
updated for loss detection, anti-amplification, and packet-number space.
The wire datagram is padded to RFC 9000 §14.1's 1200-byte minimum.
Returns {ok, <<>>, nquic_protocol:state()} if the connection is anti-amplification
limited (server has not yet validated the client). The caller should
not send anything in that case.
Returns {error, no_initial_keys, nquic_protocol:state()} if Initial keys have not
been derived yet (caller bug).
Used by flush/1 to drain pending_initial_frames and by callers that
need to send a single Initial packet directly (e.g. client first flight
once the handshake API exposes it).
-spec build_initial_packet([nquic_frame:t()], nquic_protocol:state(), binary()) -> {ok, iodata(), nquic_protocol:state()} | {error, term(), nquic_protocol:state()}.
Build an encrypted Initial-space packet with a Retry token.
Same shape as build_initial_packet/2. The token is included in the
long-header packet (see RFC 9000 §17.2.5, Retry); pass <<>> for
no-token Initials.
-spec build_packets_mtu_pre([pre_encoded()], pos_integer(), integer(), nquic_protocol:state(), [iodata()]) -> {[iodata()], nquic_protocol:state()}.
-spec build_packets_mtu_pre_ctx([pre_encoded()], pos_integer(), #app_send_ctx{cipher :: aes_128_gcm | aes_256_gcm | chacha20_poly1305, role_keys :: map(), key :: binary(), iv :: binary(), hp_source :: {ctx, crypto:crypto_state()} | {key, binary()}, dcid :: nquic:connection_id(), dcid_prefix_size :: pos_integer(), gso_size :: undefined | pos_integer(), pad_bin :: undefined | binary(), key_phase :: boolean(), largest_acked :: non_neg_integer(), time :: integer(), track_anti_amp :: boolean()}, nquic_protocol:state(), [iodata()]) -> {[iodata()], nquic_protocol:state()}.
-spec build_zero_rtt_packet_pre([pre_encoded()], integer(), nquic_protocol:state()) -> {ok, iodata(), nquic_protocol:state()} | {error, term(), nquic_protocol:state()}.
-spec check_anti_amplification(nquic_protocol:state(), non_neg_integer()) -> ok_no_track | ok_track | amplification_limited.
-spec check_congestion_control(nquic_protocol:state(), non_neg_integer()) -> ok | {blocked, non_neg_integer()}.
-spec ensure_initial_keys(nquic_packet:header(), nquic_protocol:state()) -> {ok, nquic_protocol:state()} | {error, nquic_error:any_reason()}.
-spec get_packet_len(nquic_packet:header(), non_neg_integer()) -> {ok, non_neg_integer()}.
-spec handle_handshake_timer(initial | handshake, nquic_protocol:state()) -> nquic_protocol:state().
Service the loss detection timer before the handshake completes
(RFC 9002 Appendix A.10). A pending loss time runs
handle_loss_timeout/2. Otherwise the timer is a PTO, and
queue_handshake_probes/2 queues the probes.
-spec handle_loss_timeout(nquic_packet:space(), nquic_protocol:state()) -> nquic_protocol:state().
Service the loss time of Space (RFC 9002 Appendix A.10). The frames
of the packets that the time threshold declares lost are queued again
by handle_lost_frames/3. When the congestion window decreases, one
packet can exceed it (RFC 9002 §7.3.2).
-spec handle_lost_frames([nquic_frame:t()], nquic_packet:space(), nquic_protocol:state()) -> nquic_protocol:state().
-spec make_app_send_ctx(nquic_protocol:state(), integer()) -> #app_send_ctx{cipher :: aes_128_gcm | aes_256_gcm | chacha20_poly1305, role_keys :: map(), key :: binary(), iv :: binary(), hp_source :: {ctx, crypto:crypto_state()} | {key, binary()}, dcid :: nquic:connection_id(), dcid_prefix_size :: pos_integer(), gso_size :: undefined | pos_integer(), pad_bin :: undefined | binary(), key_phase :: boolean(), largest_acked :: non_neg_integer(), time :: integer(), track_anti_amp :: boolean()}.
-spec max_datagram_size(nquic_protocol:state()) -> pos_integer().
Largest UDP payload this endpoint sends on the path:
min(max_payload_size, peer max_udp_payload_size), never below 1200
bytes. This is the maximum datagram size of the congestion controller
(RFC 9002 §7.2). The peer max_udp_payload_size is a receive limit,
not a path MTU (RFC 9000 §18.2), so it only bounds the value.
-spec maybe_update_dcid(nquic_packet:header(), nquic_protocol:state()) -> nquic_protocol:state().
-spec outgoing_spin(nquic_protocol:state()) -> 0..1.
Outgoing latency spin bit for 1-RTT packets (RFC 9000 §17.4). Returns 0 when the spin bit is disabled or no peer packet has been seen yet. When enabled, the server inverts the peer's last spin sample and the client mirrors it.
-spec packet_number_from_header(nquic_packet:header()) -> nquic_packet_number:t() | undefined.
-spec packet_overhead(nquic_protocol:state()) -> pos_integer().
Upper bound of the bytes that a 1-RTT packet adds to its payload: the short header with a 4-byte packet number and the AEAD tag.
-spec packet_payload_budget(nquic_protocol:state()) -> pos_integer().
-spec packet_space_from_header(nquic_packet:header()) -> nquic_packet:space().
-spec peer_max_ack_delay_us(#transport_params{original_destination_connection_id :: nquic:connection_id() | undefined, max_idle_timeout :: non_neg_integer(), stateless_reset_token :: binary() | undefined, max_udp_payload_size :: pos_integer(), initial_max_data :: non_neg_integer(), initial_max_stream_data_bidi_local :: non_neg_integer(), initial_max_stream_data_bidi_remote :: non_neg_integer(), initial_max_stream_data_uni :: non_neg_integer(), initial_max_streams_bidi :: non_neg_integer(), initial_max_streams_uni :: non_neg_integer(), ack_delay_exponent :: 0..20, max_ack_delay :: non_neg_integer(), disable_active_migration :: boolean(), preferred_address :: nquic_transport:preferred_address() | undefined, active_connection_id_limit :: non_neg_integer(), initial_source_connection_id :: nquic:connection_id() | undefined, retry_source_connection_id :: nquic:connection_id() | undefined, version_information :: nquic_transport:version_information() | undefined, max_datagram_frame_size :: non_neg_integer() | undefined} | undefined) -> non_neg_integer().
Get the max_ack_delay of the peer in microseconds. Before the transport
parameters of the peer arrive, the value is the 25 ms default of
RFC 9000 §18.2.
-spec queue_app_probe(nquic_protocol:state()) -> nquic_protocol:state().
Queue the content of a 1-RTT PTO probe (RFC 9002 §6.2.4). Queued STREAM
or DATAGRAM data is new data, and the probe carries it. Otherwise the
frames of the oldest ack-eliciting packet in flight that still carries
retransmittable data are queued again, as handle_lost_frames/3 does.
When no packet in flight carries such data, the probe is a PING.
-spec queue_handshake_probes(initial | handshake, nquic_protocol:state()) -> nquic_protocol:state().
Count a handshake PTO and queue its probes (RFC 9002 §6.2.4).
Each of the Initial and Handshake spaces with ack-eliciting packets in
flight gets one probe. The probe carries the CRYPTO data of those
packets, merged by offset, or a PING when they carry no CRYPTO data.
When neither space has packets in flight, the PTO is the client
anti-deadlock PTO of RFC 9002 §6.2.2.1. One PING then goes out at the
Handshake level if Handshake keys exist, else at the Phase level.
-spec take_frames_for_mtu_pre([pre_encoded()], pos_integer()) -> {[pre_encoded()], [pre_encoded()]}.