Skip to content

FLUTE v1 (RFC 3926) and MBMS Download Profile conformance, plus assorted correctness fixes - #62

Open
jordijoangimenez wants to merge 46 commits into
5G-MAG:developmentfrom
jordijoangimenez:feature/wave3-rfc6726-compliance-fixes
Open

FLUTE v1 (RFC 3926) and MBMS Download Profile conformance, plus assorted correctness fixes#62
jordijoangimenez wants to merge 46 commits into
5G-MAG:developmentfrom
jordijoangimenez:feature/wave3-rfc6726-compliance-fixes

Conversation

@jordijoangimenez

@jordijoangimenez jordijoangimenez commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Closes #70, closes #71, closes #72, closes #73, closes #74, closes #75, closes #76, closes #82, closes #83, closes #85, closes #96, closes #97, closes #109, closes #27.

For reviewers: what changed since you last looked

Ignore the branch name. It is a historical artifact and describes nothing in this pull request. It
reads feature/wave3-rfc6726-compliance-fixes: "wave3" was internal shorthand that means nothing outside
our own notes, and RFC 6726 is FLUTE version 2, which this branch does not implement and never should
have claimed. What is actually here is FLUTE version 1 (RFC 3926) and the MBMS Download Profile of
TS 26.346 annex L
, which is what the title and the sections below describe. The name stays only because
renaming the head branch of an open pull request closes it, and closing this one would discard the review
history on it. It will be renamed after merge.

The review comments on this pull request have all been acted on:

What was checked before asking you to look again

  • Every defect claimed here was verified present on development by reading the code, not inferred from
    a report or a note.
  • Every quotation in the commits was checked verbatim against the specification text, and every clause
    number was checked separately by finding the quoted sentence and reading back to its enclosing heading.
    The clause number is the part a quotation checker cannot verify, and two locators were wrong until that
    pass caught them.
  • The tier and evidence are stated per claim: T1 with case counts, T2 with wire captures taken by a
    sniffer that links none of this library's code.

Four commits added after the review comments above

These are later than everything described above, so they carry no review history yet.

Commit What
f7c9cf5 Comment only. The Transfer-Length suppression was justified by a comment claiming "Nothing is lost on the wire either: the receive path falls back to Content-Length when the attribute is absent". That holds only for an object with no content encoding, so it asserted a safety property the code did not have.
151484c File::adopt_fdt_metadata() now takes content_encoding and content_length from the FDT entry. Without them an object whose reception was bootstrapped from a content packet's own EXT_FTI was delivered still encoded, and where the FDT carried Content-MD5 the digest was compared against the encoded buffer, failed, reset every symbol and never completed. This completes the adoption path of #74.
c21ecb5 With a UDP tunnel endpoint configured, both send sites issued a plain datagram to the announced destination as well as the encapsulated copy, so every packet left twice. Closes #109.
50cd7ec Transmitter::send() declines content encoding under either 3GPP profile rather than emit an object whose transfer length cannot be stated.

On the last two, the authority.

TS 23.247 V18.8.0, clause 7.3.1, step 13: "The AF starts transmitting the DL media stream to MB-UPF using the N6mb Tunnel, or optionally un-tunnelled i.e. as an IP multicast stream using the HL MC address."

One carriage or the other, so the direct copy had no consumer: the announced destination is reached by decapsulation at the far end. A receiver that wants to join it directly is served by not configuring a tunnel.

For 50cd7ec, both carriers for an encoded object's transfer length are closed to a profiled sender. Annex L.4.4 forbids the FDT attribute, listing Transfer-Length first under "The following attributes shall not be carried in the FDT sent by the FLUTE sender:", and the in-band route is closed by

TS 26.346 V18.2.0, clause 7.2.8: "FLUTE packets carrying symbols of files (not FDT Instances) shall not include an EXT_FTI."

Content-Length cannot substitute for an encoded object, per RFC 3926 clause 3.4.2. Declining to encode is conformant because annex L.4.2 leaves the encoding to the sender, and the sender refuses rather than silently sending the object uncompressed, so a caller is not left believing the encoding was applied. The contradiction between that permission and the two prohibitions is not resolved here. It is 3GPP's to make and is raised as 5G-MAG/Standards#212.

That guard previously existed only on #68. Without it here, this branch on its own had a reachable path that emits an object no conformant receiver can size, which would have reached development ahead of its fix.

Verification. Suite 79 cases, all passing, three of them added by these commits. Each new case was checked against the previous behaviour and fails there: the adoption case reports a decoded length of 38 against the 4096 expected, the carriage case sees the direct datagram arrive immediately, and the refusal case sees the send accepted. No T2 evidence for the carriage change: it rests on counting what leaves the Transmitter through two real sockets, and a live re-measure against a broadcast run is offered but not yet done.

Consequence for the branches stacked on this one. #68 carries its own copies of the adopt_fdt_metadata change and of the content-encoding guard. Both will conflict or go empty when it rebases onto this branch; resolve in favour of the copies here, the defects being introduced at this layer.

Dependencies and issues

Depends on: nothing in this repository. This is the base of the set and merges first.
Blocks: #61, #64, #65, #68 and #98, all of which build on it.
Closes on merge: #70, #71, #72, #73, #74, #75, #76, #82, #83, #85, #96, #97, #109, #27.

Cross-repository impact. Four repositories in the organisation vendor rt-libflute. None of them
is source-broken by this
, which I checked rather than assumed after first claiming otherwise.

Paired with: nothing. No other repository has to change for this to merge.

rt-mbs-transport-function: compiles unchanged, one redundancy left behind. It passes
FileDeliveryTable::FDT_NS_3GPP_CONSOLIDATED_V2 explicitly in ObjectCarouselPackager.cc and
ObjectListPackager.cc, and references no Profile:: value anywhere. That enum constant still exists,
and the default profile Profile::Ts26517 now derives the same schema, so the argument is overridden
to exactly the value it already asked for. Behaviour is identical. The argument and the comment above
it are now redundant, and the comment is mildly misleading because it implies the caller chooses the
schema.

Do not simplify them before this merges. Dropping the argument to FDT_NS_NONE is only correct
against a library that has this change. Against the version MBSTF's subproject currently pins, which
predates it, FDT_NS_NONE means no namespace at all, so the tidy-up would make the MBSTF emit a
namespace-less FDT. I tried it, found exactly that, and reverted. Leaving the explicit argument is
safe against both versions: the old library honours it and this one overrides it to the same value.
Worth doing once MBSTF's submodule has moved, not before.

rt-mbms-mw: compiles unchanged, behaviour changes. It constructs only the five-argument
LibFlute::Receiver. But this branch makes the receiver stop using an expired FDT Instance, per
TS 26.346 clause 7.2.9, where it previously kept using one indefinitely. A deployment whose sender
emits short Expires values will see reception behave differently. It tracks branch main, so nothing
reaches it until main is updated.

rt-mbs-client, rt-mbms-examples: no affected API found. Both vendor the library; neither references
the profile, the FDT namespace or the Transmitter constructor in the source scanned. That scan covered
the first sixty source files of each repository rather than the whole tree, so it is good evidence, not
proof.

What this is

The correctness and conformance baseline every other pull request in this set sits on, and the branch
each of them is based on. 32 commits, 19 files, 2,005 added lines. Three parts:

Part one, conformance. The library could not be operated so that its FDT conformed to the profile its
primary consumers are bound by, and its receive-side packet parse had four defects, two of them reachable
from a single unauthenticated datagram.

Issue What it covers
#70 FDT emission under the profile: Transfer-Length withheld, Content-Encoding restricted to gzip, the profiled schema's mandatory schemaVersion emitted, the File Expires attribute separated from the Cache-Control directive
#71 FEC-OTI-FEC-Instance-ID no longer emitted for a Fully-Specified FEC scheme, guarded on the scheme rather than the profile, because it is equally wrong outside the profile
#72 header parse bounded by the datagram; a zero-length extension rejected instead of looping forever; Sender Current Time and Expected Residual Time counted in the header length instead of being walked as extensions; only FLUTE version 1 accepted in EXT_FDT
#73 TSI widened to the field RFC 3451 defines, refusing rather than truncating a value that will not fit; FDT Instance ID kept inside its 20-bit field, wrapping to zero as RFC 3926 clause 3.4.1 defines the sequence
#74 reception starts from a packet's own EXT_FTI when the FDT describing it has not arrived, instead of discarding the packet, and the file so started survives until the FDT gives it its metadata
#75 the FDT Complete attribute and the LCT Close Session and Close Object flags, on both sides, with a callback
#76 testing enabled in the top-level CMake file, so ctest runs the suite instead of reporting success having found nothing

Part two, correctness fixes that are not FEC and not tunnelling. These were buried inside the Raptor
branch and belong here, with general library correctness.

Issue What it covers
#82 the send socket binds to the configured source address even when a tunnel is set, so the untunnelled copy a tunnelled session also sends leaves from the announced source and a receiver filtering on it sees the traffic
#83 the IPsec netlink socket is released instead of leaking a socket and descriptor on every call
#85 IPsec handles an IPv6 destination instead of mistranslating it into a nonsense IPv4 address, and IpSec.h gains the <cstdint> it had been getting transitively
#96 the ESP association carries an authentication algorithm as well as an encryption one, so a receiver can tell a sender's packet from a forged one
#97 the UDP tunnel send path builds an IPv6 header for an IPv6 destination instead of throwing, and the payload budget accounts for the header it writes
#27 the receiver example confines received files to the output directory, so a sender's Content-Location cannot decide where they are written

It also adds regression coverage for the FDT growth fixes merged in #60, which had none, and the read-only
Transmitter::fdt() accessor those tests need.

A profile flag decides which obligations apply, so a general FLUTE session keeps its previous behaviour
wherever plain RFC 3926 permits it. That flag is the mechanism the rest depends on, not a fix of its own.

Baseline

TS 26.346 V18.2.0, TS 26.517 V18.6.0, RFC 3926, RFC 3450, RFC 3451, RFC 5052, and RFC 6726 only where it
is the sole source for a rule. Each commit quotes the sentence it rests on. Note for reviewers who saw the
earlier version: the citations were corrected from the RFC 6726/5651/5775 generation to RFC 3926/3450/3451,
per @rjb1000's review.

Part three: the profile model, and the 3GPP mandatory sweep

Added after a clause-by-clause read of TS 26.346 clause 7.2, TS 26.346 annex L and TS 26.517 clause 6.2.
Scope was set deliberately at mandatory obligations of the 3GPP profiles only; "should" and "may"
items were assessed and left unimplemented, and the general-FLUTE RFC layer was not swept.

Profile now has three values, named after the governing documents. Only one of the three profiles
has a name in the specifications at all: annex L.4 is titled "MBMS Download Profile", TS 26.517's variant
is described in clause 6.2.1 only as that profile "with the additional requirements in clause 6.2 of the
present document", and plain FLUTE has no name because it is the absence of a profile. So the enum names
documents rather than inventing profile names.

Value Governing clauses FDT schema schemaVersion
Ts26517 (default) TS 26.517 cl.6.2, on TS 26.346 cl.7.2 + annex L.4 urn:3GPP:metadata:2022:FLUTE:FDT 2
Ts26346 TS 26.346 cl.7.2 + annex L.4 urn:IETF:metadata:2005:FLUTE:FDT 4
Unprofiled RFC 3926 and the ALC/LCT documents beneath it caller's choice none

The profile now derives the FDT schema. It used to be an independent argument that could contradict
the profile, and a default-constructed 3GPP session emitted an FDT with no namespace at all,
conforming to neither schema.

Gaps closed by the sweep, each with its clause quoted at the code:

Clause Was
7.2.10.1 schemaVersion 4 never emitted for the clause 7.2.10.1 schema
7.2.7 a TSI above 65535 widened rather than refused under a 3GPP profile
7.2.9 no ceiling enforced on FEC-OTI-Maximum-Source-Block-Length for Compact No-Code
7.2.9 an expired FDT Instance still used to interpret arriving packets, which is a shall not here where RFC 3926 says only SHOULD NOT
annex L File@Expires not taking precedence over FDT@Expires
annex L.4 3GPP restrictions tested one profile where the clauses bind both

The profile is now a hard boundary. gzip, RaptorQ and FLUTE version 2 are each refused under a 3GPP
profile, naming the clause, and each remains available under Unprofiled. The gzip refusal is in #68 and
carries a specification contradiction; see that pull request and 5G-MAG/Standards#212.

Verification

T1: 67 cases passing on this branch. T2: the FDT captured off the wire by a sniffer that links no library code confirms
Transfer-Length and FEC-OTI-FEC-Instance-ID absent, File@Expires present and distinct from
FDT-Instance@Expires, and <schemaVersion>2</schemaVersion> correctly positioned for the profiled schema.

Where this leaves conformance

A clause-by-clause sweep was done alongside this work: TS 26.346 clause 7.2, TS 26.346 annex L and
TS 26.517 clause 6.2 read as prose, and RFC 3926, RFC 3450 and RFC 3451 swept by extracting all 182
MUST and SHALL statements with their clause. With this pull request and the four built on it:

Profile Mandatory status
Ts26517 Complete
Ts26346 Complete
Unprofiled Complete except congestion control, which is in #98 and unverified

Congestion control has been split out into #98 and is not in this pull request. It is the last
mandatory item for Unprofiled, it is written but unverified, and it closes no issue. Keeping it
here would have made this pull request 22 files and 3,417 lines, and would have forced a reviewer to
accept an unverified congestion controller alongside conformance fixes that are ready.

It does not affect the two 3GPP profiles either way: TS 26.346 clause 7.2.4 excludes congestion
control for MBMS download, which is exactly why this library's single channel with C=0 and a zero
CCI is conformant there.

Two limits on how far that sweep should be trusted. The RFCs were swept by extracting MUST and SHALL
rather than read as prose, so an obligation carried only in prose could have been missed. And "should"
and "may" items were assessed but deliberately not implemented. Both are recorded in the conformance
record rather than glossed over.

Merge order

This one first. Every other pull request in the set is based on this branch
(feature/wave3-rfc6726-compliance-fixes) rather than on development, so each of their diffs shows only
its own commits and no fix in this set is presented for review twice. GitHub retargets them to
development automatically as this merges.

Full order: this, then #61, then #64. #65 and #68 need only this one and can go in any order after it.

Known overlap

PR #84 fixes the same defect as #76, ctest at the build root discovering nothing. The commit here
(build: enable testing in the top-level list file so ctest sees the suite) predates it by five days and
sits in the base of this whole stack, so removing it means rewriting all five branches. #84's
include(CTest) is the more idiomatic one-liner. One of the two should be closed; whichever is kept,
please close the other rather than merging both, since they touch the same lines and will conflict. A note
to that effect has been left on #84.

Later additions

Two commits were added after the first review round, both from re-checking this branch against the
issues it closes rather than from new review comments.

Four further commits, from reading RFC 5651 and RFC 5775

These came out of a prose read of the version 2 building blocks, RFC 5775 (ALC) and RFC 5651 (LCT),
undertaken to scope #81's second step. All three turned out to belong here rather than on the version 2
branch, because the provisions they rest on are in the version 1 documents too, or because this library
has one code path for both versions.

  • receiver: a packet carrying no payload is not an FDT packet. A packet with no payload restarted FDT
    reception and reassembled a zero-length object from it: a header with no TOI decodes to TOI 0, so such
    a packet entered the TOI 0 branch. One datagram from a conformant peer ending its session does this,
    and RFC 3926 clause 3.1 is what makes such a peer send one.

    RFC 3450 clause 4.1:

    In some special cases an ALC sender may need to produce ALC packets that do not contain any payload.

    RFC 3450 clause 4.1, on how a receiver tells:

    The total datagram length, conveyed by outer protocol headers (e.g., the IP or UDP header), enables receivers to detect the absence of the ALC payload and FEC Payload ID.

    RFC 3926 clause 3.1, on the shape a FLUTE sender uses:

    the exception that ALC packets sent in a FLUTE session with the Close Session (A) flag set to 1 (signaling the end of the session) and that contain no payload (carrying no information for any file or FDT) SHALL NOT carry the TOI

    Removing the guards makes the new test fail on the stray TOI 0 entry, so it tests the fix rather than
    passing regardless.

  • alc: send the data-less Close Session packet the specification provides for. close_session() sets a
    flag on packets sent from that point on, so on a session with an empty queue it signalled nothing at
    all: once the file set empties, send_fdt() returns early and no further packet is ever built. The
    packet RFC 3450 clause 4.1 provides for is now sent, in the shape RFC 3926 clause 3.1 requires. A TSI
    wider than 32 bits cannot express it, since dropping the TOI drops the half-word the two fields share;
    that is refused and logged rather than emitting the TOI the clause forbids.

  • ipsec: select the session's traffic by protocol and port, not the destination alone. The policy
    selector named only the destination address, so it captured every datagram addressed to that group
    whatever it was for.

    RFC 5775 clause 5.1.1:

    The sender IPsec SPD entry MUST be configured to process outbound packets to the destination address and UDP port number of the applicable ALC session.

    RFC 5775 clause 5.1.2.1:

    The implementation MUST be able to use the source address, destination address, protocol (UDP), and UDP port numbers as selectors in the SPD.

    This one is strictly a version 2 obligation and it still lands here. RFC 3450 contains no IPsec
    baseline at all; the baseline arrives with RFC 5775 and reaches FLUTE through RFC 6726 clause 7.5. But
    this library has one enable_ipsec() and no per-version IPsec path, so the over-broad selector was
    over-broad for both versions. For version 1 this is a defect fix; for version 2 it is also a
    conformance fix. enable_esp() gains a port parameter and both callers pass the port they already
    hold.

    T2, since installing a policy needs CAP_NET_ADMIN and so has no test scaffolding. Built against the
    running kernel and read back:

    $ sudo ip xfrm policy show
    src 0.0.0.0/0 dst 239.255.42.7/32 proto udp dport 40085
        dir out priority 0
        tmpl src 0.0.0.0 dst 239.255.42.7
            proto esp spi 0x00001234 reqid 4660 mode transport
    

    Reproducible with a three-line program calling
    LibFlute::IpSec::enable_esp(0x1234, "239.255.42.7", 40085, Direction::Out, key, auth_key). Test
    policies and states were flushed afterwards.

  • tests: quote the field definition the IPv6 header test checks against. Comment and test-message
    only, no assertion changed. tests/test_transmitter.cpp named RFC 8200 clause 3 in a comment and
    then asserted against it with an unquoted gtest failure message on the next line, which
    tools/verify-citations.py reads as the clause's text and cannot confirm. It now sets out the
    sentence:

    RFC 8200 clause 3:

    Version 4-bit Internet Protocol version number = 6.

    Found by running the checker over every source file on every branch rather than only the files each
    change touched. It was reporting one unconfirmed citation on all six branches, and had been for as
    long as that test has existed.


Issues closed by this pull request

Closes #27, #70, #71, #72, #73, #74, #75, #76, #82, #83, #85, #96, #97, #100, #101.

Newly added since the list above was first written:

Not closed by this pull request, and not claimed: #103, which is fixed on #64 where the additional FEC schemes are defined.

One commit here closes no issue and is not a fix: tests: exercise each profile end to end, which adds a transmit-and-reconstruct case per profile through one unmodified Receiver.

@jordijoangimenez
jordijoangimenez force-pushed the feature/wave3-rfc6726-compliance-fixes branch from 52a3ed8 to a9b4650 Compare August 7, 2026 16:11
@jordijoangimenez
jordijoangimenez marked this pull request as draft August 7, 2026 16:18
@jordijoangimenez
jordijoangimenez force-pushed the feature/wave3-rfc6726-compliance-fixes branch from a9b4650 to d3b8792 Compare August 7, 2026 16:31
@jordijoangimenez

Copy link
Copy Markdown
Contributor Author

Spec-compliance verification

Each item below was checked directly against the primary RFC text (not inferred from existing code comments or secondary sources):

Item RFC Verified against primary text
FLUTE version 2 RFC 6726 §3.1/3.4.1 Yes — exact wording match ("MUST use FLUTE version 2")
TSI widening (16/48-bit) RFC 5651 §5.1 Yes — exact formula match ("TSI field is 32×S + 16×H bits")
FDT Instance ID wraparound RFC 6726 §3.4.1 Yes — "MUST NOT reuse [a] non-expired [FDT Instance ID]", reuse starts from smallest expired ID
FDT Complete attribute RFC 6726 §3.4.2 Yes — exact semantics match
Close Session/Object flags RFC 5651 §5.1 Yes — semantics match, plus the "SHOULD set... in all subsequent packets until termination" persistence rule
EXT_FTI for TOI>0 RFC 6726 §5 Yes — "receiver MUST support both methods [EXT_FTI and FDT]"

@jordijoangimenez
jordijoangimenez marked this pull request as ready for review August 7, 2026 16:39
@jordijoangimenez
jordijoangimenez requested review from davidjwbbc and dsilhavy and removed request for dsilhavy August 7, 2026 16:39
@jordijoangimenez jordijoangimenez self-assigned this Aug 7, 2026
@jordijoangimenez jordijoangimenez added the bug Something isn't working label Aug 7, 2026
@dsilhavy dsilhavy added this to the v1.0.0 milestone Aug 7, 2026
@davidjwbbc

Copy link
Copy Markdown
Contributor

3GPP TS 26.517 (v18.5.1)

  • Clause 6.2.1 states:

    If FLUTE [12] is used to realise the Object Distribution Method, the MBS Distribution Session shall conform to the MBMS Download Profile as defined in clause L.4 of TS 26.346 [7] with the additional requirements in clause 6.2 of the present document.

3GPP TS 26.346 (v18.1.0)

  • Clause L.4 states:

    The Download Profile constrains the large set of FDT parameters specified in clause 7.2 of the present document, which includes both the FDT elements and attributes defined by the IETF FLUTE standard, RFC 3926 [9] and various 3GPP-defined FDT extensions for MBMS.

  • Clause 7.2.0 states:

    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. In addition, several optional and extended aspects of FLUTE ,as described in the following clauses, shall be supported.

  • Clause 7.2.7 states:

    FLUTE and ALC mandatory header fields shall be as specified in [9, 10] with the following additional specializations

  • References:

    [9] IETF RFC 3926 (October 2004): "FLUTE - File Delivery over Unidirectional Transport", T. Paila, M. Luby, R. Lehtonen, V. Roca, R. Walsh.
    [10] IETF RFC 3450 (December 2002): "Asynchronous Layered Coding (ALC) Protocol Instantiation", M. Luby, J. Gemmell, L. Vicisano, L. Rizzo, J. Crowcroft.

Therefore the FLUTE session for 3GPP applications is based on RFC3926, which is the FLUTE v1 specification, and states:

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'.

Since we use this library with 3GPP applications it needs to support version 1 of FLUTE. It could support version 2 as well, but changes to make this version 2 only will break 3GPP based applications.

@davidjwbbc davidjwbbc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no problem with FLUET v2 being added, but this library needs to support FLUTE v1 for compliance with 3GPP specifications. Please do not make changes that remove FLUTE v1 compliance.

@jordijoangimenez

Copy link
Copy Markdown
Contributor Author

Thanks @davidjwbbc. Having discussed this now wth @dsilhavy, I'm going to defer v2 to another branch and leave that code out of this PR. Then I will check that all v1 is consistent with 3GPP and update this PR accordingly.

jordijoangimenez added a commit to jordijoangimenez/rt-libflute that referenced this pull request Aug 11, 2026
AlcPacket's transmit constructor took a uint16_t TSI while Transmitter itself
stores a uint64_t, silently truncating any TSI above 65535 on every packet.
Widened the constructor to uint64_t and now sets tsi_flag (the LCT header's
S flag, adding a second 32-bit TSI word per RFC 5651 SS4.2's S/H flag
mechanism -- unchanged since RFC 3451, so this applies identically to FLUTE
v1) whenever the value exceeds 16 bits, giving the full 48-bit field width
this wire scheme supports. Values that fit in 16 bits keep the original
single-half-word encoding unchanged.

Dropped this commit's original FLUTE-version-2 change: RFC 6726 SS3.1/3.4.1
does require version 2, but 3GPP TS 26.517 cl.6.2.1 -> TS 26.346 Annex L
mandates the FLUTE v1 profile (RFC 3926) for MBMS/5G-MAG applications, and
this library's primary consumers are exactly those applications -- per
davidjwbbc's review on 5G-MAG#62. Kept on a separate future/flute-v2-support
branch in case genuine (non-3GPP) FLUTE v2 support is wanted later.
@jordijoangimenez
jordijoangimenez force-pushed the feature/wave3-rfc6726-compliance-fixes branch from d3b8792 to 6c7137d Compare August 11, 2026 07:53
@jordijoangimenez

