RFC 7016

Adobe's Secure Real-Time Media Flow Protocol

๐Ÿ“… published November 2013, from ietf

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 (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 address
    • 2: 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 chunkLength field 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/Padding
  • 0xff: 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 ๐Ÿ’–

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 the cookieEcho field that follows ๐Ÿ“
  • cookieEcho: the cookie from the Responder Hello, copied over unaltered ๐Ÿช
  • certLength: a VLU giving the length, in bytes, of the initiatorCertificate field that follows ๐Ÿ“
  • initiatorCertificate: the initiator's identity credentials ๐Ÿชช
  • skicLength: a VLU giving the length, in bytes, of the sessionKeyInitiatorComponent field 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 the signature field ๐Ÿ“
  • signature: the initiator's digital signature over the initiatorSignedParameters, 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 the sessionKeyResponderComponent field 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 the signature field ๐Ÿ“
  • signature: the responder's digital signature over the responderSignedParameters (more on that below), made according to the Cryptography Profile โœ๏ธ
  • responderSignedParameters: the responderSignedParametersPortion (the payload of this chunk up to the signature field) glued together with the sessionKeyInitiatorComponent from 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 which TS_RX was 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 the F_OUTSTANDING_BYTES of 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, initially 0;
  • 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_SENT and S_KEYING_SENT, the open state S_OPEN, the closing states S_NEARCLOSE and S_FARCLOSE_LINGER, and the closed states S_CLOSED and S_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:

  1. If the Canonical Endpoint Discriminator for the responder certificate matches the Canonical Endpoint Discriminator of another existing session in the S_KEYING_SENT or S_OPEN states, 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,
  2. Move to the S_KEYING_SENT state. Set DESTADDR, 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:

  1. Compute, construct, and send an RHello as though the FIHello was an IHello received from the indicated reply address; or
  2. Construct and send an Implied Redirect (Section 2.3.5) to the FIHello's reply address; or
  3. 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:

  1. If the address from which this IIKeying was received corresponds to an opening session in the S_IHELLO_SENT or S_KEYING_SENT state, perform the special processing at Glare (Section 3.5.1.3); otherwise,
  2. If the address from which this IIKeying was received corresponds to a session in the S_OPEN state, then:
    1. If the receiver was the Responder for the S_OPEN session and the session identifier, certificate, and Session Key Initiator Component are identical to those of the S_OPEN session, this IIKeying is a retransmission, so resend the S_OPEN session's RIKeying using the Default Session Key as specified below; otherwise,
    2. If the certificate from this IIKeying does not override the certificate of the S_OPEN session, ignore this IIKeying; otherwise,
    3. The certificate from this IIKeying overrides the certificate of the S_OPEN session; this is a new opening session from the same identity, and the existing S_OPEN session is stale. Move the existing S_OPEN session to S_CLOSED and abort all of its flows (signaling exceptions to the user), then continue processing this IIKeying.

Otherwise,

  1. 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_OPEN state, with the far-endpoint address DESTADDR taken 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 ๐ŸŽ€.

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:

  1. 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,
  2. 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:
    1. 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,
    2. Calculate TS_RX_ELAPSED_TICKS to be the number of whole 4-millisecond periods in TS_RX_ELAPSED; then
    3. Calculate TS_ECHO = (TS_RX + TS_RX_ELAPSED_TICKS) MODULO 65536; then
    4. 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.

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:

  1. Set TS_ECHO_RX to timestampEcho;
  2. Calculate RTT_TICKS = (current send timestamp - timestampEcho) MODULO 65536;
  3. If RTT_TICKS is greater than 32767, the measurement is invalid, so discard this measurement; otherwise,
  4. Calculate RTT = RTT_TICKS * 4 milliseconds;
  5. If SRTT has a value, then calculate new values of RTTVAR and SRTT:
    1. RTT_DELTA = | SRTT - RTT |;
    2. RTTVAR = ((3 * RTTVAR) + RTT_DELTA) / 4;
    3. SRTT = ((7 * SRTT) + RTT) / 8.
  6. If SRTT has no value, then set SRTT = RTT and RTTVAR = RTT / 2;
  7. Set MRTO = SRTT + 4 * RTTVAR + 200 milliseconds;
  8. 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 ๐Ÿญ:

  1. Calculate ERTO_BACKOFF = ERTO * 1.4142;
  2. Calculate ERTO_CAPPED to be ERTO_BACKOFF or 10 seconds, whichever is less;
  3. 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:

  1. Set MOB_TX_TS to the current time;
  2. 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, and MOB_SECRET; and
  3. 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:

  1. Set MOB_RX_TS to the timestamp in the message; and
  2. Set DESTADDR to 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 the TRANSMIT_SIZE of each entry in SEND_QUEUE where entry.IN_FLIGHT is 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 states F_CLOSING and F_COMPLETE_LINGER; and the closed state F_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) ๐Ÿงš

  1. 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;
  2. If the first entry in the SEND_QUEUE is not abandoned, set FSN to entry.SEQUENCE_NUMBER - 1; otherwise,
  3. 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,
  4. 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:

  1. Starting at the head of the SEND_QUEUE, find the first eligible fragment entry;
  2. 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);
  3. If present, set chunk.flowID to flow.F_FLOW_ID;
  4. If present, set chunk.sequenceNumber to entry.SEQUENCE_NUMBER;
  5. If present, set chunk.fsnOffset to entry.SEQUENCE_NUMBER - FSN;
  6. Set chunk.fragmentControl to entry.FRA;
  7. Set chunk.abandon to entry.ABANDONED;
  8. If entry.SEQUENCE_NUMBER equals flow.F_FINAL_SN, set chunk.final to true; else set chunk.final to false;
  9. 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;
  10. If entry.ABANDONED is true, set chunk.userData to empty; otherwise, set chunk.userData to entry.DATA;
  11. 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:
  12. Set entry.IN_FLIGHT to true;
  13. Set entry.EVER_SENT to true;
  14. Set entry.NAK_COUNT to 0;
  15. Set entry.SENT_ABANDONED to entry.ABANDONED;
  16. Set entry.TRANSMIT_SIZE to the size of the assembled chunk, including the chunk header;
  17. Assemble this chunk into the packet; and
  18. 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:

  1. Set PRE_ACK_OUTSTANDING_BYTES to flow.F_OUTSTANDING_BYTES;
  2. Set flow.STARTUP_OPTIONS to empty;
  3. Set flow.RX_BUFFER_SIZE to chunk.bufferBytesAvailable;
  4. 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
  5. 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:

  1. Set WAS_LOSS = false;
  2. For each sending flow in the session, and for each entry in that flow's SEND_QUEUE:
    1. If entry.IN_FLIGHT is true, set WAS_LOSS = true; and
    2. Set entry.IN_FLIGHT to false.
  3. If WAS_LOSS is true, perform ERTO backoff (Section 3.5.2.2); and
  4. 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:

  1. 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;
  2. Set the EXCEPTION flag to true; and
  3. For each entry in SEND_QUEUE, set entry.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:

  1. Move to the F_CLOSING state;
  2. If the SEND_QUEUE is not empty, AND the tail entry of the SEND_QUEUE has a sequence number of NEXT_SN - 1, AND the tail entry.EVER_SENT is false, set F_FINAL_SN to entry.SEQUENCE_NUMBER; else
  3. The SEND_QUEUE is empty, OR the tail entry does not have a sequence number of NEXT_SN - 1, OR the tail entry.EVER_SENT is true: enqueue a new SEND_QUEUE entry with entry.SEQUENCE_NUMBER = flow.NEXT_SN, entry.FRA = 0, and entry.ABANDONED = true, and set flow.F_FINAL_SN to entry.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; and
    • 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").
  • BUFFERED_SIZE: the sum of the lengths of each fragment in RECV_BUFFER plus 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 states RF_REJECTED and RF_COMPLETE_LINGER; and the closed state RF_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:

  1. Set temporary variables METADATA, ASSOCIATED_FLOWID, and ASSOCIATION to each have no value;
  2. Create a new receiving flow context in this session, setting its RF_FLOW_ID to the flow ID encoded in the opening User Data chunk, and set to the RF_OPEN state;
  3. If the opening User Data chunk encodes a User's Per-Flow Metadata option (Section 2.3.11.1.1), set METADATA to option.userMetadata;
  4. If the opening User Data chunk encodes a Return Flow Association option (Section 2.3.11.1.2), set ASSOCIATED_FLOWID to option.flowID;
  5. If METADATA has no value, the receiver MUST reject the flow (Section 3.6.3.7), moving it to the RF_REJECTED state;
  6. If ASSOCIATED_FLOWID has a value, then if there is no sending flow in the same session with a flow ID of ASSOCIATED_FLOWID, the receiver MUST reject the flow, moving it to the RF_REJECTED state; otherwise, set ASSOCIATION to the indicated sending flow;
  7. If ASSOCIATION indicates a sending flow, AND that sending flow's state is not F_OPEN, the receiver MUST reject this receiving flow, moving it to the RF_REJECTED state;
  8. 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_REJECTED state;
  9. If this new receiving flow is still RF_OPEN, then notify the user that a new receiving flow has opened, including the METADATA and, if present, the ASSOCIATION, and set flow.BUFFER_CAPACITY according to the user;
  10. Perform the normal data processing (Section 3.6.3.2) for the opening User Data chunk; and
  11. Set this session's ACK_NOW to 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 ๐Ÿ’–:

  1. If chunk.flowID doesn't point to an existing receiving flow in the same session that's in the RF_OPEN, RF_REJECTED, or RF_COMPLETE_LINGER state, run the steps of Section 3.6.3.1 ("Startup") to spin up a brand new receiving flow ๐Ÿงš;
  2. Grab the receiving flow context for the flow that chunk.flowID points at ๐Ÿ”;
  3. Set flow.SHOULD_ACK to true ๐Ÿซถ;
  4. 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 the RF_REJECTED state ๐Ÿšซ (this one is not up for debate, bestie ๐Ÿ’€);
  5. If the flow is not in the RF_OPEN state, set session.ACK_NOW to true ๐Ÿ“ฃ;
  6. If flow.PREV_RWND has a value and that value is less than 2 blocks, set session.ACK_NOW to true ๐Ÿ‘€;
  7. If chunk.abandon is true, set session.ACK_NOW to true ๐Ÿ™…โ€โ™€๏ธ;
  8. If flow.SEQUENCE_SET has any gaps (that is, if it doesn't contain every sequence number from 0 through and including the highest sequence number in the set), set session.ACK_NOW to true ๐Ÿ•ณ๏ธ;
  9. If flow.SEQUENCE_SET contains chunk.sequenceNumber, then this chunk is a duplicate: set session.ACK_NOW to true ๐Ÿ‘ฏโ€โ™€๏ธ;
  10. If flow.SEQUENCE_SET doesn't contain chunk.sequenceNumber, AND chunk.final is true, AND flow.RF_FINAL_SN has no value, then set flow.RF_FINAL_SN to chunk.sequenceNumber, and set session.ACK_NOW to true ๐Ÿ;
  11. If the flow is in the RF_OPEN state, AND flow.SEQUENCE_SET doesn't contain chunk.sequenceNumber, AND chunk.abandon is false, then create a new RECV_BUFFER entry for this chunk's data and set entry.SEQUENCE_NUMBER to chunk.sequenceNumber, entry.DATA to chunk.userData, and entry.FRA to chunk.fragmentControl, and insert this new entry into flow.RECV_BUFFER ๐Ÿ“ฅ;
  12. Add to flow.SEQUENCE_SET the range of sequence numbers from 0 through and including the chunk.forwardSequenceNumber derived field ๐ŸŒˆ;
  13. Add chunk.sequenceNumber to flow.SEQUENCE_SET ๐Ÿงฉ;
  14. If flow.SEQUENCE_SET now has any gaps, set session.ACK_NOW to true ๐Ÿซฃ;
  15. If session.ACK_NOW is false and session.DELACK_ALARM is not set, set session.DELACK_ALARM to fire in 200 milliseconds; and โฐ
  16. Attempt delivery of completed messages in this flow's RECV_BUFFER to 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:

  1. If entry.FRA is 0-whole, deliver entry.DATA to the user, and remove this entry from RECV_BUFFER; otherwise,
  2. If entry.FRA is 2-end or 3-middle, this entry belongs to an abandoned segment, so remove and discard this entry from RECV_BUFFER; otherwise,
  3. Entry.FRA is 1-begin. Let LAST_ENTRY be the last RECV_BUFFER entry that is part of this message segment (LAST_ENTRY can be entry if the segment has only one fragment so far). Then:
    1. If LAST_ENTRY.FRA is 2-end, this segment is a complete message, so concatenate the DATA fields 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 from RECV_BUFFER; otherwise,
    2. If LAST_ENTRY.SEQUENCE_NUMBER is less than CSN, this segment is incomplete and abandoned, so remove and discard the entries for this segment from RECV_BUFFER; otherwise,
    3. LAST_ENTRY.SEQUENCE_NUMBER is equal to CSN and LAST_ENTRY.FRA is not 2-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 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_OPEN state;
  • On receipt of a User Data chunk where, before processing the chunk, the SEQUENCE_SET of 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_SET does 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_CAPACITY is not at least 1024 bytes greater than BUFFERED_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:

  1. If the flow's state is RF_REJECTED, first assemble a Flow Exception Report chunk (Section 2.3.16) for flow.flowID;
  2. Choose the acknowledgement chunk type that most compactly encodes flow.SEQUENCE_SET;
  3. Use the method described in Section 3.6.3.5 ("Flow Control") to determine the value for the acknowledgement chunk's bufferBlocksAvailable field.

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:

  1. Choose a receiving flow that is ready to send an acknowledgement;
  2. If there is no such flow, there is no work to do, set ACK_NOW to false, set RX_DATA_PACKETS to 0, clear the DELACK_ALARM, and stop; otherwise,
  3. Start a new packet;
  4. 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;
  5. Set flow.SHOULD_ACK to false;
  6. Set flow.PREV_RWND to the bufferBlocksAvailable field of the included acknowledgement chunk;
  7. Attempt to piggyback acknowledgements for any other flows that are ready to send an acknowledgement into the packet, as described below; and
  8. 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:

  1. Assemble an acknowledgement for the flow;
  2. If the acknowledgement cannot be included in the packet without exceeding the path MTU, the packet is full; stop. Otherwise,
  3. Include the acknowledgement in the packet;
  4. Set flow.SHOULD_ACK to false;
  5. Set flow.PREV_RWND to the bufferBlocksAvailable field of the included acknowledgement chunk; and
  6. If there are no longer any receiving flows in the session that are ready to send an acknowledgement, set session.ACK_NOW to false, set session.RX_DATA_PACKETS to 0, and clear session.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:

  1. If BUFFERED_SIZE is greater than or equal to BUFFER_CAPACITY, set ADVERTISE_BYTES to 0;
  2. If BUFFERED_SIZE is less than BUFFER_CAPACITY, set ADVERTISE_BYTES to BUFFER_CAPACITY - BUFFERED_SIZE;
  3. Set ADVERTISE_BLOCKS to CEIL(ADVERTISE_BYTES / 1024);
  4. If ADVERTISE_BLOCKS is 0, AND BUFFER_CAPACITY is greater than 0, AND delivery to the user is not suspended, set ADVERTISE_BLOCKS to 1; and
  5. Set the acknowledgement's bufferBlocksAvailable field to ADVERTISE_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:

  1. If chunk.flowID doesn't belong to a receiving flow in the same session in the RF_OPEN, RF_REJECTED, or RF_COMPLETE_LINGER state, ignore this Buffer Probe; otherwise,
  2. Retrieve the receiving flow context for the flow indicated by chunk.flowID; then
  3. Set flow.SHOULD_ACK to true; and
  4. Set session.ACK_NOW to 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:

  1. Move to the RF_REJECTED state;
  2. Discard all entries in flow.RECV_BUFFER, as they are no longer relevant;
  3. If the user rejected the flow, set flow.EXCEPTION_CODE to the exception code indicated by the user; otherwise, the flow was rejected automatically by the implementation, so the exception code is 0;
  4. Set flow.SHOULD_ACK to true; and
  5. Set session.ACK_NOW to 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

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 ๐ŸŽ€

  1. Set ANY_LOSS to false;
  2. Set ANY_NAKS to false;
  3. Set ACKED_BYTES_THIS_PACKET to 0; and
  4. 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