Skip to content

Tunnel mode reception, AL-FEC support and various bugfixes - #68

Open
jordijoangimenez wants to merge 20 commits into
developmentfrom
feature/issue66-receiver-tunnel-mode
Open

Tunnel mode reception, AL-FEC support and various bugfixes#68
jordijoangimenez wants to merge 20 commits into
developmentfrom
feature/issue66-receiver-tunnel-mode

Conversation

@jordijoangimenez

Copy link
Copy Markdown
Contributor

Supersedes #67, which was opened from my personal fork -- same content, pushed directly to this repo instead.

Summary

This branch (issue #66) adds tunnel-mode reception to LibFlute::Receiver — the capability MBS Broadcast reception in rt-mbs-client depends on when running against a software TUN-based radio link, where a multicast datagram written to the TUN device's write() side never reaches a socket joined to its destination group on that interface at all (see the linked rt-mbs-client PR for the full live-traced explanation). It also carries the Raptor/RaptorQ FEC work (previously reconciled from #56/#60) and one new bug fix found tonight while live-testing MBS Broadcast reception end-to-end.

Known gap, found while preparing this PR: this branch is missing 4 commits that are on #62/#65 (ff1d547 widen TSI to 48 bits, 637ce5c FDT Instance ID wraparound RFC 6726 §3.4.1, 0ceaa26 LCT Close Session/Object flags, 6c7137d EXT_FTI bootstrap without an FDT entry) — flagging this rather than silently merging it in, since I haven't verified those against this branch's current state.

What's included

  • Tunnel-mode reception (c8afd5f, fixes Add tunnelled operation to Receiver class #66): LibFlute::Receiver gains an optional tunnel endpoint + packet-modifier constructor path, so a caller (like rt-mbs-client's TunRawRelay) can forward decapsulated payloads to a local unicast socket the Receiver actually listens on, alongside its normal multicast join.
  • RFC 6726/5651 compliance fixes (ff483cd).
  • Raptor/RaptorQ FEC support, reconciled from PR Feature/end to end test tunneled mode #56 + PR Fix unbounded FDT growth (same-TOI resend, and content-change cases) #60's review-updated state (a7a233e, 9a0beeb, 92d2140, 7331294, 72dea58) — RFC 5053 Raptor and RFC 6330 RaptorQ FEC schemes (also tracked separately as Implement RFC 5053 Raptor #61/RFC 6330 RaptorQ FEC (not 3GPP-mandated — reference/future work, depends on #61) #64), needed for reliable delivery of larger objects over a lossy broadcast channel with no ARQ.
  • Fix unbounded FDT growth on repeated sends of the same TOI, and when a resent object's content changes (7d98512, a5a956d).
  • SSM source-address support; fix a receiver use-after-free and a receive-buffer overflow (184e482).
  • Tunnel transport reliability: keep encapsulated UDP tunnel buffers alive through async sends, rebuild inner IPv4/UDP headers with correct byte-wise network-order checksums so tunnelled end-to-end delivery passes reliably in release builds (12770ad, 2db5a8a).
  • End-to-end test for tunnelled mode and supporting refactoring (fe9ab9a through 1e1fa90).
  • Fix receive loop never re-arming after a transient socket error (f4100f9, new tonight): handle_receive_from()/handle_tunnel_receive_from() only re-armed the next async read in the success branch — a single transient socket error (e.g. an ICMP port-unreachable surfacing as a UDP socket error on a later read, hit live via the raw-capture relay's own loopback sendto() path) permanently killed reception for the rest of the process's life, with one log line and no way to recover short of restarting the client. Found via strace/tcpdump live debugging.

dsilhavy and others added 20 commits March 10, 2026 07:59
…r alive until Boost.Asio calls the completion lambda.
…ebuild inner IPv4/UDP headers with byte-wise network-order checksum generation so tunneled end-to-end delivery passes reliably in release builds.
…eceive-buffer overflow

Squashed re-application of accumulated fixes/features previously
developed on a personal fork whose history had diverged from
5G-MAG/rt-libflute's actual current development/main (unrelated
histories, confirmed via git merge-base returning nothing, despite
matching content up to a shared point -- likely from a prior history
rewrite on one side only). Re-applied here as a single clean commit
against the real upstream base rather than replaying the original,
now-irreconcilable commit sequence:

- Add SSM (source-specific multicast) join support to Receiver, so a
  session can admit packets only from a specified source address
  rather than any-source multicast.
- Fix a use-after-free: an async_receive_from completion already
  queued on the io_context when a Receiver is destroyed could run
  after the destructor returns and touch a freed `this` -- boost::asio
  only guarantees a cancelled operation's handler eventually runs with
  operation_aborted, not that it runs before the destructor returns.
  Receiver now tracks its own liveness via a shared atomic flag copied
  into each completion handler, checked before touching `this`.
- Fix Receiver's fixed 2048-byte receive buffer silently truncating
  larger encoding symbols: recvfrom() on a datagram socket doesn't
  error on a too-small buffer, it silently truncates, so any FEC-OTI
  configuration with larger symbols corrupted every symbol beyond
  2048 bytes without any visible error until an FDT Content-MD5 check
  (if present) caught it. Buffer is now 65536 bytes, covering the
  maximum possible IPv4 UDP payload.
Transmitter::send() reused the same FileDescription/TOI correctly for
carousel-style repeated objects, but two bugs meant each resend still
grew the file delivery state without bound:

- _files.insert() is a no-op if the TOI key is already present, so a
  resend's updated File object was silently discarded in favour of
  the stale one already in the map.
- FileDeliveryTable::add() unconditionally appends a new <File> entry
  with no dedup by TOI, so every resend left the previous cycle's
  entry for the same TOI in place -- the serialised FDT grows by one
  entry per resend indefinitely (observed growing from ~850 bytes to
  several hundred KB over a couple of hours of a 10-second carousel),
  eventually becoming too large for a receiver to reassemble at all.

Per RFC 6726 SS3.3/3.4.2, an FDT Instance describes the current state
of the file delivery session; a Content-Location may be redescribed
under a new TOI to signal a new version, but parameters already
described for a given TOI must not change -- so on an actual resend
of unchanged content, the sender should replace that TOI's single
File entry, not accumulate duplicates of it. This restores that
invariant: track whether this send is a resend (TOI already
assigned), replace the File map entry instead of no-op'ing, and
remove the TOI's existing FDT entry before adding the current one.
The previous fix (same TOI, resent unchanged content) only handled
one growth vector. A second, distinct one remained: when
set_content()/set_compression() detect the content genuinely changed,
they zero the FileDescription's TOI so Transmitter::send() assigns a
fresh one -- but nothing ever removed the FDT entry for the TOI being
vacated. Confirmed live: a carousel object whose content legitimately
changes each cycle (e.g. a randomly-regenerated MIME boundary) grew
its FDT to 135-170KB within about a minute, well before the previous
fix's growth timescale, eventually failing to parse
(XML_ERROR_PARSING_ATTRIBUTE).

FileDescription now remembers the TOI it's vacating (_previous_toi,
set by a new _reset_toi() helper used everywhere the TOI was zeroed)
so Transmitter::send() can remove that stale entry before assigning
the replacement TOI, restoring the "one current entry per logical
object" invariant regardless of which of the two ways an object's
description changes.
- File.cpp: use fmt::format instead of manual std::to_string()
  concatenation for the two bounds-check exception messages, per the
  suggestion -- fmt::format rather than std::format since this project
  targets C++17 (std::format needs C++20); same result, already a
  dependency via spdlog.
- Transmitter.h/.cpp: added FileDescription::previous_toi()/
  reset_previous_toi() public accessors instead of Transmitter::send()
  reaching into FileDescription's private _previous_toi directly via
  friend access, per the suggestion.
- Receiver.cpp: restored IPv6 support the SSM/specific-interface-join fix
  had inadvertently dropped (the original code let Boost infer v4 vs v6
  from the address types passed; the fix hardcoded .to_v4() throughout).
  Now branches on the multicast address's actual family: IPv6 ASM join
  uses join_group(address_v6, interface_index) and SSM join uses
  MCAST_JOIN_SOURCE_GROUP/group_source_req, both interface-by-index
  (unlike IPv4's interface-by-address), so a new resolve_iface_index()
  helper resolves the existing iface address string to its owning
  interface's index via getifaddrs()/if_nametoindex(). IPv4 behaviour
  (bind/ASM/SSM) is unchanged, verified by rerunning the existing
  test_end_to_end.cpp unmodified.
…nflict

Both branches touched Transmitter::send_next_packet()'s tunnelled-send path:
this branch (PR #56) made the encapsulated packet buffer lifetime-safe by
owning it in a shared_ptr<vector<char>> captured by the async_send_to
completion lambda, replacing a raw new[] with no matching delete[] (a leak
on every tunnelled packet). development, in parallel, added _source_address
as an explicit override for the local address used to build the inner
IPv4/UDP headers, falling back to _tunnel_local_address when unset.

Kept both: the shared_ptr-owned buffer from this branch, with the
_source_address-or-_tunnel_local_address fallback from development.
…56 branch

Integration base for building the Raptor/RaptorQ FEC work on top of both
outstanding PRs' assumed-merged state: PR #56's tunnel-buffer lifetime fix
(reconciled with development's _source_address fallback) plus PR #60's
FDT-growth fixes with the previous_toi()/reset_previous_toi() accessor
pattern and full IPv4/IPv6 parity in Receiver's SSM/ASM join and bind
logic. No conflicts with the prior merge; both touch disjoint enough
regions of Transmitter.cpp/.h that git combined them cleanly.
…ed state

This branch is built assuming both outstanding PRs land first: PR #56's
tunnel-buffer lifetime fix (with development's _source_address fallback,
already reconciled in an earlier commit on this branch) and PR #60's
FDT-growth fixes with the previous_toi()/reset_previous_toi() accessor
pattern and full IPv4/IPv6 parity in Receiver.

No shared git history exists between this fork's line of development and
the current upstream development branch (a previously-diagnosed history
rewrite upstream, see PR #60's own 184e482), so this reconciliation was
done as a content-level 3-way patch apply against this integration base,
not a rebase -- 14 files had genuine overlapping hunks, resolved as
follows:

- flute_types.h, AlcPacket.cpp, EncodingSymbol.cpp, File.cpp,
  FileDeliveryTable.cpp: kept the Raptor/RaptorQ-aware superset (FecOti's
  scheme-specific fields, multi-scheme FEC Payload ID / EXT_FTI / FDT
  attribute parsing) -- these fully subsume the integration base's
  CompactNoCode-only logic.
- Receiver.cpp: kept the integration base's version entirely -- PR #60's
  IPv6-aware SSM/ASM join and bind logic is strictly more complete than
  this branch's older IPv4-only copy of the same code, and Raptor/RaptorQ
  content needs no Receiver-side changes beyond what File/EncodingSymbol
  already provide.
- Transmitter.h/.cpp: combined per-hunk -- kept the accessor pattern
  (previous_toi()/reset_previous_toi()) and the graceful
  deactivate(bool finish_file_transmissions) lifecycle from the
  integration base; kept this branch's content_fec_oti constructor
  parameter and fec_oti()/fdt() accessors; and rebuilt this branch's
  dual-send fix (send both a plain copy and a tunnelled copy, needed for
  N3mb GTP-U tunnelling per TS 23.247 while still supporting direct SSM
  subscribers) on top of PR #56's cleaner byte-wise checksum/header
  implementation (create_udp_pkt/create_ip_hdr/calculate_sum operating on
  uint8_t* with explicit write_uint16_be/write_uint32_be helpers, not the
  older uint16_t*-punned struct-overlay approach) instead of keeping two
  divergent checksum implementations.
- CMakeLists.txt, tests/CMakeLists.txt, examples/flute-transmitter.cpp,
  include/File.h: trivial additive conflicts (version bump, new test
  targets, new #includes, an unrelated exception-slicing/format-string
  bug fix already on this branch).
- tests/test_transmitter.cpp, tests/test_end_to_end.cpp: kept the
  integration base's superset (adds a UDP-tunnel e2e test and a graceful-
  deactivation lifecycle test this branch didn't have); this branch's
  own duplicate of the basic transmit/receive test added nothing Raptor-
  specific -- that coverage lives in test_raptor_e2e.cpp/test_raptorq_fec.cpp.

Verified: full library + all 6 test binaries (25 tests total) build and
pass against the reconciled tree.
PR #56 landed this morning as a squash commit, superseding the hand-built
integration base this branch was previously reconciled against. Re-merged
against the real development tip:

- Transmitter.cpp: one conflict, same shape as before -- kept this branch's
  dual-send feature (plain + tunnelled copy, needed for N3mb GTP-U per
  TS 23.247) rebuilt on #56's shared_ptr-owned buffer, since dual-send was
  never part of #56 itself and #56's actual merged content confirms that.
- tests/test_end_to_end.cpp: 12 conflicts, all the same underlying change --
  the real #56 merge added a std::mutex protecting TunnelBridgeStats from a
  genuine data race between the tunnel-bridge thread and the test's main
  thread, which this branch's copy of the test lacked. Took development's
  thread-safe version throughout.
- tests/tmp/e2e_payload.bin deleted, matching development: the current test
  generates its payload in-code and no longer reads this fixture.

Verified: full library + all 6 test binaries (25 tests) build and pass.
- FLUTE version nibble in EXT_FDT: was hardcoded to 1, now 2 per RFC 6726
  SS3.1/3.4.1 (LCT header version field, a separate RFC 5651 field, is
  correctly left at 1).
- TSI truncated to 16 bits on transmit: AlcPacket's send-side constructor now
  takes a uint64_t tsi and picks the narrowest half_word_flag/tsi_flag
  combination (16/32/48 bits) able to carry it, mirroring TOI's existing wide
  receive-side decode. Also fixed a latent decode-side bug (uint32_t shifted
  before promotion to uint64_t) that silently discarded the upper 16 bits of
  a 48-bit TSI/TOI -- unreachable before this fix since nothing previously
  transmitted tsi_flag==1.
- FDT Instance ID wraparound: FileDeliveryTable now tracks expired instance
  IDs and, once the 20-bit space is exhausted, reuses the smallest expired
  ID that isn't the one just superseded (RFC 6726 SS3.4.1), instead of a
  plain ++ silently bit-masking on the wire.
- FDT Complete attribute: read and write support added, wired to
  Transmitter::close_session().
- Close Session / Close Object LCT flags: AlcPacket gained public accessors
  and send-side support; Transmitter gained close_session()/close_object(toi)
  APIs; Receiver surfaces both via callbacks and session_closed().
- EXT_FTI bootstrap: Receiver now bootstraps a File from a content packet's
  own EXT_FTI when no FDT entry exists yet for that TOI (RFC 6726 SS3.4.1),
  instead of discarding it.
- IPSec authentication: configure_state() now also sets XFRMA_ALG_AUTH
  (HMAC-SHA256), per RFC 6726 SS7.5's SHALL for authentication alongside
  encryption; derives a key from the AES key when no separate one is given.

Added tests/test_protocol_fixes.cpp covering all of the above. Full existing
suite (unit/e2e/raptor/raptorq/fdt_growth) plus the new tests all pass; a
live flute-transmitter/flute-receiver run with a 32-bit TSI over real UDP
multicast confirms round-trip delivery.
The Receiver-side counterpart to Transmitter's existing udp_tunnel_address()
support. Per the issue discussion, the library has no business knowing
about any particular encapsulation format -- that is entirely the
controlling application's concern:

  "Stripping the GTP-U header doesn't feel like something that a generic
  FLUTE library should be asked to do... A better design pattern would be
  for the controlling application to pass in a 'helper' function that the
  library invokes to do application-specific mangling of packets before
  the generic code in the library starts processing the ALC/LCT payload."
  -- rjb1000

Adds three new optional constructor parameters:
 - tunnel_address: if given, ALSO bind a plain unicast UDP socket to this
   local endpoint and accept tunnelled datagrams there, in addition to the
   normal multicast join. The two paths are independent and both feed the
   same session state -- deliberately not an either/or choice like
   Transmitter's tunnel mode, since a Receiver has no way to know in
   advance which path will actually work in a given deployment.
 - tunnel_source: if given, only accept tunnel datagrams from this source
   address -- the tunnel-socket equivalent of the existing source_address
   parameter's SSM admit-only-this-source semantics. This is the "extra
   address checking" the library itself does, on top of whatever
   packet_modifier does; source-address admission is a generic,
   encapsulation-agnostic concept the library can reasonably own, unlike
   parsing any particular header format.
 - packet_modifier: required whenever tunnel_address is set. Given the
   whole received datagram (as a mutable vector, so a modifier can also
   decrypt/rewrite in place, not just locate the payload), returns the
   byte offset at which the ALC/LCT payload begins -- an offset
   >= the buffer's size means "discard, nothing usable here". No default
   implementation is provided, since any default would itself bake an
   encapsulation assumption into the library.

This mirrors the de-tunnelling logic that already exists, hand-written, in
tests/test_end_to_end.cpp's run_tunnel_bridge() (added alongside
Transmitter's own tunnel mode in #56): a std::thread there receives on a
plain UDP socket, manually parses a hand-built inner IPv4+UDP header out of
the payload, and forwards just the FLUTE bytes onward over loopback to a
receiver with no tunnel-awareness at all. This moves that capability inside
Receiver proper as a caller-supplied, protocol-agnostic hook, so any
deployment's actual encapsulation (Transmitter's own wrapper, real GTP-U,
or anything else) is expressed purely by what modifier is passed in --
enabling rt-mbs-client (referenced in the issue as "MBSTF Client") to take
advantage of it for reception paths where local multicast delivery isn't
available at all.

Confirmed live end-to-end: real Service Announcement content broadcast
over an actual gNB/UE radio link, captured on the UE's TUN device via a
tunnel_address + packet_modifier pairing, correctly parsed into a complete
FDT and announcement bundle.
handle_receive_from() and handle_tunnel_receive_from() only called
arm_receive()/arm_tunnel_receive() again in the success branch -- a
single transient socket error (e.g. an ICMP port-unreachable surfacing
as a UDP socket error on a subsequent read, hit live via the raw
capture relay's loopback sendto() path) permanently killed reception
for the rest of the process's life, with just one log line and no way
to recover short of restarting the client.

(Attempted as a cross-repo cherry-pick of jordijoangimenez/rt-libflute
commit d453c05, 'Fix FDT/TOI
reassembly corruption, multicast bind/join, ...' -- turned out this
branch already independently has every other fix from that commit
(FDT-instance-discard reassembly logic, catch(const char*), the
INADDR_ANY bind, the per-interface multicast join, the array-specialised
shared_ptr scratch buffers); the re-arm bug above was the only genuinely
missing piece, found here by live strace/tcpdump debugging, not by
that commit.)
@rjb1000 rjb1000 changed the title Fix receive loop never re-arming after a transient socket error Tunnel mode reception, AL-FEC support and various bugfixes Aug 12, 2026
@jordijoangimenez jordijoangimenez self-assigned this Aug 12, 2026
@jordijoangimenez jordijoangimenez added the enhancement New feature or request label Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants