nquic_conn_close (nquic v1.0.0)

View Source

Connection close and draining state transitions (RFC 9000 §10.2).

Owns the CONNECTION_CLOSE emit path and the bookkeeping that moves a connection into the draining gen_statem state: cancelling the other timers, replying to parked waiters, notifying the owner, and arming the draining-period timeout. Also handles the dispatch / socket cleanup that runs from terminate/3.

Summary

Functions

Unregister all CIDs (and the original DCID for servers) from dispatch.

Close the UDP socket if this gen_statem owns it. Clients own the socket (the UDP FD is handed off via nquic_socket:controlling_process/2 in nquic:connect/3); server connections borrow the receiver's shared socket and must not close it. Server connections that have completed the server-initiated migration (RFC 9000 §9, server_per_conn_fd) own a dedicated connect(2)-bound ephemeral FD and must close it. The exported library-mode path is short-circuited at the terminate/3 entry.

Cancel every non-draining gen_statem timer. The draining state must not be woken by spurious idle / PTO / ack_delay / path_validation fires while it waits for draining_timeout. RFC 9000 §10.2.2: the only event that drives the connection during draining is the timeout itself.

Enter draining state after an explicit nquic:close/1,2 call. CONNECTION_CLOSE has already been flushed via nquic_protocol + the gen_statem's send path before we get here.

Enter draining state after sending a locally-generated CONNECTION_CLOSE. Used for transport errors detected by the local stack. StateName is the gen_statem state that was active when the violation was detected, so the close frame is emitted at an encryption level the peer can actually decrypt (RFC 9000 §10.2.3).

Common draining-state entry: notify the owner, fail parked waiters, arm the draining_timeout, and cancel the other timers.

Enter draining without sending CONNECTION_CLOSE. Used when the peer initiated the close (we must not respond, RFC 9000 §10.2.2).

Convert a transport-error stop into a draining transition. If no keys exist yet (very early failure, peer can't decrypt anything) we just stop. Otherwise we transition to draining so the close frame the local stack just emitted reaches the peer.

Emit Frame at every encryption level the peer can decrypt for StateName. RFC 9000 §10.2.3: during the handshake the peer may not yet have derived Handshake or Application keys, so a CONNECTION_CLOSE must be sent at a level the peer can decrypt. Until the handshake is confirmed (gen_statem state = established) the server SHOULD send the close in both Handshake (when keys exist) and Initial packets so at least one is processable. Once established, only the 1-RTT copy is needed.

Build a transport-level CONNECTION_CLOSE for Error and send it at the appropriate encryption levels.

Types

state_name()

-type state_name() :: nquic_conn_statem:state_name().

Functions

cleanup_dispatch/1

-spec cleanup_dispatch(#conn_state{role :: client | server,
                                   scid :: nquic:connection_id(),
                                   dcid :: nquic:connection_id(),
                                   odcid :: nquic:connection_id() | undefined,
                                   retry_scid :: nquic:connection_id() | undefined,
                                   retry_token :: binary(),
                                   version :: non_neg_integer(),
                                   version_preference :: [non_neg_integer()],
                                   socket :: nquic_socket:t() | undefined,
                                   peer :: nquic_socket:sockaddr() | undefined,
                                   select_info :: nquic_socket:select_info() | undefined,
                                   pn_spaces :: #{nquic_packet:space() => map()},
                                   app_next_pn :: non_neg_integer(),
                                   app_largest_received :: integer(),
                                   loss_state :: nquic_loss:loss_state() | undefined,
                                   dispatch_table :: nquic_dispatch:t() | undefined,
                                   listener :: pid() | undefined,
                                   connect_waiters :: [gen_server:from()],
                                   local_params ::
                                       #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},
                                   remote_params ::
                                       #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,
                                   server_packet_processed :: boolean(),
                                   owner :: pid() | undefined,
                                   owner_mon :: reference() | undefined,
                                   deferred_flush_pending :: boolean(),
                                   pending_ack_count :: non_neg_integer(),
                                   last_idle_ms :: non_neg_integer() | infinity | undefined,
                                   last_pto_ms :: non_neg_integer() | cancel | undefined,
                                   recv_ecn :: nquic_socket:ecn_mark(),
                                   pmtud :: nquic_pmtud:pmtud_state() | undefined,
                                   gso_size :: undefined | pos_integer(),
                                   max_payload_size :: pos_integer(),
                                   server_per_conn_fd :: boolean(),
                                   proactive_cids :: boolean(),
                                   socket_connected :: boolean(),
                                   self_migration_pending :: boolean(),
                                   metrics_counters :: nquic_metrics:conn_counters() | undefined,
                                   spin_enabled :: boolean(),
                                   peer_spin :: 0..1,
                                   new_token_enabled :: boolean(),
                                   new_token_lifetime :: pos_integer(),
                                   qlog :: undefined | nquic_qlog:qlog_state(),
                                   close_kind ::
                                       undefined | local | peer | idle_timeout | protocol_error,
                                   crypto ::
                                       #conn_crypto{tls_state :: term(),
                                                    keys :: #{nquic_packet:space() | rtt0 => map()},
                                                    app_send_keys :: map() | undefined,
                                                    app_recv_keys :: map() | undefined,
                                                    crypto_buffer ::
                                                        #{nquic_packet:space() =>
                                                              {non_neg_integer(), binary(), list()}},
                                                    cipher ::
                                                        aes_128_gcm | aes_256_gcm | chacha20_poly1305,
                                                    cipher_suites ::
                                                        [aes_128_gcm | aes_256_gcm | chacha20_poly1305] |
                                                        undefined,
                                                    key_phase :: boolean(),
                                                    key_update_pending :: boolean(),
                                                    client_app_secret :: binary() | undefined,
                                                    server_app_secret :: binary() | undefined,
                                                    old_read_keys ::
                                                        #{key := binary(), iv := binary()} | undefined,
                                                    zero_rtt_accepted :: boolean(),
                                                    replay_protection :: module() | undefined,
                                                    session_ticket :: map() | undefined,
                                                    resumption_secret :: binary() | undefined,
                                                    session_cache ::
                                                        atom() | false | {module, module()} | undefined,
                                                    token_cache :: atom() | false | {module, module()},
                                                    alpn :: [binary()] | undefined,
                                                    hostname :: string() | binary() | undefined,
                                                    cert :: binary() | undefined,
                                                    cert_chain :: [binary()],
                                                    key :: any() | undefined,
                                                    verify :: verify_none | verify_peer,
                                                    cacerts :: [binary()],
                                                    peer_cert :: binary() | undefined,
                                                    static_key :: binary() | undefined},
                                   streams_state ::
                                       #conn_streams{streams ::
                                                         #{nquic:stream_id() =>
                                                               #stream_state{stream_id ::
                                                                                 nquic:stream_id(),
                                                                             type :: bidi | uni,
                                                                             send_state ::
                                                                                 ready | send |
                                                                                 data_sent |
                                                                                 data_recvd |
                                                                                 reset_sent |
                                                                                 reset_recvd,
                                                                             send_offset ::
                                                                                 non_neg_integer(),
                                                                             send_max_data ::
                                                                                 non_neg_integer(),
                                                                             last_stream_data_blocked ::
                                                                                 non_neg_integer(),
                                                                             pending_send_data ::
                                                                                 [binary()],
                                                                             pending_send_size ::
                                                                                 non_neg_integer(),
                                                                             pending_send_fin ::
                                                                                 boolean(),
                                                                             recv_state ::
                                                                                 recv | size_known |
                                                                                 data_recvd |
                                                                                 reset_recvd |
                                                                                 data_read | reset_read,
                                                                             recv_offset ::
                                                                                 non_neg_integer(),
                                                                             recv_max_offset ::
                                                                                 non_neg_integer(),
                                                                             recv_window ::
                                                                                 non_neg_integer(),
                                                                             recv_buffer ::
                                                                                 gb_trees:tree(non_neg_integer(),
                                                                                               {binary(),
                                                                                                boolean()}),
                                                                             app_buffer :: iodata(),
                                                                             app_buffer_size ::
                                                                                 non_neg_integer()}},
                                                     next_bidi_stream :: nquic:stream_id() | undefined,
                                                     next_uni_stream :: nquic:stream_id() | undefined,
                                                     peer_max_streams_bidi :: non_neg_integer(),
                                                     peer_max_streams_uni :: non_neg_integer(),
                                                     local_max_streams_bidi :: non_neg_integer(),
                                                     local_max_streams_uni :: non_neg_integer(),
                                                     last_sent_max_streams_bidi :: non_neg_integer(),
                                                     last_sent_max_streams_uni :: non_neg_integer(),
                                                     max_peer_bidi_stream_id ::
                                                         non_neg_integer() | undefined,
                                                     max_peer_uni_stream_id ::
                                                         non_neg_integer() | undefined,
                                                     opened_peer_bidi_count :: non_neg_integer(),
                                                     opened_peer_uni_count :: non_neg_integer(),
                                                     closed_peer_bidi_wm :: integer(),
                                                     closed_peer_uni_wm :: integer(),
                                                     closed_peer_streams :: #{nquic:stream_id() => true},
                                                     recv_waiters ::
                                                         #{nquic:stream_id() => gen_statem:from()},
                                                     accept_stream_waiters ::
                                                         queue:queue(gen_statem:from()),
                                                     pending_streams :: queue:queue(nquic:stream_id()),
                                                     blocked_streams :: #{nquic:stream_id() => true},
                                                     pending_send_streams ::
                                                         #{nquic:stream_id() => true},
                                                     send_buffer_high_water :: pos_integer(),
                                                     send_timeout :: timeout(),
                                                     send_waiters ::
                                                         queue:queue(nquic_conn_send_waiters:t())},
                                   flow ::
                                       #conn_flow{local_max_data :: non_neg_integer(),
                                                  remote_max_data :: non_neg_integer(),
                                                  data_sent :: non_neg_integer(),
                                                  data_received :: non_neg_integer(),
                                                  last_data_blocked :: non_neg_integer(),
                                                  pending_initial_frames :: [nquic_frame:t()],
                                                  pending_handshake_frames :: [nquic_frame:t()],
                                                  pending_app_frames :: [nquic_frame:t()],
                                                  pending_app_pre_encoded ::
                                                      [{non_neg_integer(), iodata(), nquic_frame:t()}],
                                                  queued_app_send_bytes :: non_neg_integer()},
                                   path ::
                                       #conn_path_mgmt{path_state :: nquic_path:state() | undefined,
                                                       peer_cids ::
                                                           #{non_neg_integer() =>
                                                                 #{cid := nquic:connection_id(),
                                                                   token := binary()}},
                                                       local_cids ::
                                                           #{non_neg_integer() => nquic:connection_id()},
                                                       local_cid_seq :: non_neg_integer(),
                                                       peer_retire_prior_to :: non_neg_integer(),
                                                       anti_amp_bytes_received :: non_neg_integer(),
                                                       anti_amp_bytes_sent :: non_neg_integer(),
                                                       address_validated :: boolean()}}) ->
                          ok.