jordijoangimenez commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a corrected version, per this thread:

  • Dropped the FLUTE version-2 change entirely. @davidjwbbc is right — TS 26.517 cl.6.2.1 → TS 26.346 Annex L mandates the RFC 3926 (v1) profile for MBMS/MBS applications, which are this library's primary consumers. Kept it on a separate future/flute-v2-support branch in case genuine (non-3GPP) FLUTE v2 support is wanted later.
  • Re-verified the other four fixes directly against TS 26.346 v18.2.0 (not just the IETF RFCs) before re-pushing:
    • TSI widening: cl.7.2.7 mandates TSI shall be exactly 16 bits for MBMS. The fix only adds a wider on-wire encoding when a value actually exceeds 16 bits — a compliant MBMS session's TSI never does, so no compliant packet's wire format changes. Flagging the mandate here for visibility, not because the fix violates it.
    • FDT Instance ID wraparound, Complete attribute, Close Session/Object flags: all explicitly present in 3GPP's own FDT schema/clause 7.2.7, not v2-only additions — confirmed compliant.
    • EXT_FTI bootstrap: receiver-side only; the transmitter still only emits EXT_FTI on TOI=0 packets per cl.7.2.8's mandate, unchanged.

Tests/build unaffected — only the one line (FLUTE version nibble) and its commit message changed; the other three commits are untouched.

@rjb1000

rjb1000 commented Aug 11, 2026

Copy link
Copy Markdown

@jordijoangimenez: Now the scope of this PR has changed to be compliance with TS 26.517 and TS 26.346, the title should also be changed accordingly. Given that the target is now compliance with FLUTE v1, we are talking about RFC 3986 rather than RFC 6726.

Rather than burying the update in a supplementary comment, it would be better to integrate the correct scope into the main issue description to avoid confusing future readers.

jordijoangimenez added a commit to jordijoangimenez/rt-libflute that referenced this pull request Aug 11, 2026
RFC 6726 SS3.1/SS3.4.1 requires version 2 in EXT_FDT; the decoder already
tolerates up to version 2 (unchanged by this commit), only transmit lagged.

Split out of 5G-MAG#62: 3GPP TS 26.517 cl.6.2.1 -> TS 26.346 Annex L mandates the
RFC 3926 (v1) profile for MBMS/5G-MAG applications, this library's primary
consumers, so this must not be the default. Kept as a separate branch for a
genuine non-3GPP FLUTE v2 deployment, or in case 3GPP adopts v2 later.
@jordijoangimenez jordijoangimenez changed the title RFC 6726/5651/5052 compliance: FLUTE version 2, wide TSI, FDT Instance ID wraparound, Close Session/Object flags, EXT_FTI bootstrap RFC 6726/5651/5052 compliance: wide TSI, FDT Instance ID wraparound, Close Session/Object flags, EXT_FTI bootstrap Aug 11, 2026
@jordijoangimenez jordijoangimenez changed the title RFC 6726/5651/5052 compliance: wide TSI, FDT Instance ID wraparound, Close Session/Object flags, EXT_FTI bootstrap Compliance with TS 26.517 and TS 26.346 (FLUTE v1, RFC 3986): wide TSI, FDT Instance ID wraparound, Close Session/Object flags, EXT_FTI bootstrap Aug 11, 2026
@jordijoangimenez

Copy link
Copy Markdown
Contributor Author

Suggestions applied, @rjb1000

jordijoangimenez and others added 16 commits September 4, 2026 20:21
Problem
  On reaching the top of the 20-bit field, advance_instance_id() searched for the smallest
  previously-used ID whose Expires had passed, reused that, and threw
  std::runtime_error("FDT Instance ID space exhausted") when none had. That is the RFC 6726
  rule. This library implements RFC 3926, whose sequence is unconditional and has no failure
  case, so a sender could refuse to continue in a situation the specification it implements
  does not treat as an error.   [source-derived]

Basis
  RFC 3926 clause 3.4.1: "After reaching the maximum value (2^20-1), the numbering starts
  again from '0'."

  The same clause adds a recommendation, not a requirement, that a sender deliver a
  wraparound ID only once the previous instance holding it has expired. RFC 6726 clause
  3.4.1 turns that into "Senders MUST NOT reuse an FDT Instance ID value that is already in
  use for a non-expired FDT Instance", and wraps to the smallest expired ID rather than to
  zero, but that is the version 2 rule. TS 26.346 clause L.4.1 selects RFC 3926.

Raised by
  reading the authority during this work

Change
  FileDeliveryTable owns the sequence, so it is fixed there. The sequence is now
  next_instance_id(), a static pure function, so the wraparound is exercisable without a live
  session; advance_instance_id() supplies the current state and stores the result. Reuse of an
  identifier whose previous holder has not expired is logged at warning level, which is what
  the recommendation supports, instead of being avoided by choosing another identifier or by
  throwing.

Verification
  T1: protocol:FdtInstanceIdWraparoundTest passes, 4 cases. Whole suite 45 cases, all passing.

Not in this change
  No change to how Expires is chosen, nor to the receiver side, which does not track
  instance IDs.
Problem
  On completing any file, the receiver erases every other file carrying the same
  Content-Location, so that a re-sent object replaces the copy already in progress. A file
  started from a content packet's own EXT_FTI has no Content-Location until its FDT entry
  arrives, and neither does the transient file the TOI 0 FDT itself is reassembled into. The
  two empty placeholders compared equal, so completing the FDT erased the bootstrapped file
  immediately before the loop that would have given it its metadata. src/Receiver.cpp:264.
  [code-derived]

Basis
  RFC 3926 clause 5: "For the TOI values other than 0 the receiver MUST support both methods:
  the use of EXT_FTI and the use of FDT."

  That support is what the erased file was carrying out. No clause governs the replacement
  sweep itself; it is this library's own de-duplication, and the identity it keys on is a
  Content-Location, which an empty string is not.

Raised by
  reading the authority during this work

Change
  The sweep now skips the comparison when the completed file's Content-Location is empty.
  The receiver owns both the sweep and the bootstrap, so the fix belongs there. Behaviour for
  a real repeated Content-Location is unchanged.

Verification
  T1: protocol:ExtFtiBootstrapTest passes, 3 cases, the third of which fails without this
  change. Whole suite 49 cases, all passing.

Not in this change
  Nothing about what the receiver does when the FDT never arrives; such a file keeps an empty
  Content-Location and is not written out, which is unchanged.
Problem
  configure_state() attached one algorithm attribute, XFRMA_ALG_CRYPT carrying aes, so the
  security association enable_ipsec() installs offers confidentiality and no integrity. A
  receiver on such a session cannot distinguish a sender's packet from a forged one, which is
  usually the property a caller reaching for IPsec on a multicast session wants. Observed:
  ip xfrm state show on an association installed by enable_ipsec() shows an enc line and no
  auth line.   [observed]

Basis
  RFC 4303 clause 1: "Using encryption without a strong integrity mechanism on top of it
  (either in ESP or separately via AH) may render the confidentiality service insecure against
  some forms of active attacks"

  The same clause lists confidentiality with integrity as a MUST for an ESP implementation and
  confidentiality alone as a MAY.

  RFC 3926 clause 7: "At the packet level, it is RECOMMENDED that a packet level authentication
  be used to ensure that each received packet is an authentic and uncorrupted packet containing
  FEC data for the object arriving from the specified sender."

  IPsec is the only packet-level protection this library offers, so an encryption-only
  association leaves that recommendation unmet even where a caller has enabled it. Neither
  document names an algorithm. HMAC-SHA256 is an engineering choice.

Raised by
  reading the authority during this work

Change
  IpSec owns the association, so it is fixed there. configure_state() attaches XFRMA_ALG_AUTH
  with hmac(sha256) alongside the encryption algorithm. enable_esp(), Transmitter::enable_ipsec()
  and Receiver::enable_ipsec() take an authentication key. A caller supplying only an encryption
  key keeps working: the authentication key is then derived from it through SHA-256 with a
  fixed context string, which gives the two algorithms distinct key bytes. That is weaker than
  two independent keys, which is why the parameter exists.

Verification
  T2: with the two-key call, ip xfrm state show reports
  "auth-trunc hmac(sha256) 0x1112131415161718191a1b1c1d1e1f20 96" beside the enc line, the key
  exactly as supplied. With the one-key call it reports a derived key differing from the
  encryption key. Test associations flushed afterwards. Suite unaffected, 49 cases passing.

Not in this change
  The truncation length is Linux's 96-bit default for XFRMA_ALG_AUTH rather than RFC 4868's
  128-bit HMAC-SHA-256-128; changing it needs XFRMA_ALG_AUTH_TRUNC and matters only against an
  external peer expecting the longer form. No anti-replay window is configured.
Problem
  With a tunnel endpoint configured the transmitter builds the inner IP and UDP headers itself.
  Both builders took the addresses with to_v4(), which throws bad_address_cast for a v6
  address, so an IPv6 destination did not degrade, it threw out of the send path on the first
  packet. Four sites also subtracted or added a literal 20 for the IP header, which is the
  IPv4 size, so an IPv6 session set a payload 20 bytes over the budget the caller asked for,
  tunnelled or not. Observed: the new test aborts with
  "terminate called after throwing an instance of boost::asio::ip::bad_address_cast" against
  the unchanged code.   [observed]

Basis
  RFC 8200 clause 8.3: "an upper-layer protocol must take into account the larger size of the
  IPv6 header relative to the IPv4 header."

  RFC 8200 clause 8.1: "whenever originating a UDP packet, an IPv6 node must compute a UDP
  checksum over the packet and the pseudo-header, and, if that computation yields a result of
  zero, it must be changed to hex FFFF for placement in the UDP header."

  Clause 3 gives the header format and clause 8.1 the pseudo-header the checksum covers.

Raised by
  reading the authority during this work

Change
  Transmitter owns the encapsulation, so it is fixed there. create_ip_hdr() writes a 40-byte
  IPv6 header for a v6 destination, create_udp_pkt() computes the checksum over the clause 8.1
  pseudo-header and substitutes all ones for a computed zero, and the four budget sites take
  the header length from the destination's family through one helper. IPv4 behaviour is
  unchanged, and the untunnelled path, where the kernel builds the headers, is untouched.

Verification
  T1: unit:TransmitterIPv6TunnelTest passes, 1 case. It captures the bytes a real Transmitter
  sends to a local socket standing in for the tunnel peer, and checks the version nibble,
  payload length, next header and destination address, then re-derives the UDP checksum over
  the pseudo-header with its own summation and confirms it self-verifies to zero. Reverting
  the change makes it abort as quoted above. Whole suite 50 cases, all passing.

Not in this change
  No IPv6 extension headers are written; the fixed 40-byte header is all this path needs. The
  IPv4 fragmentation and identification fields keep their existing zero values.
… tunnel

Problem
  With a tunnel endpoint configured, send_next_packet() chose between the tunnel and the
  announced destination rather than using both, so a receiver that joins the announced address
  directly, instead of sitting behind the tunnel's decapsulation, received nothing at all. The
  session looked healthy from the sender: packets were transmitted continuously and reached the
  tunnel peer. src/Transmitter.cpp, the if/else on _tunnel_endpoint in send_next_packet().
  [code-derived]

Basis
  No clause governs this. A tunnel is a transport for the same session, not a different
  destination, and nothing in the FLUTE configuration says that setting one withdraws the
  announced address. Engineering choice, see rule 12.

Raised by
  reading the authority during this work

Change
  Transmitter owns the send path, so it is fixed there. When a tunnel is configured both copies
  now go out: the plain copy fire-and-forget, and the encapsulated copy carrying the completion
  bookkeeping, which preserves the completion timing tunnelled operation already had. Without a
  tunnel the single plain send carries it, as before. This is also the behaviour the earlier
  commit "transmitter: bind to the configured source address even when tunnelling" was written
  for: binding the source address matters precisely because a tunnelled session also emits an
  untunnelled copy, which until now it did not.

Verification
  T1: whole suite passes, 50 cases, including e2e:FluteEndToEndTest.TransmitsFileToReceiverThroughUdpTunnel
  and unit:TransmitterIPv6TunnelTest, which both exercise the tunnelled path.

Not in this change
  No way to ask for the tunnelled copy only. If a deployment needs to suppress the plain copy
  that is a new option, not a change to this default.
…ach to its own rules

Problem
  One profile value stood for two different 3GPP profiles that mandate different FDT schemas with
  different mandatory schemaVersion values, and the schema was a separate argument that could
  contradict it. A default-constructed 3GPP session emitted an FDT with no namespace at all,
  conforming to neither schema; a session on the clause 7.2.10.1 schema never emitted the
  schemaVersion that schema requires; and a TSI wider than the field the specifications fix was
  widened rather than refused. Three further obligations were unimplemented: the ceiling on
  Maximum-Source-Block-Length, the precedence of File@Expires over FDT@Expires, and the
  prohibition on using an expired FDT Instance.   [source-derived]

Basis
  TS 26.517 V18.6.0 clause 6.2.1: "The MBSTF shall use the Profiled FDT Schema according to clause
  L.6 of TS 26.346 [7] to describe the object list currently being transmitted in the MBS
  Distribution Session."

  TS 26.346 V18.2.0 clause 7.2.9: "The extended FLUTE FDT instance schema defined in clause 7.2.10.1
  (based on the one in RFC 3926 [9]) shall be used."

  TS 26.346 V18.2.0 clause 7.2.10.1: "In this version of the present document the network shall set
  the content of the schemaVersion element, defined as a child of the FDT-Instance element, to the
  value 4."

  TS 26.346 V18.2.0 clause L.6.3: "The BM-SC shall set the schemaVersion element to 2 in all
  instance documents"

  TS 26.346 V18.2.0 clause 7.2.7: "-The Transmission Session Identifier (TSI) field shall be of
  length 16 bits (S=0, H=1, 16 bits)."

  TS 26.346 V18.2.0 clause 7.2.9: "For MBMS operation, the UE shall not use a received FDT Instance
  to interpret packets received beyond the expiration time of the FDT Instance."

Raised by
  reading the authority during this work

Change
  Profile becomes Mbs5g, MbmsDownload and GeneralFlute, defaulting to Mbs5g, and each 3GPP profile
  derives its own FDT namespace so the two can no longer disagree. schemaVersion is emitted per
  schema, 2 for the annex L.6.1 one and 4 for the clause 7.2.10.1 one. Every 3GPP restriction now
  tests is_3gpp() rather than naming a single profile, so the annex L.4 rules apply to both, which
  they always did in the specification.

  Added: refusal of a TSI above 65535 and of a Compact No-Code Maximum-Source-Block-Length above
  65535 under either 3GPP profile; effective_expiry(), applying File@Expires over FDT@Expires; and
  expired(), which the receiver now consults before interpreting any packet against a held FDT
  Instance. Comments and messages naming annex L's profile for what are clause 7.2 rules corrected.

Verification
  T1: 61 cases pass, 10 new, covering the schema and schemaVersion each profile produces, the
  namespace argument being overridden by a 3GPP profile, both refusals at and above their ceilings,
  the expiry precedence in both directions, and the expiry boundary.

Not in this change
  Nothing above the mandatory level. The should and may items of clause 7.2 and annex L are assessed
  but not implemented, and none of the general-FLUTE RFC obligations are touched.
Problem
  set_expires() accepted any value, including a time already past. An instance created already
  expired can never be used to interpret anything, and a receiver following TS 26.346 clause 7.2.9
  discards it the moment it arrives, so the session transmits and delivers nothing. The library's own
  Transmitter always passes a future time, so this was reachable only through the public setter.
  [source-derived]

Basis
  RFC 3926 clause 3.3: "A sender MUST use an expiry time in the future upon creation of an FDT
  Instance relative to its Sender Current Time (SCT)."

  Binding under every profile, not only the 3GPP ones, which inherit it through TS 26.346 clause
  7.2.0's adoption of RFC 3926.

Raised by
  reading the authority during this work

Change
  FileDeliveryTable owns the attribute, so the check is there. set_expires() becomes a real function
  and refuses a value at or before the current time. The parse path is untouched: it assigns the
  member directly, and a receiver must be able to read an instance that expired in transit in order
  to discard it.

Verification
  T1: 63 cases pass, 2 new, covering refusal under a 3GPP profile and under plain RFC 3926, and
  acceptance of a future value. Two existing expiry tests used a fixed constant of 1000 as an expiry;
  they now compute a real future NTP time, which is what a sender does.

Not in this change
  No check that the expiry is far enough ahead to be useful. The clause requires only that it be in
  the future, and how far is the operator's choice.
…e FEC Instance ID field

Problem
  Two defects in the EXT_CENC and EXT_FTI parsing, both found by reading RFC 3926 as prose rather
  than searching it for mandatory keywords.

  An EXT_CENC naming a content encoding outside the four the library knows fell through the switch
  and left the encoding at NONE, so the undecoded FDT bytes went to the XML parser as though they
  were XML. The failure then surfaced somewhere further on, naming the wrong thing.

  Separately, the 16 bits between the transfer length and the encoding symbol length were commented
  as reserved on both the parse and the send side. They are the FEC Instance ID.   [code-derived]

