QUdpSocket: How to make multicast work also on localhost but prevent loopback per application?

Artem Pisarenko picture Artem Pisarenko · Oct 22, 2013 · Viewed 9.3k times · Source

My Qt application uses multicast QUdpSocket and need half-duplex operation (it simulates radio transfer between simplex radiostations). It means that one application instance must not receive datagrams it sends. But also it must support working multiple instances on same machine (user explicitly selects loopback interface). And, of course, it should be portable (at worst, Windows and Linux).

I'm aware of IP_MULTICAST_LOOP socket option and similar questions:
Simulating multicasting on loopback interface, Multicasting on loopback device, Is there a way to test multicast IP on same box?, How to limit traffic using multicast over localhost, Is it okay to multicast data from different processes to the same host and port?.

Discussions are nearly close to answer my one, but it's still unclear (mostly because it seems for me that behavior is varying on different platforms).
So how should I setup socket ? If it's not possible to achieve it by simple configuration of connection, then maybe using connectToHost() with ReadOnly/WriteOnly will help guaranteed ?
Update:

Here is result of my research, which SEEMS to work, but I don't believe it will work on any other combination of platform and network configuration, other than my computer have:

void initNetwork()  {
//...
  /* It will be needed to filter out own loopbacked datagrams */
  local_addresses = QNetworkInterface::allAddresses();
  /* Interface, selected by user */
  QNetworkInterface multicast_netif = <user selected>;
  Q_ASSERT(multicast_netif.isValid());
  /* Yes, I already accept the fact, that I need two separate sockets (there are more chances to make it work than when using bidirectional one) */
  udpSocketIn = new QUdpSocket(this);
  udpSocketOut = new QUdpSocket(this);
  /* It's important to bind to Any. No other combinations work (including LocalHost (in case if user selected loopback interface), MULTICAST_ADDR) */
  result = udpSocketIn->bind(QHostAddress::Any, MULTICAST_PORT, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);
  Q_ASSERT(result);
  /* It required to only make application know real(!) udpSocketOut->localPort() in order to be able filter own datagrams */
  result = udpSocketOut->bind();
  Q_ASSERT(result);
  /* One of rare things, I'm sure is correct and must be done */
  result = udpSocketIn->joinMulticastGroup(QHostAddress(MULTICAST_ADDR), multicast_netif);
  Q_ASSERT(result);
  /* It doesn't matter, but it will fail if socket not binded  */
  //result = udpSocketOut->joinMulticastGroup(QHostAddress(MULTICAST_ADDR), multicast_netif);
  //Q_ASSERT(result);
  /* No, you can't ! If socket binded previously and loopback interface selected, datagrams will not be transfered. I don't know why. And this is major thing, which makes me think, that this configuration isn't reliable, because stupid windows will select default interface for outgoing datagrams ! */
  //udpSocketOut->setMulticastInterface(multicast_netif);
  /* It doesn't matter, because it set by default. */
  //udpSocketIn->setSocketOption(QAbstractSocket::MulticastLoopbackOption, QVariant(1));
  //udpSocketOut->setSocketOption(QAbstractSocket::MulticastLoopbackOption, QVariant(1));
//...
}

void sendDatagram() {
//...
  /* It almost always return ok, regardless of datagram being sent actually or not.
     One exception is when I turn off real network interface to which it was binded by udpSocketOut->bind() call (it selected by OS, although user selected loopback interface !)
  */
  result = udpSocketOut->writeDatagram(datagram, QHostAddress((MULTICAST_ADDR), MULTICAST_PORT);
  Q_ASSERT(result == datagram.size());
//...
}

void readPendingDatagrams() {
//...
  udpSocketIn->readDatagram(datagram, &senderHost, &senderPort);
  /* Thanks to udpSocketOut->bind() we are able to filter out own packets sent from udpSocketOut */
  if ((local_addresses.contains(senderHost)) && (senderPort == udpSocketOut->localPort())) {
    // Ignore loopbacked datagram
    return;
  }
//...

Sorry for bad formatting, it's because I wasn't able to win annoying problem

Answer

Artem Pisarenko picture Artem Pisarenko · Oct 25, 2013

I came to conclusion, that it's not possible to make it work 100% correct as expected from user point of view, because of OS-specific implementations of networking.

Following code provides solution:

void initNetwork()  {
//...
  /* It will be needed to filter out own loopbacked datagrams */
  local_addresses = QNetworkInterface::allAddresses();
  /* Interface, selected by user */
  QNetworkInterface multicast_netif = <user selected>;
  /* Two separate sockets for receiving and sending (allows differentiate source port from destination port) */
  udpSocketIn = new QUdpSocket(this);
  udpSocketOut = new QUdpSocket(this);
  /* It's important to bind to Any for multicast to work, also port must be reusable by all application instances on same host */
  udpSocketIn->bind(QHostAddress::Any, MULTICAST_PORT, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);
  /* It required to only make application know real(!) udpSocketOut->localPort() in order to be able filter own datagrams */
  udpSocketOut->bind();
  /* Obvious... */
  udpSocketIn->joinMulticastGroup(QHostAddress(MULTICAST_ADDR), multicast_netif);
  udpSocketOut->setMulticastInterface(multicast_netif);
  /* Multicast loopback is set by default, but set it explicitly just in case. */
  udpSocketIn->setSocketOption(QAbstractSocket::MulticastLoopbackOption, QVariant(1));
  udpSocketOut->setSocketOption(QAbstractSocket::MulticastLoopbackOption, QVariant(1));
//...
}

void sendDatagram() {
//...
  udpSocketOut->writeDatagram(datagram, QHostAddress((MULTICAST_ADDR), MULTICAST_PORT);
//...
}

void readPendingDatagrams() {
//...
  udpSocketIn->readDatagram(datagram, &senderHost, &senderPort);
  /* Thanks to udpSocketOut->bind() we are able to filter out own packets sent from udpSocketOut */
  if ((local_addresses.contains(senderHost)) && (senderPort == udpSocketOut->localPort())) {
    // ignore loopbacked datagram
  } else {
    // accept diagram
  }
//...

Tests on Linux (with two interfaces only: lo and eth0) showed perfect results. I select desired interface and it works 99% correct as expected. It would be 100% if it weren't small bug: on binded lo interface first datagram is being sent (or received) with source ip of eth0 interface.

Tests on Windows 7 64-bit (with many different interfaces) showed, that in some cases user have to play with system network configuration to make it work. Here are some observations:

  1. with selected "Loopback Pseudo-Interface 1" diagrams not transferred, if there are any other interface is up, solution: either disable all interfaces, or modify metrics in route table
  2. with selected "Teredo Tunneling Pseudo-Interface" it works always (it acts as loopback interface)
  3. regardless of selected interface, socket(s) binded to any interface and diagrams will be transfered with source ip of that interface (i.e. if user selected loopback interface and thinks that it works locally, it's not true, diagrams go out to real network also), solution would be same as in clause 1.

Tests on Windows XP SP3 showed satisfactory results: only clause 3 (see above) retains.

Hope my research will be useful for people experiencing similar problems.