Unregister all CIDs (and the original DCID for servers) from dispatch.

close_owned_socket/1

-spec close_owned_socket(#conn_state{role :: client | server,
                                     scid :: nquic:connection_id(),
                                     dcid :: nquic:connection_id(),
                                     odcid :: nquic:connection_id() | undefined,
                                     retry_scid :: nquic:connection_id() | undefined,
                                     retry_token :: binary(),
                                     version :: non_neg_integer(),
                                     version_preference :: [non_neg_integer()],
                                     socket :: nquic_socket:t() | undefined,
                                     peer :: nquic_socket:sockaddr() | undefined,
                                     select_info :: nquic_socket:select_info() | undefined,
                                     pn_spaces :: #{nquic_packet:space() => map()},
                                     app_next_pn :: non_neg_integer(),
                                     app_largest_received :: integer(),
                                     loss_state :: nquic_loss:loss_state() | undefined,
                                     dispatch_table :: nquic_dispatch:t() | undefined,
                                     listener :: pid() | undefined,
                                     connect_waiters :: [gen_server:from()],
                                     local_params ::
                                         #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},
                                     remote_params ::
                                         #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,
                                     server_packet_processed :: boolean(),
                                     owner :: pid() | undefined,
                                     owner_mon :: reference() | undefined,
                                     deferred_flush_pending :: boolean(),
                                     pending_ack_count :: non_neg_integer(),
                                     last_idle_ms :: non_neg_integer() | infinity | undefined,
                                     last_pto_ms :: non_neg_integer() | cancel | undefined,
                                     recv_ecn :: nquic_socket:ecn_mark(),
                                     pmtud :: nquic_pmtud:pmtud_state() | undefined,
                                     gso_size :: undefined | pos_integer(),
                                     max_payload_size :: pos_integer(),
                                     server_per_conn_fd :: boolean(),
                                     proactive_cids :: boolean(),
                                     socket_connected :: boolean(),
                                     self_migration_pending :: boolean(),
                                     metrics_counters :: nquic_metrics:conn_counters() | undefined,
                                     spin_enabled :: boolean(),
                                     peer_spin :: 0..1,
                                     new_token_enabled :: boolean(),
                                     new_token_lifetime :: pos_integer(),
                                     qlog :: undefined | nquic_qlog:qlog_state(),
                                     close_kind ::
                                         undefined | local | peer | idle_timeout | protocol_error,
                                     crypto ::
                                         #conn_crypto{tls_state :: term(),
                                                      keys :: #{nquic_packet:space() | rtt0 => map()},
                                                      app_send_keys :: map() | undefined,
                                                      app_recv_keys :: map() | undefined,
                                                      crypto_buffer ::
                                                          #{nquic_packet:space() =>
                                                                {non_neg_integer(), binary(), list()}},
                                                      cipher ::
                                                          aes_128_gcm | aes_256_gcm | chacha20_poly1305,
                                                      cipher_suites ::
                                                          [aes_128_gcm | aes_256_gcm | chacha20_poly1305] |
                                                          undefined,
                                                      key_phase :: boolean(),
                                                      key_update_pending :: boolean(),
                                                      client_app_secret :: binary() | undefined,
                                                      server_app_secret :: binary() | undefined,
                                                      old_read_keys ::
                                                          #{key := binary(), iv := binary()} | undefined,
                                                      zero_rtt_accepted :: boolean(),
                                                      replay_protection :: module() | undefined,
                                                      session_ticket :: map() | undefined,
                                                      resumption_secret :: binary() | undefined,
                                                      session_cache ::
                                                          atom() |
                                                          false |
                                                          {module, module()} |
                                                          undefined,
                                                      token_cache :: atom() | false | {module, module()},
                                                      alpn :: [binary()] | undefined,
                                                      hostname :: string() | binary() | undefined,
                                                      cert :: binary() | undefined,
                                                      cert_chain :: [binary()],
                                                      key :: any() | undefined,
                                                      verify :: verify_none | verify_peer,
                                                      cacerts :: [binary()],
                                                      peer_cert :: binary() | undefined,
                                                      static_key :: binary() | undefined},
                                     streams_state ::
                                         #conn_streams{streams ::
                                                           #{nquic:stream_id() =>
                                                                 #stream_state{stream_id ::
                                                                                   nquic:stream_id(),
                                                                               type :: bidi | uni,
                                                                               send_state ::
                                                                                   ready | send |
                                                                                   data_sent |
                                                                                   data_recvd |
                                                                                   reset_sent |
                                                                                   reset_recvd,
                                                                               send_offset ::
                                                                                   non_neg_integer(),
                                                                               send_max_data ::
                                                                                   non_neg_integer(),
                                                                               last_stream_data_blocked ::
                                                                                   non_neg_integer(),
                                                                               pending_send_data ::
                                                                                   [binary()],
                                                                               pending_send_size ::
                                                                                   non_neg_integer(),
                                                                               pending_send_fin ::
                                                                                   boolean(),
                                                                               recv_state ::
                                                                                   recv | size_known |
                                                                                   data_recvd |
                                                                                   reset_recvd |
                                                                                   data_read |
                                                                                   reset_read,
                                                                               recv_offset ::
                                                                                   non_neg_integer(),
                                                                               recv_max_offset ::
                                                                                   non_neg_integer(),
                                                                               recv_window ::
                                                                                   non_neg_integer(),
                                                                               recv_buffer ::
                                                                                   gb_trees:tree(non_neg_integer(),
                                                                                                 {binary(),
                                                                                                  boolean()}),
                                                                               app_buffer :: iodata(),
                                                                               app_buffer_size ::
                                                                                   non_neg_integer()}},
                                                       next_bidi_stream :: nquic:stream_id() | undefined,
                                                       next_uni_stream :: nquic:stream_id() | undefined,
                                                       peer_max_streams_bidi :: non_neg_integer(),
                                                       peer_max_streams_uni :: non_neg_integer(),
                                                       local_max_streams_bidi :: non_neg_integer(),
                                                       local_max_streams_uni :: non_neg_integer(),
                                                       last_sent_max_streams_bidi :: non_neg_integer(),
                                                       last_sent_max_streams_uni :: non_neg_integer(),
                                                       max_peer_bidi_stream_id ::
                                                           non_neg_integer() | undefined,
                                                       max_peer_uni_stream_id ::
                                                           non_neg_integer() | undefined,
                                                       opened_peer_bidi_count :: non_neg_integer(),
                                                       opened_peer_uni_count :: non_neg_integer(),
                                                       closed_peer_bidi_wm :: integer(),
                                                       closed_peer_uni_wm :: integer(),
                                                       closed_peer_streams ::
                                                           #{nquic:stream_id() => true},
                                                       recv_waiters ::
                                                           #{nquic:stream_id() => gen_statem:from()},
                                                       accept_stream_waiters ::
                                                           queue:queue(gen_statem:from()),
                                                       pending_streams :: queue:queue(nquic:stream_id()),
                                                       blocked_streams :: #{nquic:stream_id() => true},
                                                       pending_send_streams ::
                                                           #{nquic:stream_id() => true},
                                                       send_buffer_high_water :: pos_integer(),
                                                       send_timeout :: timeout(),
                                                       send_waiters ::
                                                           queue:queue(nquic_conn_send_waiters:t())},
                                     flow ::
                                         #conn_flow{local_max_data :: non_neg_integer(),
                                                    remote_max_data :: non_neg_integer(),
                                                    data_sent :: non_neg_integer(),
                                                    data_received :: non_neg_integer(),
                                                    last_data_blocked :: non_neg_integer(),
                                                    pending_initial_frames :: [nquic_frame:t()],
                                                    pending_handshake_frames :: [nquic_frame:t()],
                                                    pending_app_frames :: [nquic_frame:t()],
                                                    pending_app_pre_encoded ::
                                                        [{non_neg_integer(), iodata(), nquic_frame:t()}],
                                                    queued_app_send_bytes :: non_neg_integer()},
                                     path ::
                                         #conn_path_mgmt{path_state :: nquic_path:state() | undefined,
                                                         peer_cids ::
                                                             #{non_neg_integer() =>
                                                                   #{cid := nquic:connection_id(),
                                                                     token := binary()}},
                                                         local_cids ::
                                                             #{non_neg_integer() =>
                                                                   nquic:connection_id()},
                                                         local_cid_seq :: non_neg_integer(),
                                                         peer_retire_prior_to :: non_neg_integer(),
                                                         anti_amp_bytes_received :: non_neg_integer(),
                                                         anti_amp_bytes_sent :: non_neg_integer(),
                                                         address_validated :: boolean()}}) ->
                            ok.

