RFC 7016
Adobe's Secure Real-Time Media Flow Protocol
Adobe's Secure Real-Time Media Flow Protocol ๐
Part 1 of 22 ๐ง
Okay bestie, here's the title block. This is RFC 7016, and it's Informational, not Standards Track ๐. The author is M. Thornburgh from Adobe ๐ . It was published in November 2013 ๐, and the ISSN is 2070-1721 โจ. The title block doesn't list anything it obsoletes or updates, so there's nothing to chase there ๐ฆ.
Abstract ๐
Okay so this doc describes Adobe's Secure Real-Time Media Flow Protocol, aka RTMFP ๐. It's an endpoint-to-endpoint protocol, which means the two ends talk directly, and it's built to securely carry multiple flows at the same time: real-time video, audio, and data messages, plus bulk data, all over IP networks ๐ฅ. It's lowkey great for peer-to-peer (P2P) setups and client-server setups too, and it still works when Network Address Translators (NATs) are in the mix. Literally iconic ๐.
Status of This Memo โจ
This document is not an Internet Standards Track specification. It's published just for information ๐.
It's a product of the Internet Engineering Task Force (IETF) and got approved for publication by the Internet Engineering Steering Group (IESG) ๐ซถ. But not every IESG-approved document is a candidate for any level of Internet Standard. See Section 2 of RFC 5741 for the details ๐.
For the latest status, errata, and feedback info, check http://www.rfc-editor.org/info/rfc7016 ๐ธ.
IESG Note ๐
This doc is tech that was developed outside the IETF process. The IETF community decided it was useful to publish it as an RFC in its current form, so here it is ๐. It's only a product of the IETF in the sense that it got public review and IESG approval for publication ๐ . The content does not represent an IETF consensus, so don't read it that way ๐ฅ.
Copyright Notice ๐ฆ
Copyright (c) 2013 IETF Trust and the persons identified as the document authors. All rights reserved ๐.
This doc is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents, at http://trustee.ietf.org/license-info, as they stand on the publication date ๐. Please read those carefully, because they spell out your rights and restrictions for this document ๐ซถ. Code Components pulled out of this document have to include the Simplified BSD License text, as described in Section 4.e of the Trust Legal Provisions, and they come without warranty, as the Simplified BSD License says ๐.
This document can't be modified, and you can't make derivative works of it, except to format it for publication as an RFC or to translate it into languages other than English ๐ โโ๏ธ.
Table of Contents ๐ง
1. Introduction ....................................................5
1.1. Design Highlights of RTMFP .................................6
1.2. Terminology ................................................7
2. Syntax ..........................................................8
2.1. Common Elements ............................................8
2.1.1. Elementary Types and Constructs .....................8
2.1.2. Variable Length Unsigned Integer (VLU) .............10
2.1.3. Option .............................................10
2.1.4. Option List ........................................11
2.1.5. Internet Socket Address (Address) ..................12
2.2. Network Layer .............................................13
2.2.1. Encapsulation ......................................13
2.2.2. Multiplex ..........................................13
2.2.3. Encryption .........................................14
2.2.4. Packet .............................................15
2.3. Chunks ....................................................18
2.3.1. Packet Fragment Chunk ..............................20
2.3.2. Initiator Hello Chunk (IHello) .....................21
2.3.3. Forwarded Initiator Hello Chunk (FIHello) ..........22
2.3.4. Responder Hello Chunk (RHello) .....................23
2.3.5. Responder Redirect Chunk (Redirect) ................24
2.3.6. RHello Cookie Change Chunk .........................26
2.3.7. Initiator Initial Keying Chunk (IIKeying) ..........27
2.3.8. Responder Initial Keying Chunk (RIKeying) ..........29
2.3.9. Ping Chunk .........................................31
2.3.10. Ping Reply Chunk ..................................32
2.3.11. User Data Chunk ...................................33
2.3.11.1. Options for User Data ....................35
2.3.11.1.1. User's Per-Flow Metadata ......35
2.3.11.1.2. Return Flow Association .......36
2.3.12. Next User Data Chunk ..............................37
2.3.13. Data Acknowledgement Bitmap Chunk (Bitmap Ack) ....39
2.3.14. Data Acknowledgement Ranges Chunk (Range Ack) .....41
2.3.15. Buffer Probe Chunk ................................43
2.3.16. Flow Exception Report Chunk .......................43
2.3.17. Session Close Request Chunk (Close) ...............44
2.3.18. Session Close Acknowledgement Chunk (Close Ack) ...44
3. Operation ......................................................45
3.1. Overview ..................................................45
3.2. Endpoint Identity .........................................46
3.3. Packet Multiplex ..........................................48
3.4. Packet Fragmentation ......................................48
3.5. Sessions ..................................................50
3.5.1. Startup ............................................53
3.5.1.1. Normal Handshake ..........................53
3.5.1.1.1. Initiator ......................54
3.5.1.1.2. Responder ......................55
3.5.1.2. Cookie Change .............................57
3.5.1.3. Glare .....................................59
3.5.1.4. Redirector ................................60
3.5.1.5. Forwarder .................................61
3.5.1.6. Redirector and Forwarder with NAT .........63
3.5.1.7. Load Distribution and Fault Tolerance .....66
3.5.2. Congestion Control .................................67
3.5.2.1. Time Critical Reverse Notification ........68
3.5.2.2. Retransmission Timeout ....................68
3.5.2.3. Burst Avoidance ...........................71
3.5.3. Address Mobility ...................................71
3.5.4. Ping ...............................................72
3.5.4.1. Keepalive .................................72
3.5.4.2. Address Mobility ..........................73
3.5.4.3. Path MTU Discovery ........................74
3.5.5. Close ..............................................74
3.6. Flows .....................................................75
3.6.1. Overview ...........................................75
3.6.1.1. Identity ..................................75
3.6.1.2. Messages and Sequencing ...................76
3.6.1.3. Lifetime ..................................77
3.6.2. Sender .............................................78
3.6.2.1. Startup ...................................80
3.6.2.2. Queuing Data ..............................80
3.6.2.3. Sending Data ..............................81
3.6.2.3.1. Startup Options ................83
3.6.2.3.2. Send Next Data .................83
3.6.2.4. Processing Acknowledgements ...............83
3.6.2.5. Negative Acknowledgement and Loss .........84
3.6.2.6. Timeout ...................................85
3.6.2.7. Abandoning Data ...........................86
3.6.2.7.1. Forward Sequence Number
Update .........................86
3.6.2.8. Examples ..................................87
3.6.2.9. Flow Control ..............................89
3.6.2.9.1. Buffer Probe ...................89
3.6.2.10. Exception ................................89
3.6.2.11. Close ....................................90
3.6.3. Receiver ...........................................90
3.6.3.1. Startup ...................................93
3.6.3.2. Receiving Data ............................94
3.6.3.3. Buffering and Delivering Data .............95
3.6.3.4. Acknowledging Data ........................97
3.6.3.4.1. Timing .........................98
3.6.3.4.2. Size and Truncation ............99
3.6.3.4.3. Constructing ...................99
3.6.3.4.4. Delayed Acknowledgement .......100
3.6.3.4.5. Obligatory Acknowledgement ....100
3.6.3.4.6. Opportunistic
Acknowledgement ...............100
3.6.3.4.7. Example .......................101
3.6.3.5. Flow Control .............................102
3.6.3.6. Receiving a Buffer Probe .................103
3.6.3.7. Rejecting a Flow .........................103
3.6.3.8. Close ....................................104
4. IANA Considerations ...........................................104
5. Security Considerations .......................................105
6. Acknowledgements ..............................................106
7. References ....................................................107
7.1. Normative References .....................................107
7.2. Informative References ...................................107
Appendix A. Example Congestion Control Algorithm .................108
A.1. Discussion ................................................108
A.2. Algorithm .................................................110
1. Introduction โจ
okay bestie, so Adobe's Secure Real-Time Media Flow Protocol (RTMFP) is basically a general purpose endpoint-to-endpoint data transport service for IP networks ๐ฆ. it's giving real-time media (low-delay video, audio, and data) AND bulk data, and it works for client-server as well as peer-to-peer (P2P) communication ๐. the features are honestly iconic: independent parallel message flows with different delivery priorities, variable message reliability (from TCP-like full reliability all the way to UDP-like best effort), multi-point congestion control, and built-in security ๐ฅ. session multiplexing and UDP hole-punching support make Network Address Translator (NAT) traversal in peer-to-peer systems way easier ๐ซถ.
RTMFP is implemented in Flash Player, Adobe Integrated Runtime (AIR), and Adobe Media Server (AMS, formerly Flash Media Server or FMS), all from Adobe Systems Incorporated ๐ . it's the foundation transport protocol for real-time video, audio, and data communication, both client-server and P2P, in those products. at the time of writing, the Adobe Flash Player runtime is installed on more than one billion end-user desktop computers ๐ that's a LOT of bestie energy.
RTMFP was developed by Adobe Systems Incorporated and is not the product of an IETF activity ๐.
This memo describes the syntax and operation of the Secure Real-Time Media Flow Protocol ๐.
This memo describes a general security framework that, when combined with an application-specific Cryptography Profile, can be used to establish a confidential and authenticated session between endpoints ๐. the application-specific Cryptography Profile, not defined herein, would detail the specific cryptographic algorithms, data formats, and semantics to be used within this framework. interoperation between applications of RTMFP requires common or compatible Cryptography Profiles ๐ง.
Note to implementers: at the time of writing, the Cryptography Profile used by the above-mentioned Adobe products is not publicly described by Adobe. Implementers should investigate the availability of documentation of that Cryptography Profile prior to implementing RTMFP for the purpose of interoperation with the above-mentioned Adobe products ๐.
1.1. Design Highlights of RTMFP
Between any pair of communicating endpoints is a single, bidirectional, secured, congestion controlled session ๐. unidirectional flows convey messages from one end to the other within the session. an endpoint can have concurrent sessions with multiple other far endpoints โจ.
Design highlights of RTMFP include the following:
- the security framework is an inherent part of the basic protocol. the application designer chooses the cryptographic formats and algorithms to suit the needs of the application, and may update them as the state of the security arts progresses ๐ฅ.
- Cryptographic Endpoint Discriminators can resist port scanning ๐.
- all header, control, and framing information, except for network addressing information and a session identifier, is encrypted according to the Cryptography Profile ๐.
- there is a single session and associated congestion control state between a pair of endpoints.
- each session may have zero or more unidirectional message-oriented flows in each direction. all of a session's sending flows share the session's congestion control state ๐ธ.
- Return Flow Association (Section 2.3.11.1.2) generalizes bidirectional communication to arbitrarily complex trees of flows ๐ณ.
- messages in flows can be arbitrarily large and are fragmented for transmission ๐งฉ.
- messages of any size may be sent with full, partial, or no reliability (sender's choice). messages may be delivered to the receiving user in original queuing order or network arrival order (receiver's choice) ๐ .
- flows are named with arbitrary, user-defined metadata (Section 2.3.11.1.1) rather than port or stream numbers ๐.
- the sequence numbers of each flow are independent of all other flows and are not permanently bound to a session-wide transmission ordering. this allows real-time priority decisions to be made at transmission or retransmission time โญ.
- each flow has its own receive window and, therefore, independent flow control ๐ฆ.
- round trips are expensive and are minimized or eliminated when possible ๐ญ.
- after a session is established, flows begin by sending the flow's messages with no additional handshake (and associated round trips) ๐.
- transmitting bytes on the network is much more expensive than moving bytes in a CPU or memory. wasted bytes are minimized or eliminated when possible and practical, and variable length encodings are used, even at the expense of breaking 32-bit alignment and making the text diagrams in this specification look awkward ๐.
- P2P lookup and peer introduction (including UDP hole-punching for NAT and firewall traversal) are supported directly by the session startup handshake ๐ซถ.
- session identifiers allow an endpoint to multiplex many sessions over a single local transport address while allowing sessions to survive changes in transport address (as may happen in mobile or wireless deployments) ๐ฑ.
The syntax of the protocol is detailed in Section 2. The operation of the protocol is detailed in Section 3 ๐.
1.2. Terminology
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC2119] ๐.
2. Syntax ๐ง
Definitions of types and structures in this specification use traditional text diagrams paired with procedural descriptions using a C-like syntax. The C-like procedural descriptions SHALL be construed as definitive ๐.
Structures are packed to take only as many bytes as explicitly indicated. There is no 32-bit alignment constraint, and fields are not padded for alignment unless explicitly indicated or described. Text diagrams may include a bit ruler across the top; this is a convenience for counting bits in individual fields and does not necessarily imply field alignment on a multiple of the ruler width ๐.
Unless specified otherwise, reserved fields SHOULD be set to 0 by a sender and MUST be ignored by a receiver ๐ซง.
The procedural syntax of this specification defines correct and error-free encoded inputs to a parser. The procedural syntax does not describe a fully featured parser, including error detection and handling. Implementations MUST include means to identify error circumstances, including truncations causing elementary or composed types to not fit inside containing structures, fields, or elements. Unless specified otherwise, an error circumstance SHALL abort the parsing and processing of an element and its enclosing elements, up to the containing packet ๐จ.
2.1. Common Elements
This section lists types and structures that are used throughout this specification ๐.
2.1.1. Elementary Types and Constructs
This section lists the elementary types and constructs out of which all of the following sections' definitions are built ๐งฑ.
uint8_t var;
An unsigned integer 8 bits (one byte) in length and byte aligned.
uint16_t var;
An unsigned integer 16 bits in length, in network byte order ("big
endian") and byte aligned.
uint32_t var;
An unsigned integer 32 bits in length, in network byte order and
byte aligned.
uint128_t var;
An unsigned integer 128 bits in length, in network byte order and
byte aligned.
uintn_t var :bitsize;
An unsigned integer of any other size, potentially not byte
aligned. Its size in bits is specified explicitly by bitsize.
bool_t var :1;
A boolean flag having the value true (1 or set) or false (0 or
clear) and being one bit in length.
type var[num];
A packed array of type with length num*sizeof(type)*8 bits.
struct name_t { ... } name :bitsize;
A packed structure. Its size in bits is specified by bitsize.
remainder();
The number of bytes from the current offset to the end of the
enclosing structure.
type var[remainder()];
A packed array of type, its size extending to the end of the
enclosing structure.
Note that a bitsize of "variable" indicates that the size of the
structure is determined by the sizes of its interior components. A
bitsize of "n*8" indicates that the size of the structure is a whole
number of bytes and is byte aligned.
2.1.2. Variable Length Unsigned Integer (VLU)
A VLU encodes any finite non-negative integer into one or more bytes. For each encoded byte, if the high bit is set, the next byte is also part of the VLU. If the high bit is clear, this is the final byte of the VLU. The remaining bits encode the number, seven bits at a time, from most significant to least significant ๐ข.
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+~+~+~+~+~+~+~+~+ +-+-+-+-+-+-+-+-+
|1| digit |...............|0| digit |
+~+~+~+~+~+~+~+~+ +-+-+-+-+-+-+-+-+
^ ^
+--------- zero or more --------+
struct vlu_t
{
value = 0;
do {
bool_t more :1;
uintn_t digit :7;
value = (value * 128) + digit;
} while(more);
} :variable*8;
+-------------/-+
| \ |
+-------------/-+
Figure 1: VLU Depiction in Following Diagrams
Unless stated otherwise in this specification, implementations SHOULD handle VLUs encoding unsigned integers at least 64 bits in length (that is, encoding a maximum value of at least 2^64 - 1) ๐งฎ.
2.1.3. Option
An Option is a Length-Type-Value triplet. Length and Type are encoded in VLU format. Length is the number of bytes of payload following the Length field. The payload comprises the Type and Value fields. Type identifies the kind of option this is. The syntax of the Value field is determined by the type of option ๐.
An Option can have a length of zero, in which case it has no type and no value and is empty. An empty Option is called a "Marker" ๐ณ๏ธโ๐.
+-------------/-+~~~~~~~~~~~~~/~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| length \ | type \ | value |
+-------------/-+~~~~~~~~~~~~~/~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
^ ^
+-------- length bytes long (may be 0) ---------+
struct option_t
{
vlu_t length :variable*8; // "L"
if(length > 0)
{
struct {
vlu_t type :variable*8; // "T"
uint8_t value[remainder()]; // "V"
} payload :length*8;
}
} :variable*8;
+---/---/-------+
| L \ T \ V |
+---/---/-------+
Figure 2: Option Depiction in Following Diagrams
โจ that's the end of part 2! โจ
2.1.4. Option List ๐
okay so an Option List is basically a train of zero or more non-empty Options that gets closed off by a Marker ๐ the Marker is just an empty Option (length 0), and that's how the receiver knows the list is done โจ
+~~~/~~~/~~~~~~~+ +~~~/~~~/~~~~~~~+-------------/-+
| L \ T \ V |...............| L \ T \ V | 0 \ |
+~~~/~~~/~~~~~~~+ +~~~/~~~/~~~~~~~+-------------/-+
^ ^ Marker
+------- zero or more non-empty Options --------+ (empty Option)
struct optionList_t
{
do
{
option_t option :variable*8;
} while(option.length > 0);
} :variable*8;
2.1.5. Internet Socket Address (Address) ๐ฆ
bestie, this is the encoding you use when you need to tell another RTMFP about an Internet socket address. that's a combo of a 32-bit IPv4 [RFC0791] or 128-bit IPv6 [RFC2460] address plus a 16-bit port number ๐ the cute extra here is that you can tag the address with an origin type, and an RTMFP MAY use that tag to change how the address gets used or handled ๐
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7|8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-----/.../-----+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|I| | O | Internet | |
|P|0 0 0 0 0| R | address | port |
|6| rsv | I |32 or 128 bits | |
+-+-+-+-+-+-+-+-+-----/.../-----+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct address_t
{
bool_t inet6 :1; // "IP6"
uintn_t reserved :5 = 0; // "rsv"
uintn_t origin :2; // "ORI"
if(inet6)
uint128_t ipAddress;
else
uint32_t ipAddress;
uint16_t port;
} :variable*8;
- inet6: if this bit is set, the Internet address is a 128-bit IPv6 address. if it's clear, the Internet address is a 32-bit IPv4 address ๐ธ
- origin: the origin tag for this address ๐ท๏ธ the possible values are:
0: Unknown, unspecified, or "other"1: the origin reported it as a local, directly attached interface address2: it was observed to be the source address a packet came from (a "reflexive transport address" in the terminology of [RFC5389]) ๐3: the address is a relay, proxy, or introducer (a Redirector and/or Forwarder) ๐ง
- ipAddress: the Internet address, in network byte order ๐ข
- port: the 16-bit port number, in network byte order ๐ฏ
2.2. Network Layer ๐
2.2.1. Encapsulation ๐
so RTMFP Multiplex packets are usually carried in UDP [RFC0768] datagrams. that's the move because it lets them get through the commonly deployed NATs and firewalls ๐งฑ and it means RTMFP can run on common operating systems without any special privileges or permissions. no cap, that's iconic ๐
RTMFP Multiplex packets MAY be carried by any suitable datagram transport or encapsulation, as long as the endpoints are addressed by an Internet socket address (an IPv4 or IPv6 address plus a 16-bit port number) โจ
the choice of port numbers isn't mandated by this specification ๐ higher protocol layers or the application decide which port numbers get used.
2.2.2. Multiplex ๐งฉ
the Multiplex packet starts with a Scrambled Session ID (SSID), followed by an encrypted packet โจ here's the layout:
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Scrambled Session ID (SSID) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| e first32[0] |
|- - - - - - n - - - - - - - - - - - - - - - - - - - - - - - -|
| c first32[1] |
+- - - - - - r - - - - - - - - - - - - - - - - - - - - - - - -+
| y |
| pted packet |
+---------------------------------------------------------------/
struct multiplex_t
{
uint32_t scrambledSessionID; // "SSID"
union {
uint32_t first32[2]; // see note
uint8_t encryptedPacket[remainder()];
} :(encapsulation.length - 4)*8;
// if encryptedPacket is less than 8 bytes long, treat it
// as if it were end-padded with 0s for the following:
sessionID = scrambledSessionID XOR first32[0] XOR first32[1];
} :encapsulation.length*8;
the Scrambled Session ID is the 32-bit session ID, scrambled by doing a bitwise exclusive-or with the bitwise exclusive-or of the first two 32-bit words of the encrypted packet ๐ wild, I know.
the session ID is a 32-bit value that the receiver asked the sender to use when sending packets to it (Sections 2.3.7 and 2.3.8) ๐จ it tells you which session the packet belongs to and which decryption key to use for the encrypted packet ๐
Note: session ID 0 (before scrambling) is the startup pseudo-session and implies the Default Session Key ๐ผ
Note: if the encrypted packet is less than 8 bytes long, then for the scrambling operation, do the exclusive-or as though the encrypted packet were end-padded with enough 0-bytes to bring its length to 8 ๐ซง
2.2.3. Encryption ๐
RTMFP packets are encrypted according to a Cryptography Profile ๐ this spec doesn't define a Cryptography Profile or mandate one particular choice of cryptography. the application defines the cryptographic syntax and algorithms, so that's its job, not ours ๐
packet encryption is RECOMMENDED to be a block cipher operating in Cipher Block Chaining [CBC] or a similar mode ๐ง encrypted packets MUST be decipherable without inter-packet dependency, because packets can get lost, duplicated, or reordered on the network ๐ช๏ธ bestie, every packet has to stand on its own.
the packet encryption layer handles data integrity and authenticity of packets, for example with a checksum or a cryptographic message authentication code ๐ก๏ธ to mitigate replay attacks, data integrity SHOULD include duplicate packet detection, for example with a session-wide packet sequence number ๐ and the packet encryption layer SHALL discard a received packet that doesn't pass the integrity or authenticity tests. no mercy for fakes ๐ซ
note that the structures described below are for plain, unencrypted packets. encrypted packets MUST be decrypted according to the Session Key associated with the Multiplex Session ID before they get interpreted under this spec ๐๏ธ
the Cryptography Profile defines a well-known Default Session Key that's used at session startup, when the two endpoints negotiate per-session key(s) ๐ค a session ID of zero means the Default Session Key gets used. the Default Session Key is also used with non-zero session IDs during the later phases of session startup (Sections 2.3.6 and 2.3.8). for more about the Default Session Key, check Security Considerations (Section 5) ๐โญ
2.2.4. Packet โจ
okay so an (unencrypted, plain) RTMFP packet is made up of a variable sized common header, zero or more chunks, and padding ๐
the padding can be added by the sender's encryption layer to fit cipher block size constraints, and the receiver just ignores it ๐ซถ a sender's encryption layer MAY pad the end of a packet with bytes that have the value 0xff, so the packet ends up a natural, appropriate size for the cipher โจ or the Cryptography Profile MAY define its own framing and padding scheme if it needs one, as long as decrypted packets still match the syntax in this section ๐ฆ
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|T|T| r |T|T| M |
|C|C| s |S|S| O |
| |R| v | |E| D |
+-+-+-+-+-+-+-+-+
+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+
| if(TS) timestamp | if(TSE) timestampEcho |
+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Chunk |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
:
:
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Chunk |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| padding |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
struct packet_t
{
bool_t timeCritical :1; // "TC"
bool_t timeCriticalReverse :1; // "TCR"
uintn_t reserved :2; // "rsv"
bool_t timestampPresent :1; // "TS"
bool_t timestampEchoPresent :1; // "TSE"
uintn_t mode :2; // "MOD"
if(0 != mode)
{
if(timestampPresent)
uint16_t timestamp;
if(timestampEchoPresent)
uint16_t timestampEcho;
while(remainder() > 2)
{
uint8_t chunkType;
uint16_t chunkLength;
if(remainder() < chunkLength)
break;
uint8_t chunkPayload[chunkLength];
} // chunks
uint8_t padding[remainder()];
}
} :plainPacket.length*8;
timeCritical: Time Critical Forward Notification. If set, this tells you the packet has real-time user data in it ๐ฅ
timeCriticalReverse: Time Critical Reverse Notification. If set, the sender is currently getting packets on other sessions that have the timeCritical flag set ๐
timestampPresent: If set, the timestamp field is there. If clear, there's no timestamp field ๐ธ
timestampEchoPresent: If set, the timestamp echo field is there. If clear, there's no timestamp echo field ๐ง
mode: The mode of this packet. There's more on packet modes below ๐ the possible values are:
0: Forbidden value
1: Initiator Mark
2: Responder Mark
3: Startup
timestamp: If the timestampPresent flag is set, this field shows up and holds the low 16 bits of the sender's 250 Hz clock (4 milliseconds per tick) at transmit time. The sender's clock MAY have its origin at any time in the past โฐ
timestampEcho: If the timestampEchoPresent flag is set, this field shows up and holds the sender's estimate of what the timestamp field of a packet received from the other end would be at the time this packet was transmitted. It uses the method described in Section 3.5.2.2 ๐ฆ
chunks: Zero or more chunks come after the header. It is RECOMMENDED that a packet contain at least one chunk ๐
padding: Zero or more bytes of padding come after the chunks. Padding shows up when either of these is true:
- Fewer than three bytes (the size of a chunk header) are left in the packet.
- The
chunkLengthfield of what would be the current chunk header says the hypothetical chunk payload wouldn't fit in the bytes left in the packet.
Packet mode 0 is not allowed ๐ โโ๏ธ packets marked with this mode are invalid and MUST be discarded, no exceptions bestie ๐
The original initiator of a session MUST mark all non-startup packets it sends in that session with packet mode 1 ("Initiator Mark"). It SHOULD ignore any packet received in that session with packet mode 1 โจ
The original responder of a session MUST mark all non-startup packets it sends in that session with packet mode 2 ("Responder Mark"). It SHOULD ignore any packet received in that session with packet mode 2 ๐
Packet mode 3 is for session startup. Session startup chunks are only allowed in packets with this mode ๐
Chunks that are not for session startup are only allowed in packets with modes 1 or 2 ๐
2.3. Chunks ๐ง
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| chunkType | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| chunkPayload (chunkLength bytes, may be zero) |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
struct chunk_t
{
uint8_t chunkType;
uint16_t chunkLength;
uint8_t chunkPayload[chunkLength];
} :variable*8;
chunkType: The chunk type code ๐ฉท
chunkLength: The size, in bytes, of the chunk payload ๐
chunkPayload: The type-specific payload of this chunk, chunkLength bytes long (it can be empty) ๐
The defined chunk types are listed here in the order you'd usually run into them during a typical session ๐ these are the chunk type codes that are defined:
0x7f: Packet Fragment (Section 2.3.1)0x30: Initiator Hello (Section 2.3.2)0x0f: Forwarded Initiator Hello (Section 2.3.3)0x70: Responder Hello (Section 2.3.4)0x71: Responder Redirect (Section 2.3.5)0x79: RHello Cookie Change (Section 2.3.6)0x38: Initiator Initial Keying (Section 2.3.7)0x78: Responder Initial Keying (Section 2.3.8)0x01: Ping (Section 2.3.9)0x41: Ping Reply (Section 2.3.10)0x10: User Data (Section 2.3.11)0x11: Next User Data (Section 2.3.12)0x50: Data Acknowledgement Bitmap (Section 2.3.13)0x51: Data Acknowledgement Ranges (Section 2.3.14)0x18: Buffer Probe (Section 2.3.15)0x5e: Flow Exception Report (Section 2.3.16)0x0c: Session Close Request (Section 2.3.17)0x4c: Session Close Acknowledgement (Section 2.3.18)0x00: Ignore/Padding0xff: Ignore/Padding
A receiver MUST ignore a chunk that has an unrecognized chunk type code ๐ a receiver MUST also ignore a chunk that shows up in a packet with a mode that isn't right for that chunk type ๐ซ
Unless the spec says otherwise, if a chunk has a syntax or processing error (for example, the chunk's payload field isn't long enough to hold the specified syntax elements), the chunk SHALL be ignored as though it was never in the packet. Parsing and processing SHALL then carry on with the next chunk in the packet, if there is one โจ
Tea time: a broken chunk gets quietly skipped and the rest of the packet keeps going. The bad one just never existed ๐ซ
2.3.1. Packet Fragment Chunk
This chunk is used to split up a plain RTMFP packet (Section 2.2.4) that's unavoidably bigger than the path MTU (like session startup packets carrying Responder Hello (Section 2.3.4) or Initiator Initial Keying (Section 2.3.7) chunks with big certificates). It cuts those into segments that don't go over the path MTU, and it lets the segments get sent at a moderated rate so they don't jam interfaces, links, or paths ๐๐จ
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x7f | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-+-+-+-+-+-+-+-+-------------/-+-------------/-+
|M| reserved | packetID \ | fragmentNum \ |
+-+-+-+-+-+-+-+-+-------------/-+-------------/-+
+---------------------------------------------------------------+
| packetFragment |
+---------------------------------------------------------------/
struct fragmentChunkPayload_t
{
bool_t moreFragments :1; // M
uintn_t reserved :7;
vlu_t packetID :variable*8;
vlu_t fragmentNum :variable*8;
uint8_t packetFragment[remainder()];
} :chunkLength*8;
moreFragments: If set, the packet has more fragments coming. If clear, this fragment is the last one of the packet ๐
reserved: Reserved for future use ๐
packetID: A VLU, the ID of this segmented packet. Every fragment of the same packet has the same packetID ๐
fragmentNum: A VLU, the index of this fragment within the packet it belongs to. The first fragment of the packet MUST be index 0. Fragments are numbered consecutively ๐ข
packetFragment: The bytes of this segment of the original plain RTMFP packet. A packetFragment MUST NOT be empty ๐ซง
The way this mechanism works is detailed in Section 3.4 ๐
2.3.2. Initiator Hello Chunk (IHello)
This chunk is what the initiator of a new session sends to kick off the startup handshake ๐ฌ it's only allowed in a packet with Session ID 0, encrypted with the Default Session Key, and having packet mode 3 (Startup) ๐
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x30 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-------------/-+-----------------------------------------------+
| epdLength \ | endpointDiscriminator (epdLength bytes) |
+-------------/-+-----------------------------------------------/
+---------------------------------------------------------------+
| tag |
+---------------------------------------------------------------/
struct ihelloChunkPayload_t
{
vlu_t epdLength :variable*8;
uint8_t endpointDiscriminator[epdLength];
uint8_t tag[remainder()];
} :chunkLength*8;
epdLength: A VLU, the length in bytes of the endpointDiscriminator field that comes after it ๐
endpointDiscriminator: The Endpoint Discriminator for the identity the initiator wants to talk to ๐ฏ
tag: Data the initiator provides, which gets sent back in a Responder Hello's tagEcho field. The tag/tagEcho pair is how Responder Hellos get matched to the initiator's session startup state, no matter what the responder's address is ๐งฉ main character energy for this field fr
The way IHello gets used is detailed in Section 3.5.1 ๐ฆ
2.3.3. Forwarded Initiator Hello Chunk (FIHello) ๐ฆ
okay so this chunk is sent on behalf of an initiator by a Forwarder ๐. It's only allowed in packets of an established session with packet mode 1 or 2. A receiver MAY treat this chunk as though it was an Initiator Hello received directly from replyAddress. Alternatively, if the receiver is selected by the Endpoint Discriminator, it MAY respond to replyAddress with an Implied Redirect (Section 2.3.5) โจ
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x0f | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-------------/-+-----------------------------------------------+
| epdLength \ | endpointDiscriminator (epdLength bytes) |
+-------------/-+-----------------------------------------------/
+---------------------------------------------------------------+
| replyAddress |
+---------------------------------------------------------------/
+---------------------------------------------------------------+
| tag |
+---------------------------------------------------------------/
struct fihelloChunkPayload_t
{
vlu_t epdLength :variable*8;
uint8_t endpointDiscriminator[epdLength];
address_t replyAddress :variable*8;
uint8_t tag[remainder()];
} :chunkLength*8;
epdLength: a VLU that tells you how many bytes long the endpointDiscriminator field after it is ๐ข
endpointDiscriminator: the Endpoint Discriminator for the identity the original initiator wants to talk to. It's copied straight from the original Initiator Hello ๐ซถ
replyAddress: an Address format (Section 2.1.5) value. This is the address the forwarding node worked out from the Initiator Hello it received, and it's where the receiver should send its reply ๐ฌ
tag: copied from the original Initiator Hello ๐ท๏ธ
The use of FIHello is detailed in Section 3.5.1.5 ๐
2.3.4. Responder Hello Chunk (RHello) ๐ธ
this chunk is sent by a responder in reply to an Initiator Hello or Forwarded Initiator Hello, but only if the Endpoint Discriminator names the responder's identity. It's only allowed in a packet with Session ID 0, encrypted with the Default Session Key, and with packet mode 3 (Startup) ๐
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x70 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-------------/-+-----------------------------------------------+
| tagLength \ | tagEcho (tagLength bytes) |
+-------------/-+-----------------------------------------------/
+-------------/-+-----------------------------------------------+
| cookieLength\ | cookie (cookieLength bytes) |
+-------------/-+-----------------------------------------------/
+---------------------------------------------------------------+
| responderCertificate |
+---------------------------------------------------------------/
struct rhelloChunkPayload_t
{
vlu_t tagLength :variable*8;
uint8_t tagEcho[tagLength];
vlu_t cookieLength :variable*8;
uint8_t cookie[cookieLength];
uint8_t responderCertificate[remainder()];
} :chunkLength*8;
tagLength: a VLU that tells you how many bytes long the tagEcho field after it is ๐ข
tagEcho: the tag from the Initiator Hello, unaltered ๐ช
cookieLength: a VLU that tells you how many bytes long the cookie field after it is ๐ช
cookie: state data the responder makes for itself, used to check a future Initiator Initial Keying message. This is how it stops denial-of-service attacks ๐ก๏ธ
responderCertificate: the responder's cryptographic credentials ๐
Note: this spec doesn't force one certificate format. The Cryptography Profile decides the syntax, the algorithms, and how to read the responderCertificate ๐ค
The use of RHello is detailed in Section 3.5.1 ๐ฆ
2.3.5. Responder Redirect Chunk (Redirect) ๐งญ
this chunk is sent in reply to an Initiator Hello or Forwarded Initiator Hello to say the requested endpoint can be reached at one or more of the listed addresses. A receiver can add none, some, or all of those addresses to the set it sends Initiator Hello messages to for the opening session tied to tagEcho. It's only allowed in a packet with Session ID 0, encrypted with the Default Session Key, and with packet mode 3 (Startup) ๐ฆ
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x71 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-------------/-+-----------------------------------------------+
| tagLength \ | tagEcho (tagLength bytes) |
+-------------/-+-----------------------------------------------/
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| redirectDestination 1 |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
:
:
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| redirectDestination N |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
struct responderRedirectChunkPayload_t
{
vlu_t tagLength :variable*8;
uint8_t tagEcho[tagLength];
addressCount = 0;
while(remainder() > 0)
{
address_t redirectDestination :variable*8;
addressCount++;
}
if(0 == addressCount)
redirectDestination = packetSourceAddress();
} :chunkLength*8;
tagLength: a VLU that tells you how many bytes long the tagEcho field after it is ๐ข
tagEcho: the tag from the Initiator Hello, unaltered ๐ช
redirectDestination: zero or more Address format (Section 2.1.5) addresses to add to the opening set for the indicated session ๐
If this chunk lists zero redirectDestination addresses, then it's an Implied Redirect, and the address it points to is the one the packet carrying this chunk came from. Iconic, right? ๐ญ
The use of Redirect is detailed in Sections 3.5.1.1.1, 3.5.1.1.2, and 3.5.1.4 ๐
2.3.6. RHello Cookie Change Chunk ๐
this chunk SHOULD be sent by a responder to an initiator in reply to an Initiator Initial Keying, if that chunk's cookie looks like the responder made it but the cookie is wrong. For example, it has a hash of the initiator's address in it, but the initiator's address is different from the one that triggered the Responder Hello with the original cookie ๐ต๏ธโโ๏ธ
It's only allowed in a packet encrypted with the Default Session Key, with packet mode 3, and with the session ID given in the initiatorSessionID field of the Initiator Initial Keying this is responding to ๐
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x79 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-------------/-+-----------------------------------------------+
| oldCookieLen\ | oldCookie (oldCookieLen bytes) |
+-------------/-+-----------------------------------------------/
+---------------------------------------------------------------+
| newCookie |
+---------------------------------------------------------------/
struct rhelloCookieChangeChunkPayload_t
{
vlu_t oldCookieLen :variable*8;
uint8_t oldCookie[oldCookieLen];
uint8_t newCookie[remainder()];
} :chunkLength*8;
oldCookieLen: a VLU that tells you how many bytes long the oldCookie field after it is ๐ข
oldCookie: the cookie that was sent in an earlier Responder Hello and Initiator Initial Keying ๐ช
newCookie: the new cookie the responder wants sent (and signed) in a replacement Initiator Initial Keying. The old and new cookies don't have to be the same length ๐
When this chunk arrives, the initiator SHOULD compute, sign, and send a new Initiator Initial Keying that has newCookie in place of oldCookie. The use of this chunk is detailed in Section 3.5.1.2 โจ
2.3.7. Initiator Initial Keying Chunk (IIKeying) ๐
okay so this chunk is how an initiator says "hey, let's start a session" to a responder ๐ the initiator MUST have obtained a valid cookie to use with the responder, usually by getting a Responder Hello from them first โจ and this chunk is ONLY allowed in a packet with Session ID 0, encrypted with the Default Session Key, and having packet mode 3 (Startup) ๐
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x38 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| initiatorSessionID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-------------/-+-----------------------------------------------+
| cookieLength\ | cookieEcho |
+-------------/-+-----------------------------------------------/
+-------------/-+-----------------------------------------------+
| certLength \ | initiatorCertificate |
+-------------/-+-----------------------------------------------/
+-------------/-+-----------------------------------------------+
| skicLength \ | sessionKeyInitiatorComponent |
+-------------/-+-----------------------------------------------/
+---------------------------------------------------------------+
| signature |
+---------------------------------------------------------------/
struct iikeyingChunkPayload_t
{
struct
{
uint32_t initiatorSessionID;
vlu_t cookieLength :variable*8;
uint8_t cookieEcho[cookieLength];
vlu_t certLength :variable*8;
uint8_t initiatorCertificate[certLength];
vlu_t skicLength :variable*8;
uint8_t sessionKeyInitiatorComponent[skicLength];
} initiatorSignedParameters :variable*8;
uint8_t signature[remainder()];
} :chunkLength*8;
Here's the field breakdown, bestie ๐ง
initiatorSessionID: the session ID the responder will use when sending packets back to the initiator ๐ฎcookieLength: a VLU (variable-length unsigned) number giving the length, in bytes, of thecookieEchofield that follows ๐cookieEcho: the cookie from the Responder Hello, copied over unaltered ๐ชcertLength: a VLU giving the length, in bytes, of theinitiatorCertificatefield that follows ๐initiatorCertificate: the initiator's identity credentials ๐ชชskicLength: a VLU giving the length, in bytes, of thesessionKeyInitiatorComponentfield that follows ๐sessionKeyInitiatorComponent: the initiator's piece of the session key negotiation, done according to the Cryptography Profile ๐งฉinitiatorSignedParameters: the payload part of this chunk, all the way up to thesignaturefield ๐signature: the initiator's digital signature over theinitiatorSignedParameters, made according to the Cryptography Profile โ๏ธ
Note: This specification doesn't mandate a specific choice of cryptography. The Cryptography Profile decides the syntax, algorithms, and interpretation of the initiatorCertificate, responderCertificate, sessionKeyInitiatorComponent, sessionKeyResponderComponent, and signature, plus how sessionKeyInitiatorComponent and sessionKeyResponderComponent get combined to derive the session keys ๐
The use of IIKeying is detailed in Section 3.5.1. ๐
2.3.8. Responder Initial Keying Chunk (RIKeying) ๐ซถ
this chunk is what a responder sends back in reply to an Initiator Initial Keying, and it's the final step of session startup ๐ it's only allowed in a packet encrypted with the Default Session Key, having packet mode 3 (Startup), and sent to the initiator using the session ID given in the initiatorSessionID field from the Initiator Initial Keying ๐
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x78 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| responderSessionID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-------------/-+-----------------------------------------------+
| skrcLength \ | sessionKeyResponderComponent |
+-------------/-+-----------------------------------------------/
+---------------------------------------------------------------+
| signature |
+---------------------------------------------------------------/
struct rikeyingChunkPayload_t
{
struct
{
uint32_t responderSessionID;
vlu_t skrcLength :variable*8;
uint8_t sessionKeyResponderComponent[skrcLength];
} responderSignedParametersPortion :variable*8;
uint8_t signature[remainder()];
} :chunkLength*8;
struct
{
responderSignedParametersPortion;
sessionKeyInitiatorComponent;
} responderSignedParameters;
the field rundown ๐
responderSessionID: the session ID the initiator will use when sending packets to the responder ๐ฎskrcLength: a VLU giving the length, in bytes, of thesessionKeyResponderComponentfield that follows ๐sessionKeyResponderComponent: the responder's piece of the session key negotiation, done according to the Cryptography Profile ๐งฉresponderSignedParametersPortion: the payload part of this chunk, up to thesignaturefield ๐signature: the responder's digital signature over theresponderSignedParameters(more on that below), made according to the Cryptography Profile โ๏ธresponderSignedParameters: theresponderSignedParametersPortion(the payload of this chunk up to thesignaturefield) glued together with thesessionKeyInitiatorComponentfrom the Initiator Initial Keying this chunk is responding to ๐งต
Note: This specification doesn't mandate a specific choice of cryptography. The Cryptography Profile decides the syntax, algorithms, and interpretation of the initiatorCertificate, responderCertificate, sessionKeyInitiatorComponent, sessionKeyResponderComponent, and signature, plus how sessionKeyInitiatorComponent and sessionKeyResponderComponent get combined to derive the session keys ๐
โจ once the responder has computed the sessionKeyResponderComponent, it has everything it needs and all the state for an established session with the initiator. once the responder has sent this chunk to the initiator, the session is established and ready to carry flows of user data ๐
okay and on the initiator side ๐ once it receives, verifies, and processes this chunk, it has everything and all the state it needs for an established session with the responder. the session is established and ready to carry flows of user data ๐ธ
The use of RIKeying is detailed in Section 3.5.1. ๐
2.3.9. Ping Chunk ๐
this chunk gets sent to make the receiver send back a Ping Reply ๐ it's only allowed in a packet that belongs to an established session and has packet mode 1 or 2 ๐ฆ
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x01 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| message |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
struct pingChunkPayload_t
{
uint8_t message[chunkLength];
} :chunkLength*8;
message: the message that's (potentially empty ๐ซง) and is expected to come back from the other end of the session in a Ping Reply ๐ฌ
The receiver of this chunk SHOULD reply as immediately as is practical with a Ping Reply. ๐โโ๏ธ๐จ
Ping and the expected Ping Reply are typically used for session keepalive, endpoint address change verification, and path MTU discovery. See Section 3.5.4 for details. ๐ง
2.3.10. Ping Reply Chunk ๐ช
this chunk is the response to a Ping chunk ๐ it's only allowed in a packet that belongs to an established session and has packet mode 1 or 2 โจ
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x41 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| messageEcho |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
struct pingReplyChunkPayload_t
{
uint8_t messageEcho[chunkLength];
} :chunkLength*8;
messageEcho: the message from the Ping this is replying to, echoed back unaltered ๐ช
2.3.11. User Data Chunk โจ
okay bestie, this is the chunk that actually carries the good stuff ๐ the user messages themselves! a user message can be split into one or more fragments, and every fragment rides in its own chunk with its own sequence number inside its flow. it's only allowed in a packet that belongs to an established session and has packet mode 1 or 2 ๐ฆ
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x10 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-+-+-+-+-+-+-+-+
|O|r| F | r |A|F|
|P|s| R | s |B|I|
|T|v| A | v |N|N|
+-+-+-+-+-+-+-+-+
+-------------/-+-------------/-+-------------/-+
| flowID \ | seq# \ | fsnOffset \ |
+-------------/-+-------------/-+-------------/-+
+~~~/~~~/~~~~~~~+ +~~~/~~~/~~~~~~~+-------------/-+
| L \ T \ V |... options ...| L \ T \ V | 0 \ |
\~~~/~~~/~~~~~~~+ [if(OPT)] +~~~/~~~/~~~~~~~+-------------/-/
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| userData |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
struct userDataChunkPayload_t
{
bool_t optionsPresent :1; // "OPT"
uintn_t reserved1 :1; // "rsv"
uintn_t fragmentControl :2; // "FRA"
// 0=whole, 1=begin, 2=end, 3=middle
uintn_t reserved2 :2; // "rsv"
bool_t abandon :1; // "ABN"
bool_t final :1; // "FIN"
vlu_t flowID :variable*8;
vlu_t sequenceNumber :variable*8; // "seq#"
vlu_t fsnOffset :variable*8;
forwardSequenceNumber = sequenceNumber - fsnOffset;
if(optionsPresent)
optionList_t options :variable*8;
uint8_t userData[remainder()];
} :chunkLength*8;
optionsPresent: if set, there's an option list sitting before the user data ๐ if clear, this chunk has no option list at all.
fragmentControl: this tells you how the fragment gets stitched together, maybe with others, into a complete user message ๐งฉ the possible values are:
0: this fragment is a complete message, all by itself ๐1: this fragment is the first of a multi-fragment message ๐ธ2: this fragment is the last of a multi-fragment message ๐3: this fragment is somewhere in the middle of a multi-fragment message ๐ซถ
a single-fragment message is "0-whole". when a message has more than one fragment, the first one is "1-begin", then zero or more "3-middle" fragments, and finally a "2-end" fragment โจ and the sequence numbers of a multi-fragment message MUST be contiguous, no gaps allowed, periodt ๐
abandon: if set, the sender has given up on this sequence number ๐ โโ๏ธ the userData, if any, MUST be ignored, so don't even look at it.
final: if set, this is the last sequence number of the flow ๐ณ๏ธ
flowID: a VLU, the flow identifier ๐ฆ
sequenceNumber: a VLU, the sequence number of this fragment. fragments get contiguous, increasing sequence numbers within a flow ๐ the first sequence number of a flow SHOULD be 1, and it MUST be greater than zero. sequence numbers are unbounded and they never wrap around, which is honestly so iconic ๐
fsnOffset: a VLU, the difference between the sequence number and the Forward Sequence Number. this field MUST NOT be zero if the abandon flag is not set, and it MUST NOT be greater than sequenceNumber ๐งฎ
forwardSequenceNumber: the flow sender will not send (or resend) any fragment with a sequence number less than or equal to the Forward Sequence Number. so once it's moved forward, it's moved forward ๐ซ
options: if the optionsPresent flag is set, there's a list of zero or more Options, ended by a Marker โจ see Section 2.3.11.1 for the defined options.
userData: the actual user data for this fragment ๐
The use of User Data is detailed in Section 3.6.2 ๐
2.3.11.1. Options for User Data ๐ง
this section lists the options that can show up in User Data option lists. a conforming implementation MUST support the options in this section, no excuses ๐ซถ
a flow receiver MUST reject a flow that contains a flow option it doesn't understand if the option type is less than 8192 (0x2000) ๐ซ but if the option type is 8192 or greater, a flow receiver MUST ignore any flow option it doesn't understand. the vibe here is "reject the serious stuff, shrug off the extras" ๐
the following option type codes are defined for User Data:
0x00: User's Per-Flow Metadata (Section 2.3.11.1.1) ๐ท๏ธ0x0a: Return Flow Association (Section 2.3.11.1.2) ๐
2.3.11.1.1. User's Per-Flow Metadata ๐ท๏ธ
this option carries the user's per-flow metadata for the flow it's attached to ๐
+-------------/-+-------------/-+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| length \ | 0x00 \ | userMetadata |
+-------------/-+-------------/-+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
struct userMetadataOptionValue_t
{
uint8_t userMetadata[remainder()];
} :remainder()*8;
the user attaches application-defined metadata to each flow, and it does not change over the life of the flow ๐ every flow MUST have metadata. a flow sender MUST send this option with the first User Data chunk for this flow in each packet until an acknowledgement for this flow is received. a flow sender SHOULD NOT send this option more than once for each flow in any one packet, and it SHOULD NOT send this option for a flow once that flow has been acknowledged ๐
this specification doesn't mandate the encoding, syntax, or interpretation of the user's per-flow metadata. that's up to the application ๐ง
the userMetadata SHOULD NOT exceed 512 bytes. the userMetadata MAY be 0 bytes in length.
2.3.11.1.2. Return Flow Association ๐
a new flow can be considered to be in return (or response) to a flow sent by the other endpoint ๐ซ this option encodes the receive flow identifier that this new sending flow is responding to.
+-------------/-+-------------/-+-------------/-+
| length \ | 0x0a \ | flowID \ |
+-------------/-+-------------/-+-------------/-+
struct returnFlowAssociationOptionValue_t
{
vlu_t flowID :variable*8;
} :variable*8;
okay picture two endpoints, A and B ๐ฏ A starts a flow with identifier 5 to B. so A is the flow sender for A's flowID=5, and B is the flow receiver for it. then B starts a return flow with identifier 7 to A, in response to A's flowID=5. now B is the flow sender for B's flowID=7, and A is the flow receiver for that one. B sends this option with flowID set to 5, which says "hey, my flowID=7 is responding to and associated with your flowID=5" ๐
if there is a return association, the flow sender MUST send this option with the first User Data chunk for this flow in each packet until an acknowledgement for this flow is received. a flow sender SHOULD NOT send this option more than once for each flow in any one packet, and it SHOULD NOT send this option for a flow once that flow has been acknowledged โจ
a flow MUST NOT indicate more than one return association. one is the limit, bestie ๐ซ
a flow MUST indicate its return association, if it has one, upon its first transmission of a User Data chunk. and a return association can't be added to a sending flow after it begins ๐ โโ๏ธ
a flow receiver MUST reject a new receiving flow that has a return flow association which doesn't indicate an F_OPEN sending flow ๐ช
2.3.12. Next User Data Chunk ๐
this chunk works the same as the User Data chunk for sending a flow's user messages ๐ when it's used, it MUST follow a User Data chunk or another Next User Data chunk in the same packet, so it's always riding in the slipstream of its bestie ๐ผ
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x11 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-+-+-+-+-+-+-+-+
|O|r| F | r |A|F|
|P|s| R | s |B|I|
|T|v| A | v |N|N|
+-+-+-+-+-+-+-+-+
+~~~/~~~/~~~~~~~+ +~~~/~~~/~~~~~~~+-------------/-+
| L \ T \ V |... options ...| L \ T \ V | 0 \ |
\~~~/~~~/~~~~~~~+ [if(OPT)] +~~~/~~~/~~~~~~~+-------------/-/
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| userData |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
struct nextUserDataChunkPayload_t
{
bool_t optionsPresent :1; // "OPT"
uintn_t reserved1 :1; // "rsv"
uintn_t fragmentControl :2; // "FRA"
// 0=whole, 1=begin, 2=end, 3=middle
uintn_t reserved2 :2; // "rsv"
bool_t abandon :1; // "ABN"
bool_t final :1; // "FIN"
if(optionsPresent)
optionList_t options :variable*8;
uint8_t userData[remainder()];
} :chunkLength*8;
this chunk is treated as belonging to the same flowID as the most recently preceding User Data or Next User Data chunk in the same packet ๐งต it also has the same Forward Sequence Number and the next sequence number. the optionsPresent, fragmentControl, abandon, and final flags, and the options (if present), mean the same thing as they do for the User Data chunk ๐
...
----------+------------------------------------
10 00 07 | User Data chunk, length=7
00 | OPT=0, FRA=0 "whole", ABN=0, FIN=0
02 05 03 | flowID=2, seq#=5, fsn=(5-3)=2
00 01 02 | data 3 bytes: 00, 01, 02
----------+------------------------------------
11 00 04 | Next User Data chunk,length=4
00 | OPT=0, FRA=0 "whole", ABN=0, FIN=0
| flowID=2, seq#=6, fsn=2
03 04 05 | data 3 bytes: 03, 04, 05
----------+------------------------------------
11 00 04 | Next User Data chunk, length=4
00 | OPT=0, FRA=0 "whole", ABN=0, FIN=0
| flowID=2, seq#=7, fsn=2
06 07 08 | data 3 bytes: 06, 07, 08
----------+------------------------------------
Figure 3: Sequential Messages in One Packet Using Next User Data
the use of Next User Data is detailed in Section 3.6.2.3.2 ๐ง
2.3.13. Data Acknowledgement Bitmap Chunk (Bitmap Ack) ๐ฆ
okay so this chunk is how the flow receiver tells the flow sender exactly which User Data fragment sequence numbers it has gotten for one flow ๐ฌ it's only valid in a packet that belongs to an established session and has packet mode 1 or 2 ๐
the flow receiver can pick between this chunk and a Range Ack to do the acknowledging. it SHOULD go with whichever format has the most compact encoding of the sequence numbers it received, so pick your fighter wisely ๐ง
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x50 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-------------/-+-------------/-+-------------/-+
| flowID \ | bufAvail \ | cumAck \ |
+-------------/-+-------------/-+-------------/-+
+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+
|C|C|C|C|C|C|C|C|C|C|C|C|C|C|C|C|C|C|C|C|C|C|C|C|
|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|
|9|8|7|6|5|4|3|2|1|1|1|1|1|1|1|1|2|2|2|2|2|2|1|1| ....
| | | | | | | | |7|6|5|4|3|2|1|0|5|4|3|2|1|0|9|8|
+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+
struct dataAckBitmapChunkPayload_t
{
vlu_t flowID :variable*8;
vlu_t bufferBlocksAvailable :variable*8; // "bufAvail"
vlu_t cumulativeAck :variable*8; // "cumAck"
bufferBytesAvailable = bufferBlocksAvailable * 1024;
acknowledge(0 through cumulativeAck);
ackCursor = cumulativeAck + 1;
while(remainder() > 0)
{
for(bitPosition = 8; bitPosition > 0; bitPosition--)
{
bool_t bit :1;
if(bit)
acknowledge(ackCursor + bitPosition);
}
ackCursor += 8;
}
} :chunkLength*8;
okay the fields, bestie ๐
- flowID: a VLU, the flow identifier ๐
- bufferBlocksAvailable: a VLU, the number of 1024-byte blocks of User Data the receiver can accept right now. Section 3.6.3.5 describes how to calculate this value. ๐งฎ
- cumulativeAck: a VLU, which acknowledges every fragment sequence number in this flow that is less than or equal to this value. this MUST NOT be less than the highest Forward Sequence Number received in this flow ๐ซ that one is non-negotiable, periodt
- bit field: zero or more bytes holding a bit field of the received fragment sequence numbers that come after the cumulative acknowledgement, least significant bit first ๐ฅ a set bit means that sequence number was received, and a clear bit means it wasn't. the least significant bit of the first byte is the second sequence number after the cumulative acknowledgement, the next bit is the third one after it, and so on ๐
Figure 4 shows an example Bitmap Ack acknowledging fragment sequence numbers 0 through 16, 18, 21 through 24, 27, and 28. it's giving tidy little puzzle energy ๐งฉ
50 00 05 | Bitmap Ack, length=5 bytes
05 7f 10 | flowID=5, bufAvail=127*1024 bytes, cumAck=0..16
79 06 | 01111001 00000110 = 18, 21, 22, 23, 24, 27, 28
Figure 4: Example Bitmap Ack
2.3.14. Data Acknowledgement Ranges Chunk (Range Ack) ๐ธ
this is the other way the flow receiver can say "hey, I got these" ๐ it's also only allowed in a packet belonging to an established session with packet mode 1 or 2, same as its bitmap cousin โจ
the flow receiver can acknowledge User Data with this chunk or with a Bitmap Ack, its choice. it SHOULD pick whichever format has the most compact encoding of the sequence numbers received ๐ซถ
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x51 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-------------/-+-------------/-+-------------/-+
| flowID \ | bufAvail \ | cumAck \ |
+-------------/-+-------------/-+-------------/-+
+~~~~~~~~~~~~~/~+~~~~~~~~~~~~~/~+
| #holes-1 \ | #recv-1 \ |
+~~~~~~~~~~~~~/~+~~~~~~~~~~~~~/~+
:
:
+~~~~~~~~~~~~~/~+~~~~~~~~~~~~~/~+
| #holes-1 \ | #recv-1 \ |
+~~~~~~~~~~~~~/~+~~~~~~~~~~~~~/~+
struct dataAckRangesChunkPayload_t
{
vlu_t flowID :variable*8;
vlu_t bufferBlocksAvailable :variable*8; // "bufAvail"
vlu_t cumulativeAck :variable*8; // "cumAck"
bufferBytesAvailable = bufferBlocksAvailable * 1024;
acknowledge(0 through cumulativeAck);
ackCursor = cumulativeAck;
while(remainder() > 0)
{
vlu_t holesMinusOne :variable*8; // "#holes-1"
vlu_t receivedMinusOne :variable*8; // "#recv-1"
ackCursor++;
rangeFrom = ackCursor + holesMinusOne + 1;
rangeTo = rangeFrom + receivedMinusOne;
acknowledge(rangeFrom through rangeTo);
ackCursor = rangeTo;
}
} :chunkLength*8;
the fields, the vibe is the same as before ๐
- flowID: a VLU, the flow identifier ๐
- bufferBlocksAvailable: a VLU, the number of 1024-byte blocks of User Data the receiver can currently accept. Section 3.6.3.5 describes how to calculate this value. ๐
- cumulativeAck: a VLU, which acknowledges every fragment sequence number in this flow that is less than or equal to this value. this MUST NOT be less than the highest Forward Sequence Number received in this flow ๐ โโ๏ธ
- holesMinusOne / receivedMinusOne: zero or more acknowledgement ranges, run-length encoded ๐ข they come in pairs of VLUs. the first one is the number of missing sequence numbers (minus one), and the second is the number of received sequence numbers (minus one), starting from the cumulative acknowledgement ๐
NOTE: If a parser syntax error is encountered here (that is, if the chunk is truncated such that not enough bytes remain to completely encode both VLUs of the acknowledgement range), then treat and process this chunk as though it was properly formed up to the last completely encoded range. ๐งฏ that's the safety net for a chunk that gets cut off mid-range, no cap
Figure 5 shows an example Range Ack acknowledging fragment sequence numbers 0 through 16, 18, 21, 22, 23, and 24 โจ
51 00 07 | Range Ack, length=7
05 7f 10 | flowID=5, bufAvail=127*1024 bytes, cumAck=0..16
00 00 | holes=1, received=1 -- missing 17, received 18
01 03 | holes=2, received=4 -- missing 19..20, received 21..24
Figure 5: Example Range Ack
Figure 6 shows an example Range Ack acknowledging fragment sequence numbers 0 through 16 and 18, with a truncated last range. note that the truncation and parse error does NOT abort the entire chunk in this case ๐ only that one range gets ignored, the rest still counts ๐ซถ
51 00 07 | Range Ack, length=9
05 7f 10 | flowID=5, bufAvail=127*1024 bytes, cumAck=0..16
00 00 | holes=1, received=1 -- missing 17, received 18
01 83 | holes=2, received=VLU parse error, ignore this range
Figure 6: Example Truncated Range Ack
2.3.15. Buffer Probe Chunk ๐
this chunk is the flow sender asking "yo, how much room do you have?" ๐โโ๏ธ it requests the current available receive buffer, in the form of a Data Acknowledgement, for a flow. it's only allowed in a packet belonging to an established session with packet mode 1 or 2 ๐
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x18 | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-------------/-+
| flowID \ |
+-------------/-+
struct bufferProbeChunkPayload_t
{
vlu_t flowID :variable*8;
} :chunkLength*8;
flowID: a VLU, the flow identifier ๐
the receiver of this chunk SHOULD reply as immediately as is practical with a Data Acknowledgement โก quick replies only, bestie ๐
2.3.16. Flow Exception Report Chunk ๐จ
this chunk is the flow receiver saying "I'm not interested in this flow anymore (or I'm not anymore ๐), please close it." ๐ โโ๏ธ it SHOULD precede every Data Acknowledgement chunk for the same flow in this condition. it's only allowed in a packet belonging to an established session with packet mode 1 or 2 ๐
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x5e | chunkLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-------------/-+-------------/-+
| flowID \ | exception \ |
+-------------/-+-------------/-+
struct flowExceptionReportChunkPayload_t
{
vlu_t flowID :variable*8;
vlu_t exception :variable*8;
} :chunkLength*8;
- flowID: a VLU, the flow identifier ๐
- exception: a VLU, the application-defined exception code being reported ๐ท๏ธ
a receiving RTMFP might reject a flow automatically, for example if it's missing metadata, or if an invalid return association is specified ๐ when an RTMFP rejects a flow automatically, the exception code MUST be 0 ๐ the application can specify any exception code when rejecting a flow, including 0 ๐ all non-zero exception codes are reserved for the application ๐
2.3.17. Session Close Request Chunk (Close) ๐
this is the "bye, it's been fun" chunk ๐ it's sent to cleanly terminate a session. it's only allowed in a packet belonging to an established or closing session and having packet mode 1 or 2 ๐ง
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x0c | 0 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
this chunk has no payload, so it's very minimal, very iconic ๐ซง
the use of Close is detailed in Section 3.5.5 ๐
2.3.18. Session Close Acknowledgement Chunk (Close Ack) ๐ฆ
and this is the reply to that goodbye ๐ฅน it's sent in response to a Session Close Request to indicate that the sender has terminated the session. it's only allowed in a packet belonging to an established or closing session and having packet mode 1 or 2 ๐
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x4c | 0 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
this chunk has no payload either ๐
the use of Close Ack is detailed in Section 3.5.5 โจ
3. Operation โจ
3.1. Overview โจ
okay so picture this bestie ๐ฆ one endpoint, a second endpoint, and a whole SESSION running between them like a glam runway ๐
+--------+ +--------+
| Peer A | S E S S I O N | Peer B |
| /=============================\ |
| || Flows || |
| ||---------------------------->|| |
| ||---------------------------->|| |
| ||<----------------------------|| |
| ||<----------------------------|| |
| ||<----------------------------|| |
| \=============================/ |
| | | |
| | +--------+
| |
| | +--------+
| | S E S S I O N | Peer C |
| /=============================\ |
| || Flows || |
| ||---------------------------->|| |
| ||<----------------------------|| |
| ||<----------------------------|| |
| \=============================/ |
| | | |
+--------+ +--------+
Figure 7: Sessions between Pairs of Communicating Endpoints
Between any pair of communicating endpoints there is ONE single session ๐ซถ and it's bidirectional, secured, and congestion controlled, so it's giving organized and safe ๐. Unidirectional flows carry messages from one end to the other inside that session ๐
An endpoint starts a session whenever it wants to talk to a far end ๐ฌ The initiator begins with one or more candidate destination socket addresses, and it can learn about and try more candidate addresses during startup handshaking ๐ Eventually a first suitable response comes back, that endpoint gets selected ๐ฏ, and startup carries on toward the selected endpoint. If both sides start at the same time (startup glare, lol ๐), one endpoint is the prevailing initiator and the other takes on the responder role ๐ Then the endpoints negotiate encryption keys and session identifiers, and the session is established ๐
Each endpoint can start sending message flows to the other end โจ For each flow, the far end can accept it and deliver its messages to the user, or it can reject the flow and send an exception back to the sender ๐ The flow receiver can also close and reject a flow later on, after it has already accepted it. No matter whether a flow was accepted, the flow receiver acknowledges ALL data sent to it ๐ Those acknowledgements drive a congestion control mechanism, which is very iconic behaviour ๐ฅ
An endpoint can have concurrent sessions with other far endpoints ๐ง The sessions are told apart by a session identifier, NOT by socket address. That means an endpoint's address can change mid-session without tearing down and re-establishing the session ๐ฆ The existing cryptographic state for a session can be used to verify an address change, while protecting against session hijacking or denial of service ๐ก๏ธ literally so protective
A sender can tell a receiver that some user messages are time critical or real-time ๐ A receiver can tell senders on concurrent sessions that it is receiving time critical messages from another endpoint. The other senders SHOULD modify their congestion control parameters so they yield capacity to the session carrying the time critical messages ๐ธ
A sender can close a flow โ The flow is completed when the receiver has no outstanding gaps before the final fragment of the flow. The sender and receiver keep a completed flow's identifier reserved for a while, so in-flight messages can drain from the network ๐
Eventually neither end has any flows open to the other ๐ The session becomes idle and quiescent. Either end can reliably close the session to recover its resources ๐ซง
Sometimes an endpoint is ceasing operation and doesn't have time to wait for the acknowledgement of a reliable session close ๐ญ In that case the halting endpoint can send an abrupt session close to tell the far end it is halting immediately ๐จ
3.2. Endpoint Identity ๐ชช
Each RTMFP endpoint has an identity, and that identity is encoded in a certificate ๐ This specification doesn't mandate any particular certificate format, cryptographic algorithms, or cryptographic properties for certificates. Your Cryptography Profile and app can decide that part ๐
An endpoint is named by an Endpoint Discriminator ๐ท๏ธ This specification doesn't mandate any particular format for Endpoint Discriminators.
An Endpoint Discriminator MAY select more than one identity and MAY match more than one distinct certificate ๐ฏ
Multiple distinct Endpoint Discriminators MAY match one certificate ๐ซ
It is RECOMMENDED that multiple endpoints do not have the same identity. Entities with the same identity are indistinguishable during session startup, and that could be undesirable in some applications ๐
An endpoint MAY have more than one address ๐
The Cryptography Profile has the following functions for identities, certificates, and Endpoint Discriminators, and their operation MUST be deterministic ๐ง
- Test whether a given certificate is authentic. Authenticity can include verifying an issuer signature chain in a public key infrastructure ๐
- Test whether a given Endpoint Discriminator selects a given certificate ๐ฏ
- Test whether a given Endpoint Discriminator selects the local endpoint ๐ช
- Generate a Canonical Endpoint Discriminator for a given certificate. Canonical Endpoint Discriminators for distinct identities SHOULD be distinct. If two distinct identities have the same Canonical Endpoint Discriminator, an initiator might abort a new opening session to the second identity (Section 3.5.1.1.1), and that behavior might not be desirable ๐
- Given a certificate, a message, and a digital signature over the message, test whether the signature is valid and was generated by the owner of the certificate โ๏ธ
- Generate a digital signature for a given message corresponding to the near identity ๐๏ธ
- Given the near identity and a far certificate, determine which one shall prevail as Initiator and which shall assume the Responder role in the case of startup glare. The far end MUST arrive at the same conclusion. A comparison function can do a lexicographic ordering of the binary certificates: if the far certificate is ordered before the near certificate, the far identity is declared the prevailing endpoint, and otherwise the near identity is declared the prevailing endpoint ๐ฅ
- Given a first certificate and a second certificate, test whether a new incoming session from the second shall override an existing session with the first. It is RECOMMENDED that the test check whether the certificates are bitwise identical ๐
All other semantics for certificates and Endpoint Discriminators are determined by the Cryptography Profile and the application ๐
3.3. Packet Multiplex ๐ฆ
An RTMFP typically has one or more interfaces it uses to talk to other RTMFP endpoints ๐ RTMFP can talk to multiple distinct other RTMFP endpoints through each local interface. Session multiplexing over a shared interface can make peer-to-peer communication through a NAT easier ๐ It does this by letting third-party endpoints such as Forwarders (Section 3.5.1.5) and Redirectors (Section 3.5.1.4) see the translated public address and tell peers about the translation ๐
An interface is typically a UDP socket (Section 2.2.1), but it MAY be any suitable datagram transport service where endpoints can be addressed by IPv4 or IPv6 socket addresses ๐ง
RTMFP uses a session ID to multiplex and demultiplex communications with distinct endpoints (Section 2.2.2), in addition to the endpoint socket address ๐ชช This lets an RTMFP detect a far-end address change (as might happen, for example, in mobile and wireless scenarios), and it lets communication sessions survive address changes ๐ฑ It also lets an RTMFP act as a Forwarder or Redirector for an endpoint it has an active session with, because it can tell startup packets apart from the packets of the active session ๐งโโ๏ธ
When a packet arrives, an RTMFP decodes the session ID to look up the matching session information context and decryption key ๐ Session ID 0 is reserved for session startup, and it MUST NOT be used for an active session. A packet for Session ID 0 uses the Default Session Key defined by the Cryptography Profile ๐
3.4. Packet Fragmentation ๐งฉ
okay so sometimes an RTMFP packet (Section 2.2.4) is just unavoidably too chunky for the path MTU ๐ญ. Think a startup packet holding an RHello (Section 2.3.4) or IIKeying (Section 2.3.7) chunk with a big certificate. When that happens, it can get split into segments that fit under the path MTU using the Packet Fragment chunk (Section 2.3.1). โจ
Fragmentation is a special-occasion move ๐ . It SHOULD be used only to segment packets that are unavoidably large, so it SHOULD be employed only during session startup with Session ID 0. It MUST NOT be used instead of the natural fragmentation of the User Data (Section 2.3.11) and Next User Data (Section 2.3.12) chunks, which split the messages of the user's data flows into segments under the path MTU. Don't be that person ๐ โโ๏ธ
Putting it back together is lowkey simple ๐. A fragmented plain RTMFP packet gets reassembled by concatenating the packetFragment fields of its fragments in contiguous ascending order, starting from index 0 and going through the final fragment.
When you're reassembling packets for Session ID 0, you SHOULD identify them by the socket address the packet containing the fragment came from, plus the indicated packetID ๐ฆ.
A receiver SHOULD give a fragmented packet up to 60 seconds to fully arrive, as long as progress is being made. A packet counts as progressing if at least one new fragment for it arrived in the last second โฑ๏ธ.
A receiver MUST discard a Packet Fragment chunk that has an empty packetFragment field. No empty fragments allowed, bestie ๐
Modes have to match too ๐ธ. The mode of each packet containing Packet Fragments for the same fragmented packet MUST match the mode of the fragmented packet. A receiver MUST discard any new Packet Fragment chunk that arrives in a packet with a mode different from the packet that carried the first received fragment. A receiver MUST also discard any reassembled packet whose mode is different from the packets that contained its fragments. Mode drama, not it ๐
Now the sender has to be chill about it ๐ซถ. In order to avoid jamming the network, the sender MUST rate limit packet transmission. If there's no specific path capacity information (like during session startup), a sender SHOULD NOT send more than 4380 bytes, nor more than four packets per distinct endpoint, every 200 ms.
And receivers need boundaries too ๐. To avoid resource exhaustion, a receiver SHOULD limit the number of concurrent packet reassembly buffers and the size of each buffer. The limits can depend on things like the expected size of reassembled packets, how often fragmented packets are expected to show up, how much interleaving is expected, and what the receiver is expected to do. They can also depend on the receiver's available resources. There can be different limits for packets with Session ID 0 and packets for established sessions. For example, a busy server might need several hundred concurrent reassembly buffers to handle hundreds of connection requests per second with possibly interleaved fragments, while a client device with constrained resources could get away with just a few ๐ฅ๏ธ๐ฑ. If you don't know the expected size of reassembled packets, a receiver should set the limit for each packet reassembly buffer to 65536 bytes.
3.5. Sessions ๐
okay, a session is the protocol relationship between a pair of communicating endpoints ๐ค. It includes the shared and endpoint-specific information context needed to get the communication done. The session context at each end includes at least:
TS_RX: the last timestamp received from the far end;TS_RX_TIME: the time at whichTS_RXwas first observed to be different than its previous value;TS_ECHO_TX: the last timestamp echo sent to the far end;MRTO: the measured retransmission timeout;ERTO: the effective retransmission timeout;- Cryptographic keys for encrypting and decrypting packets, and for verifying the validity of packets, according to the Cryptography Profile;
- Cryptographic near and far nonces according to the Cryptography Profile, where the near nonce is the far end's far nonce, and vice versa;
- The certificate of the far end;
- The receive session identifier, used by the far end when sending packets to this end;
- The send session identifier to use when sending packets to the far end;
DESTADDR: the destination socket address to use when sending packets to the far end;- The set of all sending flow contexts (Section 3.6.2);
- The set of all receiving flow contexts (Section 3.6.3);
- The transmission budget, which controls the rate at which data is sent into the network (for example, a congestion window);
S_OUTSTANDING_BYTES: the total amount of user message data outstanding, or in flight, in the network. That's the sum of theF_OUTSTANDING_BYTESof each sending flow in the session ๐;RX_DATA_PACKETS: a count of the number of received packets containing at least one User Data chunk since the last acknowledgement was sent, initially0;ACK_NOW: a boolean flag indicating whether an acknowledgement should be sent immediately, initially false;DELACK_ALARM: an alarm to trigger an acknowledgement after a delay, initially unset;- The state, which at any time is one of the following values: the opening states
S_IHELLO_SENTandS_KEYING_SENT, the open stateS_OPEN, the closing statesS_NEARCLOSEandS_FARCLOSE_LINGER, and the closed statesS_CLOSEDandS_OPEN_FAILED; and - The role, either Initiator or Responder, of this end of the session ๐ญ
Note: The following diagram is only a summary of state transitions and their causing events, and is not a complete operational specification. (It's a vibe check, not the whole rulebook ๐)
rcv IIKeying Glare
far prevails +-------------+ ultimate open timeout
+--------------|S_IHELLO_SENT|-------------+
| +-------------+ |
| |rcv RHello |
| | v
| v +-------------+
|<-----------(duplicate session?) |S_OPEN_FAILED|
| yes |no +-------------+
| | ^
| rcv IIKeying Glare v |
| far prevails +-------------+ |
|<-------------|S_KEYING_SENT|-------------+
| +-------------+ ultimate open timeout
| |rcv RIKeying
| |
| rcv v
| +-+ IIKeying +--------+ rcv Close Request
| |X|---------->| S_OPEN |--------------------+
| +-+ +--------+ |
| | |ABRUPT CLOSE |
| ORDERLY CLOSE| |or rcv Close Ack |
| | |or rcv IIKeying |
| | | session override |
| | +-------+ |
| v | v
| +-----------+ | +-----------------+
| |S_NEARCLOSE| | |S_FARCLOSE_LINGER|
| +-----------+ | +-----------------+
| rcv Close Ack| | |rcv Close Ack
| or 90 seconds| v |or 19 seconds
| | +--------+ |
| +------>|S_CLOSED|<---------+
+-------------------------->| |
+--------+
Figure 8: Session State Diagram ๐บ๏ธ
3.5.1. Startup ๐
3.5.1.1. Normal Handshake ๐ค
okay, RTMFP sessions get set up with a 4-way handshake across two round trips โจ. The initiator starts by sending an IHello to one or more candidate addresses for the desired destination endpoint. A responder statelessly sends back an RHello. The first correct RHello the initiator receives is the one that gets selected, and all the others get ignored ๐ โโ๏ธ. The initiator then computes its half of the session keying and sends an IIKeying. The responder receives the IIKeying and, if it's acceptable, computes its own half of the session keying. At that point it can also compute the shared session keying and session nonces. The responder creates a new S_OPEN session with the initiator and sends an RIKeying. The initiator receives the RIKeying and, if it's acceptable, computes the shared session keying and session nonces. And just like that, the initiator's session is S_OPEN ๐
. Initiator Responder .
| IHello |
|(EPD,Tag) |
S_IHELLO_SENT |(SID=0) |
|------------------------------->|
| |
| RHello |
| (Tag,Cookie,RCert)|
| (SID=0)|
|<-------------------------------|
S_KEYING_SENT | |
| IIKeying |
|(ISID,Cookie,ICert,SKIC,ISig) |
|(SID=0) |
|------------------------------->|
| |
| RIKeying |
| (RSID,SKRC,RSig)|
| (SID=ISID,Key=Default)| S_OPEN
|<-------------------------------|
S_OPEN | |
| S E S S I O N |
|<-------------------(SID=ISID)--|
|--(SID=RSID)------------------->|
Figure 9: Normal Handshake
In the following sections, we'll break the handshake down from the perspectives of the initiator and the responder ๐ฆ
3.5.1.1.1. Initiator ๐ฆ
Okay bestie, the initiator is the one who kicks things off ๐
. First it realizes a session is needed for an Endpoint Discriminator. Then it creates fresh state for a brand new opening session and starts with a candidate endpoint address set holding at least one address ๐. The new session goes into the S_IHELLO_SENT state โจ.
If the session doesn't reach S_OPEN before the ultimate open timeout, it's a fail ๐ and it moves to S_OPEN_FAILED. The RECOMMENDED ultimate open timeout is 95 seconds โฐ.
Next, the initiator picks a new, unique tag that no currently opening session is using ๐ง. It is RECOMMENDED that the tag be cryptographically pseudorandom and at least 8 bytes long, so it's hard to guess ๐. Then it builds an IHello chunk (Section 2.3.2) with the Endpoint Discriminator and the tag ๐.
While the initiator is in the S_IHELLO_SENT state, it sends the IHello to each candidate endpoint address in the set, on a backoff schedule โณ. The backoff SHOULD NOT be less than multiplicative, with not less than 1.5 seconds added to the interval between each attempt. The backoff SHOULD be scheduled separately for each candidate address, since new candidates can be added over time ๐ธ.
If the initiator receives a Redirect chunk (Section 2.3.5) with a tag echo matching this session, AND this session is in the S_IHELLO_SENT state, then for each redirect destination indicated in the Redirect: if the candidate endpoint address set contains fewer than REDIRECT_THRESHOLD addresses, add the indicated redirect destination to the candidate endpoint address set. REDIRECT_THRESHOLD SHOULD NOT be more than 24 ๐งญ.
If the initiator receives an RHello chunk (Section 2.3.4) with a tag echo matching this session, AND this session is in the S_IHELLO_SENT state, AND the responder certificate matches the desired Endpoint Discriminator, AND the certificate is authentic according to the Cryptography Profile, then:
- If the Canonical Endpoint Discriminator for the responder certificate matches the Canonical Endpoint Discriminator of another existing session in the
S_KEYING_SENTorS_OPENstates, AND the certificate of the other opening session matches the desired Endpoint Discriminator, then this session is a duplicate and SHOULD be aborted in favor of the other existing session; otherwise, - Move to the
S_KEYING_SENTstate. SetDESTADDR, the far-end address for the session, to the address from which this RHello was received. The initiator chooses a new, unique receive session ID, not used by any other session, for the responder to use when sending packets to the initiator. It computes a Session Key Initiator Component appropriate to the responder's certificate according to the Cryptography Profile. Using this data and the cookie from the RHello, the initiator constructs and signs an IIKeying chunk (Section 2.3.7) ๐.
While the initiator is in the S_KEYING_SENT state, it sends the IIKeying to DESTADDR on a backoff schedule. The backoff SHOULD NOT be less than multiplicative, with not less than 1.5 seconds added to the interval between each attempt ๐.
If the initiator receives an RIKeying chunk (Section 2.3.8) in a packet with this session's receive session identifier, AND this session is in the S_KEYING_SENT state, AND the signature in the chunk is authentic according to the far end's certificate (from the RHello), AND the Session Key Responder Component successfully combines with the Session Key Initiator Component and the near and far certificates to form the shared session keys and nonces according to the Cryptography Profile, then the session has opened successfully ๐. The session moves to the S_OPEN state. The send session identifier is set from the RIKeying. Packet encryption, decryption, and verification now use the newly computed shared session keys, and the session nonces are available for application-layer cryptographic challenges ๐.
3.5.1.1.2. Responder ๐
On receipt of an IHello chunk (Section 2.3.2) with an Endpoint Discriminator that selects its identity, an endpoint SHOULD construct an RHello chunk (Section 2.3.4) and send it to the address from which the IHello was received ๐. To avoid a potential resource exhaustion denial of service, the endpoint SHOULD NOT create any persistent state associated with the IHello ๐ก๏ธ. The endpoint MUST generate the cookie for the RHello in such a way that it can be recognized as authentic and valid when echoed in an IIKeying. The endpoint SHOULD use the address from which the IHello was received as part of the cookie generation formula. Cookies SHOULD be valid only for a limited time; that lifetime SHOULD NOT be less than 95 seconds (the recommended ultimate session open timeout) โฑ๏ธ.
On receipt of an FIHello chunk (Section 2.3.3) from a Forwarder (Section 3.5.1.5) where the Endpoint Discriminator selects its identity, an endpoint SHOULD do one of the following:
- Compute, construct, and send an RHello as though the FIHello was an IHello received from the indicated reply address; or
- Construct and send an Implied Redirect (Section 2.3.5) to the FIHello's reply address; or
- Ignore this FIHello.
On receipt of an IIKeying chunk (Section 2.3.7), if the cookie is not authentic or if it has expired, ignore this IIKeying; otherwise,
On receipt of an IIKeying chunk, if the cookie appears authentic but does not match the address from which the IIKeying's packet was received, perform the special processing at Cookie Change (Section 3.5.1.2); otherwise,
On receipt of an IIKeying with an authentic and valid cookie, if the certificate is authentic according to the Cryptography Profile, AND the signature in the chunk is authentic according to the far end's certificate and the Cryptography Profile, AND the Session Key Initiator Component is acceptable, then:
- If the address from which this IIKeying was received corresponds to an opening session in the
S_IHELLO_SENTorS_KEYING_SENTstate, perform the special processing at Glare (Section 3.5.1.3); otherwise, - If the address from which this IIKeying was received corresponds to a session in the
S_OPENstate, then:- If the receiver was the Responder for the
S_OPENsession and the session identifier, certificate, and Session Key Initiator Component are identical to those of theS_OPENsession, this IIKeying is a retransmission, so resend theS_OPENsession's RIKeying using the Default Session Key as specified below; otherwise, - If the certificate from this IIKeying does not override the certificate of the
S_OPENsession, ignore this IIKeying; otherwise, - The certificate from this IIKeying overrides the certificate of the
S_OPENsession; this is a new opening session from the same identity, and the existingS_OPENsession is stale. Move the existingS_OPENsession toS_CLOSEDand abort all of its flows (signaling exceptions to the user), then continue processing this IIKeying.
- If the receiver was the Responder for the
Otherwise,
- Compute a Session Key Responder Component and choose a new, unique receive session ID not used by any other session for the initiator to use when sending packets to the responder. Using this data, construct and, with the Session Key Initiator Component, sign an RIKeying chunk (Section 2.3.8). Using the Session Key Initiator and Responder Components and the near and far certificates, the responder combines and computes the shared session keys and nonces according to the Cryptography Profile. The responder creates a new session in the
S_OPENstate, with the far-endpoint addressDESTADDRtaken from the source address of the packet containing the IIKeying and the send session identifier taken from the IIKeying. The responder sends the RIKeying to the initiator using the Default Session Key and the requested send session identifier. Packet encryption, decryption, and verification of all future packets for this session use the newly computed keys, and the session nonces are available for application-layer cryptographic challenges ๐.
3.5.1.2. Cookie Change
Okay so sometimes the responder makes an RHello cookie for an initiator's address that is not the address the initiator uses when it talks to the responder directly ๐. This can happen when the initiator has more than one local address and uses one of them to reach a Forwarder (Section 3.5.1.5), but a different one to reach the responder. Iconic multi-homing moment ๐ฆ
Here's the example, bestie ๐
Initiator Forwarder Responder
| IHello | |
|(Src=Ix) | |
|------------------------------->| |
| | FIHello |
| |(RA=Ix) |
| |-------------------------------->|
| |
| RHello |
| (Cookie:Ix)|
|<-----------------------------------------------------------------|
| |
| IIKeying |
|(Cookie:Ix,Src=Iy) |
|----------------------------------------------------------------->|
| |
| RHello Cookie Change |
| (Cookie:Ix,Cookie:Iy)|
|<-----------------------------------------------------------------|
| |
| IIKeying |
|(Cookie:Iy) |
|----------------------------------------------------------------->|
| |
| RIKeying |
|<-----------------------------------------------------------------|
| |
|<======================== S E S S I O N =========================>|
Figure 10: Handshake with Cookie Change ๐
So the initiator has two network interfaces ๐ธ. The first one is the preferred one, with address Ix = 192.0.2.100:50000, and the second one is Iy = 198.51.100.101:50001. The responder has one interface, Ry = 198.51.100.200:51000, and it's on the same network as the initiator's second interface. The initiator uses its first interface to reach a Forwarder. The Forwarder sees the initiator's address as Ix and sends a Forwarded IHello (Section 2.3.3) to the responder. The responder treats that like an IHello from Ix, works out a matching cookie, and sends an RHello to Ix.
The initiator gets this RHello coming from Ry and picks that address as the destination for the session ๐ง. Then it sends an IIKeying and copies the cookie from the RHello. BUT here's the plot twist ๐ญ: the RHello came from Ry, which is on a network the initiator is directly connected to, so the initiator sends the IIKeying out its second interface, Iy. When the responder gets that IIKeying, it checks the cookie against the value it expects for the packet's source address. The IIKeying's source doesn't match the IHello source the cookie was made from, so the responder rejects the IIKeying. Not it ๐
If the responder works out that it made the cookie in the IIKeying, but the cookie doesn't match the sender's address (for example, if the cookie has two parts, where the first part is made without the initiator's address and the second part depends on it), the responder SHOULD generate a new cookie based on the address the IIKeying came from. It then sends an RHello Cookie Change chunk (Section 2.3.6) to the source of the IIKeying, using the session ID from the IIKeying and the Default Session Key โจ.
If the initiator gets an RHello Cookie Change chunk for a session in the S_KEYING_SENT state, AND the old cookie matches the one it originally sent to the responder, then the initiator adopts the new cookie ๐ซถ. It builds and signs a new IIKeying chunk and sends that new IIKeying to the responder. The initiator SHOULD NOT change the cookie for a session more than once. Once is the limit, periodt ๐
3.5.1.3. Glare
Glare is when two endpoints try to start sessions with each other at the same time ๐. It's detected when an endpoint gets a valid and authentic IIKeying from an address that is the destination of a session it's currently opening. Only one session is allowed between a pair of endpoints, so we need a tiebreaker.
Glare gets resolved by comparing the certificate in the received IIKeying with the near end's certificate. The Cryptography Profile defines a certificate comparison function that decides which endpoint prevails when there's glare ๐.
If the near end prevails, it discards and ignores the received IIKeying. The far end will abort its opening session once it gets the IIKeying from the near end ๐ฅฒ
Otherwise, the far end prevails, and then:
- If the certificate in the IIKeying overrides the certificate associated with the near opening session according to the Cryptography Profile, then abort and destroy the near opening session. Then,
- Continue with normal Responder IIKeying processing (Section 3.5.1.1.2).
3.5.1.4. Redirector
+-----------+ +------------+ +-----------+
| Initiator |---------->| Redirector | | Responder |
| |<----------| | | |
| | +------------+ | |
| |<=================================>| |
+-----------+ +-----------+
Figure 11: Redirector ๐ง
Okay, the Redirector is basically like a name server for Endpoint Discriminators ๐. An initiator MAY use a Redirector to find extra candidate endpoint addresses for the endpoint it wants.
When a Redirector gets an IHello chunk with an Endpoint Discriminator that doesn't pick the Redirector's own identity, it builds a Responder Redirect chunk (Section 2.3.5) with one or more extra candidate addresses for that endpoint and sends it back to the initiator ๐ฆ
Initiator Redirector Responder
| IHello | |
|------------------------------->| |
| | |
| Redirect | |
|<-------------------------------| |
| |
| IHello |
|----------------------------------------------------------------->|
| |
| RHello |
|<-----------------------------------------------------------------|
| |
| IIKeying |
|----------------------------------------------------------------->|
| |
| RIKeying |
|<-----------------------------------------------------------------|
| |
|<======================== S E S S I O N =========================>|
Figure 12: Handshake Using a Redirector โจ
Deployment Design Note: Redirectors SHOULD NOT initiate new sessions to endpoints that might use the Redirector's address as a candidate for another endpoint, since the far end might read the Redirector's IIKeying as glare for the far end's own initiation to the other endpoint. Keep your Redirector out of the drama ๐๐
3.5.1.5. Forwarder ๐ โจ
okay bestie, picture this ๐ง: an Initiator on one side, a Responder on the other, and a Forwarder in the middle, with a NAT sitting between the Forwarder and the Responder ๐. Here's the diagram ๐
+-----------+ +-----------+ +---+ +-----------+
| Initiator |---->| Forwarder |<===>| N |<===>| Responder |
| | +-----------+ | A | | |
| |<=====================>| T |<===>| |
+-----------+ +---+ +-----------+
Figure 13: Forwarder
so the problem is ๐ญ: a responder might be stuck behind a NAT or firewall that won't let inbound packets reach the endpoint until it has first sent an outbound packet to a particular far-endpoint address. literally so frustrating ๐
the fix is giving it main character energy ๐: a Forwarder's endpoint address MAY be a candidate address for another endpoint. and a responder MAY use a Forwarder to receive FIHello chunks that get sent on behalf of an initiator. ๐ฆ
here's how the Forwarder handles things ๐. when it gets an IHello chunk whose Endpoint Discriminator does NOT select the Forwarder's own identity, it checks whether it has an S_OPEN session with an endpoint whose certificate matches the desired Endpoint Discriminator. if it does, it builds and sends an FIHello chunk (Section 2.3.3) to that selected endpoint over the S_OPEN session. it uses the tag and Endpoint Discriminator from the IHello, plus the source address of the packet that carried the IHello, for the matching fields of the FIHello. โจ
and when the responder gets that FIHello, it might send back an RHello or an Implied Redirect to the original source of the IHello (Section 3.5.1.1.2) ๐ธ. that could let future packets flow directly between the initiator and responder through the NAT or firewall, which is iconic ๐ฅ.
here are the two handshake flows ๐ง
Figure 14 shows the flow where the responder sends an RHello:
Initiator Forwarder NAT Responder
| IHello | | |
|------------------------------->| | |
| | FIHello | |
| |--------------->|--------------->|
| | |
| | RHello |
| :<---------------|
|<------------------------------------------------: |
| : |
| IIKeying : |
|-------------------------------------------------:--------------->|
| : |
| : RIKeying |
| :<---------------|
|<------------------------------------------------: |
| : |
|<======================== S E S S I O N ========>:<==============>|
Figure 14: Forwarder Handshake where Responder Sends an RHello
Figure 15 shows the flow where the responder sends an Implied Redirect instead:
Initiator Forwarder NAT Responder
| IHello | | |
|------------------------------->| | |
| | FIHello | |
| |--------------->|--------------->|
| | |
| | Redirect |
| | (Implied,RD={})|
| :<---------------|
|<------------------------------------------------: |
| : |
| IHello : |
|------------------------------------------------>:--------------->|
| : |
| : RHello |
| :<---------------|
|<------------------------------------------------: |
| : |
| IIKeying : |
|------------------------------------------------>:--------------->|
| : |
| : RIKeying |
| :<---------------|
|<------------------------------------------------: |
| : |
|<======================== S E S S I O N ========>:<==============>|
Figure 15: Forwarder Handshake where Responder Sends an
Implied Redirect
3.5.1.6. Redirector and Forwarder with NAT ๐ฆ๐
this one is giving full redirect-plus-forward combo ๐ซถ. here's the picture of an initiator and a responder that are each behind their own NAT, with an introduction service in the middle ๐
+---+ +---+ +---+ +---+ +---+
| I | | N | | I | | N | | R |
| n |------>| A |------>| n | | A | | e |
| i | | T | | t |<====>| T |<====>| s |
| t |<------| |<------| r | | | | p |
| i | | | | o | | | | o |
| a | | | +---+ | | | n |
| t | | | | | | d |
| o |<=====>| |<================>| |<====>| e |
| r | | | | | | r |
+---+ +---+ +---+ +---+
Figure 16: Introduction Service for Initiator and Responder
behind NATs
so the situation ๐ญ: an initiator and responder might each be behind separate NATs or firewalls that don't let inbound packets reach their endpoints until each one first sends an outbound packet to a particular far-endpoint address. ngl that's a lot of gatekeeping ๐
the solution is an introduction service that combines the Redirector and Forwarder functions ๐. it can help two endpoints that are each behind a NAT talk directly to each other ๐ง
here's how it goes, step by step ๐ :
- the responder registers with the introduction service through an S_OPEN session to it. the service observes and records the responder's public NAT address as the DESTADDR of that S_OPEN session. ๐
- the service MAY also record other addresses for the responder, for example addresses the responder self-reports as being directly attached. โจ
- the initiator starts with the address of the introduction service as its initial candidate. ๐ธ
- the Redirector part of the service sends the initiator a Responder Redirect that contains at least the responder's public NAT address, as previously recorded. ๐ฅ
- the Forwarder part of the service sends the responder a Forwarded IHello that contains the initiator's public NAT address, as observed to be the source of the IHello. ๐
- the responder sends an RHello to the initiator's public NAT address in response to the FIHello. this lets inbound packets to the responder through its NAT from the initiator's public NAT address. ๐ฆ
- the initiator sends an IHello to the responder's public NAT address in response to the Responder Redirect. this lets inbound packets to the initiator through its NAT from the responder's public NAT address. ๐ซถ
once there are transit paths created in both NATs, normal session startup can proceed ๐.
the whole sequence looks like this ๐ง
Initiator NAT-I Redirector+Forwarder NAT-R Responder
| | | | |
| IHello | | | |
|(Dst=Intro) | | | |
|-------------->| | | |
| |--------------->| | |
| | | FIHello | |
| | |(RA=NAT-I-Pub) | |
| | |--------------->|--------------->|
| | Redirect | | |
| | (RD={NAT-R-Pub,| | |
| | ...})| | |
|<--------------|<---------------| | |
| | | RHello |
| | | (Dst=NAT-I-Pub)|
| | :<---------------|
| | (*) <--------------------------: |
| IHello | : |
|(Dst=NAT-R-Pub)| : |
|-------------->: : |
| :-------------------------------->:--------------->|
| : : |
| : : RHello |
| : :<---------------|
|<--------------:<--------------------------------: |
| : : |
| IIKeying : : |
|-------------->: : |
| :-------------------------------->:--------------->|
| : : |
| : : RIKeying |
| : :<---------------|
|<--------------:<--------------------------------: |
| : : |
|<=============>:<======== S E S S I O N ========>:<==============>|
Figure 17: Handshake with Redirector and Forwarder
โ ๏ธ timing alert: at the point in Figure 17 marked (*), the responder's RHello from the FIHello might reach the initiator's NAT before or after the initiator's IHello is sent outbound to the responder's public NAT address. if it arrives before, the NAT may drop it ๐ญ. if it arrives after, it transits the NAT and triggers keying without waiting for another round-trip time ๐ฅ.
the timing of this race depends on things like the relative distances of the initiator and responder from each other and from the introduction service. it's a whole vibe, and you can't control it ๐โจ
3.5.1.7. Load Distribution and Fault Tolerance ๐ธ
Okay, this part is giving multi-server glow-up energy ๐ง. Picture it like this:
+---+ IHello/RHello +-------------+
| I |<------------------->| Responder 1 |
| n | +-------------+
| i | SESSION +-------------+
| t |<=========>| Responder 2 |
| i | +-------------+
| a | IHello... +----------------+
| t |-------------------------> X | Dead Responder |
| o | +----------------+
| r | IHello/RHello +-------------+
| |<---------------->| Responder N |
+---+ +-------------+
Figure 18: Parallel Open to Multiple Endpoints
Okay so, as we already covered in Section 3.2, more than one endpoint is allowed to be selected by one Endpoint Discriminator ๐ . This is literally going to be the normal case for a set of servers, any of which could take a client that shows up ๐ซถ.
As specified in Section 3.5.1.1.1, an initiator is allowed to use multiple candidate endpoint addresses when starting a session. The sender of the first acceptable RHello chunk to arrive is the one picked to complete the session, and later responses get ignored ๐. An initiator can start with several candidate endpoint addresses, or it can learn them during startup from one or more Redirectors (Section 3.5.1.4). Main character energy for the first RHello, honestly โจ.
Parallel open to multiple endpoints for the same Endpoint Discriminator, combined with selection by earliest RHello, can be used for load distribution and fault tolerance ๐ฆ. The cost at each endpoint that does not get selected is limited to receiving and processing an IHello, and generating and sending an RHello. So the losers aren't doing much extra work, which is lowkey iconic ๐.
In one situation, multiple servers with similar processing and networking capacity may be sitting close together, such as in a data center ๐ข. Here, a server that is less heavily loaded can respond to an IHello more quickly than the busier servers, so it will tend to be the one a client selects ๐.
In another situation, multiple servers may be located in different physical places, such as different data centers ๐. Here, a server that is nearer to the client (in terms of network distance) can respond earlier than the farther servers, so it will tend to be selected by the client ๐.
Multiple servers, whether they're right next to each other or spread far apart, can form a redundant pool of servers ๐ซง. A client can perform a parallel open to the multiple servers. In normal operation, all of them will respond, and the client will select one of them as described above. If one of the servers fails, the other servers in the pool can still respond to the client, which lets the client succeed in reaching an S_OPEN session with one of them. That's the redundancy being a whole slay ๐ ๐ฅ.
3.5.2. Congestion Control ๐ฆ
Okay, big one. An RTMFP MUST implement congestion control and avoidance algorithms that are "TCP compatible", in line with Internet best current practice [RFC2914]. The algorithms SHOULD NOT be more aggressive in sending data than the ones described in "TCP Congestion Control" [RFC5681], and they MUST NOT be more aggressive in sending data than the "slow start algorithm" described in Section 3.1 of RFC 5681 ๐ซถ. No bullying the network, bestie ๐.
An endpoint keeps a transmission budget in the session information context of each S_OPEN session (Section 3.5). This budget is what controls how fast the endpoint pushes data into the network ๐.
For window-based congestion control and avoidance algorithms, the transmission budget is the congestion window. That's the amount of user data that's allowed to be outstanding, or in flight, in the network. Transmission is allowed when S_OUTSTANDING_BYTES (Section 3.5) is less than the congestion window (Section 3.6.2.3). If you want an experimental window-based congestion control algorithm for real-time and bulk data, check out Appendix A ๐งช.
An endpoint avoids sending large bursts of data or packets into the network (Section 3.5.2.3). Burst avoidance is literally the whole vibe ๐.
A sending endpoint increases and decreases its transmission budget in response to acknowledgements (Section 3.6.2.4) and to loss, according to the congestion control and avoidance algorithms. Loss is detected by negative acknowledgement (Section 3.6.2.5) and by timeout (Section 3.6.2.6) ๐.
Timeout is determined by the Effective Retransmission Timeout (ERTO) (Section 3.5.2.2). The ERTO is measured using the Timestamp and Timestamp Echo packet header fields (Section 2.2.4) โฑ๏ธ.
A receiving endpoint acknowledges all received data (Section 3.6.3.4) so the sender can measure what has arrived, or what hasn't ๐ฌ.
A receiving endpoint may be getting time critical (or real-time) data from a first sender while also receiving data from other senders. The receiving endpoint can signal those other senders (Section 2.2.4) to make their congestion control and avoidance algorithms less aggressive, so they yield network capacity to the time critical data (Section 3.5.2.1). Polite senders are the cutest senders ๐ฅน๐.
3.5.2.1. Time Critical Reverse Notification ๐จ
A sender can increase its transmission budget at a rate that is compatible with (but does not exceed) the "slow start algorithm" specified in RFC 5681. That algorithm doubles the transmission rate every round trip when beginning or restarting transmission, until loss is detected ๐. However, a sender MUST behave as though the slow start threshold SSTHRESH is clamped to 0 (which disables the slow start algorithm's exponential increase behavior) on a session where a Time Critical Reverse Notification (Section 2.2.4) indication has been received from the far end within the last 800 milliseconds, unless the sender is itself currently sending time critical data to the far end. Basically, if someone's yelling "real-time!" at you, you chill ๐คซ.
During each round trip, a sender SHOULD NOT increase the transmission budget by more than 0.5% or by 384 bytes per round trip (whichever is greater), on a session where a Time Critical Reverse Notification indication has been received from the far end within the last 800 milliseconds, unless the sender is itself currently sending time critical data to the far end ๐ขโจ.
3.5.2.2. Retransmission Timeout โณ
RTMFP uses the ERTO to detect when a user data fragment has been lost in the network. The ERTO is typically calculated in a manner similar to the one specified in "Requirements for Internet Hosts - Communication Layers" [RFC1122]. It's a function of round-trip time measurements and persistent timeout behavior ๐ง .
The ERTO SHOULD be at least 250 milliseconds, and it SHOULD allow for the receiver to delay sending an acknowledgement for up to 200 milliseconds (Section 3.6.3.4.4). The ERTO MUST NOT be less than the round-trip time ๐ โโ๏ธ.
To make round-trip time measurement easier, an endpoint MUST implement the Timestamp Echo facility:
- On a session entering the S_OPEN state, initialize TS_RX_TIME to negative infinity, and initialize TS_RX and TS_ECHO_TX to have no value.
- On receipt of a packet in an S_OPEN session with the timestampPresent (Section 2.2.4) flag set, if the timestamp field in the packet is different than TS_RX, set TS_RX to the value of the timestamp field in the packet, and set TS_RX_TIME to the current time.
- When sending a packet to the far end in an S_OPEN session:
- Calculate
TS_RX_ELAPSED = current time - TS_RX_TIME. If TS_RX_ELAPSED is more than 128 seconds, then set TS_RX and TS_ECHO_TX to have no value, and do not include a timestamp echo; otherwise, - Calculate
TS_RX_ELAPSED_TICKSto be the number of whole 4-millisecond periods in TS_RX_ELAPSED; then - Calculate
TS_ECHO = (TS_RX + TS_RX_ELAPSED_TICKS) MODULO 65536; then - If TS_ECHO is not equal to TS_ECHO_TX, then set TS_ECHO_TX to TS_ECHO, set the timestampEchoPresent flag, and set the timestampEcho field to TS_ECHO_TX.
- Calculate
The rest of this section describes an OPTIONAL method for calculating the ERTO. Real-time applications and P2P mesh applications often need to know the round-trip time and RTT variance, so this part also describes a way to measure those, and to calculate a smoothed round-trip time ๐๐.
Let the session information context contain these additional variables:
- TS_TX: the last timestamp sent to the far end, initialized to have no value;
- TS_ECHO_RX: the last timestamp echo received from the far end, initialized to have no value;
- SRTT: the smoothed round-trip time, initialized to have no value;
- RTTVAR: the round-trip time variance, initialized to 0.
Initialize MRTO to 250 milliseconds.
Initialize ERTO to 3 seconds.
On sending a packet to the far end of an S_OPEN session, if the current send timestamp is not equal to TS_TX, then set TS_TX to the current send timestamp, set the timestampPresent flag in the packet header, and set the timestamp field to TS_TX ๐ฐ๏ธ.
On receipt of a packet from the far end of an S_OPEN session, if the timestampEchoPresent flag is set in the packet header, AND the timestampEcho field is not equal to TS_ECHO_RX, then:
- Set TS_ECHO_RX to timestampEcho;
- Calculate
RTT_TICKS = (current send timestamp - timestampEcho) MODULO 65536; - If RTT_TICKS is greater than 32767, the measurement is invalid, so discard this measurement; otherwise,
- Calculate
RTT = RTT_TICKS * 4 milliseconds; - If SRTT has a value, then calculate new values of RTTVAR and SRTT:
RTT_DELTA = | SRTT - RTT |;RTTVAR = ((3 * RTTVAR) + RTT_DELTA) / 4;SRTT = ((7 * SRTT) + RTT) / 8.
- If SRTT has no value, then set SRTT = RTT and RTTVAR = RTT / 2;
- Set
MRTO = SRTT + 4 * RTTVAR + 200 milliseconds; - Set ERTO to MRTO or 250 milliseconds, whichever is greater.
A retransmission timeout occurs when the most recently transmitted user data fragment has remained outstanding in the network for ERTO. When this timeout happens, increase ERTO on an exponential backoff, with an ultimate backoff cap of 10 seconds ๐ญ:
- Calculate
ERTO_BACKOFF = ERTO * 1.4142; - Calculate ERTO_CAPPED to be ERTO_BACKOFF or 10 seconds, whichever is less;
- Set ERTO to ERTO_CAPPED or MRTO, whichever is greater.
3.5.2.3. Burst Avoidance ๐จ
Okay so an app's sending patterns can make the transmission budget get super big ๐, but sometimes the app goes quiet and only a tiny bit of data is actually floating around in the network ๐ด. If the app then suddenly has a huge pile of new data to send (like a big bulk transfer ๐ฆ), especially with a window-based congestion avoidance algorithm, it could dump everything into the network all at once to fill the whole window ๐. That kind of burst is a big no-no tbh, because it can jam interfaces, links, and buffers ๐ง.
So, in any session, an endpoint SHOULD NOT send more than six packets containing user data between receiving any acknowledgements or retransmission timeouts ๐.
Here's an OPTIONAL method for avoiding those giant packet bursts ๐:
Let the session information context hold an extra variable called DATA_PACKET_COUNT, which starts at 0 ๐งฎ.
No sending a user data fragment on this session if DATA_PACKET_COUNT is greater than or equal to 6, even if the congestion control algorithm would otherwise say yes ๐
โโ๏ธ.
On transmitting a packet with at least one User Data chunk (Section 2.3.11), do DATA_PACKET_COUNT = DATA_PACKET_COUNT + 1 โ.
On receiving an acknowledgement chunk (Sections 2.3.13 and 2.3.14), set DATA_PACKET_COUNT to 0 ๐ซง.
On a retransmission timeout, set DATA_PACKET_COUNT to 0 โฐ.
3.5.3. Address Mobility ๐ฑ
Sessions are demultiplexed with a 32-bit session ID, not by endpoint address ๐. This means an endpoint's address can change during an S_OPEN session. That can happen when you switch from wireless to wired, when you move from one wireless base station to another, or when a NAT restarts ๐.
If the near end gets a valid packet for an S_OPEN session from a source address that doesn't match DESTADDR, the far end might have changed addresses ๐. The near end SHOULD verify that the far end is definitively at the new address before changing DESTADDR. A suggested way to do that verification is in Section 3.5.4.2 ๐
.
3.5.4. Ping ๐
If an endpoint receives a Ping chunk (Section 2.3.9) in a session in the S_OPEN state, it SHOULD construct and send a Ping Reply chunk (Section 2.3.10) in response if possible, copying the message unaltered ๐ช. The Ping Reply SHOULD be sent as quickly as possible after receiving a Ping โก. The meaning of a Ping's message is reserved for the sender, so a receiver SHOULD NOT interpret the Ping's message ๐.
Endpoints can use the Ping chunk and the expected Ping Reply for any purpose ๐ง. This specification doesn't set any specific constraints on the format or semantics of a Ping message. A Ping Reply MUST be sent only as a response to a Ping ๐.
Getting a Ping Reply implies live bidirectional connectivity ๐. This specification doesn't set any other semantics for a Ping Reply.
3.5.4.1. Keepalive ๐
An endpoint can use a Ping to check for live bidirectional connectivity, to check that the far end of a session is still in the S_OPEN state, to keep NAT translations alive, and to keep firewall holes open ๐ฅ.
An endpoint can also use a Ping to speed up detecting a near-end address change by the far end ๐โโ๏ธ.
An endpoint may declare a session defunct and dead after a persistent failure by the far end to return Ping Replies in response to Pings ๐.
When used for these purposes, a Keepalive Ping SHOULD have an empty message ๐ซ.
A Keepalive Ping SHOULD NOT be sent more often than once per ERTO โณ. If a corresponding Ping Reply isn't received within ERTO of sending the Ping, ERTO SHOULD be increased according to Section 3.5.2 ("Congestion Control") ๐.
3.5.4.2. Address Mobility ๐ฆ
This section describes an OPTIONAL but suggested method for processing and verifying a far-end address change โจ.
Let the session context contain additional variables MOB_TX_TS, MOB_RX_TS, and MOB_SECRET. MOB_TX_TS and MOB_RX_TS start at negative infinity. MOB_SECRET should be a cryptographically pseudorandom value not less than 128 bits in length and known only to this end ๐.
On receipt of a packet for an S_OPEN session, after processing all the chunks in the packet: if the session is still S_OPEN, AND the packet's source address doesn't match DESTADDR, AND MOB_TX_TS is at least one second in the past, then:
- Set
MOB_TX_TSto the current time; - Construct a Ping message with the following: a marking to show (to this end, when it comes back in a Ping Reply) that it's a mobility check (for example, the first byte being ASCII 'M' for "Mobility"), a timestamp set to
MOB_TX_TS, and a cryptographic hash over the following: the preceding items, the address the packet came from, andMOB_SECRET; and - Send this Ping to the address the packet came from, instead of
DESTADDR๐ฌ.
On receipt of a Ping Reply in an S_OPEN session, if the Ping Reply's message meets all of these conditions:
- it has this end's expected marking to show it's a mobility check, and
- the timestamp in the message is not more than 120 seconds in the past, and
- the timestamp in the message is greater than
MOB_RX_TS, and - the cryptographic hash matches the expected value, based on the contents of the message plus the source address of the packet containing this Ping Reply and
MOB_SECRET,
then:
- Set
MOB_RX_TSto the timestamp in the message; and - Set
DESTADDRto the source address of the packet containing this Ping Reply ๐.
3.5.4.3. Path MTU Discovery ๐
The "Packetization Layer Path MTU Discovery" spec [RFC4821] describes a method for measuring the path MTU between communicating endpoints ๐ง.
An RTMFP SHOULD perform path MTU discovery ๐.
The method described in RFC 4821 can be adapted for RTMFP by sending a probe packet with one of the Padding chunk types (type 0x00 or 0xff) and a Ping ๐. The Ping chunk SHOULD come after the Padding chunk, so a false positive response can be avoided if the probe packet gets truncated ๐ก๏ธ.
3.5.5. Close ๐
An endpoint may close a session at any time ๐ช. Usually, an endpoint closes a session when there haven't been any open flows in either direction for a while. Another situation is when an endpoint is shutting down and closes all of its sessions, even the ones with open flows ๐ญ.
To close an S_OPEN session in a reliable and orderly way, an endpoint moves the session to the S_NEARCLOSE state ๐งน.
When a session transitions from S_OPEN to S_NEARCLOSE, and every 5 seconds after that while it's still in S_NEARCLOSE, send a Session Close Request chunk (Section 2.3.17) ๐จ.
A session that has been in the S_NEARCLOSE state for at least 90 seconds (enough time to retransmit the Session Close Request multiple times) SHOULD move to the S_CLOSED state ๐.
When a session transitions from S_OPEN to S_NEARCLOSE, S_FARCLOSE_LINGER, or S_CLOSED, immediately abort and terminate all open or closing flows ๐ฅ. Flows only exist in S_OPEN sessions.
To close an S_OPEN session abruptly, send a Session Close Acknowledgement chunk (Section 2.3.18), then move to the S_CLOSED state ๐ค.
On receipt of a Session Close Request chunk for a session in the S_OPEN, S_NEARCLOSE, or S_FARCLOSE_LINGER states, send a Session Close Acknowledgement chunk. Then, if the session is in the S_OPEN state, move it to the S_FARCLOSE_LINGER state ๐ซถ.
A session that has been in the S_FARCLOSE_LINGER state for at least 19 seconds (enough time to answer 3 retransmissions of a Session Close Request) SHOULD move to the S_CLOSED state ๐.
On receipt of a Session Close Acknowledgement chunk for a session in the S_OPEN, S_NEARCLOSE, or S_FARCLOSE_LINGER states, move to the S_CLOSED state ๐.
3.6. Flows ๐
A flow is a unidirectional communication channel in a session that carries a correlated series of user messages from a sender to a receiver ๐ฌ. Each end of a session may have zero or more sending flows to the other end. Each sending flow at one end has a matching receiving flow at the other end ๐ช.
3.6.1. Overview ๐บ๏ธ
3.6.1.1. Identity ๐ชช
Flows are multiplexed in a session by a flow identifier ๐งต. Each end of a session picks its own sending flow identifiers, independent of the other end. If both ends happen to pick similar flow identifiers, that does not imply an association ๐ โโ๏ธ. A sender MAY choose any identifier for any flow; therefore, a flow receiver MUST NOT ascribe any semantic meaning, role, or name to a flow based only on its identifier. There are no "well known" or reserved flow identifiers ๐ซ.
Bidirectional flow association is indicated at flow startup with the Return Flow Association option (Section 2.3.11.1.2) ๐. An endpoint can indicate that a new sending flow is in return (or response) to a receiving flow from the other end. A sending flow MUST NOT indicate more than one return association. A receiving flow can be the return association for any number of sending flows. The return flow association, if there is one, stays fixed for the lifetime of the sending flow ๐. Note: Closing one flow in an association does not automatically close the other flows in that association, except as specified in Section 3.6.3.1 โ ๏ธ.
Flows are named with arbitrary user metadata ๐ท๏ธ. This specification doesn't mandate any particular encoding, syntax, or semantics for the user metadata, except for the encoded size (Section 2.3.11.1.1). The user metadata is entirely reserved for the application ๐ . The user metadata stays fixed for the lifetime of the flow ๐.
3.6.1.2. Messages and Sequencing โจ
okay so flows give you message-oriented framing ๐ Big messages get chopped up into fragments for the trip through the network, and the receiver stitches them back together and only hands the user complete messages. no half-finished texts, bestie ๐ซถ
A sender lines up messages on a sending flow one after another. Even if the network shuffles things around in transit, or they get lost and resent, the receiver can still put them back in the original order using each message's fragment sequence numbers ๐ฆ Flows are the basic units of message sequencing, and every flow is sequenced on its own, separate from all the others. Ordering between flows is not guaranteed, so don't expect flow A and flow B to arrive in a neat little line ๐
This is where it gets iconic: independent sequencing lets a sender prioritize one flow's messages (sending or resending them) over another flow's in the same session, and divvy up the transmission budget by priority ๐ RTMFP is built so that flows are the basic unit of prioritization. In any flow, fragment sequence numbers are unique and go up monotonically, so the fragment sequence numbers for any message MUST be greater than those of every message already queued in that flow ๐ฅ If fragments show up out of order, the receiver ends up with gaps, which makes it acknowledge every packet and makes the encoded acknowledgements bigger, so the sender SHOULD send lower sequence numbers first ๐
A sender can abandon a queued message at any time, even if the other end already got some of its fragments ๐ The receiver MUST be able to spot a gap when a message gets abandoned, so each message SHOULD take at least one sequence number from the sequence space, even if no fragments for it are ever sent ๐ง The sender keeps transmitting and retransmitting the fragments of every message that isn't abandoned, until the receiver acknowledges all of them ๐ To move the receiver forward, the sender sends a Forward Sequence Number (FSN), which tells the receiver that sequence numbers less than or equal to the FSN won't be sent or resent. That lets the receiver skip over gaps and keep delivering complete messages in order ๐ธ Any incomplete message still missing fragments at or below the FSN was abandoned and will never finish, periodt. A gap indication MUST be passed along to the receiving user โจ
3.6.1.3. Lifetime ๐ฆ
okay, so a flow starts when the sender sends user message fragments to the other end, and includes the user metadata and, if there is one, the return flow association ๐ The sender keeps including that metadata and return flow association until the far end acknowledges the flow. At that point, the sender knows the receiver has the metadata and the return flow association, and from then on the flow identifier alone is enough ๐
Flow receivers SHOULD acknowledge all the sequence numbers they receive for any flow, whether the flow was accepted or rejected ๐ Flow receivers MUST NOT acknowledge sequence numbers higher than the FSN that they did not receive. Acks drive congestion control and avoidance, so they have to be accurate ๐ญ
An endpoint can reject a receiving flow at any point in the flow's lifetime ๐ To do it, the receiving endpoint sends a Flow Exception Report chunk (Section 2.3.16) right before every acknowledgement chunk for that rejected receiving flow โจ
An endpoint may eventually wrap up and close a sending flow ๐ธ The last sequence number of the flow gets marked with the Final flag. The sending flow is complete once every sequence number in the flow, including the final one, has been cumulatively acknowledged by the receiver. The receiving flow is complete once every sequence number from the FSN up to the final sequence number has been received ๐ซถ Both ends hold the flow identifier of a completed flow in reserve for a while, so delayed or duplicated fragments and acks can drain out of the network without accidentally starting a new receiving flow or accidentally acknowledging a new sending flow. no mix-ups, iykyk ๐
If a flow sender gets a Flow Exception indication from the other end, the flow sender SHOULD close the flow and abandon all of its undelivered queued messages ๐ The flow sender SHOULD also show the user that an exception happened ๐ฅ
3.6.2. Sender ๐
Each sending flow has its own flow-specific information context, which is everything needed to get that flow's messages to the other end. Each sending flow context includes at least:
F_FLOW_ID: this flow's identifier;STARTUP_OPTIONS: the set of options to send to the receiver until this flow is acknowledged, including the User's Per-Flow Metadata and, if set, the Return Flow Association;SEND_QUEUE: the unacknowledged message fragments queued in this flow, initially empty; each message fragment entry comprises the following:SEQUENCE_NUMBER: the sequence number of this fragment;DATA: this fragment's user data;FRA: the fragment control value for this message fragment, having one of the values enumerated for that purpose in Section 2.3.11 ("User Data Chunk");ABANDONED: a boolean flag indicating whether this fragment has been abandoned;SENT_ABANDONED: a boolean flag indicating whether this fragment was abandoned when sent;EVER_SENT: a boolean flag indicating whether this fragment has been sent at least once, initially false;NAK_COUNT: a count of the number of negative acknowledgements detected for this fragment, initially 0;IN_FLIGHT: a boolean flag indicating whether this fragment is currently outstanding, or in flight, in the network, initially false;TRANSMIT_SIZE: the size, in bytes, of the encoded User Data chunk (including the chunk header) for this fragment when it was transmitted into the network.
F_OUTSTANDING_BYTES: the sum of theTRANSMIT_SIZEof each entry inSEND_QUEUEwhereentry.IN_FLIGHTis true;RX_BUFFER_SIZE: the most recent available buffer advertisement from the other end (Sections 2.3.13 and 2.3.14), initially 65536 bytes;NEXT_SN: the next sequence number to assign to a message fragment, initially 1;F_FINAL_SN: the sequence number assigned to the final message fragment of the flow, initially having no value;EXCEPTION: a boolean flag indicating whether an exception has been reported by the receiver, initially false;- The state, at any time being one of the following values: the open state
F_OPEN; the closing statesF_CLOSINGandF_COMPLETE_LINGER; and the closed stateF_CLOSED.
Note: The following diagram is only a summary of state transitions and their causing events, and is not a complete operational specification.
+--------+
| F_OPEN |
+--------+
|CLOSE or
|rcv Flow Exception
|
v
+---------+
|F_CLOSING|
+---------+
|rcv Data Ack
| 0..F_FINAL_SN
v
+-----------------+
|F_COMPLETE_LINGER|
+-----------------+
| 130 seconds
v
+--------+
|F_CLOSED|
+--------+
Figure 19: Sending Flow State Diagram
3.6.2.1. Startup ๐
The application opens a new sending flow to the other end in an S_OPEN session. The implementation picks a new flow ID that isn't assigned to any other sending flow in that session that is in the F_OPEN, F_CLOSING, or F_COMPLETE_LINGER states. The flow starts in the F_OPEN state. STARTUP_OPTIONS for the new flow is set with the User's Per-Flow Metadata (Section 2.3.11.1.1). If this flow is a return (or response) to a receiving flow from the other end, that flow's ID is encoded in a Return Flow Association (Section 2.3.11.1.2) option and added to STARTUP_OPTIONS. A new sending flow SHOULD NOT be opened in response to a receiving flow from the other end that isn't in the RF_OPEN state when the sending flow is opened ๐
โโ๏ธ
At this point the flow exists on the sender but not on the receiver ๐ The flow really begins when user data fragments are transmitted to the receiver. A sender can start a flow without any immediate user data by sending a Forward Sequence Number Update (Section 3.6.2.7.1), or by queuing and transmitting a user data fragment that is already abandoned ๐ง
3.6.2.2. Queuing Data ๐ฌ
The application queues messages in an F_OPEN sending flow for transmission to the far end. The implementation splits each message into one or more fragments for transmission in User Data chunks (Section 2.3.11). Each fragment MUST be small enough that, when it's assembled into a packet (Section 2.2.4) with a maximum-size common header, a User Data chunk header, and, if it isn't empty, this flow's STARTUP_OPTIONS, the packet won't go over the path MTU (Section 3.5.4.3) ๐
For each fragment, create a fragment entry and set fragmentEntry.SEQUENCE_NUMBER to flow.NEXT_SN, then increment flow.NEXT_SN by one. Set fragmentEntry.FRA according to the encoding in User Data chunks:
0: This fragment is a complete message.1: This fragment is the first of a multi-fragment message.2: This fragment is the last of a multi-fragment message.3: This fragment is in the middle of a multi-fragment message.
Append fragmentEntry to flow.SEND_QUEUE ๐
3.6.2.3. Sending Data ๐ฌโจ
okay so bestie, a sending flow is ready to transmit ๐ if its SEND_QUEUE has at least one entry that's eligible to send, AND either RX_BUFFER_SIZE is greater than F_OUTSTANDING_BYTES OR EXCEPTION is set to true. the vibe check is basically "is there something to send, and does the receiver have room?" ๐
an entry is eligible to send if it is not IN_FLIGHT, AND at least one of these is true:
- The entry is not ABANDONED; or
- The entry is the first one in the SEND_QUEUE; or
- The entry's SEQUENCE_NUMBER is equal to flow.F_FINAL_SN.
no cap, the third one is giving "final boss of the queue" ๐
if the session's transmission budget allows it, a flow that's ready to transmit gets picked for transmission based on the implementation's prioritization scheme. this spec doesn't mandate how flows get prioritized, so that part is totally up to the implementer ๐ฆ
next up is trimming the abandoned messages off the front of the queue and finding the Forward Sequence Number (FSN) ๐ง
- While the SEND_QUEUE contains at least two entries, AND the first entry is not IN_FLIGHT, AND the first entry is ABANDONED, remove and discard the first entry from the SEND_QUEUE;
- If the first entry in the SEND_QUEUE is not abandoned, set FSN to entry.SEQUENCE_NUMBER - 1; otherwise,
- If the first entry in the SEND_QUEUE is IN_FLIGHT, AND entry.SENT_ABANDONED is false, set FSN to entry.SEQUENCE_NUMBER - 1; otherwise,
- The first entry in the SEND_QUEUE is abandoned and either is not IN_FLIGHT or was already abandoned when sent; set FSN to entry.SEQUENCE_NUMBER.
the FSN MUST NOT be greater than any sequence number currently outstanding, and the FSN MUST NOT be equal to any sequence number currently outstanding that was not abandoned when sent. these two are non-negotiable, bestie ๐
now we assemble user data chunks for this flow into a packet and send it to the receiver. while there's enough space in the packet and the flow is ready to transmit:
- Starting at the head of the SEND_QUEUE, find the first eligible fragment entry;
- Encode the entry into a User Data chunk (Section 2.3.11) or, if possible (Section 3.6.2.3.2), a Next User Data chunk (Section 2.3.12);
- If present, set chunk.flowID to flow.F_FLOW_ID;
- If present, set chunk.sequenceNumber to entry.SEQUENCE_NUMBER;
- If present, set chunk.fsnOffset to entry.SEQUENCE_NUMBER - FSN;
- Set chunk.fragmentControl to entry.FRA;
- Set chunk.abandon to entry.ABANDONED;
- If entry.SEQUENCE_NUMBER equals flow.F_FINAL_SN, set chunk.final to true; else set chunk.final to false;
- If any options are being sent with this chunk, set chunk.optionsPresent to true, assemble the options into the chunk, and assemble a Marker to terminate the option list;
- If entry.ABANDONED is true, set chunk.userData to empty; otherwise, set chunk.userData to entry.DATA;
- If adding the assembled chunk to the packet would cause the packet to exceed the path MTU, do not assemble this chunk into the packet; enough space no longer remains in the packet; stop. Otherwise, continue:
- Set entry.IN_FLIGHT to true;
- Set entry.EVER_SENT to true;
- Set entry.NAK_COUNT to 0;
- Set entry.SENT_ABANDONED to entry.ABANDONED;
- Set entry.TRANSMIT_SIZE to the size of the assembled chunk, including the chunk header;
- Assemble this chunk into the packet; and
- If this flow or entry is considered Time Critical (real-time), set the timeCritical flag in the packet header (Section 2.2.4).
once that's all done, complete any other appropriate packet processing and transmit the packet to the far end ๐ฆ๐ธ
3.6.2.3.1. Startup Options ๐
if STARTUP_OPTIONS is not empty, then when assembling the FIRST User Data chunk for this flow into a packet, add the encoded STARTUP_OPTIONS to that chunk's option list. ๐ง
3.6.2.3.2. Send Next Data ๐
okay so the Next User Data chunk (Section 2.3.12) is a compact encoding for a user message fragment when multiple contiguous fragments get packed into one packet. using it where possible can conserve space in a packet, which can reduce transmission overhead or leave room for extra info in the packet. โจ
if, after assembling a user message fragment of a flow into a packet (Section 3.6.2.3), the next eligible fragment to be picked for that packet belongs to the same flow, AND its sequence number is one greater than the fragment just assembled, it is RECOMMENDED that an implementation encode a Next User Data chunk instead of a User Data chunk. ๐
the FIRST fragment of a flow assembled into a packet MUST be encoded as a User Data chunk. no exceptions, babe ๐ญ
3.6.2.4. Processing Acknowledgements ๐
a Data Acknowledgement Bitmap chunk (Section 2.3.13) or a Data Acknowledgement Ranges chunk (Section 2.3.14) is how the receiver tells you it got one or more sequence numbers of a flow, and it also carries the receiver's current receive window advertisement. โจ
when you receive an acknowledgement chunk for a sending flow, here's the drill:
- Set PRE_ACK_OUTSTANDING_BYTES to flow.F_OUTSTANDING_BYTES;
- Set flow.STARTUP_OPTIONS to empty;
- Set flow.RX_BUFFER_SIZE to chunk.bufferBytesAvailable;
- For each sequence number encoded in the acknowledgement, if there is an entry in flow.SEND_QUEUE with that sequence number and its IN_FLIGHT is true, then remove the entry from flow.SEND_QUEUE; and
- Notify the congestion control and avoidance algorithms that PRE_ACK_OUTSTANDING_BYTES - flow.F_OUTSTANDING_BYTES were acknowledged. Note that negative acknowledgements (Section 3.6.2.5) affect "TCP friendly" congestion control. ๐ฆ
3.6.2.5. Negative Acknowledgement and Loss ๐
a negative acknowledgement is inferred for an outstanding fragment if an acknowledgement is received for any other fragments sent after it in the same session. ๐
an implementation SHOULD consider a fragment to be lost once that fragment receives three negative acknowledgements. a lost fragment is no longer outstanding in the network. ๐ซถ
the following describes an OPTIONAL method for detecting negative acknowledgements. ๐
let the session track the order in which fragments are transmitted across all its sending flows by way of a monotonically increasing Transmission Sequence Number (TSN) recorded with each fragment queue entry each time that fragment is transmitted.
let the session information context contain additional variables:
- NEXT_TSN: the next TSN to record with a fragment's queue entry when it is transmitted, initially 1;
- MAX_TSN_ACK: the highest acknowledged TSN, initially 0.
let each fragment queue entry contain an additional variable TSN, initially 0, to track its transmission order.
on transmission of a message fragment into the network, set its entry.TSN to session.NEXT_TSN, and increment session.NEXT_TSN.
on acknowledgement of an outstanding fragment, if its entry.TSN is greater than session.MAX_TSN_ACK, set session.MAX_TSN_ACK to entry.TSN.
after processing all acknowledgements in a packet containing at least one acknowledgement, then for each sending flow in that session, for each entry in that flow's SEND_QUEUE, if entry.IN_FLIGHT is true and entry.TSN is less than session.MAX_TSN_ACK, increment entry.NAK_COUNT and notify the congestion control and avoidance algorithms that a negative acknowledgement was detected in this packet.
for each sending flow in that session, for each entry in that flow's SEND_QUEUE, if entry.IN_FLIGHT is true and entry.NAK_COUNT is at least 3, that fragment was lost in the network and is no longer considered to be in flight. set entry.IN_FLIGHT to false. notify the congestion control and avoidance algorithms of the loss. ๐จ
3.6.2.6. Timeout โฐ
a fragment is considered lost and no longer in flight in the network if it has remained outstanding for at least ERTO. ๐ค
the following describes an OPTIONAL method to manage transmission timeouts. this method REQUIRES that either burst avoidance (Section 3.5.2.3) is implemented or the implementation's congestion control and avoidance algorithms will eventually stop sending new fragments into the network if acknowledgements are persistently not received. โ ๏ธ
let the session information context contain an alarm TIMEOUT_ALARM, initially unset.
on sending a packet containing at least one User Data chunk, set or reset TIMEOUT_ALARM to fire in ERTO.
on receiving a packet containing at least one acknowledgement, reset TIMEOUT_ALARM (if already set) to fire in ERTO.
when TIMEOUT_ALARM fires:
- Set WAS_LOSS = false;
- For each sending flow in the session, and for each entry in that flow's SEND_QUEUE:
- If entry.IN_FLIGHT is true, set WAS_LOSS = true; and
- Set entry.IN_FLIGHT to false.
- If WAS_LOSS is true, perform ERTO backoff (Section 3.5.2.2); and
- Notify the congestion control and avoidance algorithms of the timeout and, if WAS_LOSS is true, that there was loss.
3.6.2.7. Abandoning Data ๐ฆ
the application can abandon queued messages at any time and for any reason. ๐ example reasons include (but aren't limited to): one or more fragments of a message have sat in the SEND_QUEUE longer than a specified message lifetime; a fragment has been retransmitted more than a specified retransmission limit; or a prior message that this one depends on (like a key frame in a prediction chain) was abandoned and never delivered. ๐ฌ
to abandon a message fragment, set its SEND_QUEUE entry's ABANDON flag to true. when you abandon a message fragment, abandon all fragments of the message it belongs to. ๐ซง
an abandoned fragment MUST NOT be un-abandoned. once it's gone, it's gone, bestie ๐
3.6.2.7.1. Forward Sequence Number Update ๐งโโ๏ธ
abandoned data can leave gaps in a flow's sequence number space. gaps can make the receiver hold fully received messages for ordered delivery, just in case the missing fragments get retransmitted. User Data chunks (Section 2.3.11) encode a Forward Sequence Number (FSN) to tell the receiver that fragments with sequence numbers less than or equal to the FSN will not be transmitted or retransmitted. ๐ธ
when the receiver has gaps in its received sequence number space and no non-abandoned message fragments are left in the SEND_QUEUE, the sender SHOULD transmit a Forward Sequence Number Update (FSN Update) made of a User Data chunk marked abandoned, whose sequence number is the FSN and whose fsnOffset is 0. โจ an FSN Update lets the receiver skip gaps that won't ever get repaired and deliver the messages it's already holding to the user. ๐ you can think of an FSN Update as sending or retransmitting abandoned sequence numbers without actually sending any data. ๐ง
the method described in Section 3.6.2.3 ("Sending Data") generates FSN Updates when appropriate. ๐
3.6.2.8. Examples โจ
okay bestie, time for the pictures! ๐ฆ these diagrams show a sender and the other end chatting back and forth, line by line ๐
Figure 20: Normal Flow with No Loss
Sender
| :
1 |<--- Ack ID=2, seq:0-16
2 |---> Data ID=2, seq#=25, fsnOff=9 (fsn=16)
3 |---> Data ID=2, seq#=26, fsnOff=10 (fsn=16)
4 |<--- Ack ID=2, seq:0-18
5 |---> Data ID=2, seq#=27, fsnOff=9 (fsn=18)
6 |---> Data ID=2, seq#=28, fsnOff=10 (fsn=18)
| :
There are 9 sequence numbers in flight with delayed acknowledgements. ๐ธ
Figure 21: Flow with Loss
Sender
| :
1 |<--- Ack ID=3, seq:0-30
2 |---> Data ID=3, seq#=45, fsnOff=15 (fsn=30)
3 |<--- Ack ID=3, seq:0-30, 32 (nack 31:1)
4 |---> Data ID=3, seq#=46, fsnOff=16 (fsn=30)
5 |<--- Ack ID=3, seq:0-30, 32, 34 (nack 31:2, 33:1)
6 |<--- Ack ID=3, seq:0-30, 32, 34-35 (nack 31:3=lost, 33:2)
7 |---> Data ID=3, seq#=47, fsnOff=15 (fsn=32, abandon 31)
8 |<--- Ack ID=3, seq:0-30, 32, 34-36 (nack 33:3=lost)
9 |---> Data ID=3, seq#=33, fsnOff=1 (fsn=32, retransmit 33)
10 |<--- Ack ID=3, seq:0-30, 32, 34-37
11 |---> Data ID=3, seq#=48, fsnOff=16 (fsn=32)
| :
| (continues through seq#=59)
| :
12 |---> Data ID=3, seq#=60, fsnOff=28(fsn=32)
13 |<--- Ack ID=3, seq:0-30, 34-46
14 |---> Data ID=3, seq#=61, fsnOff=29 (fsn=32)
15 |<--- Ack ID=3, seq:0-32, 34-47
16 |---> Data ID=3, seq#=62, fsnOff=30 (fsn=32)
17 |<--- Ack ID=3, seq:0-47
18 |---> Data ID=3, seq#=63, fsnOff=16 (fsn=47)
19 |<--- Ack ID=3, seq:0-49
20 |---> Data ID=3, seq#=64, fsnOff=15 (fsn=49)
| :
21 |<--- Ack ID=3, seq:0-59
22 |<--- Ack ID=3, seq:0-59, 61 (nack 60:1)
23 |<--- Ack ID=3, seq:0-59, 61-62 (nack 60:2)
24 |<--- Ack ID=3, seq:0-59, 61-63 (nack 60:3=lost)
25 |---> Data ID=3, ABN=1, seq#=60, fsnOff=0 (fsn=60, abandon 60)
26 |<--- Ack ID=3, seq:0-59, 61-64
| :
27 |<--- Ack ID=3, seq:0-64
This one's giving drama ๐ญ Sequence numbers 31, 33, and 60 get lost in transit, and there's a pause at 64. 33 gets retransmitted, while 31 and 60 get abandoned. Note that line 25 is a Forward Sequence Number Update (Section 3.6.2.7.1). ๐
3.6.2.9. Flow Control ๐
The flow receiver tells the flow sender how much new data it's ready to take in. It does this with the bufferBytesAvailable derived field of an acknowledgement (Sections 2.3.13 and 2.3.14). ๐ง
The flow sender MUST NOT send new data into the network if flow.F_OUTSTANDING_BYTES is greater than or equal to the most recently received buffer advertisement, unless flow.EXCEPTION is true (Section 3.6.2.3). ๐ซ no cap, this one's a hard rule ๐
3.6.2.9.1. Buffer Probe ๐
The flow sender is suspended if the most recently received buffer advertisement is zero and the flow hasn't been rejected by the receiver. Like, that's RX_BUFFER_SIZE being zero AND EXCEPTION being false. ๐ To guard against possibly lost acknowledgements that might reopen the receive window, a suspended flow sender SHOULD send a packet comprising a Buffer Probe chunk (Section 2.3.15) for this flow from time to time. ๐ซถ
If the receive window advertisement goes from non-zero to zero, the flow sender MAY send a Buffer Probe immediately and SHOULD send a probe within one second. โฑ๏ธ
The initial period between Buffer Probes SHOULD be at least one second or ERTO, whichever is greater. The period between probes SHOULD increase over time, but the period between probes SHOULD NOT be more than one minute or ERTO, whichever is greater. ๐
The flow sender SHOULD stop sending Buffer Probes if it is no longer suspended. โจ
3.6.2.10. Exception ๐จ
The flow receiver can reject the flow at any time and for any reason. ๐ When it has rejected a flow, the flow receiver sends a Flow Exception Report (Section 2.3.16). ๐ฃ
On receiving a Flow Exception Report for a sending flow:
- If the flow is
F_OPEN, close the flow (Section 3.6.2.11) and notify the user that the far end reported an exception with the encoded exception code; - Set the
EXCEPTIONflag to true; and - For each entry in
SEND_QUEUE, setentry.ABANDONED = true.
3.6.2.11. Close ๐ช
A sending flow is closed by the user or as a result of an exception. ๐ To close an F_OPEN flow:
- Move to the
F_CLOSINGstate; - If the
SEND_QUEUEis not empty, AND the tail entry of theSEND_QUEUEhas a sequence number ofNEXT_SN - 1, AND the tail entry.EVER_SENTis false, setF_FINAL_SNtoentry.SEQUENCE_NUMBER; else - The
SEND_QUEUEis empty, OR the tail entry does not have a sequence number ofNEXT_SN - 1, OR the tail entry.EVER_SENTis true: enqueue a newSEND_QUEUEentry withentry.SEQUENCE_NUMBER = flow.NEXT_SN,entry.FRA = 0, andentry.ABANDONED = true, and setflow.F_FINAL_SNtoentry.SEQUENCE_NUMBER.
An F_CLOSING sending flow is complete when its SEND_QUEUE transitions to empty. That means all sequence numbers, including the FINAL_SN, have been acknowledged by the other end. ๐ฅน
When an F_CLOSING sending flow becomes complete, move to the F_COMPLETE_LINGER state. ๐
A sending flow MUST remain in the F_COMPLETE_LINGER state for at least 130 seconds. โณ After at least 130 seconds, move to the F_CLOSED state. The sending flow is now closed, its resources can be reclaimed, and its F_FLOW_ID MAY be used for a new sending flow. ๐ฆ main character glow-up, fr fr
3.6.3. Receiver ๐ฅ
Each receiving flow has all the flow-specific info it needs to receive that flow's messages from the sending end and hand completed messages to the user. Each receiving flow context includes at least:
RF_FLOW_ID: this flow's identifier;SEQUENCE_SET: the set of all fragment sequence numbers seen in this receiving flow, whether received or abandoned, initially empty;RF_FINAL_SN: the final fragment sequence number of the flow, initially having no value;RECV_BUFFER: the message fragments waiting to be delivered to the user, sorted by sequence number in ascending order, initially empty; each message fragment entry comprising the following:SEQUENCE_NUMBER: the sequence number of this fragment;DATA: this fragment's user data; andFRA: the fragment control value for this message fragment, having one of the values enumerated for that purpose in Section 2.3.11 ("User Data Chunk").
BUFFERED_SIZE: the sum of the lengths of each fragment inRECV_BUFFERplus any additional storage overhead for the fragments incurred by the implementation, in bytes;BUFFER_CAPACITY: the desired maximum size for the receive buffer, in bytes;PREV_RWND: the most recent receive window advertisement sent in an acknowledgement, in 1024-byte blocks, initially having no value;SHOULD_ACK: whether or not an acknowledgement should be sent for this flow, initially false;EXCEPTION_CODE: the exception code to report to the sender when the flow has been rejected, initially 0;- The state, at any time being one of the following values: the open state
RF_OPEN; the closing statesRF_REJECTEDandRF_COMPLETE_LINGER; and the closed stateRF_CLOSED.
Note: The following diagram is only a summary of state transitions and their causing events, and is not a complete operational specification. ๐
+-+
|X|
+-+
|rcv User Data for
| no existing flow
v
+---------+
| RF_OPEN |
+---------+
rcv all sequence numbers| |user reject,
0..RF_FINAL_SN | |rcv bad option,
| |no metadata at open,
| |association specified
| | but not F_OPEN at open
+---+ |
| v
| +-----------+
| |RF_REJECTED|
| +-----------+
| |rcv all sequence numbers
| | 0..RF_FINAL_SN
v v
+------------------+
|RF_COMPLETE_LINGER|
+------------------+
| 120 seconds
v
+---------+
|RF_CLOSED|
+---------+
Figure 22: Receiving Flow State Diagram
3.6.3.1. Startup ๐ฑ
A new receiving flow starts on receipt of a User Data chunk (Section 2.3.11) encoding a flow ID that doesn't belong to any other receiving flow in the same session in the RF_OPEN, RF_REJECTED, or RF_COMPLETE_LINGER states. ๐
On receipt of such a User Data chunk:
- Set temporary variables
METADATA,ASSOCIATED_FLOWID, andASSOCIATIONto each have no value; - Create a new receiving flow context in this session, setting its
RF_FLOW_IDto the flow ID encoded in the opening User Data chunk, and set to theRF_OPENstate; - If the opening User Data chunk encodes a User's Per-Flow Metadata option (Section 2.3.11.1.1), set
METADATAtooption.userMetadata; - If the opening User Data chunk encodes a Return Flow Association option (Section 2.3.11.1.2), set
ASSOCIATED_FLOWIDtooption.flowID; - If
METADATAhas no value, the receiver MUST reject the flow (Section 3.6.3.7), moving it to theRF_REJECTEDstate; - If
ASSOCIATED_FLOWIDhas a value, then if there is no sending flow in the same session with a flow ID ofASSOCIATED_FLOWID, the receiver MUST reject the flow, moving it to theRF_REJECTEDstate; otherwise, setASSOCIATIONto the indicated sending flow; - If
ASSOCIATIONindicates a sending flow, AND that sending flow's state is notF_OPEN, the receiver MUST reject this receiving flow, moving it to theRF_REJECTEDstate; - If the opening User Data chunk encodes any unrecognized option with a type code less than 8192 (Section 2.3.11.1), the receiver MUST reject the flow, moving it to the
RF_REJECTEDstate; - If this new receiving flow is still
RF_OPEN, then notify the user that a new receiving flow has opened, including theMETADATAand, if present, theASSOCIATION, and setflow.BUFFER_CAPACITYaccording to the user; - Perform the normal data processing (Section 3.6.3.2) for the opening User Data chunk; and
- Set this session's
ACK_NOWto true. ๐ฅณ
3.6.3.2. Receiving Data ๐ฆโจ
okay so a User Data chunk (Section 2.3.11) or a Next User Data chunk (Section 2.3.12) carries ONE fragment of a user data message for a flow ๐. It also tells you the flow's Forward Sequence Number, and maybe some optional parameters (Section 2.3.11.1) ๐
When one of these chunks lands, here's the checklist ๐:
- If
chunk.flowIDdoesn't point to an existing receiving flow in the same session that's in theRF_OPEN,RF_REJECTED, orRF_COMPLETE_LINGERstate, run the steps of Section 3.6.3.1 ("Startup") to spin up a brand new receiving flow ๐ง; - Grab the receiving flow context for the flow that
chunk.flowIDpoints at ๐; - Set
flow.SHOULD_ACKtotrue๐ซถ; - If the flow is
RF_OPEN, AND the chunk encodes any unrecognized option with a type code less than 8192 (Section 2.3.11.1), the flow MUST be rejected: notify the user of an exception, and reject the flow (Section 3.6.3.7), moving it to theRF_REJECTEDstate ๐ซ (this one is not up for debate, bestie ๐); - If the flow is not in the
RF_OPENstate, setsession.ACK_NOWtotrue๐ฃ; - If
flow.PREV_RWNDhas a value and that value is less than 2 blocks, setsession.ACK_NOWtotrue๐; - If
chunk.abandonistrue, setsession.ACK_NOWtotrue๐ โโ๏ธ; - If
flow.SEQUENCE_SEThas any gaps (that is, if it doesn't contain every sequence number from 0 through and including the highest sequence number in the set), setsession.ACK_NOWtotrue๐ณ๏ธ; - If
flow.SEQUENCE_SETcontainschunk.sequenceNumber, then this chunk is a duplicate: setsession.ACK_NOWtotrue๐ฏโโ๏ธ; - If
flow.SEQUENCE_SETdoesn't containchunk.sequenceNumber, ANDchunk.finalistrue, ANDflow.RF_FINAL_SNhas no value, then setflow.RF_FINAL_SNtochunk.sequenceNumber, and setsession.ACK_NOWtotrue๐; - If the flow is in the
RF_OPENstate, ANDflow.SEQUENCE_SETdoesn't containchunk.sequenceNumber, ANDchunk.abandonisfalse, then create a newRECV_BUFFERentry for this chunk's data and setentry.SEQUENCE_NUMBERtochunk.sequenceNumber,entry.DATAtochunk.userData, andentry.FRAtochunk.fragmentControl, and insert this new entry intoflow.RECV_BUFFER๐ฅ; - Add to
flow.SEQUENCE_SETthe range of sequence numbers from 0 through and including thechunk.forwardSequenceNumberderived field ๐; - Add
chunk.sequenceNumbertoflow.SEQUENCE_SET๐งฉ; - If
flow.SEQUENCE_SETnow has any gaps, setsession.ACK_NOWtotrue๐ซฃ; - If
session.ACK_NOWisfalseandsession.DELACK_ALARMis not set, setsession.DELACK_ALARMto fire in 200 milliseconds; and โฐ - Attempt delivery of completed messages in this flow's
RECV_BUFFERto the user (Section 3.6.3.3) ๐.
After processing all chunks in a packet that has at least one User Data chunk, increment session.RX_DATA_PACKETS by one. If session.RX_DATA_PACKETS is at least two, set session.ACK_NOW to true ๐ซ.
A receiving flow that is not in the RF_CLOSED state is ready to send an acknowledgement if its SHOULD_ACK flag is set. Acknowledgements for receiving flows that are ready get sent either opportunistically, by piggybacking on a packet that's already sending user data or an acknowledgement (Section 3.6.3.4.6), or when the session's ACK_NOW flag is set (Section 3.6.3.4.5) ๐ฆ.
3.6.3.3. Buffering and Delivering Data ๐
A receiving flow's information context holds a RECV_BUFFER for reordering, reassembling, and holding the user data messages of the flow ๐๏ธ. Only COMPLETE messages get delivered to the user. An implementation MUST NOT deliver partially received messages, except by special arrangement with the user ๐
โโ๏ธ.
Let the Cumulative Acknowledgement Sequence Number (CSN) be the highest number in the contiguous range of numbers in SEQUENCE_SET starting with 0. For example, if SEQUENCE_SET contains {0, 1, 2, 3, 5, 6}, the contiguous range starting with 0 is 0..3, so the CSN is 3 ๐ข.
A message is complete if all of its fragments are present in the RECV_BUFFER. The fragments of one message have contiguous sequence numbers. A message can be either a single fragment whose fragment control value is 0-whole, or two or more fragments where the first one's fragment control value is 1-begin, followed by zero or more fragments with control value 3-middle, and terminated by a last fragment with control value 2-end ๐งต.
An incomplete message segment is a contiguous sequence of one or more fragments that do not form a complete message. That means a 1-begin followed by zero or more 3-middle fragments but with no 2-end, or zero or more 3-middle fragments followed by a 2-end but with no 1-begin, or one or more 3-middle fragments with neither a 1-begin nor a 2-end ๐งฉ.
Incomplete message segments can either be in progress or abandoned. An incomplete segment is abandoned in the following cases:
- The sequence number of the segment's first fragment is less than or equal to the CSN, AND that fragment's control value is not
1-begin; or - The sequence number of the segment's last fragment is less than the CSN.
Abandoned message segments will never be completed, so they SHOULD be removed from the RECV_BUFFER to make room in the advertised receive window and the receiver's memory for messages that can be completed ๐งนโจ.
The user can suspend delivery of a flow's messages. A suspended receiving flow holds completed messages in its RECV_BUFFER until the user resumes delivery โธ๏ธ. A suspended flow can cause the receive window advertisement to go to zero even when the BUFFER_CAPACITY is non-zero. This is described in detail in Section 3.6.3.5 ("Flow Control") ๐ช.
When the receiving flow is not suspended, the original queuing order of the messages is recovered by delivering, in ascending sequence number order, complete messages in the RECV_BUFFER whose sequence numbers are less than or equal to the CSN ๐โโ๏ธ๐จ.
Here's the method for discarding abandoned message segments and delivering complete messages in original queuing order when the receiving flow is not suspended ๐ง.
While the first fragment entry in the RECV_BUFFER has a sequence number less than or equal to the CSN and delivery is still possible:
- If
entry.FRAis0-whole, deliverentry.DATAto the user, and remove this entry fromRECV_BUFFER; otherwise, - If
entry.FRAis2-endor3-middle, this entry belongs to an abandoned segment, so remove and discard this entry fromRECV_BUFFER; otherwise, Entry.FRAis1-begin. LetLAST_ENTRYbe the lastRECV_BUFFERentry that is part of this message segment (LAST_ENTRYcan beentryif the segment has only one fragment so far). Then:- If
LAST_ENTRY.FRAis2-end, this segment is a complete message, so concatenate theDATAfields of each fragment entry of this segment in ascending sequence number order and deliver the complete message to the user, then remove the entries for this complete message fromRECV_BUFFER; otherwise, - If
LAST_ENTRY.SEQUENCE_NUMBERis less than CSN, this segment is incomplete and abandoned, so remove and discard the entries for this segment fromRECV_BUFFER; otherwise, LAST_ENTRY.SEQUENCE_NUMBERis equal to CSN andLAST_ENTRY.FRAis not2-end: this segment is incomplete but still in progress. Ordered delivery is no longer possible until at least one more fragment is received. Stop. ๐
- If
If flow.RF_FINAL_SN has a value and is equal to the CSN, AND RECV_BUFFER is empty, then all complete messages have been delivered to the user, so notify the user that the flow is complete ๐๐.
3.6.3.4. Acknowledging Data ๐
A flow receiver SHOULD acknowledge all user data fragment sequence numbers seen in that flow. Acknowledgements drive the sender's congestion control and avoidance algorithms, clear data from the sender's buffers, and in some sender implementations clock new data into the network. Therefore, the acknowledgements must be accurate and timely ๐ฏ.
3.6.3.4.1. Timing โฑ๏ธ
For reasons similar to those discussed in Section 4.2.3.2 of RFC 1122 [RFC1122], it's advantageous to delay sending acknowledgements for a short time, so that multiple data fragments can be acknowledged in a single transmission ๐ฆ. However, it's also advantageous for a sender to get timely notification about the receiver's disposition of the flow, especially in unusual or exceptional circumstances, so that those circumstances can be addressed if possible ๐จ.
Therefore, a flow receiver SHOULD send an acknowledgement for a flow as soon as is practical in any of the following circumstances:
- On receipt of a User Data chunk that starts a new flow;
- On receipt of a User Data or Next User Data chunk if the flow is not in the
RF_OPENstate; - On receipt of a User Data chunk where, before processing the chunk, the
SEQUENCE_SETof the indicated flow does not contain every sequence number between 0 and the highest sequence number in the set (that is, if there was a sequence number gap before processing the chunk); - On receipt of a User Data chunk where, after processing the chunk, the flow's
SEQUENCE_SETdoes not contain every sequence number between 0 and the highest sequence number in the set (that is, if this chunk causes a sequence number gap); - On receipt of a Buffer Probe for the flow;
- On receipt of a User Data chunk if the last acknowledgement sent for the flow indicated fewer than two
bufferBlocksAvailable; - On receipt of a User Data or Next User Data chunk for the flow if, after processing the chunk, the flow's
BUFFER_CAPACITYis not at least 1024 bytes greater thanBUFFERED_SIZE; - On receipt of a User Data or Next User Data chunk for any sequence number that was already seen (that is, on receipt of a duplicate);
- On the first receipt of the final sequence number of the flow;
- On receipt of two packets in the session that contain user data for any flows since an acknowledgement was last sent, the new acknowledgements being for the flows having any User Data chunks in the received packets (that is, for every second packet containing user data);
- After receipt of a User Data chunk for the flow, if an acknowledgement for any other flow is being sent (that is, consolidate acknowledgements);
- After receipt of a User Data chunk for the flow, if any user data for a sending flow is being sent in a packet and if there is space available in the same packet (that is, attempt to piggyback an acknowledgement with user data if possible);
- No longer than 200 milliseconds after receipt of a User Data chunk for the flow. โณ
3.6.3.4.2. Size and Truncation โ๏ธ
Including an encoded acknowledgement in a packet might cause the packet to exceed the path MTU. In that case:
- If the packet is being sent primarily to send an acknowledgement, AND this is the first acknowledgement in the packet, truncate the acknowledgement so that the packet does not exceed the path MTU; otherwise,
- The acknowledgement is being piggybacked in a packet with user data or with an acknowledgement for another flow: do not include this acknowledgement in the packet, and send it later. ๐ฌ
3.6.3.4.3. Constructing ๐งฑ
The Data Acknowledgement Bitmap chunk (Section 2.3.13) and Data Acknowledgement Ranges chunk (Section 2.3.14) both encode a receiving flow's SEQUENCE_SET plus its receive window advertisement. They're basically twins ๐ฏโโ๏ธ, so implementations SHOULD send whichever one gives the most compact encoding of the SEQUENCE_SET ๐.
When you're putting together an acknowledgement for a receiving flow, do this:
- If the flow's state is
RF_REJECTED, first assemble a Flow Exception Report chunk (Section 2.3.16) forflow.flowID; - Choose the acknowledgement chunk type that most compactly encodes
flow.SEQUENCE_SET; - Use the method described in Section 3.6.3.5 ("Flow Control") to determine the value for the acknowledgement chunk's
bufferBlocksAvailablefield.
3.6.3.4.4. Delayed Acknowledgement โณ
Remember Section 3.6.3.4.1 ("Timing")? A flow receiver can hold off on sending an acknowledgement for up to 200 milliseconds after it gets user data. The method in Section 3.6.3.2 ("Receiving Data") is what sets the session's DELACK_ALARM.
When DELACK_ALARM fires, set ACK_NOW to true. Bestie, that's the wake-up call ๐ฃ.
3.6.3.4.5. Obligatory Acknowledgement ๐จ
One or more acknowledgements should be sent as soon as is practical when the session's ACK_NOW flag is set. While the ACK_NOW flag is set, do this:
- Choose a receiving flow that is ready to send an acknowledgement;
- If there is no such flow, there is no work to do, set
ACK_NOWto false, setRX_DATA_PACKETSto 0, clear theDELACK_ALARM, and stop; otherwise, - Start a new packet;
- Assemble an acknowledgement for the flow and include it in the packet, truncating it if necessary so that the packet doesn't exceed the path MTU;
- Set
flow.SHOULD_ACKto false; - Set
flow.PREV_RWNDto thebufferBlocksAvailablefield of the included acknowledgement chunk; - Attempt to piggyback acknowledgements for any other flows that are ready to send an acknowledgement into the packet, as described below; and
- Send the packet.
3.6.3.4.6. Opportunistic Acknowledgement ๐
When you're sending a packet that has user data or an acknowledgement, any other receiving flows that are ready to send an acknowledgement should try to tuck their acknowledgements into that packet if it's possible. It's giving free rides ๐.
To piggyback acknowledgements in a packet that is already being sent, where the packet contains user data or an acknowledgement, do the following while there is at least one receiving flow that is ready to send an acknowledgement:
- Assemble an acknowledgement for the flow;
- If the acknowledgement cannot be included in the packet without exceeding the path MTU, the packet is full; stop. Otherwise,
- Include the acknowledgement in the packet;
- Set
flow.SHOULD_ACKto false; - Set
flow.PREV_RWNDto thebufferBlocksAvailablefield of the included acknowledgement chunk; and - If there are no longer any receiving flows in the session that are ready to send an acknowledgement, set
session.ACK_NOWto false, setsession.RX_DATA_PACKETSto 0, and clearsession.DELACK_ALARM.
3.6.3.4.7. Example ๐ฌ
Here's a flow where sequence numbers 31 and 33 get lost in transit ๐ฉ. 31 gets abandoned, and 33 gets retransmitted. Watch the drama unfold:
Receiver
1 |<--- Data ID=3, seq#=29, fsnOff=11 (fsn=18)
2 |<--- Data ID=3, seq#=30, fsnOff=12 (fsn=18)
3 |---> Ack ID=3, seq:0-30
4 |<--- Data ID=3, seq#=32, fsnOff=12 (fsn=20)
5 |---> Ack ID=3, seq:0-30, 32
6 |<--- Data ID=3, seq#=34, fsnOff=12 (fsn=22)
7 |---> Ack ID=3, seq:0-30, 32, 34
| :
8 |<--- Data ID=3, seq#=46, fsnOff=16 (fsn=30)
9 |---> Ack ID=3, seq:0-30, 32, 34-46
10 |<--- Data ID=3, seq#=47, fsnOff=15 (fsn=32)
11 |---> Ack ID=3, seq:0-32, 34-47
12 |<--- Data ID=3, seq#=33, fsnOff=1 (fsn=32)
13 |---> Ack ID=3, seq#=0-47
14 |<--- Data ID=3, seq#=48, fsnOff=16 (fsn=32)
15 |<--- Data ID=3, seq#=49, fsnOff=17 (fsn=32)
16 |---> Ack ID=3, seq#=0-49
| :
Figure 23: Flow Example with Loss
3.6.3.5. Flow Control ๐ฆ
The flow receiver keeps a buffer for reassembling and reordering messages so they can be delivered to the user (Section 3.6.3.3). The implementation and the user might want to put a cap on how many resources (buffer memory included) a flow is allowed to use ๐ง .
RTMFP lets each receiving flow control how much data the sender sends, using the bufferBytesAvailable derived field of acknowledgement chunks (Sections 2.3.13 and 2.3.14). This derived field tells you how much data the sender is allowed to have outstanding in the network, until it's told otherwise. That amount is also called the receive window ๐ช.
The flow receiver can suspend the sender by advertising a closed (zero length) receive window. Pause button, activated โธ๏ธ.
The user can suspend delivery of messages from the receiving flow (Section 3.6.3.3). This can cause the receive buffer to fill up ๐ชฃ.
For a fragmented message to finish or a gap to get repaired for sequenced delivery in a flow, the flow receiver MUST advertise at least one buffer block in an acknowledgement if it is not suspended, even if the amount of data in the buffer exceeds the buffer capacity, unless the buffer capacity is 0. Otherwise, deadlock can occur, as the receive buffer will stay full and won't drain because of a gap or incomplete message, and the gap or incomplete message can't be repaired or completed because the sender is suspended. Nobody wins in that situation ๐.
The receive window is advertised in units of 1024-byte blocks. For example, advertisements for 1 byte, 1023 bytes, and 1024 bytes each require one block. An advertisement for 1025 bytes requires two blocks.
The following describes the RECOMMENDED method of calculating the bufferBlocksAvailable field of an acknowledgement chunk for a receiving flow:
- If
BUFFERED_SIZEis greater than or equal toBUFFER_CAPACITY, setADVERTISE_BYTESto 0; - If
BUFFERED_SIZEis less thanBUFFER_CAPACITY, setADVERTISE_BYTEStoBUFFER_CAPACITY - BUFFERED_SIZE; - Set
ADVERTISE_BLOCKStoCEIL(ADVERTISE_BYTES / 1024); - If
ADVERTISE_BLOCKSis 0, ANDBUFFER_CAPACITYis greater than 0, AND delivery to the user is not suspended, setADVERTISE_BLOCKSto 1; and - Set the acknowledgement's
bufferBlocksAvailablefield toADVERTISE_BLOCKS.
3.6.3.6. Receiving a Buffer Probe ๐
A Buffer Probe chunk (Section 2.3.15) is sent by the flow sender (Section 3.6.2.9.1) to ask the flow receiver for its current receive window advertisement (in the form of an acknowledgement). Basically, "what's your window looking like rn?" ๐
When a Buffer Probe chunk arrives:
- If
chunk.flowIDdoesn't belong to a receiving flow in the same session in theRF_OPEN,RF_REJECTED, orRF_COMPLETE_LINGERstate, ignore this Buffer Probe; otherwise, - Retrieve the receiving flow context for the flow indicated by
chunk.flowID; then - Set
flow.SHOULD_ACKto true; and - Set
session.ACK_NOWto true.
3.6.3.7. Rejecting a Flow ๐ โโ๏ธ
A receiver can reject an RF_OPEN flow at any time and for any reason. No explanation required, periodt ๐
. To reject a receiving flow in the RF_OPEN state:
- Move to the
RF_REJECTEDstate; - Discard all entries in
flow.RECV_BUFFER, as they are no longer relevant; - If the user rejected the flow, set
flow.EXCEPTION_CODEto the exception code indicated by the user; otherwise, the flow was rejected automatically by the implementation, so the exception code is 0; - Set
flow.SHOULD_ACKto true; and - Set
session.ACK_NOWto true.
The receiver tells the sender it has rejected a flow by sending a Flow Exception Report chunk (Section 2.3.16) with every acknowledgement (Section 3.6.3.4.3) for a flow in the RF_REJECTED state ๐ซ.
3.6.3.8. Close ๐
A receiving flow is complete when every sequence number from 0 through and including the final sequence number has been received. In other words, when flow.RF_FINAL_SN has a value and flow.SEQUENCE_SET contains every sequence number from 0 through flow.RF_FINAL_SN, inclusive. Mission accomplished โ
.
When an RF_OPEN or RF_REJECTED receiving flow becomes complete, move to the RF_COMPLETE_LINGER state, set flow.SHOULD_ACK to true, and set session.ACK_NOW to true.
A receiving flow SHOULD remain in the RF_COMPLETE_LINGER state for 120 seconds. After 120 seconds, move to the RF_CLOSED state. The receiving flow is now closed, and its resources can be reclaimed once all complete messages in flow.RECV_BUFFER have been delivered to the user (Section 3.6.3.3). The same flow ID might get reused for a new flow by the sender after this point ๐.
Discussion: The flow sender figures out the flow is complete when it receives an acknowledgement of all fragment sequence numbers of the flow. That can't happen until after the receiver has detected that the flow is complete and acknowledged all of the sequence numbers. The receiver's RF_COMPLETE_LINGER period is two minutes (one Maximum Segment Lifetime (MSL)). This period lets any in-flight packets drain from the network without being misidentified, and gives the sender a chance to retransmit any sequence numbers if the completing acknowledgement gets lost. The sender's F_COMPLETE_LINGER period is at least two minutes plus 10 seconds and doesn't begin until the completing acknowledgement is received. So the same flow identifier won't be reused by the flow sender for a new sending flow for at least 10 seconds after the flow receiver has closed the receiving flow context. This keeps things working correctly regardless of network delay, even when the sender's clock runs up to 8 percent faster than the receiver's. Timing queens, we did it ๐๐.
4. IANA Considerations ๐ธ
This memo specifies chunk type code values (Section 2.3) and User Data option type code values (Section 2.3.11.1). These type code values are assigned and maintained by Adobe. Therefore, this memo has no IANA actions.
5. Security Considerations ๐โจ
okay bestie, security time ๐ซถ This memo lays out a general framework for setting up a confidential and authenticated session between endpoints ๐. A Cryptography Profile, which is not specified here, is what defines the cryptographic algorithms, data formats, and semantics used inside this framework. Making sure communications are protected to the degree the application-specific threat model needs is totally outside the scope of this spec ๐.
A block cipher in CBC mode is RECOMMENDED for packet encryption (Section 2.2.3). An attacker can predict the values of some fields from one plain RTMFP packet to the next, or predict that some fields will be the same from one packet to the next ๐ญ. This SHOULD be considered when choosing and implementing a packet encryption cipher and mode.
The well-known Default Session Key of a Cryptography Profile is doing a lot ๐ . It scrambles session startup packets so middleboxes like NATs can't mess with the interior fields, it makes casual passive observation of startup packets harder, and it lets different RTMFP applications using different Default Session Keys share network transport addresses without interfering with each other (on purpose or not). But the Default Session Key is well known, so it MUST NOT be construed to contribute to the security of session startup. Session startup is essentially in the clear ๐.
Section 3.5.4.2 describes an OPTIONAL method for processing a change of network address of a communicating peer. Doing that securely, using that method or any substantially similar method, requires at least that the packet encryption function of the Cryptography Profile (Section 2.2.3) employs a cryptographic verification mechanism made of secret information known only to the two endpoints. Without this constraint, that method, or any substantially similar method, becomes "session hijacking support" ๐จ.
Flows and packet fragmentation come with semantics that could cause unbounded resource utilization in receivers, which means a denial of service ๐ฑ. Implementations SHOULD guard against unbounded or excessive resource use and abort sessions that appear abusive.
A rogue but popular Redirector (Section 3.5.1.4) could direct session initiators to flood a victim address or network with Initiator Hello packets, potentially causing a denial of service ๐ โโ๏ธ.
An attacker that can passively observe an IHello, and that possesses a certificate matching the Endpoint Discriminator (without having to know the private key, if any, associated with it), can deny the initiator access to the desired responder. They do this by sending an RHello before the desired responder does, since only the first received RHello is selected by the initiator ๐ฅบ. The attacker doesn't need to forge the desired responder's source address, because the RHello is selected based on the tag echo and not the packet's source address. That can make the attack way simpler in some network or host configurations ๐.
An attacker that can passively observe and record the packets of an established session can use traffic analysis techniques to infer the start and completion of flows, all without decrypting anything ๐ต๏ธโโ๏ธ. The User Data fragments of flows have unique sequence numbers, so flows are immune to replay while they're open. However, once a flow has completed and the linger period has concluded, the attacker could replay the recorded packets, open a new flow in the receiver, and duplicate the flow's data. That replay might have undesirable effects on the receiver's application ๐ฌ. The attacker could also infer that a new flow has begun reusing the recorded flow's identifier, then replay the final sequence number or any of the other fragments in the flow, potentially denying or interfering with legitimate traffic to the receiver. So the data integrity aspect of packet encryption SHOULD comprise anti-replay measures ๐ฅ.
6. Acknowledgements ๐
Special thanks go to Matthew Kaufman for his contributions to the creation and design of RTMFP ๐.
Thanks to Jari Arkko, Ben Campbell, Wesley Eddy, Stephen Farrell, Philipp Hancke, Bela Lubkin, Hilarie Orman, Richard Scheffenegger, and Martin Stiemerling for their detailed reviews of this memo ๐ซถโจ.
7. References ๐
7.1. Normative References
[CBC]Dworkin, M., "Recommendation for Block Cipher Modes of Operation", NIST Special Publication 800-38A, December 2001,<http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf>.[RFC0768]Postel, J., "User Datagram Protocol", STD 6, RFC 768, August 1980.[RFC0791]Postel, J., "Internet Protocol", STD 5, RFC 791, September 1981.[RFC1122]Braden, R., "Requirements for Internet Hosts - Communication Layers", STD 3, RFC 1122, October 1989.[RFC2119]Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997.[RFC2460]Deering, S. and R. Hinden, "Internet Protocol, Version 6 (IPv6) Specification", RFC 2460, December 1998.[RFC2914]Floyd, S., "Congestion Control Principles", BCP 41, RFC 2914, September 2000.[RFC4821]Mathis, M. and J. Heffner, "Packetization Layer Path MTU Discovery", RFC 4821, March 2007.[RFC5681]Allman, M., Paxson, V., and E. Blanton, "TCP Congestion Control", RFC 5681, September 2009.
7.2. Informative References
[RFC5389]Rosenberg, J., Mahy, R., Matthews, P., and D. Wing, "Session Traversal Utilities for NAT (STUN)", RFC 5389, October 2008.[ScalableTCP]Kelly, T., "Scalable TCP: Improving Performance in Highspeed Wide Area Networks", December 2002,<http://datatag.web.cern.ch/datatag/papers/pfldnet2003-ctk.pdf>.
Appendix A. Example Congestion Control Algorithm ๐ฆ
As mandated in Section 3.5.2, an RTMFP is required to use TCP-compatible congestion control. But flexibility in the exact implementation is allowed, within certain limits ๐ธ. This section describes an experimental window-based congestion control algorithm that's appropriate for real-time and bulk data transport in RTMFP. It includes slow start and congestion avoidance phases, with modified increase and decrease parameters. Those parameters are further adjusted depending on whether real-time data is being sent and whether Time Critical Reverse Notifications are received ๐ง.
A.1. Discussion
RFC 5681 defines the standard window-based congestion control algorithms for TCP. These are great for delay-insensitive bulk data transport, but they have undesirable behaviors for delay- and loss-sensitive applications ๐ค. Two of the big ones are cutting the congestion window in half during a loss event, and how fast slow start's exponential growth is. Cutting the congestion window in half needs a large channel headroom to support a real-time application, and it can cause a lot of jitter from sender-side buffering. Doubling the congestion window during slow start can make it temporarily grow to twice the size it should be, causing a period of excessive loss in the path ๐.
We found that a bunch of deployed TCP implementations use the method of equation (3) from Section 3.1 of RFC 5681. When that's combined with the recommended behavior of acknowledging every other packet, the congestion window grows at approximately half the rate the recommended method specifies ๐. So to compete fairly with these deployed TCPs, we choose 768 bytes per round trip as the increment during the normal congestion avoidance phase. That's approximately half of the typical maximum segment size of 1500 bytes, and it's also easily subdivided ๐.
The sender may be sending real-time data to the far end. When that's happening, a smoother response to congestion is desired, while still competing with reasonable fairness to other flows in the Internet ๐. To scale the sending rate quickly, slow start is desired, but slow start's normal rate of increase can cause excessive loss in the last round trip. So slow start's exponential increase rate is adjusted to double approximately every 3 round trips instead of every round trip โจ. The multiplicative decrease cuts the congestion window by one eighth on loss, which keeps the sending rate smoother ๐ซง. The additive increase is done at half the normal rate (incrementing at 384 bytes per round trip), to both compensate for the less aggressive loss response and probe the path capacity more gently ๐.
The far end may report that it's receiving real-time data from other peers, or the sender may be sending real-time data to other far ends. In these circumstances (if not sending real-time data to this far end), it's desirable to respond differently than the standard TCP algorithms specify. The goal is to both yield capacity to the real-time flows and avoid excessive losses while probing the path capacity ๐. Slow start's exponential increase is disabled, and the additive increase is done at half the normal rate (incrementing at 384 bytes per round trip). Multiplicative decrease stays at the normal rate (cutting by half) so it can yield to other flows ๐.
Since real-time messages may be small and sent regularly, it's advantageous to spread congestion window increases out across the round-trip time instead of doing them all at once ๐ง. We divide the round trip into 16 segments, with an additive increase of a useful size (48 bytes) per segment.
Scalable TCP [ScalableTCP] describes experimental methods of modifying the additive increase and multiplicative decrease of the congestion window in large delay-bandwidth scenarios, and these methods are adapted here ๐. The congestion window is increased by 1% each round trip, and decreased by one eighth on loss in the congestion avoidance phase in certain circumstances. Specifically, that's when a 1% increase is larger than the normal additive-increase amount. The scalable increase amount is 48 bytes for every 4800 bytes acknowledged, so the increase is spread out over the round trip ๐ซถ. The congestion window is decreased by one eighth on loss when it's at least 67200 bytes per round trip, which is seven eighths of 76800 (the point at which 1% is greater than 768 bytes per round trip) ๐ฅน. When sending real-time data to the far end, the scalable increase is 1% or 384 bytes per round trip, whichever is greater. Otherwise, when notified that the far end is receiving real-time data from other peers, the scaled increase is adjusted to 0.5% or 384 bytes per round trip, whichever is greater ๐ฅ๐ .
A.2. Algorithm โจ
okay so this is the actual algorithm, bestie ๐ we start with a few constants. SMSS is the Sender Maximum Segment Size from [RFC5681], for example 1460 bytes. CWND_INIT is the Initial Congestion Window (IW) from Section 3.1 of RFC 5681, for example 4380 bytes. CWND_TIMEDOUT is the congestion window after a timeout that means data got lost, and it's 1*SMSS (for example, 1460 bytes) ๐ซถ
Then the session information context gets some extra variables. It's giving bookkeeping energy ๐ง
- CWND: the congestion window, initialized to CWND_INIT;
- SSTHRESH: the slow start threshold, initialized to positive infinity;
- ACKED_BYTES_ACCUMULATOR: a count of acknowledged bytes, initialized to 0;
- ACKED_BYTES_THIS_PACKET: a count of acknowledged bytes observed in the current packet;
- PRE_ACK_OUTSTANDING: the number of bytes outstanding in the network before processing any acknowledgements in the current packet;
- ANY_LOSS: an indication of whether any loss has been detected in the current packet;
- ANY_NAKS: an indication of whether any negative acknowledgements have been detected in the current packet;
- ANY_ACKS: an indication of whether any acknowledgement chunks have been received in the current packet.
FASTGROW_ALLOWED tells you whether the congestion window is allowed to grow at the normal rate or a slower one ๐ It's false if a Time Critical Reverse Notification has been received on this session within the last 800 milliseconds (Sections 2.2.4 and 3.5.2.1), or if a Time Critical Forward Notification has been sent on ANY session in the last 800 milliseconds. Otherwise it's true. The vibe is "chill, we can grow fast" ๐ธ
TC_SENT is simpler. It's true if a Time Critical Forward Notification has been sent on this session within the last 800 milliseconds. ๐
Next, implement the method in Section 3.6.2.6 to manage transmission timeouts, including setting the TIMEOUT_ALARM. Then, when you get notified that the TIMEOUT_ALARM fired, run the function in Figure 24 ๐ฅ
on TimeoutNotification(WAS_LOSS):
set SSTHRESH to MAX(SSTHRESH, CWND * 3/4).
set ACKED_BYTES_ACCUMULATOR to 0.
if WAS_LOSS is true:
set CWND to CWND_TIMEDOUT.
else:
set CWND to CWND_INIT.
Figure 24: Pseudocode for Handling a Timeout Notification
Before you process each received packet in this session, do these four things, in this order, no skipping ๐
- Set ANY_LOSS to false;
- Set ANY_NAKS to false;
- Set ACKED_BYTES_THIS_PACKET to 0; and
- Set PRE_ACK_OUTSTANDING to S_OUTSTANDING_BYTES.
Then, as the packet gets processed, the notifications set the flags. On notification of loss (Section 3.6.2.5), set ANY_LOSS to true. On notification of negative acknowledgement (Section 3.6.2.5), set ANY_NAKS to true. On notification of acknowledgement of data (Section 3.6.2.4), set ANY_ACKS to true and add the count of acknowledged bytes to ACKED_BYTES_THIS_PACKET ๐ฆ
Once every chunk in the packet has been processed for this session, it's time for the main event. Run the function in Figure 25 ๐
if ANY_LOSS is true:
if (TC_SENT is true) OR (PRE_ACK_OUTSTANDING > 67200 AND \
FASTGROW_ALLOWED is true):
set SSTHRESH to MAX(PRE_ACK_OUTSTANDING * 7/8, CWND_INIT).
else:
set SSTHRESH to MAX(PRE_ACK_OUTSTANDING * 1/2, CWND_INIT).
set CWND to SSTHRESH.
set ACKED_BYTES_ACCUMULATOR to 0.
else if (ANY_ACKS is true) AND (ANY_NAKS is false) AND \
(PRE_ACK_OUTSTANDING >= CWND):
set var INCREASE to 0.
var AITHRESH.
if FASTGROW_ALLOWED is true:
if CWND < SSTHRESH:
set INCREASE to ACKED_BYTES_THIS_PACKET.
else:
add ACKED_BYTES_THIS_PACKET to ACKED_BYTES_ACCUMULATOR.
set AITHRESH to MIN(MAX(CWND / 16, 64), 4800).
while ACKED_BYTES_ACCUMULATOR >= AITHRESH:
subtract AITHRESH from ACKED_BYTES_ACCUMULATOR.
add 48 to INCREASE.
else FASTGROW_ALLOWED is false:
if CWND < SSTHRESH AND TC_SENT is true:
set INCREASE to CEIL(ACKED_BYTES_THIS_PACKET / 4).
else:
var AITHRESH_CAP.
if TC_SENT is true:
set AITHRESH_CAP to 2400.
else:
set AITHRESH_CAP to 4800.
add ACKED_BYTES_THIS_PACKET to ACKED_BYTES_ACCUMULATOR.
set AITHRESH to MIN(MAX(CWND / 16, 64), AITHRESH_CAP).
while ACKED_BYTES_ACCUMULATOR >= AITHRESH:
subtract AITHRESH from ACKED_BYTES_ACCUMULATOR.
add 24 to INCREASE.
set CWND to MAX(CWND + MIN(INCREASE, SMSS), CWND_INIT).
Figure 25: Pseudocode for Congestion Window Adjustment after Processing a Packet
Author's Address ๐
Michael C. Thornburgh
Adobe Systems Incorporated
345 Park Avenue
San Jose, CA 95110-2704
US
Phone: +1 408 536 6000
EMail: mthornbu@adobe.com
URI: http://www.adobe.com/
๐ฆ where this page comes from
- ietf: rfc7016 2013-11
- ๐ girlified by claude-haiku-5-5 on : a language model's rewrite of the original, under its terms