libflute
Receiver.cpp
Go to the documentation of this file.
1 // libflute - FLUTE/ALC library
2 //
3 // Copyright (C) 2021 Klaus Kühnhammer (Österreichische Rundfunksender GmbH & Co KG)
4 //
5 // Licensed under the License terms and conditions for use, reproduction, and
6 // distribution of 5G-MAG software (the “License”). You may not use this file
7 // except in compliance with the License. You may obtain a copy of the License at
8 // https://www.5g-mag.com/reference-tools. Unless required by applicable law or
9 // agreed to in writing, software distributed under the License is distributed on
10 // an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 // or implied.
12 //
13 // See the License for the specific language governing permissions and limitations
14 // under the License.
15 //
16 #include "Receiver.h"
17 #include "AlcPacket.h"
18 #include <cerrno>
19 #include <cstring>
20 #include <iostream>
21 #include <string>
22 #include <netinet/in.h>
23 #include <ifaddrs.h>
24 #include <net/if.h>
25 #include <arpa/inet.h>
26 #include "spdlog/spdlog.h"
27 #include "IpSec.h"
28 
29 namespace {
30  // IPv6 multicast join (both plain join_group() and MCAST_JOIN_SOURCE_GROUP)
31  // identifies the local interface by OS index, unlike IPv4's by-address
32  // ip_mreq[_source]/join_group(v4,v4) -- so restoring v6 support alongside
33  // the v4-specific-interface-join fix below needs a way to turn the same
34  // `iface` address string callers already pass into an interface index.
35  // Returns 0 (the kernel's "let it choose" value) if iface_address is
36  // empty/unspecified for its family or doesn't match any local interface.
37  unsigned int resolve_iface_index(const std::string& iface_address) {
38  if (iface_address.empty() || iface_address == "0.0.0.0" || iface_address == "::") {
39  return 0;
40  }
41  struct ifaddrs* ifaddr = nullptr;
42  if (getifaddrs(&ifaddr) != 0) {
43  return 0;
44  }
45  unsigned int result = 0;
46  for (auto* ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) {
47  if (!ifa->ifa_addr) continue;
48  char host[INET6_ADDRSTRLEN] = {};
49  if (ifa->ifa_addr->sa_family == AF_INET6) {
50  auto* sin6 = reinterpret_cast<struct sockaddr_in6*>(ifa->ifa_addr);
51  if (inet_ntop(AF_INET6, &sin6->sin6_addr, host, sizeof(host)) && iface_address == host) {
52  result = if_nametoindex(ifa->ifa_name);
53  break;
54  }
55  } else if (ifa->ifa_addr->sa_family == AF_INET) {
56  auto* sin = reinterpret_cast<struct sockaddr_in*>(ifa->ifa_addr);
57  if (inet_ntop(AF_INET, &sin->sin_addr, host, sizeof(host)) && iface_address == host) {
58  result = if_nametoindex(ifa->ifa_name);
59  break;
60  }
61  }
62  }
63  freeifaddrs(ifaddr);
64  return result;
65  }
66 }
67 
68 LibFlute::Receiver::Receiver ( const std::string& iface, const std::string& address,
69  short port, uint64_t tsi,
70  boost::asio::io_context& io_context,
71  const std::string& source_address)
72  : _socket(io_context)
73  , _tsi(tsi)
74  , _mcast_address(address)
75 {
76  // Restored alongside the ANY-bind/specific-interface-join fixes below:
77  // an earlier version of those fixes made this whole constructor IPv4
78  // only, where the original code let Boost pick v4 vs v6 based on the
79  // address types passed in. `address`'s family (not `iface`'s -- iface
80  // can legitimately be "0.0.0.0"/"::"/empty, meaning "any") is the
81  // authoritative signal for which family this session actually uses.
82  auto mcast_address = boost::asio::ip::make_address(address);
83  bool is_v6 = mcast_address.is_v6();
84 
85  // Bind to ANY, not the specific interface address: incoming multicast
86  // packets are addressed to the group, not to a particular unicast
87  // interface address, so binding to that unicast address is non-standard
88  // - and in practice it also breaks epoll-based readiness notification
89  // for this socket (confirmed directly: async_receive_from never
90  // completes when bound to a specific interface address, even though the
91  // data really does arrive and a plain synchronous recv() picks it up;
92  // binding to ANY fixes it). `iface` is still used below to select which
93  // interface's multicast membership to join.
94  boost::asio::ip::udp::endpoint listen_endpoint(
95  is_v6 ? boost::asio::ip::address(boost::asio::ip::address_v6::any())
96  : boost::asio::ip::address(boost::asio::ip::address_v4::any()),
97  port);
98  _socket.open(listen_endpoint.protocol());
99  _socket.set_option(boost::asio::ip::multicast::enable_loopback(true));
100  _socket.set_option(boost::asio::ip::udp::socket::reuse_address(true));
101  _socket.set_option(boost::asio::socket_base::receive_buffer_size(16*1024*1024));
102  _socket.bind(listen_endpoint);
103 
104  if (!source_address.empty()) {
105  // Source-specific multicast (SSM, RFC 4607): admits only packets from source_address,
106  // as indicated by an SDP a=source-filter line (RFC 4570; TS 26.517 cl.6.2.2.3's own
107  // examples use this for FLUTE sessions). boost::asio has no portable SSM join, so this
108  // goes straight to the socket options Linux (and most other stacks) actually define --
109  // IPv4's ip_mreq_source/IP_ADD_SOURCE_MEMBERSHIP identifies the interface by address;
110  // IPv6's group_source_req/MCAST_JOIN_SOURCE_GROUP identifies it by index instead
111  // (see resolve_iface_index() above), which is why the two branches build genuinely
112  // different structures rather than sharing one.
113  if (is_v6) {
114  struct group_source_req gsr{};
115  gsr.gsr_interface = resolve_iface_index(iface);
116 
117  struct sockaddr_in6 grp{};
118  grp.sin6_family = AF_INET6;
119  auto mcast_bytes = mcast_address.to_v6().to_bytes();
120  std::memcpy(&grp.sin6_addr, mcast_bytes.data(), mcast_bytes.size());
121  std::memcpy(&gsr.gsr_group, &grp, sizeof(grp));
122 
123  struct sockaddr_in6 src{};
124  src.sin6_family = AF_INET6;
125  auto src_bytes = boost::asio::ip::make_address(source_address).to_v6().to_bytes();
126  std::memcpy(&src.sin6_addr, src_bytes.data(), src_bytes.size());
127  std::memcpy(&gsr.gsr_source, &src, sizeof(src));
128 
129  if (setsockopt(_socket.native_handle(), IPPROTO_IPV6, MCAST_JOIN_SOURCE_GROUP,
130  &gsr, sizeof(gsr)) != 0) {
131  spdlog::error("Receiver: MCAST_JOIN_SOURCE_GROUP for {} from {} failed: {}", address,
132  source_address, strerror(errno));
133  } else {
134  spdlog::info("Receiver: joined SSM {} from source {} on iface {}", address,
135  source_address, iface);
136  }
137  } else {
138  struct ip_mreq_source mreq_source{};
139  auto mcast_bytes = mcast_address.to_v4().to_bytes();
140  auto src_bytes = boost::asio::ip::make_address(source_address).to_v4().to_bytes();
141  auto iface_bytes = boost::asio::ip::make_address(iface).to_v4().to_bytes();
142  std::memcpy(&mreq_source.imr_multiaddr, mcast_bytes.data(), mcast_bytes.size());
143  std::memcpy(&mreq_source.imr_sourceaddr, src_bytes.data(), src_bytes.size());
144  std::memcpy(&mreq_source.imr_interface, iface_bytes.data(), iface_bytes.size());
145 
146  if (setsockopt(_socket.native_handle(), IPPROTO_IP, IP_ADD_SOURCE_MEMBERSHIP,
147  &mreq_source, sizeof(mreq_source)) != 0) {
148  spdlog::error("Receiver: IP_ADD_SOURCE_MEMBERSHIP for {} from {} failed: {}", address,
149  source_address, strerror(errno));
150  } else {
151  spdlog::info("Receiver: joined SSM {} from source {} on iface {}", address,
152  source_address, iface);
153  }
154  }
155  } else if (is_v6) {
156  // Plain (any-source) multicast join, IPv6: join_group(address_v6, interface_index) --
157  // a different overload shape than IPv4's join_group(address_v4, address_v4) below,
158  // since v6 identifies the interface by index (see resolve_iface_index()).
159  _socket.set_option(
160  boost::asio::ip::multicast::join_group(
161  mcast_address.to_v6(), resolve_iface_index(iface)));
162  } else {
163  // Join the multicast group on the specific interface passed in - the
164  // single-address join_group() overload ignores `iface` entirely and
165  // joins via whatever interface the system considers the default route
166  // for the group, which silently breaks reception when the content
167  // actually arrives on a non-default interface (e.g. the modem's own
168  // TUN device rather than a physical NIC).
169  _socket.set_option(
170  boost::asio::ip::multicast::join_group(
171  mcast_address.to_v4(),
172  boost::asio::ip::make_address(iface).to_v4()));
173  }
174 
175  arm_receive();
176 }
177 
179 {
180  *_alive = false;
181 }
182 
183 auto LibFlute::Receiver::arm_receive() -> void
184 {
185  auto alive = _alive;
186  _socket.async_receive_from(
187  boost::asio::buffer(_data, max_length), _sender_endpoint,
188  [this, alive](const boost::system::error_code& error, size_t bytes_recvd) {
189  if (!*alive) return;
190  handle_receive_from(error, bytes_recvd);
191  });
192 }
193 
194 auto LibFlute::Receiver::enable_ipsec(uint32_t spi, const std::string& key) -> void
195 {
197 }
198 
199 auto LibFlute::Receiver::handle_receive_from(const boost::system::error_code& error,
200  size_t bytes_recvd) -> void
201 {
202  if (!_running) return;
203 
204  if (!error)
205  {
206  spdlog::trace("Received {} bytes", bytes_recvd);
207  try {
208  auto alc = LibFlute::AlcPacket(_data, bytes_recvd);
209 
210  if (alc.tsi() == _tsi) {
211 
212  const std::lock_guard<std::mutex> lock(_files_mutex);
213 
214  if (alc.toi() == 0 && (!_fdt || _fdt->instance_id() != alc.fdt_instance_id())) {
215  // (Re)start reception of the FDT (TOI 0) for THIS instance. The FDT is
216  // a FLUTE object reassembled from its symbols like any file, but unlike
217  // a static file its instance changes over the session's lifetime (here
218  // every few seconds, as content segments roll out of the live window,
219  // each new FDT instance carrying a different file set). If we kept
220  // feeding symbols from a newer instance into the File started for an
221  // older one, the two serialisations would overlay into one buffer and
222  // corrupt it (e.g. a dropped byte at the seam, so an attribute like
223  // Content-Location loses its '='). So whenever the in-progress TOI-0
224  // object belongs to a different instance than the arriving packet,
225  // discard it and reassemble the new instance from scratch.
226  auto existing = _files.find(0);
227  if (existing == _files.end() || _fdt_in_progress_instance_id != alc.fdt_instance_id()) {
228  FileDeliveryTable::FileEntry fe{0, "", static_cast<uint32_t>(alc.fec_oti().transfer_length), "", "", 0, alc.fec_oti()};
229  _files[0] = std::make_shared<LibFlute::File>(fe);
230  _fdt_in_progress_instance_id = alc.fdt_instance_id();
231  }
232  }
233 
234  if (_files.find(alc.toi()) != _files.end() && !_files[alc.toi()]->complete()) {
235  auto encoding_symbols = LibFlute::EncodingSymbol::from_payload(
236  _data + alc.header_length(),
237  bytes_recvd - alc.header_length(),
238  _files[alc.toi()]->fec_oti(),
239  alc.content_encoding());
240 
241  for (const auto& symbol : encoding_symbols) {
242  spdlog::debug("received TOI {} SBN {} ID {}", alc.toi(), symbol.source_block_number(), symbol.id() );
243  _files[alc.toi()]->put_symbol(symbol);
244  }
245 
246  auto file = _files[alc.toi()].get();
247  if (_files[alc.toi()]->complete()) {
248  for (auto it = _files.cbegin(); it != _files.cend();)
249  {
250  if (it->second.get() != file && it->second->meta().content_location == file->meta().content_location)
251  {
252  spdlog::debug("Replacing file with TOI {}", it->first);
253  it = _files.erase(it);
254  }
255  else
256  {
257  ++it;
258  }
259  }
260 
261  file->decode();
262 
263  spdlog::debug("File with TOI {} completed", alc.toi());
264  if (alc.toi() != 0 && _completion_cb) {
265  _completion_cb(_files[alc.toi()]);
266  _files.erase(alc.toi());
267  }
268 
269  if (alc.toi() == 0) { // parse complete FDT
270  _fdt = std::make_unique<LibFlute::FileDeliveryTable>(
271  alc.fdt_instance_id(), _files[alc.toi()]->buffer(), _files[alc.toi()]->length());
272 
273  _files.erase(alc.toi());
274  for (const auto& file_entry : _fdt->file_entries()) {
275  // automatically receive all files in the FDT
276  auto existing_file = _files.find(file_entry.toi);
277  if (existing_file != _files.end() &&
278  existing_file->second->meta().content_location != file_entry.content_location) {
279  // TOI numbers get reused across FDT instances (the live window
280  // rolls forward). If a File is still sitting here incomplete
281  // under this TOI from an earlier instance and this instance
282  // now describes a *different* content_location for the same
283  // TOI, that object is stale, abandoned reception state, not
284  // an in-progress transfer of the current file. Feeding this
285  // file's symbols into it would corrupt the buffer (mismatched
286  // source-block layout) and its received_at would keep the
287  // timestamp from the abandoned transfer, making the completed
288  // file look ancient to cache expiry the instant it lands. Drop
289  // it and start clean.
290  spdlog::debug("Discarding stale incomplete file for reused TOI {} ({} != {})",
291  file_entry.toi, existing_file->second->meta().content_location, file_entry.content_location);
292  _files.erase(existing_file);
293  existing_file = _files.end();
294  }
295  if (existing_file == _files.end()) {
296  spdlog::debug("Starting reception for file with TOI {}: {} ({})", file_entry.toi,
297  file_entry.content_location, file_entry.content_type);
298  _files.emplace(file_entry.toi, std::make_shared<LibFlute::File>(file_entry));
299  }
300  }
301  }
302  }
303  } else {
304  spdlog::trace("Discarding packet for unknown or already completed file with TOI {}", alc.toi());
305  }
306  } else {
307  spdlog::warn("Discarding packet for unknown TSI {}", alc.tsi());
308  }
309  } catch (const std::exception &ex) {
310  spdlog::warn("Failed to decode ALC/FLUTE packet: {}", ex.what());
311  } catch (const char* ex) {
312  // AlcPacket/EncodingSymbol/File/FileDeliveryTable all throw raw
313  // string literals (not std::exception) for malformed/unsupported
314  // packets - a single such packet would otherwise propagate past this
315  // handler entirely uncaught and crash the whole receiver via
316  // std::terminate().
317  spdlog::warn("Failed to decode ALC/FLUTE packet: {}", ex);
318  }
319 
320  arm_receive();
321  }
322  else
323  {
324  spdlog::error("receive_from error: {}", error.message());
325  }
326 }
327 
328 auto LibFlute::Receiver::file_list() -> std::vector<std::shared_ptr<LibFlute::File>>
329 {
330  std::vector<std::shared_ptr<LibFlute::File>> files;
331  for (auto& f : _files) {
332  files.push_back(f.second);
333  }
334  return files;
335 }
336 
337 auto LibFlute::Receiver::remove_expired_files(unsigned max_age) -> void
338 {
339  const std::lock_guard<std::mutex> lock(_files_mutex);
340  for (auto it = _files.cbegin(); it != _files.cend();)
341  {
342  auto age = time(nullptr) - it->second->received_at();
343  if ( it->second->meta().content_location != "bootstrap.multipart" && age > max_age) {
344  it = _files.erase(it);
345  } else {
346  ++it;
347  }
348  }
349 }
350 
351 auto LibFlute::Receiver::remove_file_with_content_location(const std::string& cl) -> void
352 {
353  const std::lock_guard<std::mutex> lock(_files_mutex);
354  for (auto it = _files.cbegin(); it != _files.cend();)
355  {
356  if ( it->second->meta().content_location == cl) {
357  it = _files.erase(it);
358  } else {
359  ++it;
360  }
361  }
362 }
A class for parsing and creating ALC packets.
Definition: AlcPacket.h:27
static std::vector< EncodingSymbol > from_payload(char *encoded_data, size_t data_len, const FecOti &fec_oti, ContentEncoding encoding)
Parse and construct all encoding symbols from a payload data buffer.
Receiver(const std::string &iface, const std::string &address, short port, uint64_t tsi, boost::asio::io_context &io_context, const std::string &source_address="")
Default constructor.
Definition: Receiver.cpp:68
void remove_file_with_content_location(const std::string &cl)
Remove a file from the list that matches the passed content location.
Definition: Receiver.cpp:351
virtual ~Receiver()
Destructor.
Definition: Receiver.cpp:178
std::vector< std::shared_ptr< LibFlute::File > > file_list()
List all current files.
Definition: Receiver.cpp:328
void enable_ipsec(uint32_t spi, const std::string &aes_key)
Enable IPSEC ESP decryption of FLUTE payloads.
Definition: Receiver.cpp:194
void remove_expired_files(unsigned max_age)
Remove files from the list that are older than max_age seconds.
Definition: Receiver.cpp:337
void enable_esp(uint32_t spi, const std::string &dest_address, Direction direction, const std::string &key)
Definition: IpSec.cpp:122
An entry for a file in the FDT.