Close the UDP socket if this gen_statem owns it. Clients own the socket (the UDP FD is handed off via nquic_socket:controlling_process/2 in nquic:connect/3); server connections borrow the receiver's shared socket and must not close it. Server connections that have completed the server-initiated migration (RFC 9000 §9, server_per_conn_fd) own a dedicated connect(2)-bound ephemeral FD and must close it. The exported library-mode path is short-circuited at the terminate/3 entry.

draining_cancellations()

-spec draining_cancellations() -> [gen_statem:action()].

Cancel every non-draining gen_statem timer. The draining state must not be woken by spurious idle / PTO / ack_delay / path_validation fires while it waits for draining_timeout. RFC 9000 §10.2.2: the only event that drives the connection during draining is the timeout itself.

enter_close_draining(From, Data)

-spec enter_close_draining(gen_statem:from(),
                           #conn_state{role :: client | server,
                                       scid :: nquic:connection_id(),
                                       dcid :: nquic:connection_id(),
                                       odcid :: nquic:connection_id() | undefined,
                                       retry_scid :: nquic:connection_id() | undefined,
                                       retry_token :: binary(),
                                       version :: non_neg_integer(),
                                       version_preference :: [non_neg_integer()],
                                       socket :: nquic_socket:t() | undefined,
                                       peer :: nquic_socket:sockaddr() | undefined,
                                       select_info :: nquic_socket:select_info() | undefined,
                                       pn_spaces :: #{nquic_packet:space() => map()},
                                       app_next_pn :: non_neg_integer(),
                                       app_largest_received :: integer(),
                                       loss_state :: nquic_loss:loss_state() | undefined,
                                       dispatch_table :: nquic_dispatch:t() | undefined,
                                       listener :: pid() | undefined,
                                       connect_waiters :: [gen_server:from()],
                                       local_params ::
                                           #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},
                                       remote_params ::
                                           #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,
                                       server_packet_processed :: boolean(),
                                       owner :: pid() | undefined,
                                       owner_mon :: reference() | undefined,
                                       deferred_flush_pending :: boolean(),
                                       pending_ack_count :: non_neg_integer(),
                                       last_idle_ms :: non_neg_integer() | infinity | undefined,
                                       last_pto_ms :: non_neg_integer() | cancel | undefined,
                                       recv_ecn :: nquic_socket:ecn_mark(),
                                       pmtud :: nquic_pmtud:pmtud_state() | undefined,
                                       gso_size :: undefined | pos_integer(),
                                       max_payload_size :: pos_integer(),
                                       server_per_conn_fd :: boolean(),
                                       proactive_cids :: boolean(),
                                       socket_connected :: boolean(),
                                       self_migration_pending :: boolean(),
                                       metrics_counters :: nquic_metrics:conn_counters() | undefined,
                                       spin_enabled :: boolean(),
                                       peer_spin :: 0..1,
                                       new_token_enabled :: boolean(),
                                       new_token_lifetime :: pos_integer(),
                                       qlog :: undefined | nquic_qlog:qlog_state(),
                                       close_kind ::
                                           undefined | local | peer | idle_timeout | protocol_error,
                                       crypto ::
                                           #conn_crypto{tls_state :: term(),
                                                        keys :: #{nquic_packet:space() | rtt0 => map()},
                                                        app_send_keys :: map() | undefined,
                                                        app_recv_keys :: map() | undefined,
                                                        crypto_buffer ::
                                                            #{nquic_packet:space() =>
                                                                  {non_neg_integer(), binary(), list()}},
                                                        cipher ::
                                                            aes_128_gcm | aes_256_gcm |
                                                            chacha20_poly1305,
                                                        cipher_suites ::
                                                            [aes_128_gcm | aes_256_gcm |
                                                             chacha20_poly1305] |
                                                            undefined,
                                                        key_phase :: boolean(),
                                                        key_update_pending :: boolean(),
                                                        client_app_secret :: binary() | undefined,
                                                        server_app_secret :: binary() | undefined,
                                                        old_read_keys ::
                                                            #{key := binary(), iv := binary()} |
                                                            undefined,
                                                        zero_rtt_accepted :: boolean(),
                                                        replay_protection :: module() | undefined,
                                                        session_ticket :: map() | undefined,
                                                        resumption_secret :: binary() | undefined,
                                                        session_cache ::
                                                            atom() |
                                                            false |
                                                            {module, module()} |
                                                            undefined,
                                                        token_cache ::
                                                            atom() | false | {module, module()},
                                                        alpn :: [binary()] | undefined,
                                                        hostname :: string() | binary() | undefined,
                                                        cert :: binary() | undefined,
                                                        cert_chain :: [binary()],
                                                        key :: any() | undefined,
                                                        verify :: verify_none | verify_peer,
                                                        cacerts :: [binary()],
                                                        peer_cert :: binary() | undefined,
                                                        static_key :: binary() | undefined},
                                       streams_state ::
                                           #conn_streams{streams ::
                                                             #{nquic:stream_id() =>
                                                                   #stream_state{stream_id ::
                                                                                     nquic:stream_id(),
                                                                                 type :: bidi | uni,
                                                                                 send_state ::
                                                                                     ready | send |
                                                                                     data_sent |
                                                                                     data_recvd |
                                                                                     reset_sent |
                                                                                     reset_recvd,
                                                                                 send_offset ::
                                                                                     non_neg_integer(),
                                                                                 send_max_data ::
                                                                                     non_neg_integer(),
                                                                                 last_stream_data_blocked ::
                                                                                     non_neg_integer(),
                                                                                 pending_send_data ::
                                                                                     [binary()],
                                                                                 pending_send_size ::
                                                                                     non_neg_integer(),
                                                                                 pending_send_fin ::
                                                                                     boolean(),
                                                                                 recv_state ::
                                                                                     recv | size_known |
                                                                                     data_recvd |
                                                                                     reset_recvd |
                                                                                     data_read |
                                                                                     reset_read,
                                                                                 recv_offset ::
                                                                                     non_neg_integer(),
                                                                                 recv_max_offset ::
                                                                                     non_neg_integer(),
                                                                                 recv_window ::
                                                                                     non_neg_integer(),
                                                                                 recv_buffer ::
                                                                                     gb_trees:tree(non_neg_integer(),
                                                                                                   {binary(),
                                                                                                    boolean()}),
                                                                                 app_buffer :: iodata(),
                                                                                 app_buffer_size ::
                                                                                     non_neg_integer()}},
                                                         next_bidi_stream ::
                                                             nquic:stream_id() | undefined,
                                                         next_uni_stream ::
                                                             nquic:stream_id() | undefined,
                                                         peer_max_streams_bidi :: non_neg_integer(),
                                                         peer_max_streams_uni :: non_neg_integer(),
                                                         local_max_streams_bidi :: non_neg_integer(),
                                                         local_max_streams_uni :: non_neg_integer(),
                                                         last_sent_max_streams_bidi :: non_neg_integer(),
                                                         last_sent_max_streams_uni :: non_neg_integer(),
                                                         max_peer_bidi_stream_id ::
                                                             non_neg_integer() | undefined,
                                                         max_peer_uni_stream_id ::
                                                             non_neg_integer() | undefined,
                                                         opened_peer_bidi_count :: non_neg_integer(),
                                                         opened_peer_uni_count :: non_neg_integer(),
                                                         closed_peer_bidi_wm :: integer(),
                                                         closed_peer_uni_wm :: integer(),
                                                         closed_peer_streams ::
                                                             #{nquic:stream_id() => true},
                                                         recv_waiters ::
                                                             #{nquic:stream_id() => gen_statem:from()},
                                                         accept_stream_waiters ::
                                                             queue:queue(gen_statem:from()),
                                                         pending_streams ::
                                                             queue:queue(nquic:stream_id()),
                                                         blocked_streams :: #{nquic:stream_id() => true},
                                                         pending_send_streams ::
                                                             #{nquic:stream_id() => true},
                                                         send_buffer_high_water :: pos_integer(),
                                                         send_timeout :: timeout(),
                                                         send_waiters ::
                                                             queue:queue(nquic_conn_send_waiters:t())},
                                       flow ::
                                           #conn_flow{local_max_data :: non_neg_integer(),
                                                      remote_max_data :: non_neg_integer(),
                                                      data_sent :: non_neg_integer(),
                                                      data_received :: non_neg_integer(),
                                                      last_data_blocked :: non_neg_integer(),
                                                      pending_initial_frames :: [nquic_frame:t()],
                                                      pending_handshake_frames :: [nquic_frame:t()],
                                                      pending_app_frames :: [nquic_frame:t()],
                                                      pending_app_pre_encoded ::
                                                          [{non_neg_integer(),
                                                            iodata(),
                                                            nquic_frame:t()}],
                                                      queued_app_send_bytes :: non_neg_integer()},
                                       path ::
                                           #conn_path_mgmt{path_state :: nquic_path:state() | undefined,
                                                           peer_cids ::
                                                               #{non_neg_integer() =>
                                                                     #{cid := nquic:connection_id(),
                                                                       token := binary()}},
                                                           local_cids ::
                                                               #{non_neg_integer() =>
                                                                     nquic:connection_id()},
                                                           local_cid_seq :: non_neg_integer(),
                                                           peer_retire_prior_to :: non_neg_integer(),
                                                           anti_amp_bytes_received :: non_neg_integer(),
                                                           anti_amp_bytes_sent :: non_neg_integer(),
                                                           address_validated :: boolean()}}) ->
                              gen_statem:event_handler_result(term()).