Basis
  RFC 3926 clause 3.4.3, on why an unknown value cannot be ignored: "The definition of this field is
  outside the scope of this specification."

  RFC 3926 clause 5.1.1, on the field that is not reserved: "It is only present if the value of FEC
  Encoding ID is in the range of 128-255.  When the value of FEC Encoding ID is in the range of
  0-127, this field is set to 0."

Raised by
  reading the authority during this work

Change
  An unrecognised CENC value is refused with a message naming it, rather than treated as no
  encoding. The comments on both sides now name the FEC Instance ID and cite why a Fully-Specified
  scheme leaves it zero. No wire behaviour changes for any encoding the library does know, and the
  send side already wrote the zero the clause requires.

Verification
  T1: 63 cases pass, unchanged. No new case: reaching the refusal needs a hand-built packet carrying
  an EXT_CENC with an out-of-range value, and the existing header-parse tests cover the extension
  walk that leads to it. The change is a refusal on a path that previously produced a misleading
  error further on, so a test would assert which of two errors is raised.

Not in this change
  Nothing decodes an encoding beyond the four already supported.
Problem
  A packet was accepted on a TSI match alone. The arriving source address was available, held in
  _sender_endpoint by every receive, and never compared with anything. A session is identified by the
  source and TSI together, so on an any-source join any sender on the group using the session's TSI
  was accepted as though it were the session's own, and its packets fed into object reassembly.
  [source-derived]

Basis
  RFC 3450 clause 4.5: "The receiver MUST verify that the sender IP address together with the TSI
  carried in the header matches one of the (sender IP address, TSI) pairs that was received in a
  Session Description and that the receiver is currently joined to."

  The same clause continues that on no match "the packet MUST be discarded without further
  processing", which is what this does, and it orders the receiver's steps so that this one precedes
  any processing of the packet.

  RFC 3451 clause 6.2 is what made the ordering matter: a receiver unable to process an LCT header
  "MUST drop from the session", so whether a parse failure is a session-level fact or a stray
  datagram depends on having ruled the datagram out first.

Raised by
  reading the authority during this work

Change
  Receiver holds the socket and the session identity, so the check belongs there. Where the caller
  named a source it is parsed once at construction, and every datagram's origin is compared against
  it before the packet is parsed at all, so traffic that is not this session's cannot reach the
  parser or influence how a parse failure is treated. A mismatch is discarded with the source logged.

  A source-specific join already has the kernel filtering on the same address, so there the check is
  defence in depth against a routing or membership mistake rather than the only guard. For an
  any-source session the library is given no source to compare against and the obligation cannot be
  met at all; that is recorded as a limitation rather than passed over, and a caller that wants it
  should name the source.

Verification
  T1: 63 cases pass, unchanged. No new case: reaching the discard needs a second sender on the same
  group and port as an existing source-specific session, which the test scaffolding cannot stand up
  without a second host or a routing fixture.

Not in this change
  Clause 4.5's "If multiple packets are received with non-matching (sender IP address, TSI) values
  then the receiver SHOULD leave the session" is not implemented; nothing counts mismatches. Nor is
  clause 6.2's "MUST drop from the session" on an unprocessable header: the library discards and
  continues. A receiver that leaves on a burst of foreign or malformed packets is easier to disrupt
  than one that discards them, so both are deliberate and recorded rather than overlooked.
…es for

Problem
  close_session() sets a flag on packets sent from that point on, so on a session with nothing
  left to send it signals nothing at all. Once the file set empties, send_fdt() returns early
  and no further packet is ever built. src/Transmitter.cpp, close_session().   [code-derived]

Basis
  RFC 3450 clause 4.1:
  "In some special cases an ALC sender may need to produce ALC packets that do not contain any
  payload.  This may be required, for example, to signal the end of a session or to convey
  congestion control information."

  RFC 3926 clause 3.1 gives the packet its shape in a FLUTE session:
  "the exception that ALC packets sent in a FLUTE session with the Close Session (A) flag set to
  1 (signaling the end of the session) and that contain no payload (carrying no information for
  any file or FDT) SHALL NOT carry the TOI"

  Dropping the TOI drops the half-word flag the two fields share, so the TSI becomes whole
  words.
  RFC 5651 clause 5.1:
  "The TSI field is 32*S + 16*H bits in length"

  On the receiving side the flag is advisory, which is why the packet is sent once and not
  retransmitted.
  RFC 5651 clause 5.1:
  "the receiver SHOULD assume that no more packets will be sent to the session"

Raised by
  reading the authority during this work

Change
  AlcPacket gains a constructor for a header-only Close Session packet: no payload, so no FEC
  Payload ID and no TOI. Transmitter sends one when close_session() finds the queue already
  empty, and one more when the last file drains while the session is closing. Both send paths
  are covered, plain and tunnelled, fire and forget.

  A TSI wider than 32 bits cannot express this packet, since the whole-word TSI this encoding
  builds is one word. It is refused rather than emitting the TOI the clause forbids, and the
  transmitter logs and carries on rather than failing the session.

Verification
  T1: flute_protocol_tests, 66 cases in the suite pass, three new: the header's every field
  against the clause, the refusal above its TSI ceiling, and a round trip back through this
  library's own parser.

Not in this change
  Retransmitting it, or repeating it over the last few seconds of the session, which clause 5.1
  offers as a SHOULD once the flag has been set in one packet. Sending it on a session whose TSI
  needs more than 32 bits, which the encoding cannot express: recorded as a limitation.
Problem
  A packet with no payload restarted FDT reception and reassembled a zero-length object from it.
  A header with no TOI decodes to TOI 0, so such a packet entered the TOI 0 branch and replaced
  the object in progress. Observed: with the guards removed, a data-less packet delivered to a
  live receiver leaves a TOI 0 entry in file_list().   [observed]

  Reachable beyond that, and not demonstrated here: with an FDT object present and incomplete
  while the held instance id matches, src/EncodingSymbol.cpp subtracts a four-byte FEC Payload ID
  from a payload length of zero, which wraps a size_t.   [code-derived]

Basis
  RFC 3450 clause 4.1:
  "In some special cases an ALC sender may need to produce ALC packets that do not contain any
  payload."

  RFC 3450 clause 4.1, on how a receiver tells:
  "The total datagram length, conveyed by outer protocol headers (e.g., the IP or UDP header),
  enables receivers to detect the absence of the ALC payload and FEC Payload ID."

  RFC 3926 clause 3.1 gives the shape a FLUTE sender uses, which is why a conformant peer sends
  one:
  "the exception that ALC packets sent in a FLUTE session with the Close Session (A) flag set to
  1 (signaling the end of the session) and that contain no payload (carrying no information for
  any file or FDT) SHALL NOT carry the TOI"

  RFC 3450 clause 4.5 puts the length check in step 1, before the payload is touched:
  "The receiver MUST parse the packet header and verify that it is a valid header.  If it is not
  valid then the packet MUST be discarded without further processing."

Raised by
  reading the authority during this work

Change
  Receiver measures the payload from the datagram length, as the clause says to, and returns
  after honouring the Close flags when there is none. A payload present but shorter than a FEC
  Payload ID is discarded as an invalid packet. Receiver owns this because the length is only
  known where the datagram is; AlcPacket sees the header alone.

Verification
  T1: flute_protocol_tests, 67 cases in the suite pass, one new. Removing both guards makes the
  new case fail on the TOI 0 entry, so it tests the fix rather than passing regardless.

Not in this change
  Distinguishing an absent TOI from TOI 0 in AlcPacket's own interface. The payload length
  settles this case, and no other caller needs the distinction yet.
…tination alone

Problem
  The security policy selector named only the destination address, so it captured every datagram
  addressed to that group whatever it was for: another session sharing the group on a different
  port, or any other protocol addressed there, was pushed through this session's association.
  src/IpSec.cpp, configure_policy(): neither sel.proto nor sel.dport was ever set.
  [code-derived]

