638 lines
25 KiB
Markdown
638 lines
25 KiB
Markdown
|
QUIC Connection State Machine
|
||
|
=============================
|
||
|
|
||
|
FSM Model
|
||
|
---------
|
||
|
|
||
|
QUIC client-side connection state can be broken down into five coarse phases of
|
||
|
a QUIC connection:
|
||
|
|
||
|
- The Idle substate (which is simply the state before we have started trying to
|
||
|
establish a connection);
|
||
|
- The Active state, which comprises two substates:
|
||
|
- The Establishing state, which comprises many different substates;
|
||
|
- The Open state;
|
||
|
- The Terminating state, which comprises several substates;
|
||
|
- The Terminated state, which is the terminal state.
|
||
|
|
||
|
There is monotonic progression through these phases.
|
||
|
|
||
|
These names have been deliberately chosen to use different terminology to common
|
||
|
QUIC terms such as 'handshake' to avoid confusion, as they are not the same
|
||
|
concepts. For example, the Establishing state uses Initial, Handshake and 1-RTT
|
||
|
packets.
|
||
|
|
||
|
This discussion is (currently) given from the client side perspective only.
|
||
|
State machine considerations only relevant to servers are not mentioned.
|
||
|
0-RTT is also not currently modelled in this analysis.
|
||
|
|
||
|
The synthesis of this FSM is not suggested by the QUIC RFCs but has been
|
||
|
discerned from the requirements imposed. This does not mean that the
|
||
|
implementation of this FSM as literally presented below is an optimal or
|
||
|
advisable implementation strategy, and a cursory examination of existing QUIC
|
||
|
implementations suggests that such an approach is not common. Moreover, excess
|
||
|
attention should not be given to the Open state, as 1-RTT application
|
||
|
communication can occur even still in the Establishing state (for example, when
|
||
|
the handshake has been completed but not yet confirmed).
|
||
|
|
||
|
However, the state machine described herein is helpful as an aid to
|
||
|
understanding and broadly captures the logic which our implementation will
|
||
|
embody. The design of the actual implementation is discussed further below.
|
||
|
|
||
|
The above states and their substates are defined as follows:
|
||
|
|
||
|
- The Establishing state involves the use of Initial and Handshake
|
||
|
packets. It is terminated when the handshake is confirmed.
|
||
|
|
||
|
Handshake confirmation is not the same as handshake completion.
|
||
|
Handshake confirmation occurs on the client when it receives
|
||
|
a `HANDSHAKE_DONE` frame (which occurs in a 1-RTT packet, thus
|
||
|
1-RTT packets are also invoked in the Establishing state).
|
||
|
On the server, handshake confirmation occurs as soon as
|
||
|
the handshake is considered completed (see RFC 9001 s. 4.1).
|
||
|
|
||
|
The Establishing state is subdivided into the following substates:
|
||
|
|
||
|
- Proactive Version Negotiation (optional): The client sends
|
||
|
a Version Negotiation packet with a reserved version number
|
||
|
to forcibly elicit a list of the server's supported versions.
|
||
|
This is not expected to be commonly used, as it adds a round trip.
|
||
|
|
||
|
If it is used, the time spent in this state is based on waiting for
|
||
|
the server to respond, and potentially retransmitting after a
|
||
|
timeout.
|
||
|
|
||
|
- Pre-Initial: The client has completed proactive version negotiation
|
||
|
(if it performed it), but has not yet sent any encrypted packet. This
|
||
|
substate is included for exposition; no time will generally be spent in it
|
||
|
and there is immediate transmission of the first encrypted packet and
|
||
|
transition to Initial Exchange A.
|
||
|
|
||
|
- Initial Exchange A: The client has sent at least one Initial
|
||
|
packet to the server attempting to initiate a connection.
|
||
|
|
||
|
The client is waiting for a server response, which might
|
||
|
be:
|
||
|
- a Version Negotiation packet (leading to the Reactive Version
|
||
|
Negotiation state);
|
||
|
- a Retry packet (leading to Initial Exchange B); or
|
||
|
- an Initial packet (leading to the Initial Exchange Confirmed state).
|
||
|
|
||
|
- Reactive Version Negotiation: The server has rejected the client's
|
||
|
proposed version. If proactive version negotiation was used, this
|
||
|
can be considered an error. Otherwise, we return to the Pre-Initial
|
||
|
state and proceed as though proactive version negotiation was
|
||
|
performed using the information in the version negotiation packet.
|
||
|
|
||
|
- Initial Exchange B: The client has been asked to perform a Retry.
|
||
|
It sends at least one Initial packet to the server attempting to
|
||
|
initiate a connection. Every Initial packet contains the quoted Retry
|
||
|
Token. Any data sent in `CRYPTO` frames in Initial Exchange A must be
|
||
|
retransmitted, but PNs MUST NOT be reset. Note that this is still
|
||
|
considered part of the same connection, and QUIC Transport Parameters are
|
||
|
later used to cryptographically bind the established connection state to
|
||
|
the original DCIDs used as part of the Retry process. A server is not
|
||
|
allowed to respond to a Retry-triggered Initial exchange with another
|
||
|
Retry, and if it does we ignore it, which is the major distinction of this
|
||
|
state from Initial Exchange A.
|
||
|
|
||
|
The client is waiting for a server response, which might be:
|
||
|
- a Version Negotiation packet (invalid, ignored);
|
||
|
- a Retry packet (invalid, ignored);
|
||
|
- an Initial packet (leading to the Initial Exchange Continued
|
||
|
state);
|
||
|
|
||
|
- Initial Exchange Continued: The client has sent at least one
|
||
|
Initial packet to the server and received at least one valid Initial packet
|
||
|
from the server. There is no longer any possibility of a Retry (any such
|
||
|
packet is ignored) and communications may continue via Initial packets for
|
||
|
an arbitrarily long period until the handshake layer indicates the
|
||
|
Handshake EL is ready.
|
||
|
|
||
|
The client is waiting for server packets, until one of those packets
|
||
|
causes the handshake layer (whether it is TLS 1.3 or some other
|
||
|
hypothetical handshake layer) to emit keys for the Handshake EL.
|
||
|
This will generally occur due to incoming Initial packets containing
|
||
|
crypto stream segments (in the form of `CRYPTO` frames) which deliver
|
||
|
handshake layer protocol messages to the handshake layer in use.
|
||
|
|
||
|
- Handshake: The Handshake EL is now available to the client.
|
||
|
Either client or server may send the first Handshake packet.
|
||
|
|
||
|
The client is waiting to receive a Handshake packet from the server.
|
||
|
|
||
|
- Handshake Continued: The client has received and successfully
|
||
|
decrypted at least one Handshake packet. The client now discards
|
||
|
the Initial EL. Communications via the handshake EL may continue for
|
||
|
an arbitrary period of time.
|
||
|
|
||
|
The client is waiting to receive more Handshake packets from the
|
||
|
server to advance the handshake layer and cause it to transition
|
||
|
to the Handshake Completed state.
|
||
|
|
||
|
- Handshake Completed: The handshake layer has indicated that it
|
||
|
considers the handshake completed. For TLS 1.3, this means both
|
||
|
parties have sent and received (and verified) TLS 1.3 Finished
|
||
|
messages. The handshake layer must emit keys for the 1-RTT EL
|
||
|
at this time.
|
||
|
|
||
|
Though the handshake is not yet confirmed, the client can begin
|
||
|
sending 1-RTT packets.
|
||
|
|
||
|
The QUIC Transport Parameters sent by the peer are now authenticated.
|
||
|
(Though the peer's QUIC Transport Parameters may have been received
|
||
|
earlier in the handshake process, they are only considered
|
||
|
authenticated at this point.)
|
||
|
|
||
|
The client transitions to Handshake Confirmed once either
|
||
|
- it receives a `HANDSHAKE_DONE` frame in a 1-RTT packet, or
|
||
|
- it receives acknowledgement of any 1-RTT packet it sent.
|
||
|
|
||
|
Though this discussion only covers the client state machine, it is worth
|
||
|
noting that on the server, the handshake is considered confirmed as soon as
|
||
|
it is considered completed.
|
||
|
|
||
|
- Handshake Confirmed: The client has received confirmation from
|
||
|
the server that the handshake is confirmed.
|
||
|
|
||
|
The principal effect of moving to this state is that the Handshake
|
||
|
EL is discarded. Key Update is also now permitted for the first
|
||
|
time.
|
||
|
|
||
|
The Establishing state is now done and there is immediate transition
|
||
|
to the Open state.
|
||
|
|
||
|
- The Open state is the steady state of the connection. It is a single state.
|
||
|
|
||
|
Application stream data is exchanged freely. Only 1-RTT packets are used. The
|
||
|
Initial, Handshake (and 0-RTT) ELs have been discarded, transport parameters
|
||
|
have been exchanged, and the handshake has been confirmed.
|
||
|
|
||
|
The client transitions to
|
||
|
|
||
|
- the Terminating — Closing state if the local application initiates an
|
||
|
immediate close (a `CONNECTION_CLOSE` frame is sent);
|
||
|
- the Terminating — Draining state if the remote peer initiates
|
||
|
an immediate close (i.e., a `CONNECTION_CLOSE` frame is received);
|
||
|
- the Terminated state if the idle timeout expires; a `CONNECTION_CLOSE`
|
||
|
frame is NOT sent;
|
||
|
- the Terminated state if the peer triggers a stateless reset; a
|
||
|
`CONNECTION_CLOSE` frame is NOT sent.
|
||
|
|
||
|
- The Terminating state is used when closing the connection.
|
||
|
This may occur due to an application request or a transport-level
|
||
|
protocol error.
|
||
|
|
||
|
Key updates may not be initiated in the Terminating state.
|
||
|
|
||
|
This state is divided into two substates:
|
||
|
|
||
|
- The Closing state, used for a locally initiated immediate close. In
|
||
|
this state, a packet containing a `CONNECTION_CLOSE` frame is
|
||
|
transmitted again in response to any packets received. This ensures
|
||
|
that a `CONNECTION_CLOSE` frame is received by the peer even if the
|
||
|
initially transmitted `CONNECTION_CLOSE` frame was lost. Note that
|
||
|
these `CONNECTION_CLOSE` frames are not governed by QUIC's normal loss
|
||
|
detection mechanisms; this is a bespoke mechanism unique to this
|
||
|
state, which exists solely to ensure delivery of the `CONNECTION_CLOSE`
|
||
|
frame.
|
||
|
|
||
|
The endpoint progresses to the Terminated state after a timeout
|
||
|
interval, which should not be less than three times the PTO interval.
|
||
|
|
||
|
It is also possible for the endpoint to transition to the Draining
|
||
|
state instead, if it receives a `CONNECTION_CLOSE` frame prior
|
||
|
to the timeout expiring. This indicates that the peer is also
|
||
|
closing.
|
||
|
|
||
|
- The Draining state, used for a peer initiated immediate close.
|
||
|
|
||
|
The local endpoint may not send any packets of any kind in this
|
||
|
state. It may optionally send one `CONNECTION_CLOSE` frame immediately
|
||
|
prior to entering this state.
|
||
|
|
||
|
The endpoint progresses to the Terminated state after a timeout
|
||
|
interval, which should not be less than three times the PTO interval.
|
||
|
|
||
|
- The Terminated state is the terminal state of a connection.
|
||
|
Regardless of how a connection ends (local or peer-initiated immediate close,
|
||
|
idle timeout, stateless reset), a connection always ultimately ends up in this
|
||
|
state. There is no longer any requirement to send or receive any packet. No
|
||
|
timer events related to the connection will ever need fire again. This is a
|
||
|
totally quiescent state. The state associated with the connection may now be
|
||
|
safely freed.
|
||
|
|
||
|
We express this state machine in more concrete form in the form of a table,
|
||
|
which makes the available transitions clear:
|
||
|
|
||
|
† Except where superseded by a more specific transition
|
||
|
|
||
|
ε means “where no other transition is applicable”.
|
||
|
|
||
|
Where an action is specified in the Transition/Action column but no new state,
|
||
|
no state change occurs.
|
||
|
|
||
|
<table>
|
||
|
<tr><th>State</th><th>Action On Entry/Exit</th><th>Event</th><th>Transition/Action</th></tr>
|
||
|
<tr>
|
||
|
<td rowspan="2"><tt>IDLE</tt></td>
|
||
|
<td rowspan="2"></td>
|
||
|
<td>—<tt>APP:CONNECT</tt>→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.PROACTIVE_VER_NEG</tt> (if used), else
|
||
|
<tt>ACTIVE.ESTABLISHING.PRE_INITIAL</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>APP:CLOSE</tt>→</td>
|
||
|
<td><tt>TERMINATED</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="5"><tt>ACTIVE</tt></td>
|
||
|
<td rowspan="5"></td>
|
||
|
<td>—<tt>IDLE_TIMEOUT</tt>→</td>
|
||
|
<td><tt>TERMINATED</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>PROBE_TIMEOUT</tt>→ †</td>
|
||
|
<td><tt>SendProbeIfAnySentPktsUnacked()</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>APP:CLOSE</tt>→ †</td>
|
||
|
<td><tt>TERMINATING.CLOSING</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>RX:ANY[CONNECTION_CLOSE]</tt>→</td>
|
||
|
<td><tt>TERMINATING.DRAINING</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>RX:STATELESS_RESET</tt>→</td>
|
||
|
<td><tt>TERMINATED</tt></td>
|
||
|
</tr>
|
||
|
|
||
|
<tr>
|
||
|
<td rowspan="3"><tt>ACTIVE.ESTABLISHING.PROACTIVE_VER_NEG</tt></td>
|
||
|
<td rowspan="3"><tt>enter:SendReqVerNeg</tt></td>
|
||
|
<td>—<tt>RX:VER_NEG</tt>→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.PRE_INITIAL</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>PROBE_TIMEOUT</tt>→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.PROACTIVE_VER_NEG</tt> (retransmit)</td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>APP:CLOSE</tt>→</td>
|
||
|
<td><tt>TERMINATED</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="1"><tt>ACTIVE.ESTABLISHING.PRE_INITIAL</tt></td>
|
||
|
<td rowspan="1"></td>
|
||
|
<td>—ε→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.INITIAL_EXCHANGE_A</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="4"><tt>ACTIVE.ESTABLISHING.INITIAL_EXCHANGE_A</tt></td>
|
||
|
<td rowspan="4"><tt>enter:SendPackets()</tt> (First Initial)</td>
|
||
|
<td>—<tt>RX:RETRY</tt>→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.INITIAL_EXCHANGE_B</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>RX:INITIAL</tt>→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.INITIAL_EXCHANGE_CONTINUED</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>RX:VER_NEG</tt>→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.REACTIVE_VER_NEG</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>CAN_SEND</tt>→</td>
|
||
|
<td><tt>SendPackets()</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="1"><tt>ACTIVE.ESTABLISHING.REACTIVE_VER_NEG</tt></td>
|
||
|
<td rowspan="1"></td>
|
||
|
<td>—ε→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.PRE_INITIAL</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="3"><tt>ACTIVE.ESTABLISHING.INITIAL_EXCHANGE_B</tt></td>
|
||
|
<td rowspan="3"><tt>enter:SendPackets()</tt><br/>
|
||
|
(First Initial, with token)<br/>
|
||
|
(*All further Initial packets contain the token)<br/>(*PN is not reset)</td>
|
||
|
<td>—<tt>RX:INITIAL</tt>→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.INITIAL_EXCHANGE_CONTINUED</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>PROBE_TIMEOUT</tt>→</td>
|
||
|
<td>TODO: Tail loss probe for initial packets?</td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>CAN_SEND</tt>→</td>
|
||
|
<td><tt>SendPackets()</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="2"><tt>ACTIVE.ESTABLISHING.INITIAL_EXCHANGE_CONTINUED</tt></td>
|
||
|
<td rowspan="2"><tt>enter:SendPackets()</tt></td>
|
||
|
<td>—<tt>RX:INITIAL</tt>→</td>
|
||
|
<td>(packet processed, no change)</td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>TLS:HAVE_EL(HANDSHAKE)</tt>→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.HANDSHAKE</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="3"><tt>ACTIVE.ESTABLISHING.HANDSHAKE</tt></td>
|
||
|
<td rowspan="3"><tt>enter:ProvisionEL(Handshake)</tt><br/>
|
||
|
<tt>enter:SendPackets()</tt> (First Handshake packet, if pending)</td>
|
||
|
<td>—<tt>RX:HANDSHAKE</tt>→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.HANDSHAKE_CONTINUED</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>RX:INITIAL</tt>→</td>
|
||
|
<td>(packet processed if EL is not dropped)</td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>CAN_SEND</tt>→</td>
|
||
|
<td><tt>SendPackets()</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="3"><tt>ACTIVE.ESTABLISHING.HANDSHAKE_CONTINUED</tt></td>
|
||
|
<td rowspan="3"><tt>enter:DropEL(Initial)</tt><br/><tt>enter:SendPackets()</tt></td>
|
||
|
<td>—<tt>RX:HANDSHAKE</tt>→</td>
|
||
|
<td>(packet processed, no change)</td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>TLS:HANDSHAKE_COMPLETE</tt>→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.HANDSHAKE_COMPLETE</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>CAN_SEND</tt>→</td>
|
||
|
<td><tt>SendPackets()</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="3"><tt>ACTIVE.ESTABLISHING.HANDSHAKE_COMPLETED</tt></td>
|
||
|
<td rowspan="3"><tt>enter:ProvisionEL(1RTT)</tt><br/><tt>enter:HandshakeComplete()</tt><br/><tt>enter[server]:Send(HANDSHAKE_DONE)</tt><br/><tt>enter:SendPackets()</tt></td>
|
||
|
<td>—<tt>RX:1RTT[HANDSHAKE_DONE]</tt>→</td>
|
||
|
<td><tt>ACTIVE.ESTABLISHING.HANDSHAKE_CONFIRMED</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>RX:1RTT</tt>→</td>
|
||
|
<td>(packet processed, no change)</td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>CAN_SEND</tt>→</td>
|
||
|
<td><tt>SendPackets()</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="1"><tt>ACTIVE.ESTABLISHING.HANDSHAKE_CONFIRMED</tt></td>
|
||
|
<td rowspan="1"><tt>enter:DiscardEL(Handshake)</tt><br/><tt>enter:Permit1RTTKeyUpdate()</tt></td>
|
||
|
<td>—ε→</td>
|
||
|
<td><tt>ACTIVE.OPEN</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="2"><tt>ACTIVE.OPEN</tt></td>
|
||
|
<td rowspan="2"></td>
|
||
|
<td>—<tt>RX:1RTT</tt>→</td>
|
||
|
<td>(packet processed, no change)</td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>CAN_SEND</tt>→</td>
|
||
|
<td><tt>SendPackets()</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="2"><tt>TERMINATING</tt></td>
|
||
|
<td rowspan="2"></td>
|
||
|
<td>—<tt>TERMINATING_TIMEOUT</tt>→</td>
|
||
|
<td><tt>TERMINATED</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>RX:STATELESS_RESET</tt>→</td>
|
||
|
<td><tt>TERMINATED</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="3"><tt>TERMINATING.CLOSING</tt></td>
|
||
|
<td rowspan="3"><tt>enter:QueueConnectionCloseFrame()</tt><br/><tt>enter:SendPackets()</tt></td>
|
||
|
<td>—<tt>RX:ANY[CONNECTION_CLOSE]</tt>→</td>
|
||
|
<td><tt>TERMINATING.DRAINING</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>RX:ANY</tt>→</td>
|
||
|
<td><tt>QueueConnectionCloseFrame()</tt><br/><tt>SendPackets()</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td>—<tt>CAN_SEND</tt>→</td>
|
||
|
<td><tt>SendPackets()</tt></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="1"><tt>TERMINATING.DRAINING</tt></td>
|
||
|
<td rowspan="1"></td>
|
||
|
<td></td>
|
||
|
<td></td>
|
||
|
</tr>
|
||
|
<tr>
|
||
|
<td rowspan="1"><tt>TERMINATED</tt></td>
|
||
|
<td rowspan="1"></td>
|
||
|
<td>[terminal state]</td>
|
||
|
<td></td>
|
||
|
</tr>
|
||
|
</table>
|
||
|
|
||
|
Notes on various events:
|
||
|
|
||
|
- `CAN_SEND` is raised when transmission of packets has been unblocked after previously
|
||
|
having been blocked. There are broadly two reasons why transmission of packets
|
||
|
may not have been possible:
|
||
|
|
||
|
- Due to OS buffers or network-side write BIOs being full;
|
||
|
- Due to limits imposed by the chosen congestion controller.
|
||
|
|
||
|
`CAN_SEND` is expected to be raised due to a timeout prescribed by the
|
||
|
congestion controller or in response to poll(2) or similar notifications, as
|
||
|
abstracted by the BIO system and how the application has chosen to notify
|
||
|
libssl of network I/O readiness.
|
||
|
|
||
|
It is generally implied that processing of a packet as mentioned above
|
||
|
may cause new packets to be queued and sent, so this is not listed
|
||
|
explicitly in the Transition column except for the `CAN_SEND` event.
|
||
|
|
||
|
- `PROBE_TIMEOUT` is raised after the PTO interval and stimulates generation
|
||
|
of a tail loss probe.
|
||
|
|
||
|
- `IDLE_TIMEOUT` is raised after the connection idle timeout expires.
|
||
|
Note that the loss detector only makes a determination of loss due to an
|
||
|
incoming ACK frame; if a peer becomes totally unresponsive, this is the only
|
||
|
mechanism available to terminate the connection (other than the local
|
||
|
application choosing to close it).
|
||
|
|
||
|
- `RX:STATELESS_RESET` indicates receipt of a stateless reset, but note
|
||
|
that it is not guaranteed that we are able to recognise a stateless reset
|
||
|
that we receive, thus this event may not always be raised.
|
||
|
|
||
|
- `RX:ANY[CONNECTION_CLOSE]` denotes a `CONNECTION_CLOSE` frame received
|
||
|
in any non-discarded EL.
|
||
|
|
||
|
- Any circumstance where `RX:RETRY` or `RX:VER_NEG` are not explicitly
|
||
|
listed means that these packets are not allowed and will be ignored.
|
||
|
|
||
|
- Protocol errors, etc. can be handled identically to `APP:CLOSE` events
|
||
|
as indicated in the above table if locally initiated. Protocol errors
|
||
|
signalled by the peer are handled as `RX:ANY[CONNECTION_CLOSE]` events.
|
||
|
|
||
|
Notes on various actions:
|
||
|
|
||
|
- `SendPackets()` sends packets if we have anything pending for transmission,
|
||
|
and only to the extent we are able to with regards to congestion control and
|
||
|
available BIO buffer space, etc.
|
||
|
|
||
|
Non-FSM Model
|
||
|
-------------
|
||
|
|
||
|
Common QUIC implementations appear to prefer modelling connection state as a set
|
||
|
of flags rather than as a FSM. It can be observed above that there is a fair
|
||
|
degree of commonality between many states. This has been modelled above using
|
||
|
hierarchical states with default handlers for common events. [The state machine
|
||
|
can be viewed as a diagram here (large
|
||
|
image).](./images/connection-state-machine.png)
|
||
|
|
||
|
We transpose the above table to sort by events rather than states, to discern
|
||
|
the following list of events:
|
||
|
|
||
|
- `APP:CONNECT`: Supported in `IDLE` state only.
|
||
|
|
||
|
- `RX:VER_NEG`: Handled in `ESTABLISHING.PROACTIVE_VER_NEG` and
|
||
|
`ESTABLISHING.INITIAL_EXCHANGE_A` only, otherwise ignored.
|
||
|
|
||
|
- `RX:RETRY`: Handled in `ESTABLISHING.INITIAL_EXCHANGE_A` only.
|
||
|
|
||
|
- `PROBE_TIMEOUT`: Applicable to `OPEN` and all (non-ε) `ESTABLISHING`
|
||
|
substates. Handled via `SendProbeIfAnySentPktsUnacked()` except in the
|
||
|
`ESTABLISHING.PROACTIVE_VER_NEG` state, which reenters that state to trigger
|
||
|
retransmission of a Version Negotiation packet.
|
||
|
|
||
|
- `IDLE_TIMEOUT`: Applicable to `OPEN` and all (non-ε) `ESTABLISHING` substates.
|
||
|
Action: immediate transition to `TERMINATED` (no `CONNECTION_CLOSE` frame
|
||
|
is sent).
|
||
|
|
||
|
- `TERMINATING_TIMEOUT`: Timeout used by the `TERMINATING` state only.
|
||
|
|
||
|
- `CAN_SEND`: Applicable to `OPEN` and all (non-ε) `ESTABLISHING`
|
||
|
substates, as well as `TERMINATING.CLOSING`.
|
||
|
Action: `SendPackets()`.
|
||
|
|
||
|
- `RX:STATELESS_RESET`: Applicable to all `ESTABLISHING` and `OPEN` states and
|
||
|
the `TERMINATING.CLOSING` substate.
|
||
|
Always causes a direct transition to `TERMINATED`.
|
||
|
|
||
|
- `APP:CLOSE`: Supported in `IDLE`, `ESTABLISHING` and `OPEN` states.
|
||
|
(Reasonably a no-op in `TERMINATING` or `TERMINATED.`)
|
||
|
|
||
|
- `RX:ANY[CONNECTION_CLOSE]`: Supported in all `ESTABLISHING` and `OPEN` states,
|
||
|
as well as in `TERMINATING.CLOSING`. Transition to `TERMINATING.DRAINING`.
|
||
|
|
||
|
- `RX:INITIAL`, `RX:HANDSHAKE`, `RX:1RTT`: Our willingness to process these is
|
||
|
modelled on whether we have an EL provisioned or discarded, etc.; thus
|
||
|
this does not require modelling as additional state.
|
||
|
|
||
|
Once we successfully decrypt a Handshake packet, we stop processing Initial
|
||
|
packets and discard the Initial EL, as required by RFC.
|
||
|
|
||
|
- `TLS:HAVE_EL(HANDSHAKE)`: Emitted by the handshake layer when Handshake EL
|
||
|
keys are available.
|
||
|
|
||
|
- `TLS:HANDSHAKE_COMPLETE`: Emitted by the handshake layer when the handshake
|
||
|
is complete. Implies connection has been authenticated. Also implies 1-RTT EL
|
||
|
keys are available. Whether the handshake is complete, and also whether it is
|
||
|
confirmed, is reasonably implemented as a flag.
|
||
|
|
||
|
From here we can discern state dependence of different events:
|
||
|
|
||
|
- `APP:CONNECT`: Need to know if application has invoked this event yet,
|
||
|
as if so it is invalid.
|
||
|
|
||
|
State: Boolean: Connection initiated?
|
||
|
|
||
|
- `RX:VER_NEG`: Only valid if we have not yet received any successfully
|
||
|
processed encrypted packet from the server.
|
||
|
|
||
|
- `RX:RETRY`: Only valid if we have sent an Initial packet to the server,
|
||
|
have not yet received any successfully processed encrypted packet
|
||
|
from the server, and have not previously been asked to do a Retry as
|
||
|
part of this connection (and the Retry Integrity Token validates).
|
||
|
|
||
|
Action: Note that we are now acting on a retry and start again.
|
||
|
Do not reset packet numbers. The original CIDs used for the first
|
||
|
connection attempt must be noted for later authentication in
|
||
|
the QUIC Transport Parameters.
|
||
|
|
||
|
State: Boolean: Retry requested?
|
||
|
|
||
|
State: CID: Original SCID, DCID.
|
||
|
|
||
|
- `PROBE_TIMEOUT`: If we have sent at least one encrypted packet yet,
|
||
|
we can handle this via a standard probe-sending mechanism. Otherwise, we are
|
||
|
still in Proactive Version Negotiation and should retransmit the Version
|
||
|
Negotiation packet we sent.
|
||
|
|
||
|
State: Boolean: Doing proactive version negotiation?
|
||
|
|
||
|
- `IDLE_TIMEOUT`: Only applicable in `ACTIVE` states.
|
||
|
|
||
|
We are `ACTIVE` if a connection has been initiated (see `APP:CONNECT`) and
|
||
|
we are not in `TERMINATING` or `TERMINATED`.
|
||
|
|
||
|
- `TERMINATING_TIMEOUT`: Timer used in `TERMINATING` state only.
|
||
|
|
||
|
- `CAN_SEND`: Stimulates transmission of packets.
|
||
|
|
||
|
- `RX:STATELESS_RESET`: Always handled unless we are in `TERMINATED`.
|
||
|
|
||
|
- `APP:CLOSE`: Usually causes a transition to `TERMINATING.CLOSING`.
|
||
|
|
||
|
- `RX:INITIAL`, `RX:HANDSHAKE`, `RX:1RTT`: Willingness to process
|
||
|
these is implicit in whether we currently have the applicable EL
|
||
|
provisioned.
|
||
|
|
||
|
- `TLS:HAVE_EL(HANDSHAKE)`: Handled by the handshake layer
|
||
|
and forwarded to the record layer to provision keys.
|
||
|
|
||
|
- `TLS:HANDSHAKE_COMPLETE`: Should be noted as a flag and notification
|
||
|
provided to various components.
|
||
|
|
||
|
We choose to model the CSM's state as follows:
|
||
|
|
||
|
- The `IDLE`, `ACTIVE`, `TERMINATING.CLOSED`, `TERMINATING.DRAINED` and
|
||
|
`TERMINATED` states are modelled explicitly as a state variable. However,
|
||
|
the substates of `ACTIVE` are not explicitly modelled.
|
||
|
|
||
|
- The following flags are modelled:
|
||
|
- Retry Requested? (+ Original SCID, DCID if so)
|
||
|
- Have Sent Any Packet?
|
||
|
- Are we currently doing proactive version negotiation?
|
||
|
- Have Successfully Received Any Encrypted Packet?
|
||
|
- Handshake Completed?
|
||
|
- Handshake Confirmed?
|
||
|
|
||
|
- The following timers are modelled:
|
||
|
- PTO Timeout
|
||
|
- Terminating Timeout
|
||
|
- Idle Timeout
|
||
|
|
||
|
Implementation Plan
|
||
|
-------------------
|
||
|
|
||
|
- Phase 1: “Steady state only” model which jumps to the `ACTIVE.OPEN`
|
||
|
state with a hardcoded key.
|
||
|
|
||
|
Test plan: Currently uncertain, to be determined.
|
||
|
|
||
|
- Phase 2: “Dummy handshake” model which uses a one-byte protocol
|
||
|
as the handshake layer as a standin for TLS 1.3. e.g. a 0x01 byte “represents”
|
||
|
a ClientHello, a 0x02 byte “represents” a ServerHello. Keys are fixed.
|
||
|
|
||
|
Test plan: If feasible, an existing QUIC implementation will be modified to
|
||
|
use this protocol and E2E testing will be performed against it. (This
|
||
|
can probably be done quickly but an alternate plan may be required if
|
||
|
the effort needed turns out be excessive.)
|
||
|
|
||
|
- Phase 3: Final model with TLS 1.3 handshake layer fully plumbed in.
|
||
|
|
||
|
Test plan: Testing against real world implementations.
|