Enter draining state after an explicit nquic:close/1,2 call. CONNECTION_CLOSE has already been flushed via nquic_protocol + the gen_statem's send path before we get here.

enter_draining/3

-spec enter_draining(state_name(),
                     {transport_error, atom()},
                     #conn_state{role :: client | server,
                                 scid :: nquic:connection_id(),
                                 dcid :: nquic:connection_id(),
                                 odcid :: nquic:connection_id() | undefined,
                                 retry_scid :: nquic:connection_id() | undefined,
                                 retry_token :: binary(),
                                 version :: non_neg_integer(),
                                 version_preference :: [non_neg_integer()],
                                 socket :: nquic_socket:t() | undefined,
                                 peer :: nquic_socket:sockaddr() | undefined,
                                 select_info :: nquic_socket:select_info() | undefined,
                                 pn_spaces :: #{nquic_packet:space() => map()},
                                 app_next_pn :: non_neg_integer(),
                                 app_largest_received :: integer(),
                                 loss_state :: nquic_loss:loss_state() | undefined,
                                 dispatch_table :: nquic_dispatch:t() | undefined,
                                 listener :: pid() | undefined,
                                 connect_waiters :: [gen_server:from()],
                                 local_params ::
                                     #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},
                                 remote_params ::
                                     #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,
                                 server_packet_processed :: boolean(),
                                 owner :: pid() | undefined,
                                 owner_mon :: reference() | undefined,
                                 deferred_flush_pending :: boolean(),
                                 pending_ack_count :: non_neg_integer(),
                                 last_idle_ms :: non_neg_integer() | infinity | undefined,
                                 last_pto_ms :: non_neg_integer() | cancel | undefined,
                                 recv_ecn :: nquic_socket:ecn_mark(),
                                 pmtud :: nquic_pmtud:pmtud_state() | undefined,
                                 gso_size :: undefined | pos_integer(),
                                 max_payload_size :: pos_integer(),
                                 server_per_conn_fd :: boolean(),
                                 proactive_cids :: boolean(),
                                 socket_connected :: boolean(),
                                 self_migration_pending :: boolean(),
                                 metrics_counters :: nquic_metrics:conn_counters() | undefined,
                                 spin_enabled :: boolean(),
                                 peer_spin :: 0..1,
                                 new_token_enabled :: boolean(),
                                 new_token_lifetime :: pos_integer(),
                                 qlog :: undefined | nquic_qlog:qlog_state(),
                                 close_kind :: undefined | local | peer | idle_timeout | protocol_error,
                                 crypto ::
                                     #conn_crypto{tls_state :: term(),
                                                  keys :: #{nquic_packet:space() | rtt0 => map()},
                                                  app_send_keys :: map() | undefined,
                                                  app_recv_keys :: map() | undefined,
                                                  crypto_buffer ::
                                                      #{nquic_packet:space() =>
                                                            {non_neg_integer(), binary(), list()}},
                                                  cipher ::
                                                      aes_128_gcm | aes_256_gcm | chacha20_poly1305,
                                                  cipher_suites ::
                                                      [aes_128_gcm | aes_256_gcm | chacha20_poly1305] |
                                                      undefined,
                                                  key_phase :: boolean(),
                                                  key_update_pending :: boolean(),
                                                  client_app_secret :: binary() | undefined,
                                                  server_app_secret :: binary() | undefined,
                                                  old_read_keys ::
                                                      #{key := binary(), iv := binary()} | undefined,
                                                  zero_rtt_accepted :: boolean(),
                                                  replay_protection :: module() | undefined,
                                                  session_ticket :: map() | undefined,
                                                  resumption_secret :: binary() | undefined,
                                                  session_cache ::
                                                      atom() | false | {module, module()} | undefined,
                                                  token_cache :: atom() | false | {module, module()},
                                                  alpn :: [binary()] | undefined,
                                                  hostname :: string() | binary() | undefined,
                                                  cert :: binary() | undefined,
                                                  cert_chain :: [binary()],
                                                  key :: any() | undefined,
                                                  verify :: verify_none | verify_peer,
                                                  cacerts :: [binary()],
                                                  peer_cert :: binary() | undefined,
                                                  static_key :: binary() | undefined},
                                 streams_state ::
                                     #conn_streams{streams ::
                                                       #{nquic:stream_id() =>
                                                             #stream_state{stream_id ::
                                                                               nquic:stream_id(),
                                                                           type :: bidi | uni,
                                                                           send_state ::
                                                                               ready | send |
                                                                               data_sent | data_recvd |
                                                                               reset_sent | reset_recvd,
                                                                           send_offset ::
                                                                               non_neg_integer(),
                                                                           send_max_data ::
                                                                               non_neg_integer(),
                                                                           last_stream_data_blocked ::
                                                                               non_neg_integer(),
                                                                           pending_send_data ::
                                                                               [binary()],
                                                                           pending_send_size ::
                                                                               non_neg_integer(),
                                                                           pending_send_fin :: boolean(),
                                                                           recv_state ::
                                                                               recv | size_known |
                                                                               data_recvd |
                                                                               reset_recvd | data_read |
                                                                               reset_read,
                                                                           recv_offset ::
                                                                               non_neg_integer(),
                                                                           recv_max_offset ::
                                                                               non_neg_integer(),
                                                                           recv_window ::
                                                                               non_neg_integer(),
                                                                           recv_buffer ::
                                                                               gb_trees:tree(non_neg_integer(),
                                                                                             {binary(),
                                                                                              boolean()}),
                                                                           app_buffer :: iodata(),
                                                                           app_buffer_size ::
                                                                               non_neg_integer()}},
                                                   next_bidi_stream :: nquic:stream_id() | undefined,
                                                   next_uni_stream :: nquic:stream_id() | undefined,
                                                   peer_max_streams_bidi :: non_neg_integer(),
                                                   peer_max_streams_uni :: non_neg_integer(),
                                                   local_max_streams_bidi :: non_neg_integer(),
                                                   local_max_streams_uni :: non_neg_integer(),
                                                   last_sent_max_streams_bidi :: non_neg_integer(),
                                                   last_sent_max_streams_uni :: non_neg_integer(),
                                                   max_peer_bidi_stream_id ::
                                                       non_neg_integer() | undefined,
                                                   max_peer_uni_stream_id ::
                                                       non_neg_integer() | undefined,
                                                   opened_peer_bidi_count :: non_neg_integer(),
                                                   opened_peer_uni_count :: non_neg_integer(),
                                                   closed_peer_bidi_wm :: integer(),
                                                   closed_peer_uni_wm :: integer(),
                                                   closed_peer_streams :: #{nquic:stream_id() => true},
                                                   recv_waiters ::
                                                       #{nquic:stream_id() => gen_statem:from()},
                                                   accept_stream_waiters ::
                                                       queue:queue(gen_statem:from()),
                                                   pending_streams :: queue:queue(nquic:stream_id()),
                                                   blocked_streams :: #{nquic:stream_id() => true},
                                                   pending_send_streams :: #{nquic:stream_id() => true},
                                                   send_buffer_high_water :: pos_integer(),
                                                   send_timeout :: timeout(),
                                                   send_waiters ::
                                                       queue:queue(nquic_conn_send_waiters:t())},
                                 flow ::
                                     #conn_flow{local_max_data :: non_neg_integer(),
                                                remote_max_data :: non_neg_integer(),
                                                data_sent :: non_neg_integer(),
                                                data_received :: non_neg_integer(),
                                                last_data_blocked :: non_neg_integer(),
                                                pending_initial_frames :: [nquic_frame:t()],
                                                pending_handshake_frames :: [nquic_frame:t()],
                                                pending_app_frames :: [nquic_frame:t()],
                                                pending_app_pre_encoded ::
                                                    [{non_neg_integer(), iodata(), nquic_frame:t()}],
                                                queued_app_send_bytes :: non_neg_integer()},
                                 path ::
                                     #conn_path_mgmt{path_state :: nquic_path:state() | undefined,
                                                     peer_cids ::
                                                         #{non_neg_integer() =>
                                                               #{cid := nquic:connection_id(),
                                                                 token := binary()}},
                                                     local_cids ::
                                                         #{non_neg_integer() => nquic:connection_id()},
                                                     local_cid_seq :: non_neg_integer(),
                                                     peer_retire_prior_to :: non_neg_integer(),
                                                     anti_amp_bytes_received :: non_neg_integer(),
                                                     anti_amp_bytes_sent :: non_neg_integer(),
                                                     address_validated :: boolean()}}) ->
                        gen_statem:event_handler_result(term()).

