Network interface selection for outgoing UDP multicast datagrams

Default  selection  is  the  loop-back interface.  Users  who  require
interoperation  between  WSJT-X   instances  cooperating  applications
running on different hosts should  select a suitable network interface
and  carefully choose  a multicast  group address,  and TTL,  that has
minimal scope covering the necessary  network(s). Using 224.0.0.1 is a
reasonable    strategy    if   all    hosts    are    on   the    same
subnet. Administratively  scoped multicast group addresses  like those
within 239.255.0.0/16  can cover larger  boundaries, but care  must be
taken if the local subnet has access to a multicast enabled router.

The  IPv4  broadcast  address  (255.255.255.255) may  be  used  as  an
alternative  to multicast  UDP, but  note that  WSJT-X will  only send
broadcast UDP datagrams  on the loop-back interface,  so all recipient
applications must be running on the same host system.

The reference UDP Message protocol  applications are being extended to
be configurable  with a list of  interfaces to join a  multicast group
address on. By default they will only join on the loop-back interface,
which is also  recommended for any applications designed  to take part
in the WSJT-X  UDP Message Protocol. This allows full  user control of
the  scope of  multicast  group membership  with  a very  conservative
default  mode  that will  work  with  all interoperating  applications
running on the same host system.
This commit is contained in:
Bill Somerville
2020-11-02 15:33:44 +00:00
parent 072da278ee
commit 430d57c1ca
11 changed files with 510 additions and 139 deletions
+135 -58
View File
@@ -6,16 +6,16 @@
#include <limits>
#include <QUdpSocket>
#include <QNetworkInterface>
#include <QHostInfo>
#include <QTimer>
#include <QQueue>
#include <QByteArray>
#include <QHostAddress>
#include <QColor>
#include <QDebug>
#include "NetworkMessage.hpp"
#include "qt_helpers.hpp"
#include "pimpl_impl.hpp"
#include "moc_MessageClient.cpp"
@@ -34,14 +34,15 @@ class MessageClient::impl
public:
impl (QString const& id, QString const& version, QString const& revision,
port_type server_port, MessageClient * self)
port_type server_port, int TTL, MessageClient * self)
: self_ {self}
, dns_lookup_id_ {0}
, enabled_ {false}
, id_ {id}
, version_ {version}
, revision_ {revision}
, dns_lookup_id_ {-1}
, server_port_ {server_port}
, TTL_ {TTL}
, schema_ {2} // use 2 prior to negotiation not 1 which is broken
, heartbeat_timer_ {new QTimer {this}}
{
@@ -49,9 +50,6 @@ public:
connect (this, &QIODevice::readyRead, this, &impl::pending_datagrams);
heartbeat_timer_->start (NetworkMessage::pulse * 1000);
// bind to an ephemeral port
bind ();
}
~impl ()
@@ -61,7 +59,10 @@ public:
enum StreamStatus {Fail, Short, OK};
void parse_message (QByteArray const& msg);
void set_server (QString const& server_name, QString const& network_interface_name);
Q_SLOT void host_info_results (QHostInfo);
void start ();
void parse_message (QByteArray const&);
void pending_datagrams ();
void heartbeat ();
void closedown ();
@@ -69,27 +70,26 @@ public:
void send_message (QByteArray const&);
void send_message (QDataStream const& out, QByteArray const& message)
{
if (OK == check_status (out))
{
send_message (message);
}
else
{
Q_EMIT self_->error ("Error creating UDP message");
}
if (OK == check_status (out))
{
send_message (message);
}
else
{
Q_EMIT self_->error ("Error creating UDP message");
}
}
Q_SLOT void host_info_results (QHostInfo);
MessageClient * self_;
int dns_lookup_id_;
bool enabled_;
QString id_;
QString version_;
QString revision_;
QString server_string_;
port_type server_port_;
int dns_lookup_id_;
QHostAddress server_;
port_type server_port_;
int TTL_;
QNetworkInterface network_interface_;
quint32 schema_;
QTimer * heartbeat_timer_;
std::vector<QHostAddress> blocked_addresses_;
@@ -101,37 +101,117 @@ public:
#include "MessageClient.moc"
void MessageClient::impl::set_server (QString const& server_name, QString const& network_interface_name)
{
server_.setAddress (server_name);
network_interface_ = QNetworkInterface::interfaceFromName (network_interface_name);
if (server_.isNull () && server_name.size ()) // DNS lookup required
{
// queue a host address lookup
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
dns_lookup_id_ = QHostInfo::lookupHost (server_name, this, &MessageClient::impl::host_info_results);
#else
dns_lookup_id_ = QHostInfo::lookupHost (server_name, this, SLOT (host_info_results (QHostInfo)));
#endif
}
else
{
start ();
}
}
void MessageClient::impl::host_info_results (QHostInfo host_info)
{
if (host_info.lookupId () != dns_lookup_id_) return;
dns_lookup_id_ = -1;
if (QHostInfo::NoError != host_info.error ())
{
Q_EMIT self_->error ("UDP server lookup failed:\n" + host_info.errorString ());
pending_messages_.clear (); // discard
Q_EMIT self_->error ("UDP server DNS lookup failed: " + host_info.errorString ());
}
else if (host_info.addresses ().size ())
else
{
auto server = host_info.addresses ()[0];
if (blocked_addresses_.end () == std::find (blocked_addresses_.begin (), blocked_addresses_.end (), server))
auto const& server_addresses = host_info.addresses ();
if (server_addresses.size ())
{
server_ = server;
TRACE_UDP ("resulting server:" << server);
server_ = server_addresses[0];
}
}
start ();
}
// send initial heartbeat which allows schema negotiation
heartbeat ();
void MessageClient::impl::start ()
{
if (server_.isNull ())
{
Q_EMIT self_->close ();
pending_messages_.clear (); // discard
return;
}
// clear any backlog
while (pending_messages_.size ())
if (blocked_addresses_.end () != std::find (blocked_addresses_.begin (), blocked_addresses_.end (), server_))
{
Q_EMIT self_->error ("UDP server blocked, please try another");
pending_messages_.clear (); // discard
return;
}
TRACE_UDP ("Trying server:" << server_.toString () << "on interface:" << network_interface_.humanReadableName ());
QHostAddress interface_ip {QHostAddress::Any};
if (network_interface_.isValid ())
{
if (is_multicast_address (server_) && !(network_interface_.flags () & QNetworkInterface::CanMulticast))
{
Q_EMIT self_->error ("Network interface is not multicast capable, please try another");
return;
}
for (auto const& ae : network_interface_.addressEntries ())
{
auto const& ip = ae.ip ();
if (server_.protocol () == ip.protocol ())
{
send_message (pending_messages_.dequeue ());
interface_ip = ip;
break;
}
}
else
if (QHostAddress {QHostAddress::Any} == interface_ip)
{
Q_EMIT self_->error ("UDP server blocked, please try another");
Q_EMIT self_->error ("Network interface has no suitable address for server IP protocol, please try another");
pending_messages_.clear (); // discard
return;
}
}
if (server_.isBroadcast ())
{
// only allow broadcast on the loopback interface to avoid
// flooding the local subnet which may be large with some ISPs
//interface_ip.setAddress ("127.0.0.1");
}
if (localAddress () != interface_ip)
{
if (UnconnectedState != state () || state ())
{
close ();
}
// bind to an ephemeral port on the selected interface and set
// up for sending datagrams
bind (interface_ip);
setMulticastInterface (network_interface_);
// set multicast TTL to limit scope when sending to multicast
// group addresses
setSocketOption (MulticastTtlOption, TTL_);
}
// send initial heartbeat which allows schema negotiation
heartbeat ();
// clear any backlog
while (pending_messages_.size ())
{
send_message (pending_messages_.dequeue ());
}
}
void MessageClient::impl::pending_datagrams ()
@@ -428,9 +508,11 @@ auto MessageClient::impl::check_status (QDataStream const& stream) const -> Stre
}
MessageClient::MessageClient (QString const& id, QString const& version, QString const& revision,
QString const& server, port_type server_port, QObject * self)
QString const& server_name, port_type server_port,
QString const& network_interface_name,
int TTL, QObject * self)
: QObject {self}
, m_ {id, version, revision, server_port, this}
, m_ {id, version, revision, server_port, TTL, this}
{
connect (&*m_
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
@@ -449,8 +531,8 @@ MessageClient::MessageClient (QString const& id, QString const& version, QString
#endif
Q_EMIT error (m_->errorString ());
}
});
set_server (server);
});
m_->set_server (server_name, network_interface_name);
}
QHostAddress MessageClient::server_address () const
@@ -463,20 +545,9 @@ auto MessageClient::server_port () const -> port_type
return m_->server_port_;
}
void MessageClient::set_server (QString const& server)
void MessageClient::set_server (QString const& server_name, QString const& network_interface_name)
{
m_->server_.clear ();
m_->server_string_ = server;
if (server.size ())
{
// queue a host address lookup
TRACE_UDP ("server host DNS lookup:" << server);
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
m_->dns_lookup_id_ = QHostInfo::lookupHost (server, &*m_, &MessageClient::impl::host_info_results);
#else
m_->dns_lookup_id_ = QHostInfo::lookupHost (server, &*m_, SLOT (host_info_results (QHostInfo)));
#endif
}
m_->set_server (server_name, network_interface_name);
}
void MessageClient::set_server_port (port_type server_port)
@@ -484,6 +555,12 @@ void MessageClient::set_server_port (port_type server_port)
m_->server_port_ = server_port;
}
void MessageClient::set_TTL (int TTL)
{
m_->TTL_ = TTL;
m_->setSocketOption (QAbstractSocket::MulticastTtlOption, m_->TTL_);
}
void MessageClient::enable (bool flag)
{
m_->enabled_ = flag;
@@ -499,7 +576,7 @@ void MessageClient::status_update (Frequency f, QString const& mode, QString con
, quint32 frequency_tolerance, quint32 tr_period
, QString const& configuration_name)
{
if (m_->server_port_ && !m_->server_string_.isEmpty ())
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Status, m_->id_, m_->schema_};
@@ -516,7 +593,7 @@ void MessageClient::decode (bool is_new, QTime time, qint32 snr, float delta_tim
, QString const& mode, QString const& message_text, bool low_confidence
, bool off_air)
{
if (m_->server_port_ && !m_->server_string_.isEmpty ())
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Decode, m_->id_, m_->schema_};
@@ -531,7 +608,7 @@ void MessageClient::WSPR_decode (bool is_new, QTime time, qint32 snr, float delt
, qint32 drift, QString const& callsign, QString const& grid, qint32 power
, bool off_air)
{
if (m_->server_port_ && !m_->server_string_.isEmpty ())
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::WSPRDecode, m_->id_, m_->schema_};
@@ -544,7 +621,7 @@ void MessageClient::WSPR_decode (bool is_new, QTime time, qint32 snr, float delt
void MessageClient::decodes_cleared ()
{
if (m_->server_port_ && !m_->server_string_.isEmpty ())
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::Clear, m_->id_, m_->schema_};
@@ -561,7 +638,7 @@ void MessageClient::qso_logged (QDateTime time_off, QString const& dx_call, QStr
, QString const& my_grid, QString const& exchange_sent
, QString const& exchange_rcvd, QString const& propmode)
{
if (m_->server_port_ && !m_->server_string_.isEmpty ())
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::QSOLogged, m_->id_, m_->schema_};
@@ -576,7 +653,7 @@ void MessageClient::qso_logged (QDateTime time_off, QString const& dx_call, QStr
void MessageClient::logged_ADIF (QByteArray const& ADIF_record)
{
if (m_->server_port_ && !m_->server_string_.isEmpty ())
if (m_->server_port_ && !m_->server_.isNull ())
{
QByteArray message;
NetworkMessage::Builder out {&message, NetworkMessage::LoggedADIF, m_->id_, m_->schema_};