diff --git a/README-FLUTE-V2.md b/README-FLUTE-V2.md new file mode 100644 index 0000000..f824d5f --- /dev/null +++ b/README-FLUTE-V2.md @@ -0,0 +1,79 @@ +# FLUTE version 2 (RFC 6726) on this branch + +This branch adds FLUTE version 2 to the library as a per-session choice, defaulting to version 1. +It is **not a complete RFC 6726 implementation**, and nothing here should be read as a claim that +it is. What is and is not covered is listed below. + +## Why version 2 is a separate branch, and not an option on the main one + +The two versions are different protocols, not a compatible upgrade. RFC 6726 clause 11.1: + +> Therefore, an implementation that relies on [RFC3926] and RFC 3451 will not be backwards +> compatible with FLUTE as specified in this document. + +3GPP does not use version 2. TS 26.346 clause L.4.1 references RFC 3926, and neither TS 26.346 nor +TS 26.517 references RFC 6726. The 3GPP MBMS Download Profile therefore lives entirely on the +version 1 branch, which carries none of the behaviour below: every difference here is reached only +by a caller that has selected version 2 explicitly. + +## Selecting a version + +`Transmitter::set_flute_version()` and `Receiver::set_flute_version()`. Both default to 1. The +selection reaches the ALC/LCT parser, the FDT, and the receiver's object handling. + +## What this branch implements + +- **Version signalling.** Transmit sets the EXT_FDT version field to the selected version, and + receive accepts only the version the session was configured for, rejecting the other in both + directions. RFC 6726 clause 3.4.1 requires the field to be 2 for a version 2 session. +- **The RFC 6726 LCT generation.** RFC 5651 removed the Sender Current Time and Expected Residual + Time header fields; RFC 6726 clause 11.1 states that under RFC 5651 the two bits that carried + them "MUST be set to zero and MUST be ignored by receivers". Under version 2 the parser gives + them no length and steps over nothing, so an extension placed after the TOI is reached. Under + version 1 the RFC 3451 reading is unchanged. The transmit side has always sent both bits zero. +- **The RFC 6726 FDT Instance ID sequence.** Version 2 wraps to the smallest expired identifier, + refuses to reuse one that is still live, and reports exhaustion; version 1 keeps RFC 3926's + wrap to zero. +- **Expires read in the correct NTP era.** RFC 6726 clause 3.3 has a receiver choose the epoch + "for which the expiration time is closest in time to the current time". Applied on parse for + version 2 only; RFC 3926 states no such rule. +- **Ordering of two TOIs sharing a Content-Location.** RFC 6726 clause 3.4.2 makes the declaration + from the greater FDT Instance ID the newer one. The receiver's replacement sweep follows that + under version 2 instead of keeping whichever completed first. +- **The RFC 6726 FDT namespace**, `urn:ietf:params:xml:ns:fdt`, from clause 3.4.2. + +Covered by `tests/test_flute_v2.cpp`. + +## Obligations checked and already met + +These needed no code. They are recorded so nobody re-opens them. + +- **EXT_TIME and EXT_AUTH.** RFC 5651 clause 5.2.1: senders and receivers "MUST recognize EXT_AUTH + and EXT_TIME, but are not required to be able to parse their content." The parser has explicit + cases for both and skips them by their declared length. +- **ALC, RFC 3450 to RFC 5775.** RFC 5775 clause 8's change list is almost entirely editorial. Its + two substantive items are the Source Packet Indication bit, which the LCT header struct carries, + and the definition of EXT_FTI, which is implemented. Read from the change list, not a full + re-read of the RFC. +- **The FDT schema body.** The attribute set RFC 6726 clause 3.4.2 defines matches what the library + emits; the 3GPP extension attributes and elements it also carries are admitted by that schema's + own `xs:any namespace="##other"` and `xs:anyAttribute`, and the 3GPP-only `schemaVersion` element + is not emitted for this namespace. Established by reading the schema, not by running an emitted + document through a validator. +- **IPsec/ESP.** RFC 6726 clause 7.5 makes it mandatory to implement, taking its service set from + RFC 5775: data origin authentication, content integrity and anti-replay SHALL be supported, and + confidentiality is RECOMMENDED. Encryption and HMAC-SHA256 authentication are configured, and + the association now sets a replay window. + +## What is NOT implemented + +**Congestion control.** RFC 5775: "Congestion control MUST be applied to all packets within a +session". The library has none, and the LCT Congestion Control Information field is sent as zero. +This is a building block, not a gap that can be patched, and until it exists **no RFC 6726 +conformance claim can be made for this branch whatever else is in place**. Tracked as step 3 of +the issue this branch advances. + +## If you are picking this up + +Congestion control is the only remaining item, and it is the largest. Everything else listed above +is either implemented and tested, or checked against the specification and found already met. diff --git a/include/AlcPacket.h b/include/AlcPacket.h index cce7ff5..1f2bfb4 100644 --- a/include/AlcPacket.h +++ b/include/AlcPacket.h @@ -32,7 +32,7 @@ namespace LibFlute { * @param data Received data to be parsed * @param len Length of the buffer */ - AlcPacket(char* data, size_t len); + AlcPacket(char* data, size_t len, uint8_t expected_flute_version = 1); /** * Create an ALC packet from encoding symbols @@ -45,6 +45,11 @@ namespace LibFlute { * @param fdt_instance_id FDT instance ID (only relevant for FDT with TOI=0) * @param close_session_flag Set the LCT Close Session flag (RFC 3451 clause 5.1, 'A' bit) on this packet * @param close_object_flag Set the LCT Close Object flag (RFC 3451 clause 5.1, 'B' bit) on this packet + * @param flute_version FLUTE version to signal in the EXT_FDT version field. RFC 3926 + * clause 3.4.1 requires 1 for a version 1 session and RFC 6726 + * clause 3.4.1 requires 2 for a version 2 session; the two are + * different protocols, see RFC 6726 clause 11.1. Defaults to 1, which + * is what TS 26.346 selects, so a 3GPP session needs no change. */ /** * Tag selecting the data-less Close Session packet constructor below. @@ -68,7 +73,8 @@ namespace LibFlute { AlcPacket(uint64_t tsi, CloseSession); AlcPacket(uint64_t tsi, uint16_t toi, FecOti fec_oti, const std::vector& symbols, size_t max_size, uint32_t fdt_instance_id, - bool close_session_flag = false, bool close_object_flag = false); + bool close_session_flag = false, bool close_object_flag = false, + uint8_t flute_version = 1); /** * Default destructor. @@ -160,6 +166,7 @@ namespace LibFlute { char* _buffer = nullptr; size_t _len; + uint8_t _flute_version = 1; // RFC 3451 clause 5.1 - LCT Header Format struct __attribute__((packed)) lct_header_t { diff --git a/include/FileDeliveryTable.h b/include/FileDeliveryTable.h index 15c853a..ee308fc 100644 --- a/include/FileDeliveryTable.h +++ b/include/FileDeliveryTable.h @@ -36,7 +36,7 @@ namespace LibFlute { FDT_NS_NONE = 0, FDT_NS_RFC3926, FDT_NS_DRAFT_2005, -// FDT_NS_RFC6726, // FLUTE v2 - will need other things implementing to use this correctly + FDT_NS_RFC6726, //< RFC 6726 FLUTE v2 namespace. Not for 3GPP MBMS use. FDT_NS_3GPP_CONSOLIDATED_V2 }; @@ -48,7 +48,7 @@ namespace LibFlute { * @param fdt_namespace The XML namespace to use for FDT */ FileDeliveryTable(uint32_t instance_id, FecOti fec_oti, FdtNamespace fdt_namespace = FDT_NS_NONE, - Profile profile = Profile::Ts26517); + Profile profile = Profile::Ts26517, uint8_t flute_version = 1); /** * Parse an XML string and create a FDT class from it @@ -57,7 +57,7 @@ namespace LibFlute { * @param buffer String containing the FDT XML * @param len Length of the buffer */ - FileDeliveryTable(uint32_t instance_id, char* buffer, size_t len); + FileDeliveryTable(uint32_t instance_id, char* buffer, size_t len, uint8_t flute_version = 1); /** * Default destructor. @@ -82,6 +82,14 @@ namespace LibFlute { /** The FDT-Instance Expires attribute, in NTP-epoch seconds. */ uint64_t expires() const { return _expires; } + /** + * Select the FLUTE version whose rules this table follows. Version 1 (RFC 3926) is the + * default; version 2 (RFC 6726) changes the FDT Instance ID sequence and how the Expires + * field is read. Set before the first instance is sent. + */ + void set_flute_version(uint8_t version); + uint8_t flute_version() const { return _flute_version; }; + /** 20-bit field width (RFC 3926 clause 3.4.1, "FDT Instance ID, 20 bits"). */ static constexpr uint32_t kMaxFdtInstanceId = 0xFFFFF; @@ -90,7 +98,18 @@ namespace LibFlute { * function so the wraparound is testable without a live session. */ static uint32_t next_instance_id(uint32_t current, uint64_t current_expires, uint64_t now, - std::map& expired_instance_ids); + std::map& expired_instance_ids, + uint8_t flute_version = 1); + + /** + * Interpret a 32-bit NTP expiry against the era it belongs to. + * + * RFC 6726 clause 3.3 describes how a version 2 peer reads the field: "both a sender and a + * receiver easily determine to which (136-year) epoch the FDT Instance expiration time value + * pertains by choosing the epoch for which the expiration time is closest in time to the + * current time." Version 1 states no such rule, so this is applied only for version 2. + */ + static uint64_t expiry_in_nearest_era(uint64_t wire_value, uint64_t now); /** * Which obligation set this FDT is emitted under. See Profile. @@ -216,6 +235,7 @@ namespace LibFlute { FecOti _global_fec_oti; uint64_t _expires; + uint8_t _flute_version = 1; bool _complete = false; FdtNamespace _fdt_namespace; diff --git a/include/Receiver.h b/include/Receiver.h index 0fd8f1b..22370a4 100644 --- a/include/Receiver.h +++ b/include/Receiver.h @@ -30,6 +30,24 @@ namespace LibFlute { */ class Receiver { public: + /** + * Select the FLUTE protocol version for this session. + * + * RFC 3926 clause 3.4.1 requires the EXT_FDT version field to be 1 in a version 1 session, + * and RFC 6726 clause 3.4.1 requires 2 in a version 2 session. They are separate protocols: + * RFC 6726 clause 11.1 records that version 1 uses RFC 3451 and version 2 uses RFC 5651, + * "Therefore, an implementation that relies on [RFC3926] and RFC 3451 will not be backwards + * compatible with FLUTE as specified in this document." + * + * The default is 1. TS 26.346 V18.2.0 clause L.2 selects RFC 3926, so a 3GPP MBMS download + * session must leave this at 1. + * + * Version 2 is incompletely implemented here: see the note in this branch's README on the + * RFC 6726 obligations that are not met. + */ + void set_flute_version(uint8_t version) { _flute_version = version; }; + uint8_t flute_version() const { return _flute_version; }; + /** * Definition of a file reception completion callback function that can be * registered through ::register_completion_callback. @@ -137,6 +155,7 @@ namespace LibFlute { enum { max_length = 65536 }; char _data[max_length]; uint64_t _tsi; + uint8_t _flute_version = 1; std::unique_ptr _fdt; // FDT instance currently being reassembled at TOI 0 (0xFFFFFFFF = none). // Used to discard a partial FDT object when a newer instance starts diff --git a/include/Transmitter.h b/include/Transmitter.h index 1a56c0b..cbbf6d2 100644 --- a/include/Transmitter.h +++ b/include/Transmitter.h @@ -46,6 +46,25 @@ namespace LibFlute { */ using FdtNamespace = FileDeliveryTable::FdtNamespace; + /** + * Select the FLUTE protocol version for this session. + * + * RFC 3926 clause 3.4.1 requires the EXT_FDT version field to be 1 in a version 1 session, + * and RFC 6726 clause 3.4.1 requires 2 in a version 2 session. They are separate protocols: + * RFC 6726 clause 11.1 records that version 1 uses RFC 3451 and version 2 uses RFC 5651, + * "Therefore, an implementation that relies on [RFC3926] and RFC 3451 will not be backwards + * compatible with FLUTE as specified in this document." + * + * The default is 1. TS 26.346 V18.2.0 clause L.2 selects RFC 3926, so a 3GPP MBMS download + * session must leave this at 1. + * + * Version 2 is incompletely implemented here: see the note in this branch's README on the + * RFC 6726 obligations that are not met. + */ + void set_flute_version(uint8_t version); + uint8_t flute_version() const { return _flute_version; }; + + /** * File Description object @@ -725,6 +744,7 @@ namespace LibFlute { boost::asio::steady_timer _fdt_timer; uint64_t _tsi; + uint8_t _flute_version = 1; uint16_t _mtu; std::unique_ptr _fdt; diff --git a/src/AlcPacket.cpp b/src/AlcPacket.cpp index 0ab3ca6..0e89e43 100644 --- a/src/AlcPacket.cpp +++ b/src/AlcPacket.cpp @@ -19,7 +19,7 @@ #include #include "AlcPacket.h" -LibFlute::AlcPacket::AlcPacket(char* data, size_t len) +LibFlute::AlcPacket::AlcPacket(char* data, size_t len, uint8_t expected_flute_version) { if (len < 4) { throw std::runtime_error("Packet too short"); @@ -52,13 +52,21 @@ LibFlute::AlcPacket::AlcPacket(char* data, size_t len) to zero, and the UE should ignore them." Ignoring a field still means stepping over it, so this is needed in both profiles: robustness against a non-conformant sender under the 3GPP profile, plain correctness under general FLUTE. */ + /* Version 2 sits on RFC 5651, which deleted both fields and reassigned their flag bits. + RFC 5651 clause 11: "Removal of the Sender Current Time and Expected Residual Time LCT + header fields." RFC 6726 clause 11.1 on what that means for the two bits: "In [RFC5651], + these fields MUST be set to zero and MUST be ignored by receivers (instead, the EXT_TIME + Header Extensions can convey this information if needed)." So under version 2 they contribute + no words to the header and nothing is stepped over; under version 1 the RFC 3451 reading + above still applies, unchanged. */ + const bool lct_carries_sct_ert = (expected_flute_version == 1); + const size_t standard_header_words = 2 + _lct_header.congestion_control_flag + _lct_header.half_word_flag + _lct_header.tsi_flag + _lct_header.toi_flag + - _lct_header.sct_flag + - _lct_header.ert_flag; + (lct_carries_sct_ert ? (_lct_header.sct_flag + _lct_header.ert_flag) : 0); if (_lct_header.lct_header_len < standard_header_words) { throw std::runtime_error("LCT header length is shorter than its own flags require"); @@ -119,8 +127,10 @@ LibFlute::AlcPacket::AlcPacket(char* data, size_t len) // Step over the SCT and ERT words when present, so the extension walk below starts where the // extensions actually begin. RFC 3451 clause 5.1 field order is CCI, TSI, TOI, SCT, ERT. - if (_lct_header.sct_flag) hdr_ptr += 4; - if (_lct_header.ert_flag) hdr_ptr += 4; + if (lct_carries_sct_ert) { + if (_lct_header.sct_flag) hdr_ptr += 4; + if (_lct_header.ert_flag) hdr_ptr += 4; + } if (_lct_header.codepoint == 0) { _fec_oti.encoding_id = FecScheme::CompactNoCode; @@ -185,28 +195,32 @@ LibFlute::AlcPacket::AlcPacket(char* data, size_t len) } case EXT_FDT: { uint8_t flute_version = (*ext_ptr & 0xF0) >> 4; - /* This branch implements FLUTE version 1, and the version field is not - advisory: it identifies which protocol the packet belongs to. + /* The version field identifies which protocol the packet belongs to, and + the two are not interchangeable, so a packet is accepted only if it + carries the version this session was configured for. The default is 1, + which is what TS 26.346 selects, so a 3GPP session behaves exactly as it + does on the version 1 branch. RFC 3926 clause 3.4.1: "This document specifies FLUTE version 1. Hence in any ALC packet that carries FDT Instance and that belongs to the file delivery session as specified in this specification MUST set this field to '1'." - Accepting 2 was accepting a packet from a protocol this build does not - implement, and the two are not interchangeable underneath. RFC 6726 clause 11.1: "Therefore, an implementation that relies on [RFC3926] and RFC 3451 will not be backwards compatible with FLUTE as specified in this document." - General FLUTE, not a 3GPP restriction: it holds in both profiles. What - TS 26.346 adds is only that version 1 is the one it selects, so a 3GPP - session could never legitimately carry 2 either. */ - if (flute_version != 1) { - throw std::runtime_error("Unsupported FLUTE version " + - std::to_string(flute_version) + - "; this implementation is FLUTE version 1"); + RFC 6726 clause 3.1 requires the receiver to tell sessions apart by + version: "If multiple FLUTE sessions are sent to a channel, then + receivers MUST determine the FLUTE protocol version, based on version + fields and the (source IP address, TSI) pair carried in the ALC/LCT + header of the packet." */ + if (flute_version != expected_flute_version) { + throw std::runtime_error("FLUTE version " + std::to_string(flute_version) + + " in EXT_FDT, but this session is configured for " + "version " + std::to_string(expected_flute_version)); } + _flute_version = flute_version; _fdt_instance_id = (*ext_ptr & 0x0F) << 16; ext_ptr++; _fdt_instance_id |= ntohs(*(uint16_t*)ext_ptr); @@ -240,8 +254,9 @@ LibFlute::AlcPacket::AlcPacket(char* data, size_t len) } LibFlute::AlcPacket::AlcPacket(uint64_t tsi, uint16_t toi, LibFlute::FecOti fec_oti, const std::vector& symbols, size_t max_encoding_symbol_size, uint32_t fdt_instance_id, - bool close_session_flag, bool close_object_flag) - : _fec_oti(fec_oti) + bool close_session_flag, bool close_object_flag, + uint8_t flute_version) + : _fec_oti(fec_oti), _flute_version(flute_version) { // TSI width: this wire scheme always carries a 16-bit half-word component (half_word_flag=1, // shared with TOI's own 16-bit half-word below) plus, when tsi_flag=1, an extra 32-bit word @@ -298,7 +313,19 @@ LibFlute::AlcPacket::AlcPacket(uint64_t tsi, uint16_t toi, LibFlute::FecOti fec_ if (toi == 0) { // Add extensions for FDT *((uint8_t*)hdr_ptr) = EXT_FDT; hdr_ptr += 1; - *((uint8_t*)hdr_ptr) = 1 << 4 | (fdt_instance_id & 0x000F0000) >> 16; + /* FLUTE version nibble, from the version this session was configured for. + + RFC 3926 clause 3.4.1: "This document specifies FLUTE version 1. Hence in any ALC packet + that carries FDT Instance and that belongs to the file delivery session as specified in + this specification MUST set this field to '1'." + + RFC 6726 clause 3.4.1: "This document specifies FLUTE version 2. Hence, in any ALC packet + that carries an FDT Instance and that belongs to the file delivery session as specified in + this specification MUST set this field to '2'." + + The default is 1, which is what TS 26.346 V18.2.0 clause L.2 selects by referencing RFC + 3926. Version 2 is opt-in and is not for 3GPP MBMS use. */ + *((uint8_t*)hdr_ptr) = (flute_version & 0x0F) << 4 | (fdt_instance_id & 0x000F0000) >> 16; hdr_ptr += 1; *((uint16_t*)hdr_ptr) = htons(fdt_instance_id & 0x0000FFFF); hdr_ptr += 2; diff --git a/src/FileDeliveryTable.cpp b/src/FileDeliveryTable.cpp index 24dbcfd..f52beda 100644 --- a/src/FileDeliveryTable.cpp +++ b/src/FileDeliveryTable.cpp @@ -133,13 +133,23 @@ bool LibFlute::FileDeliveryTable::FileEntry::operator==(const LibFlute::FileDeli etag == other.etag; } +namespace { + auto ntp_seconds_since_epoch() -> uint64_t + { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count() + + 2'208'988'800; /* Unix epoch -> NTP epoch offset, matching Transmitter::seconds_since_epoch() */ + } +} + LibFlute::FileDeliveryTable::FileDeliveryTable(uint32_t instance_id, FecOti fec_oti, FdtNamespace fdt_namespace, - Profile profile) + Profile profile, uint8_t flute_version) : _instance_id( instance_id ) , _instance_id_sent( instance_id - 1 ) , _global_fec_oti( fec_oti ) , _fdt_namespace( fdt_namespace ) , _profile( profile ) + , _flute_version( flute_version ) { /* Each 3GPP profile fixes the FDT schema, so the namespace is taken from the profile rather than from a separate argument that could disagree with it. @@ -170,10 +180,12 @@ LibFlute::FileDeliveryTable::FileDeliveryTable(uint32_t instance_id, FecOti fec_ } } -LibFlute::FileDeliveryTable::FileDeliveryTable(uint32_t instance_id, char* buffer, size_t len) +LibFlute::FileDeliveryTable::FileDeliveryTable(uint32_t instance_id, char* buffer, size_t len, + uint8_t flute_version) : _instance_id( instance_id ) , _instance_id_sent( instance_id - 1 ) , _global_fec_oti() + , _flute_version( flute_version ) { static const std::string mbms2007_ns("urn:3GPP:metadata:2007:MBMS:FLUTE:FDT"); // 3GPP TS 26.346 Clause 7.2.10.2 static const std::string mbms2012_ns("urn:3GPP:metadata:2012:MBMS:FLUTE:FDT"); // 3GPP TS 26.346 Clause 7.2.10.2 @@ -202,8 +214,8 @@ LibFlute::FileDeliveryTable::FileDeliveryTable(uint32_t instance_id, char* buffe _fdt_namespace = FDT_NS_RFC3926; } else if (fdt_ns == "urn:IETF:metadata:2005:FLUTE:FDT") { // 3GPP TS 26.346 Clause 7.2.10.1 _fdt_namespace = FDT_NS_DRAFT_2005; -// } else if (fdt_ns == "urn:ietf:params:xml:ns:fdt") { // RFC 6726 - FLUTEv2 - needs more work -// _fdt_namespace = FDT_NS_RFC6726; + } else if (fdt_ns == "urn:ietf:params:xml:ns:fdt") { // RFC 6726 clause 3.4.2, FLUTE version 2 + _fdt_namespace = FDT_NS_RFC6726; } else if (fdt_ns == "urn:3GPP:metadata:2022:FLUTE:FDT") { // 3GPP TS 26.346 Clause L.6.1 _fdt_namespace = FDT_NS_3GPP_CONSOLIDATED_V2; } else { @@ -211,6 +223,9 @@ LibFlute::FileDeliveryTable::FileDeliveryTable(uint32_t instance_id, char* buffe } _expires = std::stoull(root_ns.findAttribute(fdt_instance, "Expires", fdt_ns)->Value()); + if (_flute_version >= 2) { + _expires = expiry_in_nearest_era(_expires, ntp_seconds_since_epoch()); + } auto complete_attr = root_ns.findAttribute(fdt_instance, "Complete", fdt_ns); if (complete_attr != nullptr) { @@ -398,13 +413,44 @@ LibFlute::FileDeliveryTable::FileDeliveryTable(uint32_t instance_id, char* buffe } } -namespace { - auto ntp_seconds_since_epoch() -> uint64_t - { - return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count() + - 2'208'988'800; /* Unix epoch -> NTP epoch offset, matching Transmitter::seconds_since_epoch() */ +void LibFlute::FileDeliveryTable::set_flute_version(uint8_t version) +{ + /* Neither 3GPP profile admits version 2: both are built on RFC 3926, which is version 1. + TS 26.346 V18.2.0 clause 7.2.0: "MBMS Clients and servers supporting MBMS download shall + implement the FLUTE specification (RFC 3926 [9]), as well as ALC (RFC 3450 [10]) and LCT + (RFC 3451 [11]) features that FLUTE inherits." + + RFC 6726 clause 11.1 records that the two generations are not interchangeable: "Therefore, an + implementation that relies on [RFC3926] and RFC 3451 will not be backwards compatible with + FLUTE as specified in this document." So a version 2 session cannot be a 3GPP one, and asking + for it here is refused rather than quietly honoured. */ + if (version != 1 && is_3gpp(_profile)) { + throw std::runtime_error( + "FLUTE version 2 is not available under the 3GPP profiles, which are defined on RFC 3926; " + "use Profile::Unprofiled for a version 2 session"); + } + _flute_version = version; +} + +uint64_t LibFlute::FileDeliveryTable::expiry_in_nearest_era(uint64_t wire_value, uint64_t now) +{ + /* RFC 6726 clause 3.3: "both a sender and a receiver easily determine to which (136-year) epoch + the FDT Instance expiration time value pertains by choosing the epoch for which the expiration + time is closest in time to the current time." + + The wire field is 32 bits, so the candidates are that value placed in the era below the + current time and in the one above it. Whichever lands nearer to now is the one meant. */ + static constexpr uint64_t era = 1ULL << 32; + const uint64_t offset = wire_value % era; + const uint64_t base = (now / era) * era; + + uint64_t best = base + offset; + auto distance = [now](uint64_t v) { return v > now ? v - now : now - v; }; + for (uint64_t candidate : { base + offset + era, + base >= era ? base + offset - era : base + offset }) { + if (distance(candidate) < distance(best)) best = candidate; } + return best; } void LibFlute::FileDeliveryTable::set_expires(uint64_t exp) @@ -424,8 +470,38 @@ void LibFlute::FileDeliveryTable::set_expires(uint64_t exp) uint32_t LibFlute::FileDeliveryTable::next_instance_id(uint32_t current, uint64_t current_expires, uint64_t now, - std::map& expired_instance_ids) + std::map& expired_instance_ids, + uint8_t flute_version) { + if (flute_version >= 2) { + /* RFC 6726 clause 3.4.1 replaced the version 1 sequence: "After reaching the maximum value + (2^20-1), the numbering starts from the smallest FDT Instance ID value assigned to an expired + FDT Instance", and made reuse of a live identifier a prohibition rather than advice: + "Senders MUST NOT reuse an FDT Instance ID value that is already in use for a non-expired FDT + Instance." The same clause leaves the exhausted case to the implementation: "Sender behavior + when all the FDT Instance IDs are used by non-expired FEC Instances is outside the scope of + this specification and left to individual implementations of FLUTE." Refusing is the only + option that does not break the prohibition above. */ + expired_instance_ids[current] = current_expires; + + if (current < kMaxFdtInstanceId) { + return current + 1; + } + + auto chosen = expired_instance_ids.cend(); + for (auto it = expired_instance_ids.cbegin(); it != expired_instance_ids.cend(); ++it) { + if (it->second < now) { chosen = it; break; } // std::map iterates in ascending key order + } + if (chosen == expired_instance_ids.cend()) { + throw std::runtime_error("FDT Instance ID space exhausted: no previously used ID has expired, " + "and RFC 6726 clause 3.4.1 forbids reusing one that is still live"); + } + const uint32_t reused = chosen->first; + expired_instance_ids.erase(chosen); + return reused; + } + + /* RFC 3926 clause 3.4.1: "After reaching the maximum value (2^20-1), the numbering starts again from '0'." @@ -452,7 +528,7 @@ uint32_t LibFlute::FileDeliveryTable::next_instance_id(uint32_t current, uint64_ auto LibFlute::FileDeliveryTable::advance_instance_id() -> void { _instance_id = next_instance_id(_instance_id, _expires, ntp_seconds_since_epoch(), - _expired_instance_ids); + _expired_instance_ids, _flute_version); } auto LibFlute::FileDeliveryTable::add(const FileEntry& fe) -> void @@ -502,10 +578,10 @@ auto LibFlute::FileDeliveryTable::to_string() const -> std::string { // 3GPP TS 26.346 Clause 7.2.10.1 root->SetAttribute("xmlns", "urn:IETF:metadata:2005:FLUTE:FDT"); break; -// case FDT_NS_RFC6726: // FLUTE v2 - Will need other things implementing to use this -// // RFC 6726 -// root->SetAttribute("xmlns", "urn:ietf:params:xml:ns:fdt"); -// break; + case FDT_NS_RFC6726: + // RFC 6726 clause 3.4.2: targetNamespace="urn:ietf:params:xml:ns:fdt" + root->SetAttribute("xmlns", "urn:ietf:params:xml:ns:fdt"); + break; case FDT_NS_3GPP_CONSOLIDATED_V2: // 3GPP TS 26.346 Clause L.6.1 root->SetAttribute("xmlns", "urn:3GPP:metadata:2022:FLUTE:FDT"); diff --git a/src/IpSec.cpp b/src/IpSec.cpp index ec653fe..6f36cfa 100644 --- a/src/IpSec.cpp +++ b/src/IpSec.cpp @@ -156,6 +156,19 @@ namespace LibFlute::IpSec { xsinfo.family = dest_info.family; xsinfo.mode = XFRM_MODE_TRANSPORT; + /* RFC 6726 clause 7.5 makes IPsec/ESP in transport mode the mandatory-to-implement security + configuration for FLUTE, and takes its service set from ALC: "[RFC5775] specifies that the + data origin authentication, content integrity, and anti-replay services SHALL be supported, + and that the confidentiality service is RECOMMENDED." The authentication algorithm attached + below covers the first two. Anti-replay is a property of the association rather than of an + algorithm, and needs a non-zero window; left at zero the kernel accepts a replayed packet. + 32 is the largest window this attribute can express: the legacy replay state the kernel + keeps for it (struct xfrm_replay_state) holds its bitmap in a __u32, and a request for more + is clamped to 32. A larger window needs the extended sequence-number attribute + (XFRMA_REPLAY_ESN_VAL) instead, which is not needed here. Applies to both FLUTE versions: + nothing in RFC 3926 argues against it. */ + xsinfo.replay_window = 32; + std::vector algo_buf(sizeof(struct xfrm_algo) + 512, 0); auto* algo = reinterpret_cast(algo_buf.data()); diff --git a/src/Receiver.cpp b/src/Receiver.cpp index 70be42a..6d342fd 100644 --- a/src/Receiver.cpp +++ b/src/Receiver.cpp @@ -240,7 +240,7 @@ auto LibFlute::Receiver::handle_receive_from(const boost::system::error_code& er } try { - auto alc = LibFlute::AlcPacket(_data, bytes_recvd); + auto alc = LibFlute::AlcPacket(_data, bytes_recvd, _flute_version); if (alc.tsi() == _tsi) { @@ -353,6 +353,22 @@ auto LibFlute::Receiver::handle_receive_from(const boost::system::error_code& er if (it->second.get() != file && !file->meta().content_location.empty() && it->second->meta().content_location == file->meta().content_location) { + /* Version 2 orders the two by the instance that declared them. RFC 6726 clause + 3.4.2: "The semantics for any two "File" elements declaring + the same "Content-Location" but differing "TOI" is that the element appearing in + the FDT Instance with the greater FDT Instance ID is considered to declare a + newer instance (e.g., version) of the same "File"." So the older declaration is the + one discarded, whichever of the two happens to have completed first. Version 1 + states no such ordering, and keeps the completed file. */ + if (_flute_version >= 2 && + it->second->fdt_instance_id() > file->fdt_instance_id()) + { + spdlog::debug("Keeping TOI {} for '{}': declared by FDT instance {}, newer than {}", + it->first, it->second->meta().content_location, + it->second->fdt_instance_id(), file->fdt_instance_id()); + ++it; + continue; + } spdlog::debug("Replacing file with TOI {}", it->first); it = _files.erase(it); } @@ -372,7 +388,8 @@ auto LibFlute::Receiver::handle_receive_from(const boost::system::error_code& er if (alc.toi() == 0) { // parse complete FDT _fdt = std::make_unique( - alc.fdt_instance_id(), _files[alc.toi()]->buffer(), _files[alc.toi()]->length()); + alc.fdt_instance_id(), _files[alc.toi()]->buffer(), _files[alc.toi()]->length(), + _flute_version); _files.erase(alc.toi()); for (const auto& file_entry : _fdt->file_entries()) { @@ -385,6 +402,7 @@ auto LibFlute::Receiver::handle_receive_from(const boost::system::error_code& er // in-progress transfer, not a stale one. Adopt the FDT's metadata in place // rather than discarding and restarting it. existing_file->second->adopt_fdt_metadata(file_entry); + existing_file->second->set_fdt_instance_id(_fdt->instance_id()); } else if (existing_file != _files.end() && existing_file->second->meta().content_location != file_entry.content_location) { // TOI numbers get reused across FDT instances (the live window @@ -406,7 +424,11 @@ auto LibFlute::Receiver::handle_receive_from(const boost::system::error_code& er if (existing_file == _files.end()) { spdlog::debug("Starting reception for file with TOI {}: {} ({})", file_entry.toi, file_entry.content_location, file_entry.content_type); - _files.emplace(file_entry.toi, std::make_shared(file_entry)); + /* Record which instance declared this file: RFC 6726 clause 3.4.2 orders two + declarations of the same Content-Location by their instance ID. */ + auto created = std::make_shared(file_entry); + created->set_fdt_instance_id(_fdt->instance_id()); + _files.emplace(file_entry.toi, std::move(created)); } } } diff --git a/src/Transmitter.cpp b/src/Transmitter.cpp index 4ee8eed..62d69b8 100644 --- a/src/Transmitter.cpp +++ b/src/Transmitter.cpp @@ -851,7 +851,8 @@ auto Transmitter::send_next_packet() -> void spdlog::debug("sending TOI {} SBN {} ID {}", file->meta().toi, symbol.source_block_number(), symbol.id() ); } auto packet = std::make_shared(_tsi, file->meta().toi, file->meta().fec_oti, symbols, _max_payload, file->fdt_instance_id(), - _session_closing, _closing_objects.count(file->meta().toi) > 0); + _session_closing, _closing_objects.count(file->meta().toi) > 0, + _flute_version); bytes_queued += packet->size(); /* A tunnel is an additional path, not a replacement for the announced one. Sending only the @@ -931,6 +932,14 @@ auto Transmitter::send_next_packet() -> void } } +auto Transmitter::set_flute_version(uint8_t version) -> void +{ + /* The table is built in the constructor, before a caller can select a version, so the choice is + forwarded to it here. Its instance-ID sequence and Expires handling differ between versions. */ + _flute_version = version; + if (_fdt) _fdt->set_flute_version(version); +} + auto Transmitter::activate() -> void { if (!_active) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index eb70162..6ac397d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,3 +48,4 @@ add_flute_test_executable(flute_unit_tests test_transmitter.cpp "unit:") add_flute_test_executable(flute_e2e_tests test_end_to_end.cpp "e2e:") add_flute_test_executable(flute_protocol_tests test_protocol_fixes.cpp "protocol:") add_flute_test_executable(flute_fdt_growth_tests test_fdt_growth.cpp "fdt_growth:") +add_flute_test_executable(flute_v2_tests test_flute_v2.cpp "flute_v2:") diff --git a/tests/test_flute_v2.cpp b/tests/test_flute_v2.cpp new file mode 100644 index 0000000..819362e --- /dev/null +++ b/tests/test_flute_v2.cpp @@ -0,0 +1,287 @@ +// libflute - FLUTE/ALC library +// +// Copyright (C) 2021 Klaus Kühnhammer (Österreichische Rundfunksender GmbH & Co KG) +// +// Licensed under the License terms and conditions for use, reproduction, and +// distribution of 5G-MAG software (the “License”). You may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// https://www.5g-mag.com/reference-tools. Unless required by applicable law or +// agreed to in writing, software distributed under the License is distributed on +// an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. +// +// See the License for the specific language governing permissions and limitations +// under the License. +// +// Tests for FLUTE version 2 (RFC 6726) session selection. +// +// Version 1 (RFC 3926) and version 2 (RFC 6726) are separate protocols. RFC 6726 clause 11.1: +// "an implementation that relies on [RFC3926] and RFC 3451 will not be backwards compatible with +// FLUTE as specified in this document." These tests cover the version selector that keeps the two +// apart, not a complete version 2 implementation; see README-FLUTE-V2.md for what is missing. + +#include + +#include + +#include +#include +#include + +#include "AlcPacket.h" +#include "EncodingSymbol.h" +#include "FileDeliveryTable.h" + +using namespace LibFlute; + +namespace { + +std::vector make_symbols(const char* payload, size_t len) { + std::vector symbols; + symbols.emplace_back(0, 0, const_cast(payload), len, FecScheme::CompactNoCode); + return symbols; +} + +uint8_t version_nibble(const AlcPacket& packet) { + const auto* bytes = reinterpret_cast(packet.data()); + EXPECT_EQ(bytes[12], 192); // EXT_FDT HET + return (bytes[13] & 0xF0) >> 4; +} + +} // namespace + +// RFC 3926 clause 3.4.1: "This document specifies FLUTE version 1. Hence in any ALC packet that +// carries FDT Instance and that belongs to the file delivery session as specified in this +// specification MUST set this field to '1'." +TEST(FluteV2, TransmitDefaultsToVersion1) { + const char payload[] = ""; + auto symbols = make_symbols(payload, sizeof(payload) - 1); + FecOti fec_oti{FecScheme::CompactNoCode, 0, sizeof(payload) - 1, 1400, 64, 0}; + + AlcPacket packet(/*tsi*/1234, /*toi*/0, fec_oti, symbols, 1400, /*fdt_instance_id*/5); + + EXPECT_EQ(version_nibble(packet), 1u); +} + +// RFC 6726 clause 3.4.1: "This document specifies FLUTE version 2. Hence, in any ALC packet that +// carries an FDT Instance and that belongs to the file delivery session as specified in this +// specification MUST set this field to '2'." +TEST(FluteV2, TransmitSignalsVersion2WhenSelected) { + const char payload[] = ""; + auto symbols = make_symbols(payload, sizeof(payload) - 1); + FecOti fec_oti{FecScheme::CompactNoCode, 0, sizeof(payload) - 1, 1400, 64, 0}; + + AlcPacket packet(/*tsi*/1234, /*toi*/0, fec_oti, symbols, 1400, /*fdt_instance_id*/5, + /*close_session*/false, /*close_object*/false, /*flute_version*/2); + + EXPECT_EQ(version_nibble(packet), 2u); + // The FDT Instance ID shares the octet with the version nibble, so confirm the wider field + // survived the change of version. + AlcPacket decoded(packet.data(), packet.size(), /*expected_flute_version*/2); + EXPECT_EQ(decoded.fdt_instance_id(), 5u); +} + +// RFC 6726 clause 3.1: "If multiple FLUTE sessions are sent to a channel, then receivers MUST +// determine the FLUTE protocol version, based on version fields and the (source IP address, TSI) +// pair carried in the ALC/LCT header of the packet." +TEST(FluteV2, ReceiverAcceptsOnlyTheVersionItWasConfiguredFor) { + const char payload[] = ""; + auto symbols = make_symbols(payload, sizeof(payload) - 1); + FecOti fec_oti{FecScheme::CompactNoCode, 0, sizeof(payload) - 1, 1400, 64, 0}; + + AlcPacket v1(/*tsi*/1234, /*toi*/0, fec_oti, symbols, 1400, /*fdt_instance_id*/5); + AlcPacket v2(/*tsi*/1234, /*toi*/0, fec_oti, symbols, 1400, /*fdt_instance_id*/5, + false, false, /*flute_version*/2); + + // Matching version accepted. + EXPECT_NO_THROW(AlcPacket(v1.data(), v1.size(), 1)); + EXPECT_NO_THROW(AlcPacket(v2.data(), v2.size(), 2)); + + // Mismatched version refused in both directions: a version 2 session must not decode version 1 + // packets any more than a version 1 session may decode version 2 packets. + EXPECT_THROW(AlcPacket(v2.data(), v2.size(), 1), std::runtime_error); + EXPECT_THROW(AlcPacket(v1.data(), v1.size(), 2), std::runtime_error); +} + +// RFC 6726 clause 3.4.2 defines the FDT schema with +// targetNamespace="urn:ietf:params:xml:ns:fdt". +TEST(FluteV2, FdtCarriesTheRfc6726Namespace) { + /* General FLUTE: the 3GPP profiles derive their own schema and neither is RFC 6726's, so a + version 2 session is by definition not one of them. */ + FileDeliveryTable fdt(1, FecOti{FecScheme::CompactNoCode, 0, 0, 1400, 64, 0}, + FileDeliveryTable::FDT_NS_RFC6726, Profile::Unprofiled); + auto xml = fdt.to_string(); + EXPECT_NE(xml.find("urn:ietf:params:xml:ns:fdt"), std::string::npos) << xml; +} + + +/* RFC 5651 clause 11 lists among its changes from RFC 3451: "Removal of the Sender Current Time + and Expected Residual Time LCT header fields." RFC 6726 clause 11.1 says what a version 2 peer + does with the two flag bits that carried them: "In [RFC5651], these fields MUST be set to zero + and MUST be ignored by receivers (instead, the EXT_TIME Header Extensions can convey this + information if needed)." + + The packets below are hand-built with T and R set and two words following the TOI, which a + version 1 receiver must step over as SCT and ERT and a version 2 receiver must not. */ +namespace { + +std::vector packet_with_t_and_r_set(const std::string& payload) { + // 2 words (LCT header + CCI) + 1 word (TSI/TOI half-words) + 2 words (SCT, ERT) = 5 + const size_t header_words = 5; + const size_t header_bytes = header_words * 4; + std::vector buf(header_bytes + 4 /* SBN+ESI */ + payload.size(), 0); + auto* b = reinterpret_cast(buf.data()); + + b[0] = (1 << 4); // LCT version 1, which both RFC 3451 and RFC 5651 use + /* Byte 1, low bit first: close_object, close_session, ert, sct, half_word, toi(2), tsi. */ + b[1] = 0x10 /* half_word */ | 0x08 /* sct */ | 0x04 /* ert */; + b[2] = static_cast(header_words); + b[3] = 0; // Compact No-Code + + uint16_t tsi_be = htons(1), toi_be = htons(7); + std::memcpy(b + 8, &tsi_be, 2); + std::memcpy(b + 10, &toi_be, 2); + // Bytes 12-19 are the SCT and ERT words, left zero. + std::memcpy(buf.data() + header_bytes + 4, payload.data(), payload.size()); + return buf; +} + +} // namespace + +TEST(FluteV2, Version1StepsOverTheSctAndErtHeaderFields) { + auto buf = packet_with_t_and_r_set("payload-v1"); + AlcPacket alc(buf.data(), buf.size(), /*expected_flute_version*/ 1); + EXPECT_EQ(alc.toi(), 7u); + // 5 declared words, all of them standard for version 1, so no extension space is left over. + EXPECT_EQ(alc.header_length(), 20u); +} + +namespace { + +/* The same two bit positions, but with a real EXT_FTI occupying the four words that follow the + TOI. A version 2 receiver, for which those bits size nothing, finds the extension there. A + version 1 receiver would consume the first two of those words as SCT and ERT and misread the + rest, which is the incompatibility RFC 6726 clause 11.1 describes. */ +std::vector packet_with_t_and_r_set_and_ext_fti(uint16_t encoding_symbol_length) { + const size_t header_words = 3 /* LCT header, CCI, TSI/TOI */ + 4 /* EXT_FTI */; + const size_t header_bytes = header_words * 4; + std::vector buf(header_bytes + 4 /* SBN+ESI */ + 8, 0); + auto* b = reinterpret_cast(buf.data()); + + b[0] = (1 << 4); + b[1] = 0x10 /* half_word */ | 0x08 /* sct */ | 0x04 /* ert */; + b[2] = static_cast(header_words); + b[3] = 0; + + uint16_t tsi_be = htons(1), toi_be = htons(7); + std::memcpy(b + 8, &tsi_be, 2); + std::memcpy(b + 10, &toi_be, 2); + + size_t off = 12; + b[off] = 64; // EXT_FTI + b[off + 1] = 4; // HEL, in words + uint32_t transfer_len_be = htonl(8); + std::memcpy(b + off + 4, &transfer_len_be, 4); + uint16_t esl_be = htons(encoding_symbol_length); + std::memcpy(b + off + 10, &esl_be, 2); + uint32_t msbl_be = htonl(64); + std::memcpy(b + off + 12, &msbl_be, 4); + return buf; +} + +} // namespace + +TEST(FluteV2, Version2TreatsTheSctAndErtBitsAsReservedAndUnsized) { + auto buf = packet_with_t_and_r_set_and_ext_fti(1200); + AlcPacket alc(buf.data(), buf.size(), /*expected_flute_version*/ 2); + EXPECT_EQ(alc.toi(), 7u); + ASSERT_TRUE(alc.has_fec_oti()) + << "the extension after the TOI was not reached, so the two bits still sized the header"; + EXPECT_EQ(alc.fec_oti().encoding_symbol_length, 1200u); +} + +TEST(FluteV2, Version1WouldNotReachThatExtension) { + /* The counterpart, kept so the difference between the two readings is visible rather than + asserted only on one side. Under version 1 the first two words of the extension are taken as + SCT and ERT, and what remains does not parse as a header extension. */ + auto buf = packet_with_t_and_r_set_and_ext_fti(1200); + EXPECT_THROW(AlcPacket(buf.data(), buf.size(), /*expected_flute_version*/ 1), std::runtime_error); +} + +/* RFC 6726 clause 3.4.1: "After reaching the maximum value (2^20-1), the numbering starts from the + smallest FDT Instance ID value assigned to an expired FDT Instance." Version 1 wraps to 0 + instead, which the version 1 suite covers. */ +TEST(FluteV2, InstanceIdWrapsToTheSmallestExpiredIdentifier) { + std::map expired = {{9u, 500u}, {3u, 500u}, {40u, 500u}}; + auto next = FileDeliveryTable::next_instance_id(FileDeliveryTable::kMaxFdtInstanceId, + /*current_expires*/ 1500, /*now*/ 1000, expired, + /*flute_version*/ 2); + EXPECT_EQ(next, 3u); + EXPECT_EQ(expired.count(3u), 0u) << "the reused identifier is no longer available"; + EXPECT_EQ(expired.at(FileDeliveryTable::kMaxFdtInstanceId), 1500u); +} + +TEST(FluteV2, InstanceIdSkipsAnIdentifierThatHasNotExpired) { + // 3 is still live at now=1000, so the smallest *expired* identifier is 9. + std::map expired = {{3u, 5000u}, {9u, 500u}}; + auto next = FileDeliveryTable::next_instance_id(FileDeliveryTable::kMaxFdtInstanceId, + /*current_expires*/ 1500, /*now*/ 1000, expired, + /*flute_version*/ 2); + EXPECT_EQ(next, 9u); +} + +/* "Senders MUST NOT reuse an FDT Instance ID value that is already in use for a non-expired FDT + Instance." With none expired there is no identifier the sender is permitted to take, and the + same clause leaves that case to the implementation. Version 1 wraps to 0 and warns instead. */ +TEST(FluteV2, InstanceIdRefusesToReuseALiveIdentifier) { + std::map expired = {{0u, 5000u}, {1u, 5000u}}; + EXPECT_THROW(FileDeliveryTable::next_instance_id(FileDeliveryTable::kMaxFdtInstanceId, + /*current_expires*/ 5000, /*now*/ 1000, expired, + /*flute_version*/ 2), + std::runtime_error); +} + +TEST(FluteV2, InstanceIdIncrementsBelowTheCeilingAsInVersion1) { + std::map expired; + EXPECT_EQ(FileDeliveryTable::next_instance_id(11, 1000, 2000, expired, /*flute_version*/ 2), 12u); +} + +/* RFC 6726 clause 3.3: "both a sender and a receiver easily determine to which (136-year) epoch + the FDT Instance expiration time value pertains by choosing the epoch for which the expiration + time is closest in time to the current time." The clause's own worked example is used here. */ +TEST(FluteV2, ExpiryIsReadInTheEraNearestTheCurrentTime) { + static constexpr uint64_t era = 1ULL << 32; + + // The clause's example: a session started at NTP 4,294,944,000, a few hours before era 0 ends, + // declaring an expiry of 149,504, which belongs to the next era. + EXPECT_EQ(FileDeliveryTable::expiry_in_nearest_era(149504u, 4294944000ULL), era + 149504ULL); + + // A receiver joining at NTP 63,104 in era 1 reads the same value as era 1 too. + EXPECT_EQ(FileDeliveryTable::expiry_in_nearest_era(149504u, era + 63104ULL), era + 149504ULL); + + // Well inside an era, the value is taken at face value. + EXPECT_EQ(FileDeliveryTable::expiry_in_nearest_era(2000000000u, 1999999000ULL), 2000000000ULL); +} + + +/* Version 2 is not available under either 3GPP profile, both being built on RFC 3926. + + TS 26.346 V18.2.0 clause 7.2.0: "MBMS + Clients and servers supporting MBMS download shall implement the FLUTE specification (RFC 3926 + [9]), as well as ALC (RFC 3450 [10]) and LCT (RFC 3451 [11]) features that FLUTE inherits." + RFC 3926 is version 1, and RFC 6726 clause 11.1 records that the two are not interchangeable. */ +TEST(FluteV2, RefusedUnderThe3gppProfiles) { + auto oti = FecOti{FecScheme::CompactNoCode, 0, 0, 1400, 64, 0}; + FileDeliveryTable mbs(1, oti, FileDeliveryTable::FDT_NS_NONE, Profile::Ts26517); + EXPECT_THROW(mbs.set_flute_version(2), std::runtime_error); + + FileDeliveryTable mbms(1, oti, FileDeliveryTable::FDT_NS_NONE, Profile::Ts26346); + EXPECT_THROW(mbms.set_flute_version(2), std::runtime_error); +} + +TEST(FluteV2, AcceptedUnderGeneralFlute) { + auto oti = FecOti{FecScheme::CompactNoCode, 0, 0, 1400, 64, 0}; + FileDeliveryTable fdt(1, oti, FileDeliveryTable::FDT_NS_RFC6726, Profile::Unprofiled); + EXPECT_NO_THROW(fdt.set_flute_version(2)); + EXPECT_EQ(fdt.flute_version(), 2); +}