Enter draining state after sending a locally-generated CONNECTION_CLOSE. Used for transport errors detected by the local stack. StateName is the gen_statem state that was active when the violation was detected, so the close frame is emitted at an encryption level the peer can actually decrypt (RFC 9000 §10.2.3).

enter_draining_common(Data)

-spec enter_draining_common(#conn_state{role :: client | server,
                                        scid :: nquic:connection_id(),
                                        dcid :: nquic:connection_id(),
                                        odcid :: nquic:connection_id() | undefined,
                                        retry_scid :: nquic:connection_id() | undefined,
                                        retry_token :: binary(),
                                        version :: non_neg_integer(),
                                        version_preference :: [non_neg_integer()],
                                        socket :: nquic_socket:t() | undefined,
                                        peer :: nquic_socket:sockaddr() | undefined,
                                        select_info :: nquic_socket:select_info() | undefined,
                                        pn_spaces :: #{nquic_packet:space() => map()},
                                        app_next_pn :: non_neg_integer(),
                                        app_largest_received :: integer(),
                                        loss_state :: nquic_loss:loss_state() | undefined,
                                        dispatch_table :: nquic_dispatch:t() | undefined,
                                        listener :: pid() | undefined,
                                        connect_waiters :: [gen_server:from()],
                                        local_params ::
                                            #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},
                                        remote_params ::
                                            #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,
                                        server_packet_processed :: boolean(),
                                        owner :: pid() | undefined,
                                        owner_mon :: reference() | undefined,
                                        deferred_flush_pending :: boolean(),
                                        pending_ack_count :: non_neg_integer(),
                                        last_idle_ms :: non_neg_integer() | infinity | undefined,
                                        last_pto_ms :: non_neg_integer() | cancel | undefined,
                                        recv_ecn :: nquic_socket:ecn_mark(),
                                        pmtud :: nquic_pmtud:pmtud_state() | undefined,
                                        gso_size :: undefined | pos_integer(),
                                        max_payload_size :: pos_integer(),
                                        server_per_conn_fd :: boolean(),
                                        proactive_cids :: boolean(),
                                        socket_connected :: boolean(),
                                        self_migration_pending :: boolean(),
                                        metrics_counters :: nquic_metrics:conn_counters() | undefined,
                                        spin_enabled :: boolean(),
                                        peer_spin :: 0..1,
                                        new_token_enabled :: boolean(),
                                        new_token_lifetime :: pos_integer(),
                                        qlog :: undefined | nquic_qlog:qlog_state(),
                                        close_kind ::
                                            undefined | local | peer | idle_timeout | protocol_error,
                                        crypto ::
                                            #conn_crypto{tls_state :: term(),
                                                         keys :: #{nquic_packet:space() | rtt0 => map()},
                                                         app_send_keys :: map() | undefined,
                                                         app_recv_keys :: map() | undefined,
                                                         crypto_buffer ::
                                                             #{nquic_packet:space() =>
                                                                   {non_neg_integer(), binary(), list()}},
                                                         cipher ::
                                                             aes_128_gcm | aes_256_gcm |
                                                             chacha20_poly1305,
                                                         cipher_suites ::
                                                             [aes_128_gcm | aes_256_gcm |
                                                              chacha20_poly1305] |
                                                             undefined,
                                                         key_phase :: boolean(),
                                                         key_update_pending :: boolean(),
                                                         client_app_secret :: binary() | undefined,
                                                         server_app_secret :: binary() | undefined,
                                                         old_read_keys ::
                                                             #{key := binary(), iv := binary()} |
                                                             undefined,
                                                         zero_rtt_accepted :: boolean(),
                                                         replay_protection :: module() | undefined,
                                                         session_ticket :: map() | undefined,
                                                         resumption_secret :: binary() | undefined,
                                                         session_cache ::
                                                             atom() |
                                                             false |
                                                             {module, module()} |
                                                             undefined,
                                                         token_cache ::
                                                             atom() | false | {module, module()},
                                                         alpn :: [binary()] | undefined,
                                                         hostname :: string() | binary() | undefined,
                                                         cert :: binary() | undefined,
                                                         cert_chain :: [binary()],
                                                         key :: any() | undefined,
                                                         verify :: verify_none | verify_peer,
                                                         cacerts :: [binary()],
                                                         peer_cert :: binary() | undefined,
                                                         static_key :: binary() | undefined},
                                        streams_state ::
                                            #conn_streams{streams ::
                                                              #{nquic:stream_id() =>
                                                                    #stream_state{stream_id ::
                                                                                      nquic:stream_id(),
                                                                                  type :: bidi | uni,
                                                                                  send_state ::
                                                                                      ready | send |
                                                                                      data_sent |
                                                                                      data_recvd |
                                                                                      reset_sent |
                                                                                      reset_recvd,
                                                                                  send_offset ::
                                                                                      non_neg_integer(),
                                                                                  send_max_data ::
                                                                                      non_neg_integer(),
                                                                                  last_stream_data_blocked ::
                                                                                      non_neg_integer(),
                                                                                  pending_send_data ::
                                                                                      [binary()],
                                                                                  pending_send_size ::
                                                                                      non_neg_integer(),
                                                                                  pending_send_fin ::
                                                                                      boolean(),
                                                                                  recv_state ::
                                                                                      recv |
                                                                                      size_known |
                                                                                      data_recvd |
                                                                                      reset_recvd |
                                                                                      data_read |
                                                                                      reset_read,
                                                                                  recv_offset ::
                                                                                      non_neg_integer(),
                                                                                  recv_max_offset ::
                                                                                      non_neg_integer(),
                                                                                  recv_window ::
                                                                                      non_neg_integer(),
                                                                                  recv_buffer ::
                                                                                      gb_trees:tree(non_neg_integer(),
                                                                                                    {binary(),
                                                                                                     boolean()}),
                                                                                  app_buffer :: iodata(),
                                                                                  app_buffer_size ::
                                                                                      non_neg_integer()}},
                                                          next_bidi_stream ::
                                                              nquic:stream_id() | undefined,
                                                          next_uni_stream ::
                                                              nquic:stream_id() | undefined,
                                                          peer_max_streams_bidi :: non_neg_integer(),
                                                          peer_max_streams_uni :: non_neg_integer(),
                                                          local_max_streams_bidi :: non_neg_integer(),
                                                          local_max_streams_uni :: non_neg_integer(),
                                                          last_sent_max_streams_bidi ::
                                                              non_neg_integer(),
                                                          last_sent_max_streams_uni :: non_neg_integer(),
                                                          max_peer_bidi_stream_id ::
                                                              non_neg_integer() | undefined,
                                                          max_peer_uni_stream_id ::
                                                              non_neg_integer() | undefined,
                                                          opened_peer_bidi_count :: non_neg_integer(),
                                                          opened_peer_uni_count :: non_neg_integer(),
                                                          closed_peer_bidi_wm :: integer(),
                                                          closed_peer_uni_wm :: integer(),
                                                          closed_peer_streams ::
                                                              #{nquic:stream_id() => true},
                                                          recv_waiters ::
                                                              #{nquic:stream_id() => gen_statem:from()},
                                                          accept_stream_waiters ::
                                                              queue:queue(gen_statem:from()),
                                                          pending_streams ::
                                                              queue:queue(nquic:stream_id()),
                                                          blocked_streams ::
                                                              #{nquic:stream_id() => true},
                                                          pending_send_streams ::
                                                              #{nquic:stream_id() => true},
                                                          send_buffer_high_water :: pos_integer(),
                                                          send_timeout :: timeout(),
                                                          send_waiters ::
                                                              queue:queue(nquic_conn_send_waiters:t())},
                                        flow ::
                                            #conn_flow{local_max_data :: non_neg_integer(),
                                                       remote_max_data :: non_neg_integer(),
                                                       data_sent :: non_neg_integer(),
                                                       data_received :: non_neg_integer(),
                                                       last_data_blocked :: non_neg_integer(),
                                                       pending_initial_frames :: [nquic_frame:t()],
                                                       pending_handshake_frames :: [nquic_frame:t()],
                                                       pending_app_frames :: [nquic_frame:t()],
                                                       pending_app_pre_encoded ::
                                                           [{non_neg_integer(),
                                                             iodata(),
                                                             nquic_frame:t()}],
                                                       queued_app_send_bytes :: non_neg_integer()},
                                        path ::
                                            #conn_path_mgmt{path_state :: nquic_path:state() | undefined,
                                                            peer_cids ::
                                                                #{non_neg_integer() =>
                                                                      #{cid := nquic:connection_id(),
                                                                        token := binary()}},
                                                            local_cids ::
                                                                #{non_neg_integer() =>
                                                                      nquic:connection_id()},
                                                            local_cid_seq :: non_neg_integer(),
                                                            peer_retire_prior_to :: non_neg_integer(),
                                                            anti_amp_bytes_received :: non_neg_integer(),
                                                            anti_amp_bytes_sent :: non_neg_integer(),
                                                            address_validated :: boolean()}}) ->
                               gen_statem:event_handler_result(term()).

