-module(nquic_protocol_cid). -moduledoc """ Connection ID management for the QUIC protocol state. Pure functions over `#conn_state{}` that handle peer CID issuance (NEW_CONNECTION_ID), retirement (RETIRE_CONNECTION_ID), local CID rotation, and DCID switching for path migration. Extracted from `nquic_protocol` as part of REVIEW_PLAN.md Phase 4.4. Side effects are limited to `nquic_listener:dispatch_register/3` and `nquic_listener:dispatch_unregister/2` for keeping the dispatch table in sync when the connection has one set; everything else is functional state manipulation. """. -include("nquic_conn.hrl"). -include("nquic_frame.hrl"). -include("nquic_transport.hrl"). -export([ find_cid_seq/2, handle_new_connection_id/5, handle_retire_connection_id/2, issue_new_connection_id/1, issue_spare_cids/1, retire_peer_cids/2, rotate_dcid/1 ]). %%%----------------------------------------------------------------------------- %% PUBLIC API %%%----------------------------------------------------------------------------- -spec find_cid_seq(nquic:connection_id(), #{non_neg_integer() => map()}) -> non_neg_integer() | undefined. find_cid_seq(CID, PeerCids) -> maps:fold( fun(Seq, #{cid := C}, Acc) -> case C of CID -> Seq; _ -> Acc end end, undefined, PeerCids ). -spec handle_new_connection_id( non_neg_integer(), non_neg_integer(), nquic:connection_id(), binary(), nquic_protocol:state() ) -> {ok, nquic_protocol:state()}. handle_new_connection_id(SeqNum, RetirePriorTo, CID, Token, State) -> #conn_state{path = Path0} = State, #conn_path_mgmt{ peer_cids = PeerCids, peer_retire_prior_to = CurrentRetirePrior } = Path0, NewRetirePrior = max(CurrentRetirePrior, RetirePriorTo), PeerCids1 = case SeqNum >= NewRetirePrior of true -> PeerCids#{SeqNum => #{cid => CID, token => Token}}; false -> PeerCids end, {ToRetire, PeerCids2} = retire_peer_cids(NewRetirePrior, PeerCids1), NewPath = Path0#conn_path_mgmt{ peer_cids = PeerCids2, peer_retire_prior_to = NewRetirePrior }, State1 = State#conn_state{path = NewPath}, State2 = send_retire_frames(ToRetire, State1), {ok, State2}. -spec handle_retire_connection_id(non_neg_integer(), nquic_protocol:state()) -> {ok, nquic_protocol:state()}. handle_retire_connection_id(SeqNum, State) -> #conn_state{path = Path0, dispatch_table = Table} = State, LocalCids = Path0#conn_path_mgmt.local_cids, case maps:get(SeqNum, LocalCids, undefined) of undefined -> {ok, State}; CID -> case Table of undefined -> ok; _ -> nquic_listener:dispatch_unregister(Table, CID) end, NewLocalCids = maps:remove(SeqNum, LocalCids), NewPath = Path0#conn_path_mgmt{local_cids = NewLocalCids}, State1 = State#conn_state{path = NewPath}, issue_new_connection_id(State1) end. -spec retire_peer_cids(non_neg_integer(), #{non_neg_integer() => map()}) -> {[non_neg_integer()], #{non_neg_integer() => map()}}. retire_peer_cids(RetirePriorTo, PeerCids) -> maps:fold( fun(Seq, _Entry, {Retired, Remaining}) -> case Seq < RetirePriorTo of true -> {[Seq | Retired], maps:remove(Seq, Remaining)}; false -> {Retired, Remaining} end end, {[], PeerCids}, PeerCids ). %%%----------------------------------------------------------------------------- %% INTERNAL %%%----------------------------------------------------------------------------- -spec issue_n_cids(non_neg_integer(), nquic_protocol:state()) -> {ok, nquic_protocol:state()}. issue_n_cids(0, State) -> {ok, State}; issue_n_cids(N, State) -> case issue_new_connection_id(State) of {ok, State1} -> issue_n_cids(N - 1, State1) end. -spec issue_new_connection_id(nquic_protocol:state()) -> {ok, nquic_protocol:state()}. issue_new_connection_id(State) -> #conn_state{path = Path0, dispatch_table = Table, crypto = Crypto} = State, #conn_path_mgmt{local_cids = LocalCids, local_cid_seq = NextSeq} = Path0, NewCID = nquic_keys:generate_connection_id(8), ResetToken = case Crypto#conn_crypto.static_key of undefined -> crypto:strong_rand_bytes(16); SK -> nquic_stateless_reset:generate_token(SK, NewCID) end, case Table of undefined -> ok; _ -> nquic_listener:dispatch_register(Table, NewCID, self()) end, Frame = #new_connection_id{ seq_num = NextSeq, retire_prior_to = 0, cid = NewCID, stateless_reset_token = ResetToken }, NewLocalCids = LocalCids#{NextSeq => NewCID}, NewPath = Path0#conn_path_mgmt{ local_cids = NewLocalCids, local_cid_seq = NextSeq + 1 }, State1 = State#conn_state{path = NewPath}, nquic_protocol_send_queues:queue_app_frame(Frame, State1). -doc """ Issue spare NEW_CONNECTION_ID frames up to the peer's `active_connection_id_limit`. Both endpoints account the SCID delivered in transport parameters as a single local CID (sequence 0); peers tolerate up to `limit` concurrent CIDs (RFC 9000 ยง5.1.1). We top up the gap so a future migration has spare CIDs on both sides; without this, server-initiated migration (per-conn FDs) cannot rotate DCID and the path validation refuses to flip. No-op when remote transport parameters have not been processed yet: the limit is unknown, and the existing reactive `issue_new_connection_id` path picks up the slack on demand. """. -spec issue_spare_cids(nquic_protocol:state()) -> {ok, nquic_protocol:state()}. issue_spare_cids(#conn_state{remote_params = undefined} = State) -> {ok, State}; issue_spare_cids(#conn_state{remote_params = RP, path = Path} = State) -> Limit = RP#transport_params.active_connection_id_limit, Current = map_size(Path#conn_path_mgmt.local_cids), issue_n_cids(max(0, Limit - Current), State). -spec rotate_dcid(nquic_protocol:state()) -> {ok, nquic_protocol:state()} | {error, no_available_cids}. rotate_dcid(#conn_state{path = Path0, dcid = CurrentDCID} = State) -> PeerCids = Path0#conn_path_mgmt.peer_cids, Available = maps:filter( fun(_Seq, #{cid := CID}) -> CID =/= CurrentDCID end, PeerCids ), case map_size(Available) of 0 -> {error, no_available_cids}; _ -> MinSeq = lists:min(maps:keys(Available)), #{cid := NewDCID} = maps:get(MinSeq, Available), OldSeq = find_cid_seq(CurrentDCID, PeerCids), State1 = State#conn_state{dcid = NewDCID}, State2 = case OldSeq of undefined -> State1; Seq -> send_retire_frames([Seq], State1) end, {ok, State2} end. -spec send_retire_frames([non_neg_integer()], nquic_protocol:state()) -> nquic_protocol:state(). send_retire_frames([], State) -> State; send_retire_frames([Seq | Rest], State) -> Frame = #retire_connection_id{seq_num = Seq}, {ok, State1} = nquic_protocol_send_queues:queue_app_frame(Frame, State), send_retire_frames(Rest, State1).