Basis
  No version 1 document governs this: RFC 3450 contains no IPsec baseline at all. The obligation
  belongs to version 2, which inherits it.

  RFC 6726 clause 7.5:
  "Since FLUTE relies on ALC/LCT, it inherits the "baseline secure ALC operation" of [RFC5775]."

  RFC 5775 clause 5.1.1:
  "The sender IPsec SPD entry MUST be configured to process outbound packets to the destination
  address and UDP port number of the applicable ALC session."

  RFC 5775 clause 5.1.2.1:
  "The implementation MUST be able to use the source address, destination address, protocol
  (UDP), and UDP port numbers as selectors in the SPD."

  It lands here rather than on the version 2 branch because the library has one enable_ipsec()
  and no per-version IPsec path, so the over-broad selector was over-broad for both versions. For
  version 1 this is a defect fix; for version 2 it is also a conformance fix.

Raised by
  reading the authority during this work

Change
  configure_policy() sets the protocol to UDP and the destination port to the session's own, with
  a full port mask. enable_esp() takes the port, and both callers pass the port they already
  hold: Transmitter from its endpoint, Receiver from a new member holding the constructor's
  argument.

  The source port is deliberately not selected on. A sender's source port is not part of the
  session description, so pinning it would exclude legitimate traffic.

Verification
  T2: installed against the running kernel and read back with `ip xfrm policy show`. The policy
  reads `src 0.0.0.0/0 dst 239.255.42.7/32 proto udp dport 40085`, `dir out`, `mode transport`.
  Reproducible by the short program in the pull request description. Test policies and states
  flushed afterwards.
  T1 alongside: 67 cases in the suite pass, none new; installing a policy needs CAP_NET_ADMIN, so
  this path has no test scaffolding.

Not in this change
  A selector on the source address, which the same clause lists as available but does not
  require, and which the library cannot fill for an any-source session.
Problem
  `tests/test_transmitter.cpp` named RFC 8200 clause 3 in a comment and then asserted against it
  with an unquoted failure message on the following line, which the citation checker reads as the
  clause's text and cannot confirm. A reader has no sentence to check the assertion against either.
  [code-derived]

Basis
  RFC 8200 clause 3:
  "Version             4-bit Internet Protocol version number = 6."

Raised by
  reading the authority during this work, prompted by `tools/verify-citations.py` reporting the line
  as an unconfirmed citation on every branch in the stack

Change
  The clause reference and its sentence are set out above the assertions, in the shape the checker
  reads and a reader can follow. The gtest failure message no longer doubles as a quotation.

  Comment and test-message only. No behaviour changes and no assertion changes.

Verification
  T1: 67 cases pass, unchanged. The citation checker reports 58 confirmed and none unconfirmed
  across this branch's sources, where it previously reported one failure.

Not in this change
  The other assertions in the same test, whose fields the same clause defines and which the comment
  now covers collectively rather than one quotation each.
Problem
  A sender-side FileDeliveryTable left _expires indeterminate. The constructor's initialiser list
  sets _instance_id, _instance_id_sent, _global_fec_oti, _fdt_namespace and _profile, and the
  member carried no default initialiser. expired() and advance_instance_id() then read an
  indeterminate value, and to_string() would write one into the FDT-Instance Expires attribute for
  a caller reaching it before set_expires().
  Observed: protocol:FdtExpiryTest.AnInstanceWithNoExpiresNeverExpires passes here and fails on a
  branch that only adds code above it, stack layout being the only difference.
  [code-derived]

Basis
  No clause governs a member initialiser; code-derived, no spec claim. Zero is the value
  expired() already reads as "no expiry set" (include/FileDeliveryTable.h), so this makes the
  member hold the meaning its own reader assigns it.

Raised by
  observation while running the suite on a dependent branch

Change
  Default-initialise _expires at its declaration, where a reader looking for the invariant will
  find it, rather than in one of the two constructors. The parse constructor already assigns it
  from the Expires attribute and is unaffected.

  Transmitter::send_fdt() calls set_expires() before to_string(), so the indeterminate value did
  not reach the wire through this library's own transmit path.

Verification
  T1: 68 cases pass, 1 new (FdtExpiryTest.AFreshlyConstructedInstanceReportsNoExpiry), 0 failures.
  67 passed before the change.

Not in this change
  FEC Encoding ID validation on receive, and per-profile end-to-end coverage: separate commits.
Problem
  Both FDT parse sites cast the FEC-OTI-FEC-Encoding-ID attribute straight into FecScheme:
  src/FileDeliveryTable.cpp, FDT-Instance level and File level. The attribute is an arbitrary
  integer off the network, so any value at all became a FecScheme enumerator that no branch
  handles. The FDT was accepted, File entries were built from it, and the failure surfaced later
  and elsewhere as EncodingSymbol's generic "Unsupported FEC scheme", pointing at packet assembly
  rather than at the FDT that declared it.
  [code-derived]

Basis
  TS 26.346 V18.2.0 clause L.4.7: "the two FEC schemes referenced in this specification, the
  Compact No-Code FEC scheme as specified in RFC 3695 [13], and the Raptor FEC scheme as specified
  in RFC 5053 [91] are optional to implement by the BM-SC and mandatory to support by the UE."

  A sender under either 3GPP profile may therefore not use any other scheme, so an FDT declaring
  one is non-conformant. Neither TS 26.346 nor RFC 3926 states what a receiver does on encountering
  an FEC Encoding ID it cannot decode; that part is code-derived, no spec claim. Failing at the
  declaration rather than at the first packet is the choice made here, on the same reasoning as
  rule 12: refuse loudly rather than proceed on a value nothing can honour.

Raised by
  reading the authority during this work

Change
  fec_scheme_from_encoding_id() in include/flute_types.h maps a wire identifier onto a scheme this
  library implements, or reports that none matches. It sits beside the enumeration so the two are
  extended together; a branch adding a scheme adds an enumerator and a case in one place. Both
  parse sites use it and throw on no match, naming the offending value.

  Written as a match against known identifiers rather than a rejection of known-bad ones, so that
  adding an enumerator cannot silently widen what the parser accepts.

Verification
  T1: 71 cases pass, 3 new (FdtFecEncodingId suite: a recognised identifier accepted, an
  unrecognised one refused at the FDT-Instance level and at the File level), 0 failures. 68 passed
  before the change.

Not in this change
  The sender-side check on which schemes a profile admits, which belongs where the additional
  schemes are defined. Per-profile end-to-end coverage: separate commit.
Problem
  The end-to-end suite ran one profile. Every other profile assertion in the suite is made against
  a FileDeliveryTable in isolation, so for Ts26346 and Unprofiled the library was known to build a
  conformant FDT and not known to produce one a receiver could reconstruct an object from. Those
  are separate claims, and the FDT schema differs between profiles, which is exactly where the
  two can come apart.
  [code-derived]

Basis
  TS 26.346 V18.2.0 clause L.4.1: "The MBMS Download Profile primarily defines the required,
  expected and permitted usage of FLUTE FDT attributes and elements by the BM-SC, and the
  corresponding mandatory versus optional support for those FDT parameters by the MBMS Client."

  The obligation is asymmetric: the profile constrains the sender, the receiver carries a support
  obligation instead. That is why Receiver takes no profile and why one receiver must handle all
  three senders. These cases assert that directly rather than leaving it as a design claim.

Raised by
  reading the authority during this work

Change
  EndToEndOptions gains a profile, defaulted to Ts26517 so the existing two cases are unchanged.
  Three cases added, one per profile, each transmitting and reconstructing a file through the same
  unmodified Receiver.

  No namespace option: FileDeliveryTable derives the FDT namespace from the profile and overrides
  the argument, so the two cannot disagree and there is nothing for a test to vary independently.

Verification
  T1: 74 cases pass, 3 new (FluteEndToEndProfileTest), 0 failures. 71 passed before.

Not in this change
  Tunnelled variants per profile: the tunnel is below FLUTE and profile-independent, and the
  existing tunnelled case already covers that path.
Written by meson when this repository is consumed as a subproject (as rt-mbs-transport-function
does), so it appears as untracked in any such checkout while being purely a build artefact.
…ress

Problem
  A Transmitter configured with a source address sent multicast packets out the host's
  default-route interface instead of the interface implied by that address. Observed: an
  independent multicast listener joined on loopback for a bound source address and received
  zero packets in a 22s window; `ip route get <group>` with no source hint resolved to the
  default-route interface, the same query with `from <source>` resolved correctly to `lo`.
  [observed]

Basis
  No clause governs this; engineering choice. bind() sets the packet's claimed source address
  only -- it does not steer which interface a multicast send goes out on. Only
  IP_MULTICAST_IF (boost::asio::ip::multicast::outbound_interface) does that. This is host
  networking behaviour, not FLUTE's own, so it rests on the kernel's own documented multicast
  send semantics, not a spec clause.