Common draining-state entry: notify the owner, fail parked waiters, arm the draining_timeout, and cancel the other timers.

enter_draining_silent(Data)

-spec enter_draining_silent(#conn_state{role :: client | server,
                                        scid :: nquic:connection_id(),
                                        dcid :: nquic:connection_id(),
                                        odcid :: nquic:connection_id() | undefined,
                                        retry_scid :: nquic:connection_id() | undefined,
                                        retry_token :: binary(),
                                        version :: non_neg_integer(),
                                        version_preference :: [non_neg_integer()],
                                        socket :: nquic_socket:t() | undefined,
                                        peer :: nquic_socket:sockaddr() | undefined,
                                        select_info :: nquic_socket:select_info() | undefined,
                                        pn_spaces :: #{nquic_packet:space() => map()},
                                        app_next_pn :: non_neg_integer(),
                                        app_largest_received :: integer(),
                                        loss_state :: nquic_loss:loss_state() | undefined,
                                        dispatch_table :: nquic_dispatch:t() | undefined,
                                        listener :: pid() | undefined,
                                        connect_waiters :: [gen_server:from()],
                                        local_params ::
                                            #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},
                                        remote_params ::
                                            #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,
                                        server_packet_processed :: boolean(),
                                        owner :: pid() | undefined,
                                        owner_mon :: reference() | undefined,
                                        deferred_flush_pending :: boolean(),
                                        pending_ack_count :: non_neg_integer(),
                                        last_idle_ms :: non_neg_integer() | infinity | undefined,
                                        last_pto_ms :: non_neg_integer() | cancel | undefined,
                                        recv_ecn :: nquic_socket:ecn_mark(),
                                        pmtud :: nquic_pmtud:pmtud_state() | undefined,
                                        gso_size :: undefined | pos_integer(),
                                        max_payload_size :: pos_integer(),
                                        server_per_conn_fd :: boolean(),
                                        proactive_cids :: boolean(),
                                        socket_connected :: boolean(),
                                        self_migration_pending :: boolean(),
                                        metrics_counters :: nquic_metrics:conn_counters() | undefined,
                                        spin_enabled :: boolean(),
                                        peer_spin :: 0..1,
                                        new_token_enabled :: boolean(),
                                        new_token_lifetime :: pos_integer(),
                                        qlog :: undefined | nquic_qlog:qlog_state(),
                                        close_kind ::
                                            undefined | local | peer | idle_timeout | protocol_error,
                                        crypto ::
                                            #conn_crypto{tls_state :: term(),
                                                         keys :: #{nquic_packet:space() | rtt0 => map()},
                                                         app_send_keys :: map() | undefined,
                                                         app_recv_keys :: map() | undefined,
                                                         crypto_buffer ::
                                                             #{nquic_packet:space() =>
                                                                   {non_neg_integer(), binary(), list()}},
                                                         cipher ::
                                                             aes_128_gcm | aes_256_gcm |
                                                             chacha20_poly1305,
                                                         cipher_suites ::
                                                             [aes_128_gcm | aes_256_gcm |
                                                              chacha20_poly1305] |
                                                             undefined,
                                                         key_phase :: boolean(),
                                                         key_update_pending :: boolean(),
                                                         client_app_secret :: binary() | undefined,
                                                         server_app_secret :: binary() | undefined,
                                                         old_read_keys ::
                                                             #{key := binary(), iv := binary()} |
                                                             undefined,
                                                         zero_rtt_accepted :: boolean(),
                                                         replay_protection :: module() | undefined,
                                                         session_ticket :: map() | undefined,
                                                         resumption_secret :: binary() | undefined,
                                                         session_cache ::
                                                             atom() |
                                                             false |
                                                             {module, module()} |
                                                             undefined,
                                                         token_cache ::
                                                             atom() | false | {module, module()},
                                                         alpn :: [binary()] | undefined,
                                                         hostname :: string() | binary() | undefined,
                                                         cert :: binary() | undefined,
                                                         cert_chain :: [binary()],
                                                         key :: any() | undefined,
                                                         verify :: verify_none | verify_peer,
                                                         cacerts :: [binary()],
                                                         peer_cert :: binary() | undefined,
                                                         static_key :: binary() | undefined},
                                        streams_state ::
                                            #conn_streams{streams ::
                                                              #{nquic:stream_id() =>
                                                                    #stream_state{stream_id ::
                                                                                      nquic:stream_id(),
                                                                                  type :: bidi | uni,
                                                                                  send_state ::
                                                                                      ready | send |
                                                                                      data_sent |
                                                                                      data_recvd |
                                                                                      reset_sent |
                                                                                      reset_recvd,
                                                                                  send_offset ::
                                                                                      non_neg_integer(),
                                                                                  send_max_data ::
                                                                                      non_neg_integer(),
                                                                                  last_stream_data_blocked ::
                                                                                      non_neg_integer(),
                                                                                  pending_send_data ::
                                                                                      [binary()],
                                                                                  pending_send_size ::
                                                                                      non_neg_integer(),
                                                                                  pending_send_fin ::
                                                                                      boolean(),
                                                                                  recv_state ::
                                                                                      recv |
                                                                                      size_known |
                                                                                      data_recvd |
                                                                                      reset_recvd |
                                                                                      data_read |
                                                                                      reset_read,
                                                                                  recv_offset ::
                                                                                      non_neg_integer(),
                                                                                  recv_max_offset ::
                                                                                      non_neg_integer(),
                                                                                  recv_window ::
                                                                                      non_neg_integer(),
                                                                                  recv_buffer ::
                                                                                      gb_trees:tree(non_neg_integer(),
                                                                                                    {binary(),
                                                                                                     boolean()}),
                                                                                  app_buffer :: iodata(),
                                                                                  app_buffer_size ::
                                                                                      non_neg_integer()}},
                                                          next_bidi_stream ::
                                                              nquic:stream_id() | undefined,
                                                          next_uni_stream ::
                                                              nquic:stream_id() | undefined,
                                                          peer_max_streams_bidi :: non_neg_integer(),
                                                          peer_max_streams_uni :: non_neg_integer(),
                                                          local_max_streams_bidi :: non_neg_integer(),
                                                          local_max_streams_uni :: non_neg_integer(),
                                                          last_sent_max_streams_bidi ::
                                                              non_neg_integer(),
                                                          last_sent_max_streams_uni :: non_neg_integer(),
                                                          max_peer_bidi_stream_id ::
                                                              non_neg_integer() | undefined,
                                                          max_peer_uni_stream_id ::
                                                              non_neg_integer() | undefined,
                                                          opened_peer_bidi_count :: non_neg_integer(),
                                                          opened_peer_uni_count :: non_neg_integer(),
                                                          closed_peer_bidi_wm :: integer(),
                                                          closed_peer_uni_wm :: integer(),
                                                          closed_peer_streams ::
                                                              #{nquic:stream_id() => true},
                                                          recv_waiters ::
                                                              #{nquic:stream_id() => gen_statem:from()},
                                                          accept_stream_waiters ::
                                                              queue:queue(gen_statem:from()),
                                                          pending_streams ::
                                                              queue:queue(nquic:stream_id()),
                                                          blocked_streams ::
                                                              #{nquic:stream_id() => true},
                                                          pending_send_streams ::
                                                              #{nquic:stream_id() => true},
                                                          send_buffer_high_water :: pos_integer(),
                                                          send_timeout :: timeout(),
                                                          send_waiters ::
                                                              queue:queue(nquic_conn_send_waiters:t())},
                                        flow ::
                                            #conn_flow{local_max_data :: non_neg_integer(),
                                                       remote_max_data :: non_neg_integer(),
                                                       data_sent :: non_neg_integer(),
                                                       data_received :: non_neg_integer(),
                                                       last_data_blocked :: non_neg_integer(),
                                                       pending_initial_frames :: [nquic_frame:t()],
                                                       pending_handshake_frames :: [nquic_frame:t()],
                                                       pending_app_frames :: [nquic_frame:t()],
                                                       pending_app_pre_encoded ::
                                                           [{non_neg_integer(),
                                                             iodata(),
                                                             nquic_frame:t()}],
                                                       queued_app_send_bytes :: non_neg_integer()},
                                        path ::
                                            #conn_path_mgmt{path_state :: nquic_path:state() | undefined,
                                                            peer_cids ::
                                                                #{non_neg_integer() =>
                                                                      #{cid := nquic:connection_id(),
                                                                        token := binary()}},
                                                            local_cids ::
                                                                #{non_neg_integer() =>
                                                                      nquic:connection_id()},
                                                            local_cid_seq :: non_neg_integer(),
                                                            peer_retire_prior_to :: non_neg_integer(),
                                                            anti_amp_bytes_received :: non_neg_integer(),
                                                            anti_amp_bytes_sent :: non_neg_integer(),
                                                            address_validated :: boolean()}}) ->
                               gen_statem:event_handler_result(term()).

