Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
21e7493
alc: widen the transmit TSI to the full field width RFC 3451 defines
jordijoangimenez Aug 7, 2026
61cc567
fdt: bound the FDT Instance ID to its field, and reuse only expired i…
jordijoangimenez Aug 7, 2026
2f792f6
Add FDT Complete attribute and LCT Close Session/Object flag support
jordijoangimenez Aug 7, 2026
d3c7a9c
Bootstrap object reception from a packet's own EXT_FTI when no FDT en…
jordijoangimenez Aug 7, 2026
4e2680a
build: enable testing in the top-level list file so ctest sees the su…
jordijoangimenez Aug 17, 2026
f876cbb
fdt: do not carry Transfer-Length in an MBMS Download Profile session…
jordijoangimenez Aug 21, 2026
e074ba7
docs: cite the RFC generation this branch's baseline actually references
jordijoangimenez Aug 21, 2026
1f73fbb
flute: separate the 3GPP obligations from the general FLUTE ones behi…
jordijoangimenez Aug 21, 2026
9c4386b
fdt: do not emit the Complete attribute under the MBMS Download Profile
jordijoangimenez Aug 21, 2026
d647aa1
fdt: suppress FEC-OTI-FEC-Instance-ID under the MBMS Download Profile…
jordijoangimenez Aug 21, 2026
1f2a65b
fdt: refuse a Content-Encoding other than gzip under the MBMS Downloa…
jordijoangimenez Aug 21, 2026
448abe8
docs: state which obligations are 3GPP and which are general FLUTE
jordijoangimenez Aug 21, 2026
57d7a38
alc: bound the LCT header parse by the received length, and reject a …
jordijoangimenez Aug 21, 2026
8c9671e
alc: account for the SCT and ERT header fields instead of walking the…
jordijoangimenez Aug 21, 2026
f146328
docs: record congestion control as conformant for 3GPP and not implem…
jordijoangimenez Aug 21, 2026
05f0a2c
alc: require FLUTE version 1 in EXT_FDT rather than accepting up to 2…
jordijoangimenez Aug 21, 2026
e6972aa
fdt: emit the mandatory schemaVersion element for the profiled FDT sc…
jordijoangimenez Aug 21, 2026
0d7f678
fdt: separate the File expiry from the Cache-Control directive (L6, L10)
jordijoangimenez Aug 21, 2026
5a59025
fdt: never emit FEC-OTI-FEC-Instance-ID, not merely withhold it under…
jordijoangimenez Aug 21, 2026
678b019
transmitter: bind to the configured source address even when tunnelling
jordijoangimenez Aug 22, 2026
36cd824
ipsec: release the netlink socket instead of leaking it on every call
jordijoangimenez Aug 22, 2026
88d6bab
Add missing <cstdint> include to IpSec.h
jordijoangimenez Aug 12, 2026
9ad4a2c
Add IPv6 support to IpSec.cpp's XFRM state/policy netlink messages (C…
jordijoangimenez Aug 12, 2026
a852c94
examples: confine received files to the output directory
jordijoangimenez Aug 22, 2026
2750ff0
tests: add regression coverage for the FDT growth fixes
jordijoangimenez Aug 22, 2026
c90e4e5
fdt: wrap the FDT Instance ID to 0, as RFC 3926 defines the sequence
jordijoangimenez Aug 22, 2026
3d12aa4
receiver: keep a file bootstrapped from EXT_FTI when the FDT completes
jordijoangimenez Aug 22, 2026
5bf0f30
ipsec: authenticate the ESP association as well as encrypting it
jordijoangimenez Aug 22, 2026
e861881
transmitter: build an IPv6 header on the tunnel path, and budget for it
jordijoangimenez Aug 22, 2026
aa3ba86
transmitter: send to the announced destination as well as through the…
jordijoangimenez Aug 22, 2026
12fb037
flute: model the three profiles the specifications define, and hold e…
jordijoangimenez Aug 22, 2026
e6bbe48
fdt: refuse an FDT Instance expiry that is not in the future
jordijoangimenez Aug 22, 2026
fb7de87
alc: refuse a content encoding the library cannot decode, and name th…
jordijoangimenez Aug 23, 2026
e90c13d
receiver: verify the sender address before parsing, not the TSI alone
jordijoangimenez Aug 23, 2026
0d6caef
alc: send the data-less Close Session packet the specification provid…
jordijoangimenez Aug 23, 2026
4321e63
receiver: a packet carrying no payload is not an FDT packet
jordijoangimenez Aug 23, 2026
4435e3b
ipsec: select the session's traffic by protocol and port, not the des…
jordijoangimenez Aug 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,11 @@ target_link_libraries( flute
# ---- Tests Subdirectory (optional) ----
option(BUILD_TESTING "Build unit tests" ON)
if (BUILD_TESTING AND NOT DEFINED GTEST_DISABLE)
# enable_testing() must be called here, in the top-level list file, and not only in tests/.
# CTest writes CTestTestfile.cmake into the directory that enables testing and its children, so
# calling it only in the subdirectory leaves the build root with no test list at all. `ctest` run
# from the build root then prints "No tests were found" and exits 0, which is the normal
# invocation and the one CI uses: a green result having executed nothing.
enable_testing()
add_subdirectory(tests)
endif()
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,66 @@ sudo setcap 'cap_net_admin=eip' ./flute-transmitter
sudo setcap 'cap_net_admin=eip' ./flute-receiver
````

## Conformance profiles: 3GPP MBMS versus general FLUTE

This library serves two different sets of obligations, and they are not degrees of strictness.
A session correct as general FLUTE can be non-conformant as 3GPP MBMS, because the MBMS Download
Profile forbids the sender things RFC 3926 permits.

Select with the trailing `profile` argument on the `Transmitter` and `FileDeliveryTable`
constructors. **The default is `Profile::Mbms3gpp`**, since that is what the specifications
mandating FLUTE for this project require. Pass `Profile::GeneralFlute` for a non-3GPP session.

```cpp
LibFlute::Transmitter tx(addr, port, tsi, mtu, rate, io); // 3GPP, default
LibFlute::Transmitter tx(addr, port, tsi, mtu, rate, io, {}, ns, true, {},
LibFlute::Profile::GeneralFlute); // plain RFC 3926
```

The profile decides which obligations apply. The FDT namespace, a separate argument, decides
which XML schema is emitted. They are independent.

### What the 3GPP profile adds, over general FLUTE

Every row below is a restriction on the **sender** only. Receive-side parsing is unchanged in all
cases, because TS 26.346 annex L.4 keeps most of these optional-to-support for receivers and
mandatory for two of them, so a receiver that refused them would break against a conformant peer.

| Attribute / element | General FLUTE (RFC 3926) | 3GPP MBMS (TS 26.346 annex L.4) |
|---|---|---|
| `Transfer-Length` | permitted | not carried (clause L.4.4) |
| `Complete` | permitted on FDT-Instance | not used by the sender (clause L.4.3) |
| `FEC-OTI-FEC-Instance-ID` | permitted | not used at either level (clause L.4.2) |
| `Content-Encoding` | any value | absent, or `gzip` only; other values refused (clause L.4.2) |
| `Group` element | permitted | not used (clause L.4.2); this library never emits it |

Behaviour required by RFC 3926 and the ALC/LCT documents beneath it applies in **both** profiles
and is not switchable: the LCT header format, the 20-bit FDT Instance ID and its wraparound, the
mandatory `Expires` attribute on FDT-Instance, and EXT_FTI support on any TOI other than 0.

FLUTE version 2 (RFC 6726) and RaptorQ (RFC 6330) are referenced by neither TS 26.346 nor
TS 26.517 at this baseline and are not part of either profile here. They live on their own
branches.

### Congestion control: conformant for 3GPP, not implemented for general FLUTE

This library implements no congestion control. It offers a static, operator-set transmit rate
limit, which is rate limiting with no feedback and no response to loss, and it writes the CCI
field as zeros without reading it on receive.

Under the **3GPP profile that is conformant**, and deliberately so.
TS 26.346 V18.2.0 clause L.4.7: "As indicated in clause 7.2.4 of this specification, congestion
control is not used for FLUTE delivery in MBMS, and therefore, FLUTE channelization should be
provided by a single FLUTE channel with single rate transport."

Under **general FLUTE it is not implemented**, and the requirement is a MUST.
RFC 3450 clause 2.2: "Implementors of ALC MUST implement a multiple rate feedback-free
congestion control building block that is in accordance to RFC 2357 [12]."

So a non-3GPP deployment using `Profile::GeneralFlute` over a path where congestion matters is
outside RFC 3450, and this is stated rather than claimed either way. Nothing here is presented as
implementing that clause.

## Testing

To execute the tests make sure to have built the project with testing enabled (see Step 3: Build setup).
Expand Down
49 changes: 46 additions & 3 deletions examples/flute-receiver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,44 @@ void print_version(FILE *stream, struct argp_state * /*state*/) {
* @param argv Command line arguments
* @return 0 on clean exit, -1 on failure
*/
/**
* Turn a sender-supplied Content-Location into a path that is safe to write.
*
* Content-Location is a URI under the sender's control, so it is untrusted: it may be absolute,
* walk upwards with "..", or carry a query or fragment, and any of those used directly as a path
* lets the sender choose where the receiver writes. The URI's path is kept, so hierarchical
* content locations still land in matching subdirectories, but every segment that could escape is
* dropped: a leading slash, "." and "..". The result always sits under `output_path`, or the
* current directory when none was given. Returns an empty string when nothing usable remains.
*/
static auto safe_output_path(const std::string& content_location, const char* output_path) -> std::string {
std::string s = content_location;
auto scheme = s.find("://");
if (scheme != std::string::npos) {
auto slash = s.find('/', scheme + 3);
s = (slash == std::string::npos) ? std::string() : s.substr(slash);
}
s = s.substr(0, s.find_first_of("?#"));

std::filesystem::path rel;
size_t pos = 0;
while (pos <= s.size()) {
auto next = s.find('/', pos);
auto seg = s.substr(pos, (next == std::string::npos) ? std::string::npos : next - pos);
if (!seg.empty() && seg != "." && seg != "..") {
rel /= seg;
}
if (next == std::string::npos) break;
pos = next + 1;
}
if (rel.empty() || rel.filename().empty()) return {};

std::filesystem::path dir = (output_path && std::strlen(output_path) > 0)
? std::filesystem::path(output_path)
: std::filesystem::path(".");
return (dir / rel).string();
}

auto main(int argc, char **argv) -> int {
struct ft_arguments arguments;
/* Default values */
Expand Down Expand Up @@ -168,9 +206,14 @@ auto main(int argc, char **argv) -> int {

receiver.register_completion_callback(
[output_path = arguments.output_path](std::shared_ptr<LibFlute::File> file) { //NOLINT
std::string out_file = file->meta().content_location;
if (output_path && std::strlen(output_path) > 0) {
out_file = (std::filesystem::path(output_path) / std::filesystem::path(out_file).filename()).string();
// Content-Location is a URI chosen by the sender, so it is untrusted input: it can name an
// absolute path, walk upwards with "..", or carry a query string. Reduce it to a single safe
// filename before it reaches the filesystem, under the chosen output directory.
std::string out_file = safe_output_path(file->meta().content_location, output_path);
if (out_file.empty()) {
spdlog::warn("Refusing to write TOI {}: Content-Location yields no usable filename",
file->meta().toi);
return;
}

spdlog::info("{} (TOI {}) has been received",
Expand Down
62 changes: 57 additions & 5 deletions include/AlcPacket.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,32 @@ namespace LibFlute {
* @param symbols Vector of encoding symbols
* @param max_size Maximum payload size
* @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
*/
AlcPacket(uint16_t tsi, uint16_t toi, FecOti fec_oti, const std::vector<EncodingSymbol>& symbols, size_t max_size, uint32_t fdt_instance_id);
/**
* Tag selecting the data-less Close Session packet constructor below.
*/
struct CloseSession {};

/**
* Build a packet that carries the Close Session flag and nothing else: no payload, and
* therefore no FEC Payload ID and no TOI.
*
* RFC 3450 clause 4.1 provides for such a packet: "In some special cases an ALC sender may
* need to produce ALC packets that do not contain any payload." RFC 3926 clause 3.1 gives it
* this shape in a FLUTE session, requiring that it not carry the TOI.
*
* Only expressible for a TSI of 32 bits or fewer. Dropping the TOI means dropping the
* half-word flag the two fields share, which leaves the TSI a whole number of 32-bit words,
* and one word is all this encoding uses. Throws above that.
*
* @param tsi The session's Transport Session Identifier.
*/
AlcPacket(uint64_t tsi, CloseSession);

AlcPacket(uint64_t tsi, uint16_t toi, FecOti fec_oti, const std::vector<EncodingSymbol>& symbols, size_t max_size, uint32_t fdt_instance_id,
bool close_session_flag = false, bool close_object_flag = false);

/**
* Default destructor.
Expand Down Expand Up @@ -72,15 +96,40 @@ namespace LibFlute {
size_t header_length() const { return _lct_header.lct_header_len * 4; };

/**
* Get the FDT instance ID
* Get the FDT instance ID
*/
uint32_t fdt_instance_id() const { return _fdt_instance_id; };

/**
* Whether the sender set the LCT Close Session flag on this packet, signalling that no
* further objects will be sent in this session (RFC 3451 clause 5.1, 'A' bit).
*/
bool close_session_flag() const { return _lct_header.close_session_flag; };

/**
* Whether the sender set the LCT Close Object flag on this packet, signalling that this
* is the last packet for this TOI (RFC 3451 clause 5.1, 'B' bit).
*/
bool close_object_flag() const { return _lct_header.close_object_flag; };

/**
* Get the FEC scheme
*/
FecScheme fec_scheme() const { return _fec_oti.encoding_id; };

/**
* Whether this packet carried its own EXT_FTI header extension.
*
* A sender may put EXT_FTI on individual object packets (TOI > 0), not only on the FDT
* (TOI 0), and a receiver is obliged to accept it there, so an object's FEC OTI can be
* bootstrapped straight from the packet stream without waiting for, or ever seeing, that
* object's <File> entry in the FDT.
*
* 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."
*/
bool has_fec_oti() const { return _has_fti; };

/**
* Get the content encoding
*/
Expand All @@ -107,11 +156,12 @@ namespace LibFlute {

ContentEncoding _content_encoding = ContentEncoding::NONE;
FecOti _fec_oti = {};
bool _has_fti = false;

char* _buffer = nullptr;
size_t _len;

// RFC5651 5.1 - LCT Header Format
// RFC 3451 clause 5.1 - LCT Header Format
struct __attribute__((packed)) lct_header_t {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
uint8_t res1:1;
Expand All @@ -121,7 +171,8 @@ namespace LibFlute {

uint8_t close_object_flag:1;
uint8_t close_session_flag:1;
uint8_t res:2;
uint8_t ert_flag:1;
uint8_t sct_flag:1;
uint8_t half_word_flag:1;
uint8_t toi_flag:2;
uint8_t tsi_flag:1;
Expand All @@ -134,7 +185,8 @@ namespace LibFlute {
uint8_t tsi_flag:1;
uint8_t toi_flag:2;
uint8_t half_word_flag:1;
uint8_t res2:2;
uint8_t sct_flag:1;
uint8_t ert_flag:1;
uint8_t close_session_flag:1;
uint8_t close_object_flag:1;
#else
Expand Down
15 changes: 15 additions & 0 deletions include/File.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ namespace LibFlute {
*/
const LibFlute::FileDeliveryTable::FileEntry& meta() const { return _meta; };

/**
* Fill in the FDT-derived fields (content_location, content_type, ...) once the FDT
* entry for this TOI becomes available. Used when reception started from a packet's own
* EXT_FTI before the describing FDT arrived, so the in-progress reception isn't discarded
* and restarted once it does. Only content_location/content_type/content_md5/expires are
* taken from the FDT entry -- fec_oti is left as-is, since it already came from the
* packet's own EXT_FTI and is what the in-flight reassembly is keyed on.
*/
void adopt_fdt_metadata(const LibFlute::FileDeliveryTable::FileEntry& fdt_entry) {
_meta.content_location = fdt_entry.content_location;
_meta.content_type = fdt_entry.content_type;
_meta.content_md5 = fdt_entry.content_md5;
_meta.expires = fdt_entry.expires;
};

/**
* Timestamp of file reception
*/
Expand Down
Loading
Loading