RFC 4997
Formal Notation for RObust Header Compression (ROHC-FN)
RFC 4997: Formal Notation for RObust Header Compression (ROHC-FN) ๐
okay bestie, this is RFC 4997, and it's giving "we built a spec-writing language for compressed headers" ๐ซถ It's an IETF Network Working Group doc from July 2007, and it's on the Standards Track โจ so it's a real internet standards-track protocol doc, not just somebody's opinion ๐
The authors are R. Finking from Siemens/Roke Manor Research and G. Pelletier from Ericsson ๐ The title block doesn't list any RFCs it obsoletes or updates, so nothing is getting replaced or patched here ๐ธ
Status of This Memo ๐
So this doc is a standards track protocol spec for the whole Internet community, and it's literally asking you to chime in with discussion and suggestions for improvements ๐ฆ For the current standardization state and status, check the current edition of the "Internet Official Protocol Standards" (STD 1). And distribution of this memo is unlimited, so share away ๐
Copyright Notice ยฉ๏ธ
Copyright (C) The IETF Trust (2007). ๐
Abstract ๐
This doc defines Robust Header Compression - Formal Notation (ROHC-FN), which is a formal notation for specifying field encodings for compressed formats when you're defining new profiles in the ROHC framework ๐ง ROHC-FN comes with a library of encoding methods that show up a lot in ROHC profiles, so it can help make future profile work way less painful โจ
Table of Contents ๐๏ธ
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 4
2. Terminology . . . . . . . . . . . . . . . . . . . . . . . . . 4
3. Overview of ROHC-FN . . . . . . . . . . . . . . . . . . . . . 5
3.1. Scope of the Formal Notation . . . . . . . . . . . . . . . 6
3.2. Fundamentals of the Formal Notation . . . . . . . . . . . 7
3.2.1. Fields and Encodings . . . . . . . . . . . . . . . . . 7
3.2.2. Formats and Encoding Methods . . . . . . . . . . . . . 9
3.3. Example Using IPv4 . . . . . . . . . . . . . . . . . . . . 11
4. Normative Definition of ROHC-FN . . . . . . . . . . . . . . . 13
4.1. Structure of a Specification . . . . . . . . . . . . . . . 13
4.2. Identifiers . . . . . . . . . . . . . . . . . . . . . . . 14
4.3. Constant Definitions . . . . . . . . . . . . . . . . . . . 15
4.4. Fields . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4.4.1. Attribute References . . . . . . . . . . . . . . . . . 17
4.4.2. Representation of Field Values . . . . . . . . . . . . 17
4.5. Grouping of Fields . . . . . . . . . . . . . . . . . . . . 17
4.6. "THIS" . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4.7. Expressions . . . . . . . . . . . . . . . . . . . . . . . 19
4.7.1. Integer Literals . . . . . . . . . . . . . . . . . . . 20
4.7.2. Integer Operators . . . . . . . . . . . . . . . . . . 20
4.7.3. Boolean Literals . . . . . . . . . . . . . . . . . . . 20
4.7.4. Boolean Operators . . . . . . . . . . . . . . . . . . 20
4.7.5. Comparison Operators . . . . . . . . . . . . . . . . . 21
4.8. Comments . . . . . . . . . . . . . . . . . . . . . . . . . 21
4.9. "ENFORCE" Statements . . . . . . . . . . . . . . . . . . . 22
4.10. Formal Specification of Field Lengths . . . . . . . . . . 23
4.11. Library of Encoding Methods . . . . . . . . . . . . . . . 24
4.11.1. uncompressed_value . . . . . . . . . . . . . . . . . . 24
4.11.2. compressed_value . . . . . . . . . . . . . . . . . . . 25
4.11.3. irregular . . . . . . . . . . . . . . . . . . . . . . 26
4.11.4. static . . . . . . . . . . . . . . . . . . . . . . . . 27
4.11.5. lsb . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.11.6. crc . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.12. Definition of Encoding Methods . . . . . . . . . . . . . . 29
4.12.1. Structure . . . . . . . . . . . . . . . . . . . . . . 30
4.12.2. Arguments . . . . . . . . . . . . . . . . . . . . . . 37
4.12.3. Multiple Formats . . . . . . . . . . . . . . . . . . . 38
4.13. Profile-Specific Encoding Methods . . . . . . . . . . . . 40
5. Security Considerations . . . . . . . . . . . . . . . . . . . 41
6. Contributors . . . . . . . . . . . . . . . . . . . . . . . . . 41
7. Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . 41
8. References . . . . . . . . . . . . . . . . . . . . . . . . . . 42
8.1. Normative References . . . . . . . . . . . . . . . . . . . 42
8.2. Informative References . . . . . . . . . . . . . . . . . . 42
Appendix A. Formal Syntax of ROHC-FN . . . . . . . . . . . . . . 43
Appendix B. Bit-level Worked Example . . . . . . . . . . . . . . 45
B.1. Example Packet Format . . . . . . . . . . . . . . . . . . 45
B.2. Initial Encoding . . . . . . . . . . . . . . . . . . . . . 46
B.3. Basic Compression . . . . . . . . . . . . . . . . . . . . 47
B.4. Inter-Packet Compression . . . . . . . . . . . . . . . . . 48
B.5. Specifying Initial Values . . . . . . . . . . . . . . . . 50
B.6. Multiple Packet Formats . . . . . . . . . . . . . . . . . 51
B.7. Variable Length Discriminators . . . . . . . . . . . . . . 53
B.8. Default Encoding . . . . . . . . . . . . . . . . . . . . . 55
B.9. Control Fields . . . . . . . . . . . . . . . . . . . . . . 56
B.10. Use of "ENFORCE" Statements as Conditionals . . . . . . . 59
1. Introduction ๐ฆ
ROHC-FN is a formal notation built to help define ROHC [RFC4995] header compression profiles ๐ Earlier profiles were written with English text plus ASCII Box notation, and that was sometimes unclear and ambiguous. It showed how hard it gets to define complex structures and encodings for compressed formats that way ๐ญ The main goal of the Formal Notation is a more rigorous way to define header formats (compressed and uncompressed) and the relationships between them. No other formal notation exists that meets these requirements, so ROHC-FN is going for it ๐
ROHC-FN also comes with a library of encoding methods that get used a lot in ROHC profiles. That means you can specify a new profile with the formal notation without rebuilding this whole library from scratch ๐ง Basically, an encoding method is a two-way mapping between uncompressed data and compressed data ๐ฅ
2. Terminology ๐
- Compressed format: A compressed format is a list of fields that binds encodings to the fields they compress. You can combine one or more compressed formats to represent an entire compressed header format ๐
- Context: Context is info about the current (de)compression state of the flow. A context for a specific field can be uninitialised, or it can hold a set of one or more values for the field's attributes that the compression algorithm defines, where a value may come from the field's attributes in a previous packet. See also the more generalised definition in Section 2.2 of [RFC4995] ๐ธ
- Control field: Control fields get sent from a ROHC compressor to a ROHC decompressor, but they aren't part of the uncompressed header itself ๐ซถ
- Encoding method, encodings: Encoding methods are two-way relations that can be applied to compress and decompress fields of a protocol header ๐
- Field: The protocol header is split into a set of contiguous bit patterns called fields. Each field is defined by a collection of attributes that say its value and its length in bits, for both the compressed and uncompressed headers. How the header gets divided into fields is specific to a profile's definition, and the field divisions don't have to match the ones in the specification(s) for the header being compressed ๐ฆ
- Library of encoding methods: The library of encoding methods holds a bunch of commonly used encoding methods for compressing header fields โจ
- Profile: A ROHC [RFC4995] profile describes how to compress a certain protocol stack. Each profile has a set of formats (like uncompressed and compressed formats) plus a set of rules that control compressor and decompressor behaviour ๐
- ROHC-FN specification: The specification of the set of formats of a ROHC profile, written using ROHC-FN ๐
- Uncompressed format: An uncompressed format is a list of fields that gives the order of the fields to be compressed, for a contiguous set of bits whose bit layout matches the protocol header being compressed ๐
3. Overview of ROHC-FN ๐
This section gives you the overview of ROHC-FN ๐ง It also explains how ROHC-FN can be used to specify the compression of header fields as part of a ROHC profile ๐
3.1. Scope of the Formal Notation ๐
This section explains how the formal notation connects to the ROHC framework and to specifications of ROHC profiles ๐ธ
The ROHC framework [RFC4995] sets the general principles for robust header compression. It defines the concept of a profile, which makes ROHC a general platform for different compression schemes ๐ฆ It sets link layer requirements, and in particular negotiation requirements, for all ROHC profiles. It defines common functions like Context Identifiers (CIDs), padding, and segmentation โจ It also defines common formats (IR, IR-DYN, Feedback, Add-CID, etc.), and finally it defines a generic, profile independent, feedback mechanism ๐
A ROHC profile describes how to compress a certain protocol stack. For example, ROHC profiles exist for RTP/UDP/IP and many other protocol stacks ๐
At a high level, each ROHC profile has a set of formats (defining the bits to be transmitted) plus a set of rules that control compressor and decompressor behaviour ๐ฅ The formats are there to define how to compress and decompress headers. They define one or more compressed versions of each uncompressed header, and they also define the inverse: how to relate a compressed header back to the original uncompressed header ๐ซถ
The set of formats will typically define compression of headers relative to a context of field values from previous headers in a flow. That way the overall compression gets better by using the redundancies between headers of successive packets ๐ So, on top of defining the formats, a profile has to:
- specify how to manage the context for both the compressor and the decompressor,
- define when and what to send in feedback messages, if any, from decompressor to compressor,
- outline compression principles to make the profile robust against bit errors and dropped packets.
All of this is needed to keep the compressor and decompressor contexts consistent with each other, while still getting the best possible compression performance ๐
ROHC-FN is designed to help specify the compressed formats that, when put together based on the profile definition, make up the formats used in a ROHC profile ๐ง It offers a library of encoding methods for compressing fields, plus a mechanism for combining those methods to create compressed formats tailored to a specific protocol stack โจ
The scope of ROHC-FN is limited to specifying the relationship between the compressed and uncompressed formats. To form a complete profile specification, the control logic for the profile behaviour has to be defined by other means ๐
Part 1 of 12 is done ๐ Part 2 coming up next ๐
3.2. Fundamentals of the Formal Notation ๐ฆ
okay so this formal notation is built on two fundamental elements, and they're the core of the whole thing ๐
- Fields and their encodings ๐: these define the mapping between a header's uncompressed form and its compressed form.
- Encoding methods โจ: these define the way headers get broken down into fields. They define lists of uncompressed fields and the lists of compressed fields they map onto.
we'll unpack both of these below, bestie ๐ง
3.2.1. Fields and Encodings ๐
headers are made up of fields! version number, header length, and sequence number are all fields used in real protocols, no cap ๐ธ
fields have attributes, and attributes describe different things about the field. for example:
field.ULENGTH
that one tells you the uncompressed length of the field ๐. a field is said to have a value attribute (a compressed value or an uncompressed value) if the corresponding length attribute is greater than zero. see Section 4.4 for more details on field attributes ๐
the relationship between the compressed and uncompressed attributes of a field gets specified with encoding methods, using this notation:
field =:= encoding_method;
in that definition, =:= means "is encoded by" ๐ฆ. but here's the twist: it is not an assignment from the right side to the left side! it's a two-way mapping between the compressed and uncompressed attributes. one definition covers both the compression and the decompression operation through two-way matching. iconic, honestly ๐ฅ
two-way matching is a binary operation that tries to make the operands match. it's kind of like unification in logic. the operands represent one unspecified data object and one specified object, and values can be matched from either operand ๐งฉ
here's how it plays out:
- during compression, the uncompressed attributes are already defined. the given encoding matches the compressed attributes against them.
- during decompression, the compressed attributes are already defined, so the uncompressed attributes get matched to them using the given encoding method.
so compression AND decompression are defined by a single field definition. main character energy ๐
so an encoding method (including any parameters it's given) creates a reversible binding between the attributes of a field ๐. at the compressor, a format can be used if a set of bindings that works for all the attributes in all its fields can be found. at the decompressor, the operation gets reversed using the same bindings, and the attributes in each field are filled in according to those bindings. if the binding for an attribute fails, decoding fails โ
for example, the "static" encoding method creates a binding between the attribute for the uncompressed value of the field and the matching value of the field in the context ๐
- for the compressor, the
"static"binding is successful when both the context value and the uncompressed value are the same. if the two values differ, the binding fails. - for the decompressor, the
"static"binding succeeds only if a valid context entry containing the value of the uncompressed field exists. otherwise, the binding fails.
both the compressed and uncompressed forms of each field are represented as a string of bits, most significant bit first, of the length given by the length attribute ๐. the bit string is the binary representation of the field's value attribute, modulo 2^length, where length is the length attribute of the field. BUT (and this is important, bestie ๐) that's only how the bits get exchanged between the compressor and the decompressor, designed for maximum compression efficiency. the FN itself uses the full range of integers. see Section 4.4.2 for further details โจ
3.2.2. Formats and Encoding Methods ๐ง
the ROHC-FN gives you a library of commonly used encoding methods ๐. encoding methods can be defined in plain English, or with a formal definition made up of things like a collection of expressions (Section 4.7) and ENFORCE statements (Section 4.9) ๐
ROHC-FN also has mechanisms for combining fields and their encoding methods into higher level encoding methods, following a well-defined structure. it's kind of like functions and procedures in a regular programming language ๐ซถ. this lets complexity get broken down into manageable parts. new encoding methods are defined at the top level of a profile, and then they can be used inside the definition of other higher level encoding methods, and so on ๐ธ
here's an example:
new_encoding_method // This block is an encoding method
{
UNCOMPRESSED { // This block is an uncompressed format
field_1 [ 16 ];
field_2 [ 32 ];
field_3 [ 48 ];
}
CONTROL { // This block defines control fields
ctrl_field_1;
ctrl_field_2;
}
DEFAULT { // This block defines default encodings
// for specified fields
ctrl_field_2 =:= encoding_method_2;
field_1 =:= encoding_method_1;
}
COMPRESSED format_0 { // This block is a compressed format
field_1;
field_2 =:= encoding_method_2;
field_3 =:= encoding_method_3;
ctrl_field_1 =:= encoding_method_4;
ctrl_field_2;
}
COMPRESSED format_1 { // This block is a compressed format
field_1;
field_2 =:= encoding_method_3;
field_3 =:= encoding_method_4;
ctrl_field_2 =:= encoding_method_5;
ctrl_field_3 =:= encoding_method_6; // This is a control field
// with no uncompressed value
}
}
so in that example, the encoding method being defined is called new_encoding_method ๐. the section headed UNCOMPRESSED shows the order of the fields in the uncompressed header (the uncompressed header format). the number of bits in each field is in square brackets. next up is the CONTROL section, which defines two control fields ๐๏ธ. then comes the DEFAULT section, which sets default encoding methods for two of the fields (see below) ๐. last but not least, there are two alternative compressed formats, each in a section headed COMPRESSED ๐ฆ
the fields that show up in the compressed formats are one of these:
- fields that occur in the uncompressed format; or
- control fields that have an uncompressed value and that occur in the
CONTROLsection; or - control fields that do not have an uncompressed value, and so are defined as part of the compressed format.
every one of these formats is built around a field list ๐. a field list defines the fields in the format and also the order they appear in. for the DEFAULT and CONTROL sections, field order is not significant ๐
on top of field order, the field list can also specify bindings for any or all of the fields in it. fields with no bindings defined for them get bound using the default bindings from the DEFAULT section (see Section 4.12.1.5) ๐
fields in the compressed format have the same names as they do in the uncompressed format. if there are any fields that exist ONLY in the compressed format but DO have an uncompressed value, they must be declared in the CONTROL section of the encoding method definition (see Section 4.12.1.3 for more details on defining control fields) โญ
fields with no uncompressed value don't appear in an UNCOMPRESSED field list, and they don't have to appear in the CONTROL field list either. instead, they just get declared in the compressed field lists where they're used ๐ง
in the example above, every field that appears in the compressed format is also found in the uncompressed format or in the control field list, EXCEPT for ctrl_field_3. that's allowed because ctrl_field_3 has no "uncompressed" value at all. fields like a checksum on the compressed information fall into this category ๐
3.3. Example Using IPv4 โจ
okay so this section is a walkthrough example ๐ it builds the notation for an encoding method that can compress one well-known header, the IPv4 header [RFC791]. it's giving tutorial, bestie ๐
first up, we need the overall shape of the IPv4 header. we call the encoding method "ipv4_header" (more on defining encoding methods in Section 4.12). it looks like this:
ipv4_header
{
the opening brace is where the definition starts (see Section 4.12). ๐ง
everything inside those braces is local to "ipv4_header". this scoping keeps track of which fields belong to which formats, and it's super useful for complex protocol stacks with several headers that reuse the same field names (see Section 4.2). main character organisation energy ๐
next, we list the fields in the uncompressed IPv4 header. that's the uncompressed format the encoding method will use to define one or more compressed formats. we write it in ROHC-FN like this:
UNCOMPRESSED {
version [ 4 ];
header_length [ 4 ];
dscp [ 6 ];
ecn [ 2 ];
length [ 16 ];
id [ 16 ];
reserved [ 1 ];
dont_frag [ 1 ];
more_fragments [ 1 ];
offset [ 13 ];
ttl [ 8 ];
protocol [ 8 ];
checksum [ 16 ];
src_addr [ 32 ];
dest_addr [ 32 ];
}
the number in square brackets is each field's width. this part is just there to help you follow along ๐ indicating field lengths like this is optional, because the width can usually be worked out from the encoding used to compress and decompress the field in a given format. this bit is formally defined in Section 4.10.
now the compressed format ๐ธ this includes the encoding for each field that maps between the compressed and uncompressed forms. most of these encodings come from the ROHC-FN library (see Section 4.11). since this is about showing how the notation works, not the best possible way to compress IPv4 headers, the example only uses three encoding methods.
the first is "uncompressed_value" (defined in Section 4.11.1). it can compress any field whose uncompressed length and value are fixed, or can be worked out with an expression. no compressed bits have to be sent, because the decompressor can rebuild the field from its known size and value. this one is used on five fields in the IPv4 header:
COMPRESSED {
header_length =:= uncompressed_value(4, 5);
version =:= uncompressed_value(4, 4);
reserved =:= uncompressed_value(1, 0);
offset =:= uncompressed_value(13, 0);
more_fragments =:= uncompressed_value(1, 0);
the first parameter is the uncompressed field's length in bits, and the second is its integer value ๐
the order of fields in the compressed format doesn't have to match the order in the uncompressed format. โจ
the second is "irregular" (defined in Section 4.11.3). it can encode any field where both uncompressed attributes (ULENGTH and UVALUE) are defined, and where ULENGTH is either fixed or can be calculated with an expression. it's the fail-safe option for when no other encoding method fits. every bit of the uncompressed field also shows up in the compressed form, so this one doesn't compress anything at all ๐ญ but it's always there when you need it ๐ซถ
src_addr =:= irregular(32);
dest_addr =:= irregular(32);
length =:= irregular(16);
id =:= irregular(16);
ttl =:= irregular(8);
protocol =:= irregular(8);
dscp =:= irregular(6);
ecn =:= irregular(2);
dont_frag =:= irregular(1);
the third is only used with the uncompressed format defined above for the IPv4 header: "inferred_ip_v4_header_checksum".
checksum =:= inferred_ip_v4_header_checksum [ 0 ];
}
}
"inferred_ip_v4_header_checksum" is different from the other two because it isn't in the ROHC-FN library. its definition could go either in the formal notation as part of the profile definition itself (see Section 4.12), or in plain English text (see Section 4.13). ๐ฆ
in this example, "inferred_ip_v4_header_checksum" is a specific encoding method that calculates the IP checksum from the rest of the header values. just like "uncompressed_value", it sends no compressed bits, since the decompressor can reconstruct the field value. that's shown explicitly by writing a length of 0 in square brackets for the checksum field in the compressed format. the notation is optional, since the encoding method itself is defined as sending zero compressed bits, but it helps the reader to include it (see Section 4.10 for details on this part of the notation). โจ
finally, the format definition ends with a closing brace. the example now defines a compressed format that can represent the entire compressed IPv4 header. it also gives enough information for an implementation to build the compressed format from an uncompressed one (compression), and to go the other way (decompression). iconic ๐
4. Normative Definition of ROHC-FN ๐
this section is the normative definition of ROHC-FN. ROHC-FN is a declarative language that is referentially transparent, with no side effects. so whenever you evaluate an expression, obtaining its value has no other effects. the same expression is guaranteed to have the same value wherever it shows up in the notation, and you can always swap it for its value in any format it appears in (subject to the scope rules for identifiers in Section 4.2). ๐ฅ
the formal notation describes the structure of the formats and how their uncompressed and compressed forms relate. it doesn't describe how compression and decompression are actually carried out. ๐
in a few places in this section, text inside angle brackets is used as a descriptive placeholder. those brackets are only there for the reader. the brackets and their contents are not part of the notation. ๐
4.1. Structure of a Specification ๐
the specification of a ROHC profile's compressed formats written in ROHC-FN is called a ROHC-FN specification. ROHC-FN specifications are case sensitive and are written in the 7-bit ASCII character set (as defined in [RFC2822]). they consist of a sequence of zero or more constant definitions (Section 4.3), an optional global control field list (Section 4.12.1.3), and one or more encoding method definitions (Section 4.12).
encoding methods can be defined using the formal notation, or they can be predefined encoding methods. โจ
encoding methods defined with the formal notation give one or more uncompressed formats for the uncompressed header, plus one or more compressed formats. these formats are linked to each other by "fields", and each field describes a certain part of an uncompressed and/or compressed header. besides the formats, each encoding method may contain control fields, initial values, and default field encodings sections. a field's attributes are bound by using an encoding method on it and/or by using "ENFORCE" statements (Section 4.9) within the formats. each of these is terminated by a semi-colon. ๐ฉท
predefined encoding methods aren't defined in the formal notation. instead, each gets a short textual reference saying where it's defined. it's not necessary to define the library of encoding methods in this document this way, because their definition is implicit in how the formal notation is used. ๐ง
4.2. Identifiers ๐
in ROHC-FN, identifiers are used for any of the following:
- encoding methods
- formats
- fields
- parameters
- constants
all identifiers can be any length and can contain any combination of alphanumeric characters and underscores, within the restrictions in this section.
all identifiers must start with an alphabetic character.
it's illegal to have two or more identifiers that differ only in capitalisation, in the same scope.
all letters in identifiers for constants must be upper case.
it's also illegal to use any of these as identifiers (including alternative capitalisations):
"false","true""ENFORCE","THIS","VARIABLE""ULENGTH","UVALUE""CLENGTH","CVALUE""UNCOMPRESSED","COMPRESSED","CONTROL","INITIAL", or"DEFAULT"
format names can't be referred to in the notation, even though they count as identifiers. (see Section 4.12.3.1 for more on format names.) ๐
every identifier used in ROHC-FN has a scope. the scope defines the parts of the specification where the identifier applies and where you can refer to it from. if an identifier has a global scope, it applies throughout the specification that contains it and can be referred to from anywhere in it. if an identifier has a local scope, it only applies to the encoding method where it's defined, and it can't be referenced from outside that local scope. because local scopes are separate, the same identifier can be used in several local scopes to refer to different items.
every instance of an identifier within its scope refers to the same item. one identifier can never refer to different items within the same scope. for this reason, if an identifier has a global scope, it can't also be used separately in a local scope, since a globally-scoped identifier already applies in all local scopes.
the identifiers for each encoding method and each constant all have a global scope. each format and field also has an identifier. format and field identifiers have a local scope, except for global control fields, which have a global scope. so it's illegal for a format or field to share an identifier with another format or field in the same scope, or with an encoding method or a constant (since those have global scope). ๐ซถ
note that format names (see Section 4.12.3.1) count as identifiers, but they aren't referred to in the notation. they're mainly for the reader's benefit. ๐ธ
4.3. Constant Definitions โจ
okay so constants are the easy-mode values ๐ You make one with the "=" operator, and the name has to be ALL CAPS:
SOME_CONSTANT = 3;
The right-hand side is an expression (see Section 4.7), and it has to come out as a constant value. That means every term is either a constant or a literal, and it can't change depending on which header is being compressed ๐
Constants are global ๐ธ Define them at the top level, outside any encoding method definition. They're completely the same as the value they point to, so you can swap one in anywhere. Field attributes can change from packet to packet, but constants keep the same value for every single packet ๐ซถ
4.4. Fields ๐งฑ
Fields are the building blocks of a ROHC-FN specification. Headers get split up into fields, and each field can have two forms: a compressed form and an uncompressed form. Both forms get sent between the compressor and the decompressor the same way, as an unsigned string of bits, with the most significant bit first ๐ฆ
The properties of the compressed form come from an encoding method and/or "ENFORCE" statements. Together, those fully define how the uncompressed and compressed forms relate, by spelling out how the field's attributes connect to each other โจ
The notation has four field attributes, two for the uncompressed form and two for the compressed form. Here's the lineup:
uncompressed attributes of a field:
- "UVALUE" and "ULENGTH",
compressed attributes of a field:
- "CVALUE" and "CLENGTH".
The two value attributes hold the numerical values of the field. "UVALUE" is the numerical value of the uncompressed form, and "CVALUE" is the numerical value of the compressed form. Those numbers come from reading the bit-string representations as bit strings, most significant bit first ๐ข
The two length attributes tell you how many bits long the matching bit string is. "ULENGTH" is for the uncompressed form and "CLENGTH" is for the compressed form ๐
Attributes are undefined until they're bound to a value, and once they are, they become defined. If two conflicting bindings get given for the same field attribute, the bindings fail, along with the (combination of) formats where those bindings were defined. Yikes ๐
Uncompressed attributes don't always match up with an aspect of the uncompressed header. Some fields don't come from the uncompressed header at all. They're control fields ๐
4.4.1. Attribute References ๐
To formally refer to an attribute of a field, you write the field's name, then a ".", then the attribute's identifier:
rtp_seq_number.UVALUE
That example gives you the uncompressed value of the rtp_seq_number field. The main reason you reference attributes is to use them in expressions, which get explained in Section 4.7 ๐
4.4.2. Representation of Field Values ๐
Fields are represented as bit strings. The bit string gets calculated from the value attribute ("val") and the length attribute ("len"). It's the binary representation of val % (2 ^ len).
For example, say a field's "CLENGTH" is 8 and its "CVALUE" is -1. The compressed representation would be -1 % (2 ^ 8), which equals -1 % 256, which equals 255, or 11111111 in binary. Main character energy ๐
ROHC-FN supports the full range of integers for use in expressions (see Section 4.7). The formats, meaning the bits actually exchanged between the compressor and the decompressor, are always in the form above ๐ง
4.5. Grouping of Fields ๐
The order of fields in a "COMPRESSED" field list (Section 4.12.1.2) doesn't have to match the order in an "UNCOMPRESSED" field list (Section 4.12.1.1). That means you can group together any number of fields that sit next to each other in a "COMPRESSED" format, so they all get encoded with a single encoding method. The group goes immediately to the left of "=:=", standing in for a single field name ๐
You notate a group with a colon-separated list of the fields in it. Say an uncompressed header has two non-contiguous fields that are really two halves of one sequence number, here's how that looks:
grouping_example
{
UNCOMPRESSED {
minor_seq_num; // 12 bits
other_field; // 8 bits
major_seq_num; // 4 bits
}
COMPRESSED {
other_field =:= irregular(8);
major_seq_num
: minor_seq_num =:= lsb(3, 0);
}
}
The grouped fields get handed to the encoding method as one contiguous group of bits, made by concatenating the fields in the order they're listed in the group. The most significant bit of the combined field is the most significant bit of the first field in the list, and the least significant bit of the combined field is the least significant bit of the last field in the list. Bit math, but make it iconic ๐ฆ
Last part: the length attributes of the combined field equal the sum of the matching length attributes for all the fields in the group ๐งฎ
4.6. "THIS" ๐ช
Inside the definition of an encoding method, you can refer to the field the method is encoding (the group of contiguous bits) with the keyword "THIS" ๐
It's super handy for getting at the attributes of the field being encoded. For example, it's often useful to know the total uncompressed length of the uncompressed format being encoded:
THIS.ULENGTH
4.7. Expressions ๐
ROHC-FN includes the usual infix style of expressions, with parentheses "(" and ")" for grouping. Expressions can be made up of any of the components described in the subsections that follow ๐
The semantics of expressions are generally similar to the expressions in the ANSI-C programming language [C90]. The definitive list of expressions in ROHC-FN comes in the next subsections, but here are some examples of how ROHC-FN expressions differ from ANSI-C:
- There is no limit on the range of integers.
- "x ^ y" evaluates to x raised to the power of y. It has a precedence higher than
*,/and%, but lower than unary-, and it's right to left associative. - There is no comma operator.
- There are no "modify" operators (no assignment operators and no increment or decrement).
- There are no bitwise operators.
Expressions can refer to any of the attributes of a field (as described in Section 4.4), to any defined constant (see Section 4.3), and to encoding method parameters, if any are in scope (see Section 4.12) ๐ง
If any of the attributes, constants, or parameters used in an expression are undefined, the value of the expression is undefined. Undefined expressions make the environment they're used in (for example, the compressed format) fail whenever a defined value is needed. Defined values are required for all compressed attributes of fields that appear in the compressed format. Defined values are not required for all uncompressed attributes of fields that appear in the uncompressed format. It's up to the profile creator to decide what happens to unbound field attributes in this case. Worth noting: in that situation, transparency of the compression process is lost. In other words, the decompressor won't be able to reproduce the original header ๐ญ
Expressions can't be used as encoding methods directly, because they don't completely characterise a field. An expression only specifies a single value, but a field is made up of several values, which are its attributes. For example, the following is illegal:
tcp_list_length =:= (data_offset + 20) / 4;
There's only enough information there to define a single attribute of "tcp_list_length". Formally it makes no sense, but you could intuitively read it as defining the "UVALUE" attribute. That would still leave the length of the uncompressed field undefined at the decompressor, though. So that usage is prohibited ๐ซ
4.7.1. Integer Literals ๐ข
Integers can be written as decimal values, binary values (prefixed by "0b"), or hexadecimal values (prefixed by "0x"). Negative integers get a "-" sign in front. For example, "10", "0b1010", and "-0x0a" are all valid integer literals, with the values 10, 10, and -10 respectively ๐
4.7.2. Integer Operators ๐งฎ
These "integer" operators are available. They take integer arguments and return an integer result:
^, for exponentiation. "x ^ y" returns the value of "x" to the power of "y".*,/for multiplication and division. "x * y" returns the product of "x" and "y". "x / y" returns the quotient, rounded down to the next integer (the next one towards negative infinity).+,-for addition and subtraction. "x + y" returns the sum of "x" and "y". "x - y" returns the difference.%for modulo. "x % y" returns "x" modulo "y"; x - y * (x / y).
4.7.3. Boolean Literals โ
The boolean literals are "false" and "true" โโ
4.7.4. Boolean Operators ๐
These "boolean" operators are available. They take boolean arguments and return a boolean result:
&&, for logical "and". Returns true if both arguments are true. Returns false otherwise.||, for logical "or". Returns true if at least one argument is true. Returns false otherwise.!, for logical "not". Returns true if its argument is false. Returns false otherwise.
4.7.5. Comparison Operators ๐ฏ
These "comparison" operators are available. They take integer arguments and return a boolean result:
==,!=, for equality and its negative. "x == y" returns true if x is equal to y. Returns false otherwise. "x != y" returns true if x is not equal to y. Returns false otherwise.<,>, for less than and greater than. "x < y" returns true if x is less than y. Returns false otherwise. "x > y" returns true if x is greater than y. Returns false otherwise.>=,<=, for greater than or equal and less than or equal, the inverse functions of<,>. "x >= y" returns false if x is less than y. Returns true otherwise. "x <= y" returns false if x is greater than y. Returns true otherwise. ๐
4.8. Comments โจ
okay so ROHC-FN specs can have free English text thrown in to explain why something was done a certain way, clarify what the notation means, or just elaborate on a point. it's giving "leave yourself notes" energy ๐
The FN uses end-of-line comments, marked with //. Anything between the // and the end of the line has no formal meaning, so it's basically a sticky note ๐ฆ For example:
//-----------------------------------------------------------------
// IR-REPLICATE header formats
//-----------------------------------------------------------------
// The following fields are included in all of the IR-REPLICATE
// header formats:
//
UNCOMPRESSED {
discriminator; // 8 bits
tcp_seq_number; // 32 bits
tcp_flags_ecn; // 2 bits
Comments don't change what the notation formally means, but they make it way easier to read ๐ Using them is optional, totally your call.
Comments can clear things up for the reader, and they serve different purposes for implementers. So don't treat them as an afterthought when you write a ROHC-FN spec ๐ง They should match the normative part of the spec, no contradictions allowed, bestie ๐ญ
4.9. "ENFORCE" Statements ๐ฅ
The ENFORCE statement lets you attach predicates to a format. All of them must be satisfied for the format to succeed. ENFORCE kinda has a vibe with encoding methods: an encoding method binds several field attributes at once, while an ENFORCE usually binds just one. Honestly, every binding an encoding method makes can be written as a bunch of ENFORCE statements ๐คฏ Here's one that binds the UVALUE attribute of a field to 5:
ENFORCE(field.UVALUE == 5);
An ENFORCE statement must only be used inside a field list (see Section 4.12). It tries to make the expression true for the format it belongs to.
There's a shortcut form for binding length attributes with [ and ], see Section 4.10 โจ
Like an encoding method, an ENFORCE only works in a format if the binding is actually achievable. The example above would be unusable if the same format also bound the field with uncompressed_value encoding that gave it a UVALUE other than 5. Major clash ๐
An ENFORCE takes a boolean expression. You can use it to assert that the expression is true, so a particular format gets picked from a list of possible formats in an encoding method (see Section 4.12), or just to bind an expression like the one above. The general form is therefore:
ENFORCE(<boolean expression>);
The expression can end up in one of three conditions:
- The boolean expression evaluates to false, in which case the local scope of the format containing the
ENFORCEcan't be used. - The boolean expression evaluates to true, in which case the binding is created and successful.
- The value of the boolean expression is undefined. In this case, the binding is also created and successful.
In all three cases, any undefined term gets bound by the expression ๐ธ Basically, an ENFORCE is either acting as an assignment (condition 3) or testing whether a format is usable (conditions 1 and 2). Iconic multitasking.
4.10. Formal Specification of Field Lengths ๐ฆ
In a lot of the examples, each field is followed by a comment saying how long it is. Adding that is optional, but it's super helpful for the reader ๐ซถ Just remember that comments have no formal meaning, however useful they are.
One of the most common uses of ENFORCE (see Section 4.9) is pinning down the length of a field in a header. Doing that with ENFORCE is formal but kinda clunky to read, so there's an abbreviated form that's easy to read and has formal meaning โจ
You can put an expression for a field's length in square brackets right after the field in a format. If the field can have several alternative lengths, list the expressions as a comma separated list inside the brackets. For example:
field_1 [ 4 ];
field_2 [ a+b, 2 ];
field_3 =:= lsb(16, 16) [ 26 ];
Which attribute gets bound depends on whether the field is in a COMPRESSED, UNCOMPRESSED, or CONTROL field list (see Section 4.12.1 and its subsections). In a COMPRESSED field list, the field's CLENGTH attribute is bound. In UNCOMPRESSED and CONTROL field lists, the field's ULENGTH attribute is bound. Abbreviated ENFORCE statements are not allowed in DEFAULT sections (see Section 4.12.1.5), so the notation above can't go in a DEFAULT section ๐
โโ๏ธ But if it appeared in an UNCOMPRESSED or CONTROL section, it would be the same as:
field_1; ENFORCE(field_1.ULENGTH == 4);
field_2; ENFORCE((field_2.ULENGTH == 2)
|| (field_2.ULENGTH == a+b));
field_3 =:= lsb(16, 16); ENFORCE(field_3.ULENGTH == 26);
There's a special case for fields whose length is variable and the notator either doesn't want to, or can't, write as an expression. Use the keyword VARIABLE for that:
variable_length_field [ VARIABLE ];
Formally, this puts no restriction on the field length. It maps onto any positive integer or to zero ๐ So the length has to be defined somewhere else (see the final paragraphs of Section 4.12.1.1 and Section 4.12.1.2). That could be in the notation or in the English text of the profile the FN lives in. Inside the square brackets, VARIABLE can be used as a term in an expression, just like any other term. For example:
field [ 8 * (5 + VARIABLE) ];
This defines a field whose length is a whole number of octets and at least 40 bits (5 octets). Mathy and cute ๐งฎ
4.11. Library of Encoding Methods ๐
There's a bunch of common techniques for compressing header fields built into the ROHC-FN library, so you can reuse them when you make new ROHC-FN specs. Their notation is described below ๐
You can also, or instead, define your own encoding methods in a ROHC-FN spec, either with the formal notation (see Section 4.12) or with a textual definition (see Section 4.13).
4.11.1. uncompressed_value ๐ธ
The uncompressed_value encoding method is for header fields whose uncompressed value can be defined with a mathematical expression (constant values count too). It's defined like this:
uncompressed_value(len, val) {
UNCOMPRESSED {
field;
ENFORCE(field.ULENGTH == len);
ENFORCE(field.UVALUE == val);
}
COMPRESSED {
field;
ENFORCE(field.CLENGTH == 0);
}
}
For example, the IPv6 header version number is a 4-bit field that's always 6:
version =:= uncompressed_value(4, 6);
Here's another one, using an expression to calculate the length:
padding =:= uncompressed_value(nbits - 8, 0);
That expression uses an encoding method parameter called nbits, which in this example says how many significant bits there are in the data, and that tells you how many pad bits to use. See Section 4.12.2 for more on encoding method parameters ๐ง
4.11.2. compressed_value ๐
The compressed_value encoding method is for defining fields in compressed formats that have no counterpart in the uncompressed format (i.e., control fields). It can specify compressed fields whose value can be defined with a mathematical expression (constant values included). It's defined like this:
compressed_value(len, val) {
UNCOMPRESSED {
field;
ENFORCE(field.ULENGTH == 0);
}
COMPRESSED {
field;
ENFORCE(field.CLENGTH == len);
ENFORCE(field.CVALUE == val);
}
}
One use is padding in a compressed format:
pad_to_octet_boundary =:= compressed_value(3, 0);
A more common use is a discriminator field, so you can tell apart different compressed formats inside an encoding method (see Section 4.12). For convenience, the notation has a shortcut for compressed_value in the form of a binary string. You put the binary string in single quotes, the field's CLENGTH binds to the number of bits in the string, and its CVALUE binds to the value the string represents. For example:
discriminator =:= '01101';
That has exactly the same meaning as:
discriminator =:= compressed_value(5, 13);
4.11.3. irregular ๐ฅ
The irregular encoding method is for encoding a field in the compressed format with a bit pattern identical to the uncompressed field. It's defined like this:
irregular(len) {
UNCOMPRESSED {
field;
ENFORCE(field.ULENGTH == len);
}
COMPRESSED {
field;
ENFORCE(field.CLENGTH == len);
ENFORCE(field.CVALUE == field.UVALUE);
}
}
For example, the TCP header checksum is a 16-bit field that doesn't follow any predictable pattern from one header to the next, so it can't be compressed ๐ค
tcp_checksum =:= irregular(16);
Note that the length doesn't have to be constant. An expression can derive the length of the field from the value of another field, which is very giga-brain of it ๐ง
4.11.4. static ๐
The static encoding method compresses a field whose length and value are the same as in a previous header in the flow. In other words, the field completely matches an existing entry in the context:
field =:= static;
The field's UVALUE and ULENGTH attributes bind to their respective values in the context, and the CLENGTH attribute is bound to zero.
Since the field value matches a previous field value, the whole field can be rebuilt from the context. So it gets compressed to zero bits and doesn't show up in the compressed format at all ๐ฆ
For example, the TCP source port is a field whose value doesn't change from one packet to the next within a given flow:
src_port =:= static;
4.11.5. lsb โจ
okay so the least significant bits method, "lsb", is giving efficiency ๐ It compresses a field whose value is only a little bit different from the value already stored in the context. Instead of sending the whole field value, you just send its least significant bits ๐ฆ
field =:= lsb(<num_lsbs_param>, <offset_param>);
Here, "num_lsbs_param" is how many least significant bits you're using, and "offset_param" is the interpretation interval offset, which we'll get to in a sec ๐ง
The "num_lsbs_param" parameter binds to the "CLENGTH" attribute. The "UVALUE" attribute binds to whatever value inside the interval has least significant bits that match the "CVALUE" attribute ๐ธ The "ULENGTH" can be worked out from info already stored in the context, so you don't have to send it.
Here's a real-life example, the TCP sequence number:
tcp_sequence_number =:= lsb(14, 8192);
This takes up 14 bits, and it can communicate any value between 8192 lower than the value stored in context and 8191 above it. The way that works is so clever ๐คฏ
The interpretation interval can be described as a function of a value stored in the context, ref_value, and of num_lsbs_param:
f(context_value, num_lsbs_param) = [ref_value - offset_param,
ref_value + (2^num_lsbs_param - 1) - offset_param]
where offset_param is an integer.
<-- interpretation interval (size is 2^num_lsbs_param) -->
|---------------------------+----------------------------|
lower ref_value upper
bound bound
where:
lower bound = ref_value - offset_param
upper bound = ref_value + (2^num_lsbs_param-1) - offset_param
So the "lsb" method can compress any field whose value sits between the lower and upper bounds of the interpretation interval, inclusive ๐ Now the offset is the secret sauce. If offset_param = 0, the field value can only stay the same or go up compared to the reference value ref_value. If offset_param = -1, it can only go up. And if offset_param = 2^num_lsbs_param, it can only go down. Iconic range control, no cap ๐
The compressed field takes up the number of bits you specified in the compressed format, which is num_lsbs_param ๐
Now here's the tricky part ๐ The compressor might not know the exact reference value that the decompressor has stored in its context, because some packets that would have updated the context might have gotten lost or damaged along the way. BUT, from feedback it gets back, or from making assumptions, the compressor can narrow down a candidate set of values. Then it picks a format that uses "lsb" encoding, with suitable values for num_lsbs_param and offset_param, so that no matter which context value in that candidate set the decompressor ends up using, the decompression still comes out correct. Main character problem-solving โจ If that's not possible, the "lsb" method just fails, and the compressor usually ends up picking a less efficient compressed format instead ๐ญ How the compressor decides which reference values to store and keep in its candidate set is outside the scope of this notation, so we're not getting into that one today ๐ซถ
4.11.6. crc
The "crc" encoding method gives you a CRC calculated over a block of data ๐ฅ The algorithm used to calculate the CRC is the one specified in [RFC4995]. The "crc" method takes a few parameters:
- the number of bits for the CRC (crc_bits),
- the bit-pattern for the polynomial (bit_pattern),
- the initial value for the CRC register (initial_value),
- the value of the block of data, represented using either the "UVALUE" or "CVALUE" attribute of a field (block_data_value); and
- the size in octets of the block of data (block_data_length).
So it looks like this:
field =:= crc(<num_bits>, <bit_pattern>, <initial_value>,
<block_data_value>, <block_data_length>);
When you're specifying the bit pattern for the polynomial, each bit represents the coefficient for the matching term in the polynomial. The highest order term is always there (by definition), so you don't need to specify it in the bit pattern ๐ That means a CRC polynomial with n terms in it gets represented by a bit pattern with n-1 bits set. Little trick, big payoff ๐
The CRC is calculated in least significant bit (LSB) order.
For example:
// 3 bit CRC, C(x) = x^0 + x^1 + x^3
crc_field =:= crc(3, 0x6, 0xF, THIS.CVALUE, THIS.CLENGTH);
Using the "THIS" keyword (see Section 4.6) like in the example above is typical when you're using "crc" encoding. For instance, when it's used in the encoding method for an entire header, it makes the CRC get calculated over all the fields in the header ๐ฆ
4.12. Definition of Encoding Methods
New encoding methods can be defined in a formal specification ๐ These compose groups of individual fields into one contiguous block.
Encoding methods have names and may have parameters. They can also be used in the same way as any other encoding method from the library of encoding methods. Since they can contain references to other encoding methods, you can break complicated formats down into manageable pieces in a hierarchical fashion. Very organised, very bestie ๐ง
This section describes the various features used to define new encoding methods.
4.12.1. Structure
This is the simplest form of defining an encoding method: you specify a single encoding. For example:
compound_encoding_method
{
UNCOMPRESSED {
field_1; // 4 bits
field_2; // 12 bits
}
COMPRESSED {
field_2 =:= uncompressed_value(12, 9); // 0 bits
field_1 =:= irregular(4); // 4 bits
}
}
The above begins with the new method's identifier, "compound_encoding_method". The definition of the method then follows inside curly brackets, "{" and "}". The first item in the definition is the "UNCOMPRESSED" field list, which gives the order of the fields in the uncompressed format. After that comes the compressed format field list ("COMPRESSED"). This list gives the order of the fields in the compressed format and also gives the encoding method for each field ๐
In the example, both formats list each field exactly once. However, sometimes you need to specify more than one binding for a given field, which means it shows up more than once in the field list. In that case, it's the first occurrence of the field in the list that tells you its position in the field order. The later occurrences only specify binding information, not field order information ๐
The different components of this example are described in more detail below. Other components that can be used in defining encoding methods are also defined after that ๐ธ
4.12.1.1. Uncompressed Format - "UNCOMPRESSED"
The uncompressed field list is defined by "UNCOMPRESSED", which specifies the fields of the uncompressed format in the order they appear in the uncompressed header ๐ The sum of the lengths of each individual uncompressed field in the list must be equal to the length of the field being encoded. Finally, the representation of the uncompressed format, described using the list of fields in the "UNCOMPRESSED" section (for which compressed formats are being defined), always consists of one single contiguous block of bits ๐ซถ
In the example above in Section 4.12.1, the uncompressed field list is "field_1", followed by "field_2". This means a field being encoded by this method gets divided into two subfields, "field_1" and "field_2". The total uncompressed length of these two fields therefore equals the length of the field being encoded:
field_1.ULENGTH + field_2.ULENGTH == THIS.ULENGTH
The example only has two fields, but any number of fields may be used. This relationship applies to however many fields you actually use. Any arrangement of fields that efficiently describes the content of the uncompressed header may be chosen. It doesn't have to match the one in the specifications for the protocol header being compressed โจ
For example, a protocol might have a header with a 16-bit sequence number, but its sessions tend to be short-lived. That would mean the high bits of the sequence number are almost always constant. The "UNCOMPRESSED" format could reflect this by splitting the original uncompressed field into two fields: one to represent the almost-always-zero part of the sequence number, and a second to represent the salient part ๐ง
An "UNCOMPRESSED" field list may specify encoding methods in the same way as the "COMPRESSED" field list in the example. Encoding methods specified there are used whenever a packet with that uncompressed format is being encoded. The encoding of a packet with a given uncompressed format can only succeed if all of its encoding methods and "ENFORCE" statements succeed (see Section 4.9) ๐ฅ
The total length of each uncompressed format must always be defined. The length of each of the fields in an uncompressed format must also be defined. This means the bindings in the "UNCOMPRESSED", "COMPRESSED" (see Section 4.12.1.2 below), "CONTROL" (see Section 4.12.1.3 below), "INITIAL" (see Section 4.12.1.4 below), and "DEFAULT" (see Section 4.12.1.5 below) field lists must, between them, define the "ULENGTH" attribute of every field in an uncompressed format. That way there's an unambiguous mapping from the bits in the uncompressed format to the fields listed in the "UNCOMPRESSED" field list. Not optional, bestie ๐
4.12.1.2. Compressed Format - "COMPRESSED"
okay so the compressed header works kinda like the uncompressed field list ๐ the fields show up in whatever order the compressed field list for that format says, and each one gets encoded with its own method. the total compressed length is just all the compressed lengths added together โจ in the example from Section 4.12.1, the encoding methods make the fields zero and 4 bits long, so the total is 4 bits ๐
the order in a "COMPRESSED" field list does not have to match the "UNCOMPRESSED" one ๐ฆ you might wanna reorder things so the compressed header lines up on an octet boundary, or just for other reasons. in the example above it's literally the reverse of the uncompressed order ๐ iconic
the compressed field list says field_1 uses irregular(4), so it's 4 bits in both the compressed and uncompressed formats. field_2 uses uncompressed_value(12, 9), which means it's a fixed value. it's 12 bits wide uncompressed, but its value is always 9, so it compresses down to zero bits ๐ธ
fields like field_2, the ones that compress to zero bits, can sit anywhere in the list without changing the compressed format, because their position doesn't matter ๐ and if the encoding method was defined somewhere else (like in the "UNCOMPRESSED" section), this field could be left out of the "COMPRESSED" section entirely ๐ญ
compound_encoding_method
{
UNCOMPRESSED {
field_1; // 4 bits
field_2 =:= uncompressed_value(12, 9); // 12 bits
}
COMPRESSED {
field_1 =:= irregular(4); // 4 bits
}
}
the total length of each compressed format must always be defined โจ and the length of each field in a compressed format has to be defined too ๐ซถ so the bindings across the "UNCOMPRESSED", "COMPRESSED", "CONTROL" (see Section 4.12.1.3 below), "INITIAL" (see Section 4.12.1.4 below), and "DEFAULT" (see Section 4.12.1.5 below) field lists have to work together to define the CLENGTH attribute of every field. that way there's one clear mapping from the bits in the compressed format to the fields in the "COMPRESSED" field list. no guesswork, bestie ๐
4.12.1.3. Control Fields - "CONTROL"
control fields are defined with the "CONTROL" field list ๐ง this list covers all the fields that aren't in the uncompressed format but still have an uncompressed value (specifically, ones with a ULENGTH greater than zero). they're used to compress the uncompressed fields more efficiently. a control field can show off some commonality between a bunch of uncompressed fields, or track info about the flow that isn't actually in the protocol headers ๐
for example, in IPv4, the IP-ID field can behave differently depending on how the endpoints handle IP-IDs. sometimes it's basically random, and sometimes it follows a predictable sequence ๐ the type of IP-ID behaviour is never sent explicitly in the uncompressed header, so it's kinda hidden tea โ
the good news is a profile can still figure out the behaviour and tweak its compression strategy to match, which makes compression way better โจ to do that, the ROHC-FN specification can add an explicit field that communicates the IP-ID behaviour in the compressed format. that's done by introducing a control field:
ipv4
{
UNCOMPRESSED {
version; // 4 bits
hdr_length; // 4 bits
protocol; // 8 bits
dscp; // 6 bits
ip_ecn_flags; // 2 bits
ttl_hopl; // 8 bits
df; // 1 bit
mf; // 1 bit
rf; // 1 bit
frag_offset; // 13 bits
ip_id; // 16 bits
src_addr; // 32 bits
dst_addr; // 32 bits
checksum; // 16 bits
length; // 16 bits
}
CONTROL {
ip_id_behavior; // 1 bit
:
:
the "CONTROL" field list is basically the same as the "UNCOMPRESSED" field list, but for fields that don't show up in the uncompressed format ๐
it defines a field with the same properties (the same defined attributes, etc.) as the fields that do appear in the uncompressed format ๐ฆ
control fields get initialised with the right encoding methods and/or with ENFORCE statements ๐ธ and you can do that inside the "CONTROL" field list too โจ
for example:
example_encoding_method_definition
{
UNCOMPRESSED {
field_1 =:= some_encoding;
}
CONTROL {
scaled_field;
ENFORCE(scaled_field.UVALUE == field_1.UVALUE / 8);
ENFORCE(scaled_field.ULENGTH == field_1.ULENGTH - 3);
}
COMPRESSED {
scaled_field =:= lsb(4, 0);
}
}
this control field scales a field from the uncompressed format down by a factor of 8 before it gets encoded with lsb. scaling it down makes the lsb encoding way more efficient ๐ฅ main character energy
control fields can also have a global scope! in that case, the declaration has to live outside of any encoding method definition ๐ then they're visible inside any encoding method, which means encoding methods can share info directly with each other ๐ซถ
4.12.1.4. Initial Values - "INITIAL"
so the first packet in a specific format might need to be compressed with static, lsb, or other context-dependent encoding methods, but there's no context yet ๐ญ that's where "INITIAL" comes in. it lets you specify initial bindings for those fields. it looks like this:
INITIAL {
field =:= uncompressed_value(4, 6);
}
that initialises the UVALUE of field to 6, and its ULENGTH to 4 โจ the sneaky part is that unlike every other binding in the formal notation, these ones get applied to the field's context, but only if that context is undefined ๐ it's especially useful for encoding methods that need context to exist, like static or lsb, on the first packet in a flow ๐ง
because the "INITIAL" field list only binds the context, it makes zero sense to give it initial bindings that rely on the context themselves (like lsb). that kind of usage is not allowed ๐ don't do it bestie
4.12.1.5. Default Field Bindings - "DEFAULT"
default bindings can be specified for each field or attribute ๐ the default encoding methods tell you which method to use for a field if no binding is given anywhere else for that field's value. this keeps the format definitions concise, since you don't have to repeat the same encoding method in every format when you're defining multiple formats (see Section 4.12.3) ๐
default bindings are optional and can be given for any combination of fields and attributes that are in scope ๐
the syntax is kinda like what you'd use for a compressed or uncompressed format. the difference is that the order of the fields in the field list doesn't affect the order of the fields in either the compressed or uncompressed format ๐ฆ that's because the field order gets specified separately for each "COMPRESSED" format and each "UNCOMPRESSED" format โจ
here's an example:
DEFAULT {
field_1 =:= uncompressed_value(4, 1);
field_2 =:= uncompressed_value(4, 2);
field_3 =:= lsb(3, -1);
ENFORCE(field_4.ULENGTH == 4);
}
so here default bindings are set for fields 1 to 3, and there's also a default binding for the ULENGTH attribute of field_4 ๐
fields that have a default encoding method don't need their bindings written out in any format's field list that uses that default method ๐ซถ but any format that doesn't use the default method has to explicitly specify a binding for that field's attribute values ๐
if no binding is specified elsewhere for a field's attributes, the default encoding method gets used ๐ธ and if the default method always squishes the field down to zero bits, the field can be left out of the compressed format's field list ๐ same as any other zero-bit field, its position in the list doesn't matter ๐ง
the "DEFAULT" field list can hold default bindings for individual attributes too, using ENFORCE statements ๐ฅ a default for an individual attribute only gets used if there's no binding for that attribute or for the field it belongs to anywhere else. if somewhere else there's an ENFORCE statement binding that attribute, or an encoding method binding the field it belongs to, the default for the attribute is ignored ๐ that's true even if the encoding method doesn't bind that particular attribute from the "DEFAULT" section ๐ญ
one plot twist: an ENFORCE statement elsewhere that only binds the field's length still lets the default bindings get used, except for default ENFORCE statements that bind nothing but the field's length ๐
to make it clearer, using the default bindings from the example above, the first three of these four compressed formats would not use the default binding for field_4.ULENGTH ๐
COMPRESSED format1 {
ENFORCE(field_4.ULENGTH == 3); // set ULENGTH to 3
ENFORCE(field_4.UVALUE == 7); // set UVALUE to 7
}
COMPRESSED format2 {
field_4 =:= irregular(3); // set ULENGTH to 3
}
COMPRESSED format3 {
field_4 =:= '1010'; // set ULENGTH to zero
}
COMPRESSED format4 {
ENFORCE(field_4.UVALUE == 12); // use default ULENGTH
}
the fourth format is the only one that uses the default binding for field_4.ULENGTH ๐
in short, an encoding method's default bindings only get used for formats that don't already specify a binding for the value of all of their fields ๐ for the formats that do use defaults, only the fields and attributes without a binding get looked up in the "DEFAULT" field list โจ and that's the tea, periodt ๐
4.12.2. Arguments
Encoding methods can take arguments that control how compressed fields map to uncompressed ones, bestie ๐ They go right after the method's name, inside parentheses, as a comma-separated list ๐
For example:
poor_mans_lsb(variable_length)
{
UNCOMPRESSED {
constant_bits;
variable_bits;
}
COMPRESSED {
variable_bits =:= irregular(variable_length);
constant_bits =:= static;
}
}
Okay so, every argument takes an individual value, like an integer literal or a field attribute, never a whole field ๐ You can't pass entire fields as arguments, but you can pass each of their attributes, and that works out to the same thing โจ
Also, every binding is two-way ๐ฆ So the arguments aren't really "inputs" to the encoding method. The result of an encoding method can end up binding the parameters passed to it, which is so iconic ๐
For example:
set_to_double(arg1, arg2)
{
CONTROL {
ENFORCE(arg1 == 2 * arg2);
}
}
This encoding method will try to bind the first argument to twice the value of the second ๐ Honestly, this "encoding" method is pathological ๐ Since it doesn't define any fields, it doesn't actually encode anything. A "CONTROL" section is the better tool for this job than "UNCOMPRESSED" ๐ง
4.12.3. Multiple Formats
Encoding methods can also define multiple formats for a single header ๐ That lets the compressor use different methods depending on which one squeezes a particular header the best ๐
For example, a field might have a fixed value most of the time but change every now and then. With a single format, you'd have to encode that field using "irregular" (see Section 4.11.3), even though the value only rarely changes ๐ญ But if you define multiple formats, you get two alternative encodings: one for when the value stays fixed, and one for when it changes โจ
The following sub-sections cover this in more detail ๐ซถ
4.12.3.1. Naming Convention
When compressed formats are defined, they must be defined with the reserved word "COMPRESSED" ๐ Uncompressed formats must be defined with the reserved word "UNCOMPRESSED" ๐ธ After each of these keywords, you can give the format a name. If you don't give the format a name, its name is empty ๐ฆ
Format names follow the syntactic rules for identifiers from Section 4.2, except when the name is empty ๐
Format names must be unique within the scope of the encoding method they belong to ๐ The one exception is the empty name, which can be used for one "COMPRESSED" format and one "UNCOMPRESSED" format ๐ง
4.12.3.2. Format Discrimination
Each compressed format has its own field list โจ A compressor can pick any of these alternative formats to compress a header, as long as the field bindings it uses also work with the uncompressed format ๐ For example, the compressor couldn't choose a compressed format that had a "static" encoding for a field whose "UVALUE" attribute differs from its matching value in the context ๐
More formally, the compressor can choose any combination of an uncompressed format and a compressed format where none of the field's attribute bindings "fail". That means the encoding methods and "ENFORCE" statements (see Section 4.9) that bind their compressed attributes all succeed ๐ If there's more than one successful combination, the compressor can pick any one of them ๐ But if there are no successful combinations, the encoding method "fails" ๐ A format never fails just because it doesn't define the "UVALUE" attribute of a field ๐ซถ A format only fails if it doesn't define one of the compressed attributes of one of the fields in the compressed format, or if it leaves the length of the uncompressed format undefined ๐ญ
Here's the catch: because the compressor gets to choose, the decompressor has to be able to tell apart the different compressed formats the compressor could have picked ๐ A simple way to do this is to give each compressed format a "discriminator" that uniquely identifies that particular "COMPRESSED" format โจ A discriminator is a control field, not something derived from any of the uncompressed field values (see Section 4.11.2) ๐
4.12.3.3. Example of Multiple Formats
Putting it all together, here's a complete example of an encoding method with multiple compressed formats ๐ฆ
example_multiple_formats
{
UNCOMPRESSED {
field_1; // 4 bits
field_2; // 4 bits
field_3; // 24 bits
}
DEFAULT {
field_1 =:= static;
field_2 =:= uncompressed_value(4, 2);
field_3 =:= lsb(4, 0);
}
COMPRESSED format0 {
discriminator =:= '0'; // 1 bit
field_3; // 4 bits
}
COMPRESSED format1 {
discriminator =:= '1'; // 1 bit
field_1 =:= irregular(4); // 4 bits
field_3 =:= irregular(24); // 24 bits
}
}
A few things to notice, babe ๐
field_1andfield_3both have default encoding methods, which get used informat0but are overridden informat1๐ The default encoding method forfield_2isn't overridden, though ๐ธfield_1andfield_2have default encoding methods that compress to zero bits ๐ When these are used informat0, their field names don't show up in the field list ๐งfield_3has an encoding method that doesn't compress to zero bits ๐ญ So even thoughformat0doesn't specify an encoding for it in the field list,field_3still has to appear there to show where it goes in the compressed format ๐ซถ- In this example, every field in the uncompressed format has a default encoding method, but that's not a requirement โจ You can specify defaults for only some of the fields, or for none of them at all ๐
- In this example, all the default encoding methods are on fields from the uncompressed format, but that's not a requirement either ๐ Default encoding methods can also be specified for control fields ๐
4.13. Profile-Specific Encoding Methods
The library of encoding methods that ROHC-FN defines in Section 4.11 is basic and generic ๐ธ When you use a ROHC-FN specification in a ROHC profile, though, you may need some extra encodings that are specific to the particular protocol header being compressed ๐ฆ Things like methods that infer the value of a field from other values, for example โจ
These methods depend on the properties of the protocol being compressed, so they have to be defined inside the profile specification itself ๐ Profile-specific encoding methods can be defined either in ROHC-FN syntax or rigorously in plain text ๐ Either way, the ROHC-FN specification of the profile's formats can refer to them just like any method in the ROHC-FN library ๐ง
Encoding methods that aren't defined in the formal notation get specified by giving their name, followed by a short description of where they're defined, in double quotes, and then a semi-colon ๐
For example:
inferred_ip_v4_header_checksum "defined in RFCxxxx Section 6.4.1";
5. Security Considerations
This document describes a formal notation similar to ABNF [RFC4234], and so it isn't believed to raise any security issues ๐ (Note that ABNF has a completely separate purpose to the ROHC formal notation ๐ซข Don't mix them up, bestie.)
6. Contributors
Richard Price did much of the foundational work on the formal notation ๐ He wrote the initial document describing a formal notation, and this document is based on it ๐
Kristofer Sandlund contributed by applying new ideas to the ROHC-TCP profile, by giving feedback, and by helping resolve different issues throughout the entire development of the notation ๐ซถ
Carsten Bormann provided the translation of the formal notation syntax into ABNF in Appendix A ๐ Carsten also contributed feedback and reviews to validate the notation's completeness and correctness โจ
7. Acknowledgements
A bunch of important concepts and ideas were borrowed from ROHC [RFC3095] ๐ฆ
Huge thanks to Mark West, Eilert Brinkmann, Alan Ford, and Lars-Erik Jonsson for their contributions, reviews, and feedback, which led to major improvements in the notation's readability, completeness, and overall quality ๐
Thanks to Stewart Sadler, Caroline Daniels, Alan Finney, and David Findlay for their reviews and comments ๐ Thanks to Rob Hancock and Stephen McCann for their early work on the formal notation ๐ธ The authors would also like to thank Christian Schmidt, Qian Zhang, Hongbin Liao, and Max Riegel for their comments and valuable input ๐ง
Additional thanks: this document was reviewed during working group last-call by the committed reviewers Mark West, Carsten Bormann, and Joe Touch ๐ฅ Sally Floyd also gave a review at the request of the Transport Area Directors ๐ Thanks as well to Magnus Westerlund for his feedback in preparation for the IESG review ๐ซถ
8. References
8.1. Normative References
[C90]ISO/IEC, "ISO/IEC 9899:1990 Information technology -- Programming Language C", ISO 9899:1990, April 1990. ๐[RFC2822]Resnick, P., Ed., "STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES", RFC 2822, April 2001. ๐[RFC4234]Crocker, D., Ed. and P. Overell, "Augmented BNF for Syntax Specifications: ABNF", RFC 4234, October 2005. ๐[RFC4995]Jonsson, L-E., Pelletier, G., and K. Sandlund, "The RObust Header Compression (ROHC) Framework", RFC 4995, July 2007. ๐
8.2. Informative References
[RFC3095]Bormann, C., Burmeister, C., Degermark, M., Fukushima, H., Hannu, H., Jonsson, L-E., Hakenberg, R., Koren, T., Le, K., Liu, Z., Martensson, A., Miyazaki, A., Svanbro, K., Wiebke, T., Yoshimura, T., and H. Zheng, "RObust Header Compression (ROHC): Framework and four profiles: RTP, UDP, ESP, and uncompressed", RFC 3095, July 2001. ๐[RFC791]University of Southern California, "DARPA INTERNET PROGRAM PROTOCOL SPECIFICATION", RFC 791, September 1981. ๐
Appendix A. Formal Syntax of ROHC-FN โจ
okay so this section is the official grammar for ROHC-FN, written in ABNF [RFC4234]. the start rule is "fnspec", so that's where you begin reading ๐ง
; overall structure
fnspec = S *(constdef S) [globctl S] 1*(methdef S)
constdef = constname S "=" S expn S ";"
globctl = CONTROL S formbody
methdef = id S [parmlist S] "{" S 1*(formatdef S) "}"
/ id S [parmlist S] STRQ *STRCHAR STRQ S ";"
parmlist = "(" S id S *( "," S id S ) ")"
formatdef = formhead S formbody
formhead = UNCOMPRESSED [ 1*WS id ]
/ COMPRESSED [ 1*WS id ]
/ CONTROL / INITIAL / DEFAULT
formbody = "{" S *((fielddef/enforcer) S) "}"
fielddef = fieldgroup S ["=:=" S encspec S] [lenspec S] ";"
fieldgroup = fieldname *( S ":" S fieldname )
fieldname = id
encspec = "'" *("0"/"1") "'"
/ id [ S "(" S expn S *( "," S expn S ) ")"]
lenspec = "[" S expn S *("," S expn S) "]"
enforcer = ENFORCE S "(" S expn S ")" S ";"
; expressions
expn = *(expnb S "||" S) expnb
expnb = *(expna S "&&" S) expna
expna = *(expn7 S ("=="/"!=") S) expn7
expn7 = *(expn6 S ("<"/"<="/">"/">=") S) expn6
expn6 = *(expn4 S ("+"/"-") S) expn4
expn4 = *(expn3 S ("*"/"/"/"%") S) expn3
expn3 = expn2 [S "^" S expn3]
expn2 = ["!" S] expn1
expn1 = expn0 / attref / constname / litval / id
expn0 = "(" S expn S ")" / VARIABLE
attref = fieldnameref "." attname
fieldnameref = fieldname / THIS
attname = ( U / C ) ( LENGTH / VALUE )
litval = ["-"] "0b" 1*("0"/"1")
/ ["-"] "0x" 1*(DIGIT/"a"/"b"/"c"/"d"/"e"/"f")
/ ["-"] 1*DIGIT
/ false / true
; lexical categories
constname = UPCASE *(UPCASE / DIGIT / "_")
id = ALPHA *(ALPHA / DIGIT / "_")
ALPHA = %x41-5A / %x61-7A
UPCASE = %x41-5A
DIGIT = %x30-39
COMMENT = "//" *(SP / HTAB / VCHAR) CRLF
SP = %x20
HTAB = %x09
VCHAR = %x21-7E
CRLF = %x0A / %x0D.0A
NL = COMMENT / CRLF
WS = SP / HTAB / NL
S = *WS
STRCHAR = SP / HTAB / %x21 / %x23-7E
STRQ = %x22
; case-sensitive literals
C = %d67
COMPRESSED = %d67.79.77.80.82.69.83.83.69.68
CONTROL = %d67.79.78.84.82.79.76
DEFAULT = %d68.69.70.65.85.76.84
ENFORCE = %d69.78.70.79.82.67.69
INITIAL = %d73.78.73.84.73.65.76
LENGTH = %d76.69.78.71.84.72
THIS = %d84.72.73.83
U = %d85
UNCOMPRESSED = %d85.78.67.79.77.80.82.69.83.83.69.68
VALUE = %d86.65.76.85.69
VARIABLE = %d86.65.82.73.65.66.76.69
false = %d102.97.108.115.101
true = %d116.114.117.101
Appendix B. Bit-level Worked Example ๐
okay bestie, this appendix is a worked example at the bit level. it shows how a simple ROHC-FN spec describes the compression of real data from an imaginary protocol header ๐ฆ I kept it pretty simple, but it still shows off some of the tricky bits you'll hit when you use the notation. the fields are short so you can actually read the binary without your brain melting ๐ซ
B.1. Example Packet Format
our imaginary header is just 16 bits long and has these fields:
- version number -- 2 bits
- type -- 2 bits
- flow id -- 4 bits
- sequence number -- 4 bits
- flag bits -- 4 bits
so for example, 0101000100010000 means a header with version number one, type one, flow id one, sequence number one, and all flag bits set to zero. iconic ๐
here's an ASCII box diagram of the imaginary header:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
|version| type | flow_id |
+---+---+---+---+---+---+---+---+
| sequence_no | flag_bits |
+---+---+---+---+---+---+---+---+
B.2. Initial Encoding
this is the first definition, based only on the info above ๐
eg_header
{
UNCOMPRESSED {
version_no [ 2 ];
type [ 2 ];
flow_id [ 4 ];
sequence_no [ 4 ];
flag_bits [ 4 ];
}
COMPRESSED initial_definition {
version_no =:= irregular(2);
type =:= irregular(2);
flow_id =:= irregular(4);
sequence_no =:= irregular(4);
flag_bits =:= irregular(4);
}
}
it defines the format really nicely, but it doesn't actually compress anything yet ๐ when we use it on the header above, we get:
Uncompressed header: 0101000100010000
Compressed header: 0101000100010000
that's because every field is marked "irregular", which just means we haven't said anything about how it behaves ๐คทโโ๏ธ
also, since we only have one compressed format and one uncompressed format, it doesn't matter whether the encoding methods are written in the compressed or the uncompressed format. honestly it would make zero difference if we wrote this instead:
eg_header
{
UNCOMPRESSED {
version_no =:= irregular(2);
type =:= irregular(2);
flow_id =:= irregular(4);
sequence_no =:= irregular(4);
flag_bits =:= irregular(4);
}
COMPRESSED initial_definition {
version_no [ 2 ];
type [ 2 ];
flow_id [ 4 ];
sequence_no [ 4 ];
flag_bits [ 4 ];
}
}
B.3. Basic Compression
to get any compression at all, we need to write down more knowledge about the header and how it behaves in a flow โจ for example, we might know these facts about it:
- version number -- indicates which version of the protocol this is: always one for this version of the protocol.
- type -- may take any value.
- flow id -- may take any value.
- sequence number -- can take any value.
- flag bits -- contains three flags, a, b, and c, each of which may be set or clear, and a reserved flag bit, which is always clear (i.e., zero).
we could write that knowledge down like this:
eg_header
{
UNCOMPRESSED {
version_no [ 2 ];
type [ 2 ];
flow_id [ 4 ];
sequence_no [ 4 ];
abc_flag_bits [ 3 ];
reserved_flag [ 1 ];
}
COMPRESSED basic {
version_no =:= uncompressed_value(2, 1) [ 0 ];
type =:= irregular(2) [ 2 ];
flow_id =:= irregular(4) [ 4 ];
sequence_no =:= irregular(4) [ 4 ];
abc_flag_bits =:= irregular(3) [ 3 ];
reserved_flag =:= uncompressed_value(1, 0) [ 0 ];
}
}
with this simple scheme, we've managed to encode that one field is permanently fixed at one, so it has no useful info in it at all ๐ซถ we've also encoded that the last flag bit is always zero, which is equally useless info. both of these facts use the "uncompressed_value" encoding method (see Section 4.11.1).
using this new encoding on the header above, we get:
Uncompressed header: 0101000100010000
Compressed header: 0100010001000
that cuts the amount of data we need to send by roughly 20% ๐ but this encoding doesn't take advantage of how a field's value in one packet relates to its value in later packets ๐ค for example, every header in this sequence gets compressed by the same amount, even though they're clearly similar:
Uncompressed header: 0101000100010000
Compressed header: 0100010001000
Uncompressed header: 0101000101000000
Compressed header: 0100010100000
Uncompressed header: 0110000101110000
Compressed header: 1000010111000
B.4. Inter-Packet Compression
the profile we've defined so far doesn't compress the sequence number or flow ID fields at all, since they can take any value ๐ฅฒ but the value of each of these fields has a really simple relationship to its value in earlier headers:
- the sequence number is unusual -- it increases by three each time,
- the flow_id stays the same -- it always has the same value that it did in the previous header in the flow,
- the abc_flag_bits stay the same most of the time -- they usually have the same value that they did in the previous header in the flow.
the obvious way to write this down looks like this:
// This obvious encoding will not work (correct encoding below)
eg_header
{
UNCOMPRESSED {
version_no [ 2 ];
type [ 2 ];
flow_id [ 4 ];
sequence_no [ 4 ];
abc_flag_bits [ 3 ];
reserved_flag [ 1 ];
}
COMPRESSED obvious {
version_no =:= uncompressed_value(2, 1);
type =:= irregular(2);
flow_id =:= static;
sequence_no =:= lsb(0, -3);
abc_flag_bits =:= irregular(3);
reserved_flag =:= uncompressed_value(1, 0);
}
}
this uses the "static" and "lsb" encoding methods to express the dependency on previous packets (see Section 4.11.4 and Section 4.11.5 respectively). BUT there are a few problems with it, so let's talk through them ๐
firstly, and this is the most important one, the "flow_id" field is marked as "static", which means it doesn't change from packet to packet. however, the notation never says how the value gets communicated in the first place. there's no point saying "it's the same value as last time" if there was never a first time where we defined that value so it can be referred back to. the notation above has no way to do that. the sequence number has the same issue, since there needs to be a way to communicate its initial value too. honestly, apart from the explicit notation for their lengths, even the lengths of these two fields are left undefined ๐ญ this gets solved below, in Appendix B.5.
secondly, the sequence number is sent super efficiently in zero bits, but it's not at all robust against packet loss. if a packet goes missing, there's no way to handle the missing sequence number. when you're communicating sequence numbers, or any other field using "lsb" encoding, a really important thing for the notator to think about is how robust the compressed protocol should be against packet loss. this changes a lot from one protocol stack to another. for this example protocol, we'll assume short, low-overhead flows and say we need to survive the loss of just one packet. we can do that with two bits of "lsb" encoding (one bit isn't enough, since the sequence number increases by three each time -- see Section 4.11.5). this gets addressed below in Appendix B.5 ๐
finally, even though the flag bits are usually the same as in the previous header in the flow, the profile doesn't use that fact at all. since they're sometimes different from the previous header, it isn't safe to say they're always the same, so "static" encoding can't be used on its own. this gets solved later on, through multiple formats, in Appendix B.6 ๐
B.5. Specifying Initial Values โจ
okay so, to tell the decompressor the starting values for fields that get compressed with a context dependent encoding like "static" or "lsb", we use an INITIAL field list ๐ this is super handy for fields whose starting value is fixed and known. like, if we knew the flow always starts with flow_id at 1 and sequence_no at 0, we could write it down like this:
// This encoding will not work either (correct encoding below)
eg_header
{
UNCOMPRESSED {
version_no [ 2 ];
type [ 2 ];
flow_id [ 4 ];
sequence_no [ 4 ];
abc_flag_bits [ 3 ];
reserved_flag [ 1 ];
}
INITIAL {
// set initial values of fields before flow starts
flow_id =:= uncompressed_value(4, 1);
sequence_no =:= uncompressed_value(4, 0);
}
COMPRESSED obvious {
version_no =:= uncompressed_value(2, 1);
type =:= irregular(2);
flow_id =:= static;
sequence_no =:= lsb(2, -3);
abc_flag_bits =:= irregular(3);
reserved_flag =:= uncompressed_value(1, 0);
}
}
bestie, though, this use of INITIAL is no good ๐ญ the starting values of both flow_id and sequence_no change from flow to flow, so hardcoding them just doesn't work. INITIAL is only for fields whose starting value is fixed, which is often the case with control fields ๐
B.6. Multiple Packet Formats ๐ฆ
so, to send the starting values for the sequence number and flow ID fields correctly, and to take advantage of the fact that the flag bits are usually the same as in the previous header, we have to stop using a single format and switch to multiple formats ๐ here we've written the encodings for two of the fields in the uncompressed format, since they'll always be true for uncompressed headers of that format. the rest of the fields, whose encoding can depend on exactly how the header is being compressed, get their encodings in the compressed formats.
eg_header
{
UNCOMPRESSED {
version_no =:= uncompressed_value(2, 1) [ 2 ];
type [ 2 ];
flow_id [ 4 ];
sequence_no [ 4 ];
abc_flag_bits [ 3 ];
reserved_flag =:= uncompressed_value(1, 0) [ 1 ];
}
COMPRESSED irregular_format {
discriminator =:= '0' [ 1 ];
version_no [ 0 ];
type =:= irregular(2) [ 2 ];
flow_id =:= irregular(4) [ 4 ];
sequence_no =:= irregular(4) [ 4 ];
abc_flag_bits =:= irregular(3) [ 3 ];
reserved_flag [ 0 ];
}
COMPRESSED compressed_format {
discriminator =:= '1' [ 1 ];
version_no [ 0 ];
type =:= irregular(2) [ 2 ];
flow_id =:= static [ 0 ];
sequence_no =:= lsb(2, -3) [ 2 ];
abc_flag_bits =:= static [ 0 ];
reserved_flag [ 0 ];
}
}
we've added a discriminator field, so the decompressor can tell which format the compressor picked โจ the format with a "static" flow ID and an "lsb" encoded sequence number is now 5 bits long. and even though we had to add the discriminator, this format is still the same size as the original wrong "obvious" format, because it takes advantage of the fact that the abc flag bits rarely change ๐ธ
BUT the original "basic" format has grown by one bit too, thanks to the new discriminator (that's irregular_format now) ๐ฉ an important thing to think about when making multiple formats is whether each one shows up often enough that the average compressed header gets shorter because of it. like, if the flag bits always changed between packets, the compressed_format could never be used, and all we'd have done is make the "basic" format one bit longer ๐
using the notation above, we now get:
Uncompressed header: 0101000100010000
Compressed header: 00100010001000
Uncompressed header: 0101000101000000
Compressed header: 10100 ; 00100010100000
Uncompressed header: 0110000101110000
Compressed header: 11011 ; 01000010111000
the first header in the stream gets compressed the same way as before, except it now has the extra 1-bit discriminator at the start (0). when a second header shows up with the same flow ID as the first and a sequence number three higher, it can be compressed in one of two ways: either with compressed_format, or the same way as before, with irregular_format ๐ง
fyi, we show every theoretically possible encoding of a header as defined by the ROHC-FN specification, separated by semi-colons. either of the encodings above for each header could come from a valid implementation, but a good implementation would always aim to pick the one that gives the best compression. a good implementation would also think about robustness, so on the second packet it probably wouldn't assume the decompressor has the context it needs to decompress the shorter compressed_format form ๐ซถ
last thing: the fields whose encodings are set in the uncompressed format have zero length when compressed. so their position in the compressed format doesn't matter, and there's no need to write them down when defining the compressed formats. in the next part of the example, they're removed from the compressed formats altogether ๐
B.7. Variable Length Discriminators ๐
okay so suppose we analyze flows of our example protocol and find out that, while successive packets usually have the same flags, when they don't, the packet is almost always a "flags set" packet where all three of the abc flags are set ๐ฅ to encode the flow more efficiently, we need to write a format that reflects this.
now we have three formats in total, which means we need three discriminators to tell them apart. the obvious move is to make the discriminator two bits long and use 00, 01, and 10, for example. but we can do slightly better than that โจ
any uniquely identifiable discriminator will do, so we can use 00, 01, and 1. if the discriminator starts with 1, that's the whole thing. if it starts with 0, the decompressor knows it has to check one more bit to figure out which format it is ๐
be careful with variable length discriminators though ๐ for example, it would be wrong to use 0, 01, and 10 as discriminators, because after reading an initial 0, the decompressor couldn't tell whether the next bit is a second bit of the discriminator or the first bit of the next field in the format. but 0, 10, and 11 would be correct, since the first bit still tells you whether more discriminator bits follow.
this gives us the following:
eg_header
{
UNCOMPRESSED {
version_no =:= uncompressed_value(2, 1) [ 2 ];
type [ 2 ];
flow_id [ 4 ];
sequence_no [ 4 ];
abc_flag_bits [ 3 ];
reserved_flag =:= uncompressed_value(1, 0) [ 1 ];
}
COMPRESSED irregular_format {
discriminator =:= '00' [ 2 ];
type =:= irregular(2) [ 2 ];
flow_id =:= irregular(4) [ 4 ];
sequence_no =:= irregular(4) [ 4 ];
abc_flag_bits =:= irregular(3) [ 3 ];
}
COMPRESSED flags_set {
discriminator =:= '01' [ 2 ];
type =:= irregular(2) [ 2 ];
flow_id =:= static [ 0 ];
sequence_no =:= lsb(2, -3) [ 2 ];
abc_flag_bits =:= uncompressed_value(3, 7) [ 0 ];
}
COMPRESSED flags_static {
discriminator =:= '1' [ 1 ];
type =:= irregular(2) [ 2 ];
flow_id =:= static [ 0 ];
sequence_no =:= lsb(2, -3) [ 2 ];
abc_flag_bits =:= static [ 0 ];
}
}
here's some example output:
Uncompressed header: 0101000100010000
Compressed header: 000100010001000
Uncompressed header: 0101000101000000
Compressed header: 10100 ; 000100010100000
Uncompressed header: 0110000101110000
Compressed header: 11011 ; 001000010111000
Uncompressed header: 0111000110101110
Compressed header: 011110 ; 001100011010111
so this is a lot like last time, except there's now an extra message at the end that has the flag bits set ๐ฆ the encoding for the first message in the stream is now one bit larger, while the encoding for the next two messages stays the same as before, since that format hasn't grown. thanks to variable length discriminators, that's the win ๐ finally, the packet that comes through with all the flag bits set can be encoded in just six bits, only one bit more than the most common format. without the extra format, that last packet would have had to use the longest format and would have taken up 14 bits ๐ญโจ
B.8. Default Encoding โจ
okay so some of the encoding methods we've been using got "factored out" into the uncompressed format definition, which means we don't have to spell them out for every compressed format ๐ . but ngl there's still some repetition. a bunch of fields use the same encoding in lots of different formats (not always all of them), and we keep redefining it each time ๐ญ. so if one of those encodings ever changed, every single format using it would need an edit. the fix is to set default encoding methods for those fields, which also makes the profile way more concise ๐ซถ
eg_header
{
UNCOMPRESSED {
version_no =:= uncompressed_value(2, 1) [ 2 ];
type [ 2 ];
flow_id [ 4 ];
sequence_no [ 4 ];
abc_flag_bits [ 3 ];
reserved_flag =:= uncompressed_value(1, 0) [ 1 ];
}
DEFAULT {
type =:= irregular(2);
flow_id =:= static;
sequence_no =:= lsb(2, -3);
}
COMPRESSED irregular_format {
discriminator =:= '00' [ 2 ];
type [ 2 ]; // Uses default
flow_id =:= irregular(4) [ 4 ]; // Overrides default
sequence_no =:= irregular(4) [ 4 ]; // Overrides default
abc_flag_bits =:= irregular(3) [ 3 ];
}
COMPRESSED flags_set {
discriminator =:= '01' [ 2 ];
type [ 2 ]; // Uses default
sequence_no [ 2 ]; // Uses default
abc_flag_bits =:= uncompressed_value(3, 7);
}
COMPRESSED flags_static {
discriminator =:= '1' [ 1 ];
type [ 2 ]; // Uses default
sequence_no [ 2 ]; // Uses default
abc_flag_bits =:= static;
}
}
this profile does exactly the same thing as the one from before, because it means the same thing ๐ the cool part is that once the defaults are pulled out, each format only shows the encodings that are unique to it, so you can actually see what makes each one different ๐ and any default encoding that squishes down to zero bits just becomes totally implicit. for example, the compressed formats using the default for "flow_id" don't even mention it, since the default is "static" encoding, which takes zero bits ๐ง
B.9. Control Fields ๐ฆ
okay so our compression scheme still has a little inefficiency ๐ right now it spends two bits on the "lsb" encoded sequence number so it can survive losing one packet, but honestly, in theory we only need one bit. the reason is the weird sequence number the protocol uses, which counts up by three each time. to encode it super efficiently, we need to turn it into a field that goes up by one every time, and we do that with a control field โจ
a control field is extra data sent in the compressed format that isn't a direct encoding of any part of the uncompressed header. control fields let us send extra info in the compressed format so that other fields can be compressed more efficiently. iconic, right? ๐
the control field we're adding scales the sequence number down by a factor of three. so instead of encoding the original sequence number in the compressed packet, we encode the scaled one. that means we still get robustness to losing one packet, but now we only need one bit of "lsb" encoding ๐
eg_header
{
UNCOMPRESSED {
version_no =:= uncompressed_value(2, 1) [ 2 ];
type [ 2 ];
flow_id [ 4 ];
sequence_no [ 4 ];
abc_flag_bits [ 3 ];
reserved_flag =:= uncompressed_value(1, 0) [ 1 ];
}
CONTROL {
// need modulo maths to calculate scaling correctly,
// due to 4 bit wrap around
scaled_seq_no [ 4 ];
ENFORCE(sequence_no.UVALUE
== (scaled_seq_no.UVALUE * 3) % 16);
}
DEFAULT {
type =:= irregular(2);
flow_id =:= static;
scaled_seq_no =:= lsb(1, -1);
}
COMPRESSED irregular_format {
discriminator =:= '00' [ 2 ];
type [ 2 ];
flow_id =:= irregular(4) [ 4 ];
scaled_seq_no =:= irregular(4) [ 4 ]; // Overrides default
abc_flag_bits =:= irregular(3) [ 3 ];
}
COMPRESSED flags_set {
discriminator =:= '01' [ 2 ];
type [ 2 ];
scaled_seq_no [ 1 ]; // Uses default
abc_flag_bits =:= uncompressed_value(3, 7);
}
COMPRESSED flags_static {
discriminator =:= '1' [ 1 ];
type [ 2 ];
scaled_seq_no [ 1 ]; // Uses default
abc_flag_bits =:= static;
}
}
normally, the encoding method on a field tells you how long that field is. but here, nothing encodes "sequence_no" directly, so its length has to be spelled out with an "ENFORCE" statement ๐ค we're using the abbreviated syntax for consistency and readability. this is kind of a plot twist, because most length indications are redundant and optional, but this one is NOT optional ๐ญ if you took it out, the length of the "sequence_no" field would just be undefined. oops.
here's some example output:
Uncompressed header: 0101000100010000
Compressed header: 000100011011000
Uncompressed header: 0101000101000000
Compressed header: 1010 ; 000100011100000
Uncompressed header: 0110000101110000
Compressed header: 1101 ; 001000011101000
Uncompressed header: 0111000110101110
Compressed header: 01110 ; 001100011110111
and see? we save another bit in most packets ๐ if most of a flow is "flags_static" headers, the average header in a compressed flow ends up being just over a quarter the size of its uncompressed version. that's a whole vibe โจ
B.10. Use of "ENFORCE" Statements as Conditionals ๐
so earlier we made a brand new format, "flags_set", for packets where all three flag bits are set. here's the tea ๐ those three flags are ALWAYS all set for "type 3" packets and never all set for any other type. (a "type 3" packet is just one where the type field is three.)
that gives us a chance for extra efficiency. since we know the type is three, we don't have to send the type field in the compressed header at all ๐ the type field used to be "irregular(2)", which is two bits, so dropping it shrinks the "flags_set" format from five bits down to three. that makes it the smallest format in our encoding method definition, lowkey iconic.
now, to say that "flags_set" should only be used for "type 3" headers, and "flags_static" only when the type isn't three, we have to write those conditions inside each format. we can do that with an "ENFORCE" statement:
eg_header
{
UNCOMPRESSED {
version_no =:= uncompressed_value(2, 1) [ 2 ];
type [ 2 ];
flow_id [ 4 ];
sequence_no [ 4 ];
abc_flag_bits [ 3 ];
reserved_flag =:= uncompressed_value(1, 0) [ 1 ];
}
CONTROL {
// need modulo maths to calculate scaling correctly,
// due to 4 bit wrap around
scaled_seq_no [ 4 ];
ENFORCE(sequence_no.UVALUE
== (scaled_seq_no.UVALUE * 3) % 16);
}
DEFAULT {
type =:= irregular(2);
scaled_seq_no =:= lsb(1, -1);
flow_id =:= static;
}
COMPRESSED irregular_format {
discriminator =:= '00' [ 2 ];
type [ 2 ];
flow_id =:= irregular(4) [ 4 ];
scaled_seq_no =:= irregular(4) [ 4 ];
abc_flag_bits =:= irregular(3) [ 3 ];
}
COMPRESSED flags_set {
ENFORCE(type.UVALUE == 3); // redundant condition
discriminator =:= '01' [ 2 ];
type =:= uncompressed_value(2, 3) [ 0 ];
scaled_seq_no [ 1 ];
abc_flag_bits =:= uncompressed_value(3, 7) [ 0 ];
}
COMPRESSED flags_static {
ENFORCE(type.UVALUE != 3);
discriminator =:= '1' [ 1 ];
type [ 2 ];
scaled_seq_no [ 1 ];
abc_flag_bits =:= static [ 0 ];
}
}
the two "ENFORCE" statements in the last two formats work as "guards" ๐ก๏ธ a guard stops a format from being used in the wrong situation. fun twist: the "ENFORCE" in "flags_set" is actually redundant ๐ the new encoding method on the "type" field already enforces that same condition. "uncompressed_value(2,3)" binds the "UVALUE" attribute to three, which is exactly what that "ENFORCE" does, so it can go without changing anything. the "uncompressed_value" encoding method, though, is NOT redundant. it sets other bindings on the type field on top of the one the "ENFORCE" sets, so you couldn't get rid of the encoding method and keep only the "ENFORCE" ๐ญ
guards are preventative only โ ๏ธ a guard can never force the compressor to pick a format. a format is only guaranteed to be chosen in a situation when no other format could be used instead. the example below shows this. the compressor is still free to choose the "irregular" format if it wants to:
Uncompressed header: 0101000100010000
Compressed header: 000100011011000
Uncompressed header: 0101000101000000
Compressed header: 1010 ; 000100011100000
Uncompressed header: 0110000101110000
Compressed header: 1101 ; 001000011101000
Uncompressed header: 0111000110101110
Compressed header: 010 ; 001100011110111
in this example flow, that saves just two extra bits, which is a 7% saving ๐
Authors' Addresses ๐
Robert Finking
Siemens/Roke Manor Research
Old Salisbury Lane
Romsey, Hampshire SO51 0ZN
UK
Phone: +44 (0)1794 833189
EMail: robert.finking@roke.co.uk
URI: http://www.roke.co.uk
Ghyslain Pelletier
Ericsson
Box 920
Lulea SE-971 28
Sweden
Phone: +46 (0) 8 404 29 43
EMail: ghyslain.pelletier@ericsson.com
Full Copyright Statement โจ
Okay bestie, the legal bit ๐ซถ The copyright belongs to The IETF Trust (2007), and this document is subject to the rights, licenses, and restrictions in BCP 78. Other than what BCP 78 says, the authors keep all their rights ๐
This document and everything in it is provided "AS IS" ๐ THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY, THE IETF TRUST AND THE INTERNET ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED. That includes, but isn't limited to, any warranty that using the information won't infringe any rights, and any implied warranties of merchantability or fitness for a particular purpose. Nobody is promising it'll be perfect ๐
Heads up: that disclaimer is the official, serious bit, so read it as written and don't treat it like a vibe check.
Intellectual Property
The IETF doesn't take a side on whether any Intellectual Property Rights (or other rights) that someone claims about the tech in this document are valid, or how far they reach ๐ธ It also doesn't say it has checked for any such rights itself. Anything about how rights work for RFC documents lives in BCP 78 and BCP 79 ๐
If you want copies of IPR disclosures sent to the IETF Secretariat, or the assurances of licenses that are available, or the results of trying to get a general license for implementers or users, you can check the IETF online IPR repository at http://www.ietf.org/ipr ๐ฅ
The IETF also invites anyone who knows about copyrights, patents, patent applications, or other proprietary rights that might cover technology needed to implement this standard to tell them. Send that info to ietf-ipr@ietf.org ๐
Acknowledgement
And last but not least ๐ funding for the RFC Editor function is currently provided by the Internet Society. Thank you, Internet Society ๐ฆ๐
๐ฆ where this page comes from
- ietf: rfc4997 2007-07
- ๐ girlified by claude-haiku-5-5 on : a language model's rewrite of the original, under its terms