Enter draining without sending CONNECTION_CLOSE. Used when the peer initiated the close (we must not respond, RFC 9000 §10.2.2).

maybe_drain/2

Convert a transport-error stop into a draining transition. If no keys exist yet (very early failure, peer can't decrypt anything) we just stop. Otherwise we transition to draining so the close frame the local stack just emitted reaches the peer.

send_close_frame(Frame, Data, StateName)

-spec send_close_frame(#connection_close{error_code :: non_neg_integer(),
                                         frame_type :: non_neg_integer(),
                                         reason_phrase :: binary(),
                                         is_application :: boolean()},
                       #conn_state{role :: client | server,
                                   scid :: nquic:connection_id(),
                                   dcid :: nquic:connection_id(),
                                   odcid :: nquic:connection_id() | undefined,
                                   retry_scid :: nquic:connection_id() | undefined,
                                   retry_token :: binary(),
                                   version :: non_neg_integer(),
                                   version_preference :: [non_neg_integer()],
                                   socket :: nquic_socket:t() | undefined,
                                   peer :: nquic_socket:sockaddr() | undefined,
                                   select_info :: nquic_socket:select_info() | undefined,
                                   pn_spaces :: #{nquic_packet:space() => map()},
                                   app_next_pn :: non_neg_integer(),
                                   app_largest_received :: integer(),
                                   loss_state :: nquic_loss:loss_state() | undefined,
                                   dispatch_table :: nquic_dispatch:t() | undefined,
                                   listener :: pid() | undefined,
                                   connect_waiters :: [gen_server:from()],
                                   local_params ::
                                       #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},
                                   remote_params ::
                                       #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,
                                   server_packet_processed :: boolean(),
                                   owner :: pid() | undefined,
                                   owner_mon :: reference() | undefined,
                                   deferred_flush_pending :: boolean(),
                                   pending_ack_count :: non_neg_integer(),
                                   last_idle_ms :: non_neg_integer() | infinity | undefined,
                                   last_pto_ms :: non_neg_integer() | cancel | undefined,
                                   recv_ecn :: nquic_socket:ecn_mark(),
                                   pmtud :: nquic_pmtud:pmtud_state() | undefined,
                                   gso_size :: undefined | pos_integer(),
                                   max_payload_size :: pos_integer(),
                                   server_per_conn_fd :: boolean(),
                                   proactive_cids :: boolean(),
                                   socket_connected :: boolean(),
                                   self_migration_pending :: boolean(),
                                   metrics_counters :: nquic_metrics:conn_counters() | undefined,
                                   spin_enabled :: boolean(),
                                   peer_spin :: 0..1,
                                   new_token_enabled :: boolean(),
                                   new_token_lifetime :: pos_integer(),
                                   qlog :: undefined | nquic_qlog:qlog_state(),
                                   close_kind ::
                                       undefined | local | peer | idle_timeout | protocol_error,
                                   crypto ::
                                       #conn_crypto{tls_state :: term(),
                                                    keys :: #{nquic_packet:space() | rtt0 => map()},
                                                    app_send_keys :: map() | undefined,
                                                    app_recv_keys :: map() | undefined,
                                                    crypto_buffer ::
                                                        #{nquic_packet:space() =>
                                                              {non_neg_integer(), binary(), list()}},
                                                    cipher ::
                                                        aes_128_gcm | aes_256_gcm | chacha20_poly1305,
                                                    cipher_suites ::
                                                        [aes_128_gcm | aes_256_gcm | chacha20_poly1305] |
                                                        undefined,
                                                    key_phase :: boolean(),
                                                    key_update_pending :: boolean(),
                                                    client_app_secret :: binary() | undefined,
                                                    server_app_secret :: binary() | undefined,
                                                    old_read_keys ::
                                                        #{key := binary(), iv := binary()} | undefined,
                                                    zero_rtt_accepted :: boolean(),
                                                    replay_protection :: module() | undefined,
                                                    session_ticket :: map() | undefined,
                                                    resumption_secret :: binary() | undefined,
                                                    session_cache ::
                                                        atom() | false | {module, module()} | undefined,
                                                    token_cache :: atom() | false | {module, module()},
                                                    alpn :: [binary()] | undefined,
                                                    hostname :: string() | binary() | undefined,
                                                    cert :: binary() | undefined,
                                                    cert_chain :: [binary()],
                                                    key :: any() | undefined,
                                                    verify :: verify_none | verify_peer,
                                                    cacerts :: [binary()],
                                                    peer_cert :: binary() | undefined,
                                                    static_key :: binary() | undefined},
                                   streams_state ::
                                       #conn_streams{streams ::
                                                         #{nquic:stream_id() =>
                                                               #stream_state{stream_id ::
                                                                                 nquic:stream_id(),
                                                                             type :: bidi | uni,
                                                                             send_state ::
                                                                                 ready | send |
                                                                                 data_sent |
                                                                                 data_recvd |
                                                                                 reset_sent |
                                                                                 reset_recvd,
                                                                             send_offset ::
                                                                                 non_neg_integer(),
                                                                             send_max_data ::
                                                                                 non_neg_integer(),
                                                                             last_stream_data_blocked ::
                                                                                 non_neg_integer(),
                                                                             pending_send_data ::
                                                                                 [binary()],
                                                                             pending_send_size ::
                                                                                 non_neg_integer(),
                                                                             pending_send_fin ::
                                                                                 boolean(),
                                                                             recv_state ::
                                                                                 recv | size_known |
                                                                                 data_recvd |
                                                                                 reset_recvd |
                                                                                 data_read | reset_read,
                                                                             recv_offset ::
                                                                                 non_neg_integer(),
                                                                             recv_max_offset ::
                                                                                 non_neg_integer(),
                                                                             recv_window ::
                                                                                 non_neg_integer(),
                                                                             recv_buffer ::
                                                                                 gb_trees:tree(non_neg_integer(),
                                                                                               {binary(),
                                                                                                boolean()}),
                                                                             app_buffer :: iodata(),
                                                                             app_buffer_size ::
                                                                                 non_neg_integer()}},
                                                     next_bidi_stream :: nquic:stream_id() | undefined,
                                                     next_uni_stream :: nquic:stream_id() | undefined,
                                                     peer_max_streams_bidi :: non_neg_integer(),
                                                     peer_max_streams_uni :: non_neg_integer(),
                                                     local_max_streams_bidi :: non_neg_integer(),
                                                     local_max_streams_uni :: non_neg_integer(),
                                                     last_sent_max_streams_bidi :: non_neg_integer(),
                                                     last_sent_max_streams_uni :: non_neg_integer(),
                                                     max_peer_bidi_stream_id ::
                                                         non_neg_integer() | undefined,
                                                     max_peer_uni_stream_id ::
                                                         non_neg_integer() | undefined,
                                                     opened_peer_bidi_count :: non_neg_integer(),
                                                     opened_peer_uni_count :: non_neg_integer(),
                                                     closed_peer_bidi_wm :: integer(),
                                                     closed_peer_uni_wm :: integer(),
                                                     closed_peer_streams :: #{nquic:stream_id() => true},
                                                     recv_waiters ::
                                                         #{nquic:stream_id() => gen_statem:from()},
                                                     accept_stream_waiters ::
                                                         queue:queue(gen_statem:from()),
                                                     pending_streams :: queue:queue(nquic:stream_id()),
                                                     blocked_streams :: #{nquic:stream_id() => true},
                                                     pending_send_streams ::
                                                         #{nquic:stream_id() => true},
                                                     send_buffer_high_water :: pos_integer(),
                                                     send_timeout :: timeout(),
                                                     send_waiters ::
                                                         queue:queue(nquic_conn_send_waiters:t())},
                                   flow ::
                                       #conn_flow{local_max_data :: non_neg_integer(),
                                                  remote_max_data :: non_neg_integer(),
                                                  data_sent :: non_neg_integer(),
                                                  data_received :: non_neg_integer(),
                                                  last_data_blocked :: non_neg_integer(),
                                                  pending_initial_frames :: [nquic_frame:t()],
                                                  pending_handshake_frames :: [nquic_frame:t()],
                                                  pending_app_frames :: [nquic_frame:t()],
                                                  pending_app_pre_encoded ::
                                                      [{non_neg_integer(), iodata(), nquic_frame:t()}],
                                                  queued_app_send_bytes :: non_neg_integer()},
                                   path ::
                                       #conn_path_mgmt{path_state :: nquic_path:state() | undefined,
                                                       peer_cids ::
                                                           #{non_neg_integer() =>
                                                                 #{cid := nquic:connection_id(),
                                                                   token := binary()}},
                                                       local_cids ::
                                                           #{non_neg_integer() => nquic:connection_id()},
                                                       local_cid_seq :: non_neg_integer(),
                                                       peer_retire_prior_to :: non_neg_integer(),
                                                       anti_amp_bytes_received :: non_neg_integer(),
                                                       anti_amp_bytes_sent :: non_neg_integer(),
                                                       address_validated :: boolean()}},
                       state_name()) ->
                          ok.