Raised by
  observation, live packet-capture testing during rt-mbs bypass-path debugging.

Change
  Transmitter::start() and both source_address() overloads now call
  set_option(outbound_interface(...)) alongside the existing bind(), for IPv4 sources.

Verification
  T2: live. An independent Python multicast listener received 0 packets/10s before the fix
  and 531 packets/10s after, joined on the same interface and group.

Not in this change
  IPv6 source addresses (guarded out via is_v4(); not exercised by this project's own
  deployments, no basis to choose a v6 equivalent without one).
Problem
  The comment at the Transfer-Length suppression states "Nothing is lost on the wire
  either: the receive path falls back to Content-Length when the attribute is absent".
  That holds only for an object with no content encoding. Where Content-Encoding is
  applied the two lengths differ, and the profile provides no other carrier, so the
  comment documents a safety property the code does not have.
  Observed: src/FileDeliveryTable.cpp:602.   [code-derived]

Basis
  RFC 3926 clause 3.4.2 makes Content-Length equal to the transfer length only where no
  content encoding was applied.

  TS 26.346 V18.2.0 clause 7.2.8: "FLUTE packets carrying symbols of files (not FDT
  Instances) shall not include an EXT_FTI."

Raised by
  Reading the authority during review of the profile documentation, prompted by review
  comments on 5G-MAG#99.

Change
  Comment only. Replaces the incorrect justification with the condition under which
  Content-Length does substitute, and points at 5G-MAG/Standards#212 for the case it
  does not cover. No behaviour changes; the suppression itself is unaltered.

Verification
  T0: comment-only change, not compiled. The build and test runner were unavailable in
  the environment this was written in.

Not in this change
  The sender-side guard that declines content encoding under the 3GPP profiles, which
  exists on the branch behind 5G-MAG#68. Adopting Content-Encoding in
  File::adopt_fdt_metadata, which is a separate defect and needs its own issue and test.
Problem
  An object whose reception was bootstrapped from a content packet's own EXT_FTI never
  learned its Content-Encoding, nothing in EXT_FTI carrying it and
  File::adopt_fdt_metadata() not taking it from the FDT entry. decode() is guarded on
  content_encoding, so the caller received the on-wire bytes: a 4096-byte object arrived
  as its 38 compressed bytes. Where the FDT also carries Content-MD5 the outcome is worse
  than a wrong result, check_file_completion() running its digest comparison only when
  content_encoding is empty, so it compares a digest taken over the decoded file against
  the still-encoded buffer, fails, resets every symbol and clears _complete, and the
  transfer never finishes.
  Observed: include/File.h adopt_fdt_metadata, src/Receiver.cpp bootstrap entry.   [code-derived]

Basis
  No clause governs the omission; it is an internal inconsistency between the two paths
  that construct a File. The reachability limit is spec-derived:

  TS 26.346 V18.2.0 clause 7.2.8: "FLUTE packets carrying symbols of files (not FDT
  Instances) shall not include an EXT_FTI."

  So neither 3GPP profile can reach this, and nor can this library's own sender, which
  attaches EXT_FTI only to FDT Instance packets. An unprofiled RFC 3926 sender that
  content encodes and signals EXT_FTI does.

Raised by
  Reading the code while answering review comments on 5G-MAG#99; recorded
  against 5G-MAG#74, whose adoption path this completes.

Change
  adopt_fdt_metadata() now also takes content_encoding and content_length. File owns the
  defect: it is the object that holds both halves of the metadata and decides, in decode()
  and check_file_completion(), what to do with them. content_length comes with the
  encoding because for an encoded object it is the decoded length, while the bootstrapped
  value was the transfer length, and decode() otherwise logs a mismatch against it.

Verification
  T1: protocol:ExtFtiBootstrapTest.AdoptedFdtMetadataCarriesTheContentEncoding passes.
  Full suite 75 cases, all passing. The new case fails without the two-line change,
  reporting a decoded length of 38 against the 4096 expected.

Not in this change
  cache_control and etag, which the same function also leaves untaken and which no
  observed failure depends on. The sender-side guard declining content encoding under the
  3GPP profiles, which is on the branch behind 5G-MAG#68.
Problem
  With a UDP tunnel endpoint configured, both send sites issued a plain datagram to the
  announced destination as well as the encapsulated copy, so every packet left twice and
  half the egress had no consumer: the announced destination is reached by decapsulation
  at the far end. Measured on a broadcast run, 8446 datagrams to the SSM destination and
  8446 to the session's tunnel address over 22 seconds.
  Observed: src/Transmitter.cpp, send_next_packet() tunnel branch and
  send_close_session_packet().   [observed, confirmed code-derived]

Basis
  TS 23.247 V18.8.0 clause 7.3.1 step 13: "The AF starts transmitting the DL media stream
  to MB-UPF using the N6mb Tunnel, or optionally un-tunnelled i.e. as an IP multicast
  stream using the HL MC address."

  One carriage or the other, not both.

Raised by
  Measurement reported by a colleague working on MBS multicast, recorded in the project
  findings register for rt-mbs-transport-function. The clause was re-derived from primary
  text before this change. Issue 5G-MAG#109.

Change
  Both send sites now issue exactly one datagram: the encapsulated copy when a tunnel
  endpoint is configured, the plain one otherwise. Transmitter owns the defect, being the
  component that chooses the carriage. The comment that argued for sending both is
  replaced with the clause; a receiver that joins the announced destination directly is
  served by not configuring a tunnel.

Verification
  T1: unit:TransmitterTunnelCarriageTest.ConfiguredTunnelSuppressesTheDirectCopy passes.
  Full suite 76 cases, all passing. The new case fails against the previous behaviour,
  the direct datagram arriving immediately.

Not in this change
  The IPv4-only limitation of the tunnel send path, which is 5G-MAG#97. No change to the
  un-tunnelled path, or to how the tunnel endpoint is configured.
Problem
  A caller could ask for gzip under either 3GPP profile and the sender would encode the
  object, though the profile provides no way to state the resulting transfer length. The
  receiver then falls back to Content-Length, which is the decoded size, partitions for more
  source symbols than are ever transmitted, and never completes the object.
  Observed: Transmitter::FileDescription::set_compression is public and ungated;
  src/File.cpp sets fec_oti.transfer_length to the encoded size while content_length stays
  decoded; src/FileDeliveryTable.cpp withholds Transfer-Length under is_3gpp.   [code-derived]

Basis
  Both carriers for the transfer length are closed to a profiled sender.

  TS 26.346 V18.2.0 clause L.4.4: "The following attributes shall not be carried in the FDT
  sent by the FLUTE sender:" listing Transfer-Length first.

  TS 26.346 V18.2.0 clause 7.2.8: "FLUTE packets carrying symbols of files (not FDT
  Instances) shall not include an EXT_FTI."

  Content-Length cannot substitute for an encoded object, per RFC 3926 clause 3.4.2.
  Declining to encode is conformant because the profile leaves it to the sender:

  TS 26.346 V18.2.0 clause L.4.2: "The following FDT attribute, defined at both the
  FDT-Instance and File levels, may be carried in the FDT sent by the FLUTE sender, under
  either the File-Instance or File element, and shall be supported by the FLUTE receiver:"

  The contradiction between that permission and the two prohibitions is 3GPP's to resolve
  and is raised as 5G-MAG/Standards#212. It is not resolved here.

Raised by
  Reading the authority during this work, while checking whether this branch is conformant
  standing alone rather than only in combination with the branches stacked on it.

Change
  Transmitter::send() refuses a FileDescription that asked for compression when the profile
  is a 3GPP one, naming the reason and the issue. Transmitter owns the choice of carriage, so
  the check belongs there. Refusing rather than silently sending the object uncompressed,
  because a caller told nothing would believe the encoding was applied. Receiving is
  untouched: clause L.4.2 obliges a receiver to support gzip and this library does.

Verification
  T1: ProfileContentEncodingTest, 3 cases, covering refusal under both 3GPP profiles,
  acceptance under Unprofiled, and that an unencoded object is unaffected. Full suite 79
  cases, all passing. The refusal case fails without this change, the send being accepted.

Not in this change
  No resolution of the specification contradiction. No change to the receive path, or to the
  un-tunnelled and unprofiled paths where RFC 3926 permits Transfer-Length.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

Development

Successfully merging this pull request may close these issues.

4 participants