Emit Frame at every encryption level the peer can decrypt for StateName. RFC 9000 §10.2.3: during the handshake the peer may not yet have derived Handshake or Application keys, so a CONNECTION_CLOSE must be sent at a level the peer can decrypt. Until the handshake is confirmed (gen_statem state = established) the server SHOULD send the close in both Handshake (when keys exist) and Initial packets so at least one is processable. Once established, only the 1-RTT copy is needed.

send_connection_close(Data, Error, StateName)

-spec send_connection_close(#conn_state{role :: client | server,
                                        scid :: nquic:connection_id(),
                                        dcid :: nquic:connection_id(),
                                        odcid :: nquic:connection_id() | undefined,
                                        retry_scid :: nquic:connection_id() | undefined,
                                        retry_token :: binary(),
                                        version :: non_neg_integer(),
                                        version_preference :: [non_neg_integer()],
                                        socket :: nquic_socket:t() | undefined,
                                        peer :: nquic_socket:sockaddr() | undefined,
                                        select_info :: nquic_socket:select_info() | undefined,
                                        pn_spaces :: #{nquic_packet:space() => map()},
                                        app_next_pn :: non_neg_integer(),
                                        app_largest_received :: integer(),
                                        loss_state :: nquic_loss:loss_state() | undefined,
                                        dispatch_table :: nquic_dispatch:t() | undefined,
                                        listener :: pid() | undefined,
                                        connect_waiters :: [gen_server:from()],
                                        local_params ::
                                            #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},
                                        remote_params ::
                                            #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,
                                        server_packet_processed :: boolean(),
                                        owner :: pid() | undefined,
                                        owner_mon :: reference() | undefined,
                                        deferred_flush_pending :: boolean(),
                                        pending_ack_count :: non_neg_integer(),
                                        last_idle_ms :: non_neg_integer() | infinity | undefined,
                                        last_pto_ms :: non_neg_integer() | cancel | undefined,
                                        recv_ecn :: nquic_socket:ecn_mark(),
                                        pmtud :: nquic_pmtud:pmtud_state() | undefined,
                                        gso_size :: undefined | pos_integer(),
                                        max_payload_size :: pos_integer(),
                                        server_per_conn_fd :: boolean(),
                                        proactive_cids :: boolean(),
                                        socket_connected :: boolean(),
                                        self_migration_pending :: boolean(),
                                        metrics_counters :: nquic_metrics:conn_counters() | undefined,
                                        spin_enabled :: boolean(),
                                        peer_spin :: 0..1,
                                        new_token_enabled :: boolean(),
                                        new_token_lifetime :: pos_integer(),
                                        qlog :: undefined | nquic_qlog:qlog_state(),
                                        close_kind ::
                                            undefined | local | peer | idle_timeout | protocol_error,
                                        crypto ::
                                            #conn_crypto{tls_state :: term(),
                                                         keys :: #{nquic_packet:space() | rtt0 => map()},
                                                         app_send_keys :: map() | undefined,
                                                         app_recv_keys :: map() | undefined,
                                                         crypto_buffer ::
                                                             #{nquic_packet:space() =>
                                                                   {non_neg_integer(), binary(), list()}},
                                                         cipher ::
                                                             aes_128_gcm | aes_256_gcm |
                                                             chacha20_poly1305,
                                                         cipher_suites ::
                                                             [aes_128_gcm | aes_256_gcm |
                                                              chacha20_poly1305] |
                                                             undefined,
                                                         key_phase :: boolean(),
                                                         key_update_pending :: boolean(),
                                                         client_app_secret :: binary() | undefined,
                                                         server_app_secret :: binary() | undefined,
                                                         old_read_keys ::
                                                             #{key := binary(), iv := binary()} |
                                                             undefined,
                                                         zero_rtt_accepted :: boolean(),
                                                         replay_protection :: module() | undefined,
                                                         session_ticket :: map() | undefined,
                                                         resumption_secret :: binary() | undefined,
                                                         session_cache ::
                                                             atom() |
                                                             false |
                                                             {module, module()} |
                                                             undefined,
                                                         token_cache ::
                                                             atom() | false | {module, module()},
                                                         alpn :: [binary()] | undefined,
                                                         hostname :: string() | binary() | undefined,
                                                         cert :: binary() | undefined,
                                                         cert_chain :: [binary()],
                                                         key :: any() | undefined,
                                                         verify :: verify_none | verify_peer,
                                                         cacerts :: [binary()],
                                                         peer_cert :: binary() | undefined,
                                                         static_key :: binary() | undefined},
                                        streams_state ::
                                            #conn_streams{streams ::
                                                              #{nquic:stream_id() =>
                                                                    #stream_state{stream_id ::
                                                                                      nquic:stream_id(),
                                                                                  type :: bidi | uni,
                                                                                  send_state ::
                                                                                      ready | send |
                                                                                      data_sent |
                                                                                      data_recvd |
                                                                                      reset_sent |
                                                                                      reset_recvd,
                                                                                  send_offset ::
                                                                                      non_neg_integer(),
                                                                                  send_max_data ::
                                                                                      non_neg_integer(),
                                                                                  last_stream_data_blocked ::
                                                                                      non_neg_integer(),
                                                                                  pending_send_data ::
                                                                                      [binary()],
                                                                                  pending_send_size ::
                                                                                      non_neg_integer(),
                                                                                  pending_send_fin ::
                                                                                      boolean(),
                                                                                  recv_state ::
                                                                                      recv |
                                                                                      size_known |
                                                                                      data_recvd |
                                                                                      reset_recvd |
                                                                                      data_read |
                                                                                      reset_read,
                                                                                  recv_offset ::
                                                                                      non_neg_integer(),
                                                                                  recv_max_offset ::
                                                                                      non_neg_integer(),
                                                                                  recv_window ::
                                                                                      non_neg_integer(),
                                                                                  recv_buffer ::
                                                                                      gb_trees:tree(non_neg_integer(),
                                                                                                    {binary(),
                                                                                                     boolean()}),
                                                                                  app_buffer :: iodata(),
                                                                                  app_buffer_size ::
                                                                                      non_neg_integer()}},
                                                          next_bidi_stream ::
                                                              nquic:stream_id() | undefined,
                                                          next_uni_stream ::
                                                              nquic:stream_id() | undefined,
                                                          peer_max_streams_bidi :: non_neg_integer(),
                                                          peer_max_streams_uni :: non_neg_integer(),
                                                          local_max_streams_bidi :: non_neg_integer(),
                                                          local_max_streams_uni :: non_neg_integer(),
                                                          last_sent_max_streams_bidi ::
                                                              non_neg_integer(),
                                                          last_sent_max_streams_uni :: non_neg_integer(),
                                                          max_peer_bidi_stream_id ::
                                                              non_neg_integer() | undefined,
                                                          max_peer_uni_stream_id ::
                                                              non_neg_integer() | undefined,
                                                          opened_peer_bidi_count :: non_neg_integer(),
                                                          opened_peer_uni_count :: non_neg_integer(),
                                                          closed_peer_bidi_wm :: integer(),
                                                          closed_peer_uni_wm :: integer(),
                                                          closed_peer_streams ::
                                                              #{nquic:stream_id() => true},
                                                          recv_waiters ::
                                                              #{nquic:stream_id() => gen_statem:from()},
                                                          accept_stream_waiters ::
                                                              queue:queue(gen_statem:from()),
                                                          pending_streams ::
                                                              queue:queue(nquic:stream_id()),
                                                          blocked_streams ::
                                                              #{nquic:stream_id() => true},
                                                          pending_send_streams ::
                                                              #{nquic:stream_id() => true},
                                                          send_buffer_high_water :: pos_integer(),
                                                          send_timeout :: timeout(),
                                                          send_waiters ::
                                                              queue:queue(nquic_conn_send_waiters:t())},
                                        flow ::
                                            #conn_flow{local_max_data :: non_neg_integer(),
                                                       remote_max_data :: non_neg_integer(),
                                                       data_sent :: non_neg_integer(),
                                                       data_received :: non_neg_integer(),
                                                       last_data_blocked :: non_neg_integer(),
                                                       pending_initial_frames :: [nquic_frame:t()],
                                                       pending_handshake_frames :: [nquic_frame:t()],
                                                       pending_app_frames :: [nquic_frame:t()],
                                                       pending_app_pre_encoded ::
                                                           [{non_neg_integer(),
                                                             iodata(),
                                                             nquic_frame:t()}],
                                                       queued_app_send_bytes :: non_neg_integer()},
                                        path ::
                                            #conn_path_mgmt{path_state :: nquic_path:state() | undefined,
                                                            peer_cids ::
                                                                #{non_neg_integer() =>
                                                                      #{cid := nquic:connection_id(),
                                                                        token := binary()}},
                                                            local_cids ::
                                                                #{non_neg_integer() =>
                                                                      nquic:connection_id()},
                                                            local_cid_seq :: non_neg_integer(),
                                                            peer_retire_prior_to :: non_neg_integer(),
                                                            anti_amp_bytes_received :: non_neg_integer(),
                                                            anti_amp_bytes_sent :: non_neg_integer(),
                                                            address_validated :: boolean()}},
                            nquic_error:any_reason(),
                            state_name()) ->
                               ok.

Build a transport-level CONNECTION_CLOSE for Error and send it at the appropriate encryption levels.