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 9e71d07075
commit 662ed0fa7a
No known key found for this signature in database
GPG Key ID: D864B06D1E81618F
11 changed files with 510 additions and 139 deletions

View File

@ -163,6 +163,9 @@
#include <QFontDialog>
#include <QSerialPortInfo>
#include <QScopedPointer>
#include <QNetworkInterface>
#include <QHostInfo>
#include <QHostAddress>
#include <QDebug>
#include "pimpl_impl.hpp"
@ -439,6 +442,10 @@ private:
void load_audio_devices (QAudio::Mode, QComboBox *, QAudioDeviceInfo *);
void update_audio_channels (QComboBox const *, int, QComboBox *, bool);
void load_network_interfaces (QComboBox *, QString const& current);
Q_SLOT void host_info_results (QHostInfo);
void check_multicast (QHostAddress const&);
void find_tab (QWidget *);
void initialize_models ();
@ -492,6 +499,8 @@ private:
Q_SLOT void on_add_macro_line_edit_editingFinished ();
Q_SLOT void delete_macro ();
void delete_selected_macros (QModelIndexList);
Q_SLOT void on_udp_server_line_edit_textChanged (QString const&);
Q_SLOT void on_udp_server_line_edit_editingFinished ();
Q_SLOT void on_save_path_select_push_button_clicked (bool);
Q_SLOT void on_azel_path_select_push_button_clicked (bool);
Q_SLOT void on_calibration_intercept_spin_box_valueChanged (double);
@ -641,7 +650,11 @@ private:
bool use_dynamic_grid_;
QString opCall_;
QString udp_server_name_;
bool udp_server_name_edited_;
int dns_lookup_id_;
port_type udp_server_port_;
QString udp_interface_name_;
int udp_TTL_;
QString n1mm_server_name_;
port_type n1mm_server_port_;
bool broadcast_to_n1mm_;
@ -741,6 +754,8 @@ QString Configuration::opCall() const {return m_->opCall_;}
void Configuration::opCall (QString const& call) {m_->opCall_ = call;}
QString Configuration::udp_server_name () const {return m_->udp_server_name_;}
auto Configuration::udp_server_port () const -> port_type {return m_->udp_server_port_;}
QString Configuration::udp_interface_name () const {return m_->udp_interface_name_;}
int Configuration::udp_TTL () const {return m_->udp_TTL_;}
bool Configuration::accept_udp_requests () const {return m_->accept_udp_requests_;}
QString Configuration::n1mm_server_name () const {return m_->n1mm_server_name_;}
auto Configuration::n1mm_server_port () const -> port_type {return m_->n1mm_server_port_;}
@ -995,6 +1010,8 @@ Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network
, transceiver_command_number_ {0}
, degrade_ {0.} // initialize to zero each run, not
// saved in settings
, udp_server_name_edited_ {false}
, dns_lookup_id_ {-1}
{
ui_->setupUi (this);
@ -1044,6 +1061,7 @@ Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network
// this must be done after the default paths above are set
read_settings ();
// set up dynamic loading of audio devices
connect (ui_->sound_input_combo_box, &LazyFillComboBox::about_to_show_popup, [this] () {
QGuiApplication::setOverrideCursor (QCursor {Qt::WaitCursor});
load_audio_devices (QAudio::AudioInput, ui_->sound_input_combo_box, &next_audio_input_device_);
@ -1059,6 +1077,13 @@ Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network
QGuiApplication::restoreOverrideCursor ();
});
// set up dynamic loading of network interfaces
connect (ui_->udp_interface_combo_box, &LazyFillComboBox::about_to_show_popup, [this] () {
QGuiApplication::setOverrideCursor (QCursor {Qt::WaitCursor});
load_network_interfaces (ui_->udp_interface_combo_box, udp_interface_name_);
QGuiApplication::restoreOverrideCursor ();
});
// set up LoTW users CSV file fetching
connect (&lotw_users_, &LotWUsers::load_finished, [this] () {
ui_->LotW_CSV_fetch_push_button->setEnabled (true);
@ -1335,7 +1360,14 @@ void Configuration::impl::initialize_models ()
ui_->CAT_poll_interval_spin_box->setValue (rig_params_.poll_interval);
ui_->opCallEntry->setText (opCall_);
ui_->udp_server_line_edit->setText (udp_server_name_);
on_udp_server_line_edit_editingFinished ();
ui_->udp_server_port_spin_box->setValue (udp_server_port_);
load_network_interfaces (ui_->udp_interface_combo_box, udp_interface_name_);
if (!udp_interface_name_.size ())
{
udp_interface_name_ = ui_->udp_interface_combo_box->currentData ().toString ();
}
ui_->udp_TTL_spin_box->setValue (udp_TTL_);
ui_->accept_udp_requests_check_box->setChecked (accept_udp_requests_);
ui_->n1mm_server_name_line_edit->setText (n1mm_server_name_);
ui_->n1mm_server_port_spin_box->setValue (n1mm_server_port_);
@ -1513,6 +1545,8 @@ void Configuration::impl::read_settings ()
rig_params_.split_mode = settings_->value ("SplitMode", QVariant::fromValue (TransceiverFactory::split_mode_none)).value<TransceiverFactory::SplitMode> ();
opCall_ = settings_->value ("OpCall", "").toString ();
udp_server_name_ = settings_->value ("UDPServer", "127.0.0.1").toString ();
udp_interface_name_ = settings_->value ("UDPInterface").toString ();
udp_TTL_ = settings_->value ("UDPTTL").toInt ();
udp_server_port_ = settings_->value ("UDPServerPort", 2237).toUInt ();
n1mm_server_name_ = settings_->value ("N1MMServer", "127.0.0.1").toString ();
n1mm_server_port_ = settings_->value ("N1MMServerPort", 2333).toUInt ();
@ -1641,6 +1675,8 @@ void Configuration::impl::write_settings ()
settings_->setValue ("OpCall", opCall_);
settings_->setValue ("UDPServer", udp_server_name_);
settings_->setValue ("UDPServerPort", udp_server_port_);
settings_->setValue ("UDPInterface", udp_interface_name_);
settings_->setValue ("UDPTTL", udp_TTL_);
settings_->setValue ("N1MMServer", n1mm_server_name_);
settings_->setValue ("N1MMServerPort", n1mm_server_port_);
settings_->setValue ("BroadcastToN1MM", broadcast_to_n1mm_);
@ -1843,6 +1879,12 @@ bool Configuration::impl::validate ()
return false;
}
if (dns_lookup_id_ > -1)
{
MessageBox::information_message (this, tr ("Pending DNS lookup, please try again later"));
return false;
}
return true;
}
@ -2061,20 +2103,30 @@ void Configuration::impl::accept ()
pwrBandTxMemory_ = ui_->checkBoxPwrBandTxMemory->isChecked ();
pwrBandTuneMemory_ = ui_->checkBoxPwrBandTuneMemory->isChecked ();
opCall_=ui_->opCallEntry->text();
auto new_server = ui_->udp_server_line_edit->text ();
if (new_server != udp_server_name_)
auto new_server = ui_->udp_server_line_edit->text ().trimmed ();
auto new_interface = ui_->udp_interface_combo_box->currentData ().toString ();
if (new_server != udp_server_name_ || new_interface != udp_interface_name_)
{
udp_server_name_ = new_server;
Q_EMIT self_->udp_server_changed (new_server);
udp_interface_name_ = new_interface;
Q_EMIT self_->udp_server_changed (udp_server_name_, udp_interface_name_);
}
auto new_port = ui_->udp_server_port_spin_box->value ();
if (new_port != udp_server_port_)
{
udp_server_port_ = new_port;
Q_EMIT self_->udp_server_port_changed (new_port);
Q_EMIT self_->udp_server_port_changed (udp_server_port_);
}
auto new_TTL = ui_->udp_TTL_spin_box->value ();
if (new_TTL != udp_TTL_)
{
udp_TTL_ = new_TTL;
Q_EMIT self_->udp_TTL_changed (udp_TTL_);
}
if (ui_->accept_udp_requests_check_box->isChecked () != accept_udp_requests_)
{
accept_udp_requests_ = ui_->accept_udp_requests_check_box->isChecked ();
@ -2130,6 +2182,12 @@ void Configuration::impl::accept ()
void Configuration::impl::reject ()
{
if (dns_lookup_id_ > -1)
{
QHostInfo::abortHostLookup (dns_lookup_id_);
dns_lookup_id_ = -1;
}
initialize_models (); // reverts to settings as at exec ()
// check if the Transceiver instance changed, in which case we need
@ -2344,6 +2402,72 @@ void Configuration::impl::on_add_macro_push_button_clicked (bool /* checked */)
}
}
void Configuration::impl::on_udp_server_line_edit_textChanged (QString const&)
{
udp_server_name_edited_ = true;
}
void Configuration::impl::on_udp_server_line_edit_editingFinished ()
{
if (udp_server_name_edited_)
{
auto const& server = ui_->udp_server_line_edit->text ().trimmed ();
QHostAddress ha {server};
if (server.size () && ha.isNull ())
{
// queue a host address lookup
qDebug () << "server host DNS lookup:" << server;
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
dns_lookup_id_ = QHostInfo::lookupHost (server, this, &Configuration::impl::host_info_results);
#else
dns_lookup_id_ = QHostInfo::lookupHost (server, this, SLOT (host_info_results (QHostInfo)));
#endif
}
else
{
check_multicast (ha);
}
}
}
void Configuration::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 ())
{
MessageBox::critical_message (this, tr ("UDP server DNS lookup failed"), host_info.errorString ());
}
else
{
auto const& server_addresses = host_info.addresses ();
qDebug () << "message server addresses:" << server_addresses;
if (server_addresses.size ())
{
check_multicast (server_addresses[0]);
}
}
}
void Configuration::impl::check_multicast (QHostAddress const& ha)
{
auto is_multicast = is_multicast_address (ha);
ui_->udp_interface_label->setVisible (is_multicast);
ui_->udp_interface_combo_box->setVisible (is_multicast);
ui_->udp_TTL_label->setVisible (is_multicast);
ui_->udp_TTL_spin_box->setVisible (is_multicast);
if (isVisible ())
{
if (is_MAC_ambiguous_multicast_address (ha))
{
MessageBox::warning_message (this, tr ("MAC-ambiguous multicast groups addresses not supported"));
find_tab (ui_->udp_server_line_edit);
ui_->udp_server_line_edit->clear ();
}
}
udp_server_name_edited_ = false;
}
void Configuration::impl::delete_frequencies ()
{
auto selection_model = ui_->frequencies_table_view->selectionModel ();
@ -2868,6 +2992,31 @@ void Configuration::impl::load_audio_devices (QAudio::Mode mode, QComboBox * com
combo_box->setCurrentIndex (current_index);
}
// load the available network interfaces into the selection combo box
void Configuration::impl::load_network_interfaces (QComboBox * combo_box, QString const& current)
{
combo_box->clear ();
int current_index = -1;
for (auto const& interface : QNetworkInterface::allInterfaces ())
{
if (interface.flags () & QNetworkInterface::IsUp)
{
auto const& name = interface.name ();
combo_box->addItem (interface.humanReadableName (), name);
// select the first loopback interface as a default to
// discourage spamming the network (possibly the Internet),
// particularly important with administratively scoped
// multicast UDP
if (name == current
|| (!current.size () && (interface.flags () & QNetworkInterface::IsLoopBack)))
{
current_index = combo_box->count () - 1;
}
}
}
combo_box->setCurrentIndex (current_index);
}
// enable only the channels that are supported by the selected audio device
void Configuration::impl::update_audio_channels (QComboBox const * source_combo_box, int index, QComboBox * combo_box, bool allow_both)
{

View File

@ -21,7 +21,6 @@ class Bands;
class FrequencyList_v2;
class StationList;
class QStringListModel;
class QHostAddress;
class LotWUsers;
class DecodeHighlightingModel;
class LogBook;
@ -152,6 +151,8 @@ public:
void opCall (QString const&);
QString udp_server_name () const;
port_type udp_server_port () const;
QString udp_interface_name () const;
int udp_TTL () const;
QString n1mm_server_name () const;
port_type n1mm_server_port () const;
bool valid_n1mm_info () const;
@ -273,8 +274,9 @@ public:
//
// This signal is emitted when the UDP server changes
//
Q_SIGNAL void udp_server_changed (QString const& udp_server) const;
Q_SIGNAL void udp_server_changed (QString& udp_server_name, QString const& network_interface) const;
Q_SIGNAL void udp_server_port_changed (port_type server_port) const;
Q_SIGNAL void udp_TTL_changed (int TTL) const;
Q_SIGNAL void accept_udp_requests_changed (bool checked) const;
// signal updates to decode highlighting

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>554</width>
<height>556</height>
<height>560</height>
</rect>
</property>
<property name="windowTitle">
@ -1864,12 +1864,6 @@ and DX Grid fields when a 73 or free text message is sent.</string>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="udp_server_line_edit">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Optional hostname of network service to receive decodes.&lt;/p&gt;&lt;p&gt;Formats:&lt;/p&gt;&lt;ul style=&quot;margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;&quot;&gt;&lt;li style=&quot; margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;hostname&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;IPv4 address&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;IPv6 address&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;IPv4 multicast group address&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;IPv6 multicast group address&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Clearing this field will disable the broadcasting of UDP status updates.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
@ -1898,6 +1892,39 @@ and DX Grid fields when a 73 or free text message is sent.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="udp_interface_label">
<property name="text">
<string>Outgoing interface:</string>
</property>
<property name="buddy">
<cstring>udp_interface_combo_box</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="LazyFillComboBox" name="udp_interface_combo_box"/>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="udp_TTL_spin_box">
<property name="maximum">
<number>255</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="udp_TTL_label">
<property name="text">
<string>Multicast TTL:</string>
</property>
<property name="buddy">
<cstring>udp_TTL_spin_box</cstring>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
@ -3084,6 +3111,8 @@ Right click for insert and delete options.</string>
<tabstop>psk_reporter_tcpip_check_box</tabstop>
<tabstop>udp_server_line_edit</tabstop>
<tabstop>udp_server_port_spin_box</tabstop>
<tabstop>udp_interface_combo_box</tabstop>
<tabstop>udp_TTL_spin_box</tabstop>
<tabstop>accept_udp_requests_check_box</tabstop>
<tabstop>udpWindowToFront</tabstop>
<tabstop>udpWindowRestore</tabstop>
@ -3101,8 +3130,8 @@ Right click for insert and delete options.</string>
<tabstop>include_WAE_check_box</tabstop>
<tabstop>rescan_log_push_button</tabstop>
<tabstop>LotW_CSV_URL_line_edit</tabstop>
<tabstop>LotW_days_since_upload_spin_box</tabstop>
<tabstop>LotW_CSV_fetch_push_button</tabstop>
<tabstop>LotW_days_since_upload_spin_box</tabstop>
<tabstop>sbNtrials</tabstop>
<tabstop>sbAggressive</tabstop>
<tabstop>cbTwoPass</tabstop>
@ -3118,11 +3147,11 @@ Right click for insert and delete options.</string>
<tabstop>rbHound</tabstop>
<tabstop>rbNA_VHF_Contest</tabstop>
<tabstop>rbField_Day</tabstop>
<tabstop>Field_Day_Exchange</tabstop>
<tabstop>rbEU_VHF_Contest</tabstop>
<tabstop>rbRTTY_Roundup</tabstop>
<tabstop>RTTY_Exchange</tabstop>
<tabstop>rbWW_DIGI</tabstop>
<tabstop>Field_Day_Exchange</tabstop>
<tabstop>RTTY_Exchange</tabstop>
</tabstops>
<resources/>
<connections>
@ -3192,13 +3221,13 @@ Right click for insert and delete options.</string>
</connection>
</connections>
<buttongroups>
<buttongroup name="PTT_method_button_group"/>
<buttongroup name="CAT_data_bits_button_group"/>
<buttongroup name="special_op_activity_button_group"/>
<buttongroup name="CAT_stop_bits_button_group"/>
<buttongroup name="TX_audio_source_button_group"/>
<buttongroup name="split_mode_button_group"/>
<buttongroup name="TX_mode_button_group"/>
<buttongroup name="CAT_stop_bits_button_group"/>
<buttongroup name="special_op_activity_button_group"/>
<buttongroup name="CAT_handshake_button_group"/>
<buttongroup name="CAT_data_bits_button_group"/>
<buttongroup name="TX_mode_button_group"/>
<buttongroup name="PTT_method_button_group"/>
<buttongroup name="TX_audio_source_button_group"/>
</buttongroups>
</ui>

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_};

View File

@ -5,6 +5,7 @@
#include <QTime>
#include <QDateTime>
#include <QString>
#include <QHostAddress>
#include "Radio.hpp"
#include "pimpl_h.hpp"
@ -34,19 +35,25 @@ public:
//
// messages will be silently dropped until a server host lookup is complete
MessageClient (QString const& id, QString const& version, QString const& revision,
QString const& server, port_type server_port, QObject * parent = nullptr);
QString const& server_name, port_type server_port,
QString const& network_interface_name,
int TTL, QObject * parent = nullptr);
// query server details
QHostAddress server_address () const;
port_type server_port () const;
// initiate a new server host lookup or is the server name is empty
// the sending of messages is disabled
Q_SLOT void set_server (QString const& server = QString {});
// initiate a new server host lookup or if the server name is empty
// the sending of messages is disabled, if an interface is specified
// then that interface is used for outgoing datagrams
Q_SLOT void set_server (QString const& server_name, QString const& network_interface_name);
// change the server port messages are sent to
Q_SLOT void set_server_port (port_type server_port = 0u);
// change the server port messages are sent to
Q_SLOT void set_TTL (int TTL);
// enable incoming messages
Q_SLOT void enable (bool);

View File

@ -32,7 +32,6 @@ public:
: self_ {self}
, version_ {version}
, revision_ {revision}
, port_ {0u}
, clock_ {new QTimer {this}}
{
// register the required types with Qt
@ -78,8 +77,8 @@ public:
MessageServer * self_;
QString version_;
QString revision_;
port_type port_;
QHostAddress multicast_group_address_;
QStringList network_interfaces_;
static BindMode constexpr bind_mode_ = ShareAddress | ReuseAddressHint;
struct Client
{
@ -109,56 +108,39 @@ MessageServer::impl::BindMode constexpr MessageServer::impl::bind_mode_;
void MessageServer::impl::leave_multicast_group ()
{
if (!multicast_group_address_.isNull () && BoundState == state ()
#if QT_VERSION >= 0x050600
&& multicast_group_address_.isMulticast ()
#endif
)
if (BoundState == state () && is_multicast_address (multicast_group_address_))
{
for (auto const& interface : QNetworkInterface::allInterfaces ())
for (auto const& if_name : network_interfaces_)
{
if (QNetworkInterface::CanMulticast & interface.flags ())
{
leaveMulticastGroup (multicast_group_address_, interface);
}
leaveMulticastGroup (multicast_group_address_, QNetworkInterface::interfaceFromName (if_name));
}
}
}
void MessageServer::impl::join_multicast_group ()
{
if (BoundState == state ()
&& !multicast_group_address_.isNull ()
#if QT_VERSION >= 0x050600
&& multicast_group_address_.isMulticast ()
#endif
)
if (BoundState == state () && is_multicast_address (multicast_group_address_))
{
auto mcast_iface = multicastInterface ();
if (IPv4Protocol == multicast_group_address_.protocol ()
&& IPv4Protocol != localAddress ().protocol ())
if (network_interfaces_.size ())
{
close ();
bind (QHostAddress::AnyIPv4, port_, bind_mode_);
}
bool joined {false};
for (auto const& interface : QNetworkInterface::allInterfaces ())
{
if (QNetworkInterface::CanMulticast & interface.flags ())
for (auto const& if_name : network_interfaces_)
{
// Windows requires outgoing interface to match
// interface to be joined while joining, at least for
// IPv4 it seems to
setMulticastInterface (interface);
joined |= joinMulticastGroup (multicast_group_address_, interface);
joinMulticastGroup (multicast_group_address_, QNetworkInterface::interfaceFromName (if_name));
}
}
if (!joined)
else
{
multicast_group_address_.clear ();
// find the loop-back interface and join on that
for (auto const& net_if : QNetworkInterface::allInterfaces ())
{
auto flags = QNetworkInterface::IsUp | QNetworkInterface::IsLoopBack | QNetworkInterface::CanMulticast;
if ((net_if.flags () & flags) == flags)
{
joinMulticastGroup (multicast_group_address_, net_if);
break;
}
}
}
setMulticastInterface (mcast_iface);
}
}
@ -448,27 +430,34 @@ MessageServer::MessageServer (QObject * parent, QString const& version, QString
{
}
void MessageServer::start (port_type port, QHostAddress const& multicast_group_address)
void MessageServer::start (port_type port, QHostAddress const& multicast_group_address
, QStringList const& network_interface_names)
{
if (port != m_->port_
|| multicast_group_address != m_->multicast_group_address_)
if (port != m_->localPort () || multicast_group_address != m_->multicast_group_address_)
{
m_->leave_multicast_group ();
if (impl::BoundState == m_->state ())
if (impl::UnconnectedState != m_->state ())
{
m_->close ();
}
m_->multicast_group_address_ = multicast_group_address;
auto address = m_->multicast_group_address_.isNull ()
|| impl::IPv4Protocol != m_->multicast_group_address_.protocol () ? QHostAddress::Any : QHostAddress::AnyIPv4;
if (port && m_->bind (address, port, m_->bind_mode_))
if (!(multicast_group_address.isNull () || is_multicast_address (multicast_group_address)))
{
m_->port_ = port;
m_->join_multicast_group ();
Q_EMIT error ("Invalid multicast group address");
}
else if (is_MAC_ambiguous_multicast_address (multicast_group_address))
{
Q_EMIT error ("MAC-ambiguous IPv4 multicast group address not supported");
}
else
{
m_->port_ = 0;
m_->multicast_group_address_ = multicast_group_address;
m_->network_interfaces_ = network_interface_names;
QHostAddress local_addr {is_multicast_address (multicast_group_address)
&& impl::IPv4Protocol == multicast_group_address.protocol () ? QHostAddress::AnyIPv4 : QHostAddress::Any};
if (port && m_->bind (local_addr, port, m_->bind_mode_))
{
m_->join_multicast_group ();
}
}
}
}

View File

@ -2,6 +2,8 @@
#define MESSAGE_SERVER_HPP__
#include <QObject>
#include <QString>
#include <QStringList>
#include <QTime>
#include <QDateTime>
#include <QHostAddress>
@ -38,8 +40,9 @@ public:
// start or restart the server, if the multicast_group_address
// argument is given it is assumed to be a multicast group address
// which the server will join
Q_SLOT void start (port_type port,
QHostAddress const& multicast_group_address = QHostAddress {});
Q_SLOT void start (port_type port
, QHostAddress const& multicast_group_address = QHostAddress {}
, QStringList const& network_interface_names = QStringList {});
// ask the client to clear one or both of the decode windows
Q_SLOT void clear_decodes (QString const& id, quint8 window = 0);

View File

@ -20,6 +20,9 @@
#include <QCoreApplication>
#include <QCommandLineParser>
#include <QCommandLineOption>
#include <QString>
#include <QStringList>
#include <QDateTime>
#include <QTime>
#include <QHash>
@ -144,7 +147,7 @@ class Server
Q_OBJECT
public:
Server (port_type port, QHostAddress const& multicast_group)
Server (port_type port, QHostAddress const& multicast_group, QStringList const& network_interface_names)
: server_ {new MessageServer {this}}
{
// connect up server
@ -154,7 +157,7 @@ public:
connect (server_, &MessageServer::client_opened, this, &Server::add_client);
connect (server_, &MessageServer::client_closed, this, &Server::remove_client);
server_->start (port, multicast_group);
server_->start (port, multicast_group, network_interface_names);
}
private:
@ -232,9 +235,19 @@ int main (int argc, char * argv[])
app.translate ("UDPDaemon", "GROUP"));
parser.addOption (multicast_addr_option);
QCommandLineOption network_interface_option (QStringList {"i", "network-interface"},
app.translate ("UDPDaemon",
"Where <INTERFACE> is the network interface name to join on.\n"
"This option can be passed more than once to specify multiple network interfaces\n"
"The default is use just the loop back interface."),
app.translate ("UDPDaemon", "INTERFACE"));
parser.addOption (network_interface_option);
parser.process (app);
Server server {static_cast<port_type> (parser.value (port_option).toUInt ()), QHostAddress {parser.value (multicast_addr_option)}};
Server server {static_cast<port_type> (parser.value (port_option).toUInt ())
, QHostAddress {parser.value (multicast_addr_option).trimmed ()}
, parser.values (network_interface_option)};
return app.exec ();
}

View File

@ -117,6 +117,32 @@ namespace std
}
#endif
inline
bool is_multicast_address (QHostAddress const& host_addr)
{
#if QT_VERSION >= 0x050600
return host_addr.isMulticast ();
#else
bool ok;
return (((host_addr.toIPv4Address (&ok) & 0xf0000000u) == 0xe0000000u) && ok)
|| host_addr.toIPv6Address ()[0] == 0xff;
#endif
}
inline
bool is_MAC_ambiguous_multicast_address (QHostAddress const& host_addr)
{
// sub-ranges 224.128.0.0/24, 225.0.0.0/24, 225.128.0.0/24,
// 226.0.0.0/24, 226.128.0.0/24, ..., 239.0.0.0/24, 239.128.0.0/24
// are not supported as they are inefficient due to ambiguous
// mappings to Ethernet MAC addresses. 224.0.0.0/24 alone is allowed
// from these ranges
bool ok;
auto ipv4 = host_addr.toIPv4Address (&ok);
return ok && !((ipv4 & 0xffffff00u) == 0xe0000000) && (ipv4 & 0xf07fff00) == 0xe0000000;
}
// Register some useful Qt types with QMetaType
Q_DECLARE_METATYPE (QHostAddress);

View File

@ -131,6 +131,80 @@ private:
QDateTime dt {QDate {2020, 8, 6}, QTime {14, 15, 22, 501}};
QCOMPARE (qt_truncate_date_time_to (dt, 7500), QDateTime (QDate (2020, 8, 6), QTime (14, 15, 22, 500)));
}
Q_SLOT void is_multicast_address_data ()
{
QTest::addColumn<QString> ("addr");
QTest::addColumn<bool> ("result");
QTest::newRow ("loopback") << "127.0.0.1" << false;
QTest::newRow ("looback IPv6") << "::1" << false;
QTest::newRow ("lowest-") << "223.255.255.255" << false;
QTest::newRow ("lowest") << "224.0.0.0" << true;
QTest::newRow ("lowest- IPv6") << "feff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" << false;
QTest::newRow ("lowest IPv6") << "ff00::" << true;
QTest::newRow ("highest") << "239.255.255.255" << true;
QTest::newRow ("highest+") << "240.0.0.0" << false;
QTest::newRow ("highest IPv6") << "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" << true;
}
Q_SLOT void is_multicast_address ()
{
QFETCH (QString, addr);
QFETCH (bool, result);
QCOMPARE (::is_multicast_address (QHostAddress {addr}), result);
}
Q_SLOT void is_MAC_ambiguous_multicast_address_data ()
{
QTest::addColumn<QString> ("addr");
QTest::addColumn<bool> ("result");
QTest::newRow ("loopback") << "127.0.0.1" << false;
QTest::newRow ("looback IPv6") << "::1" << false;
QTest::newRow ("lowest- R1") << "223.255.255.255" << false;
QTest::newRow ("lowest R1") << "224.0.0.0" << false;
QTest::newRow ("highest R1") << "224.0.0.255" << false;
QTest::newRow ("highest+ R1") << "224.0.1.0" << false;
QTest::newRow ("lowest- R1A") << "224.127.255.255" << false;
QTest::newRow ("lowest R1A") << "224.128.0.0" << true;
QTest::newRow ("highest R1A") << "224.128.0.255" << true;
QTest::newRow ("highest+ R1A") << "224.128.1.0" << false;
QTest::newRow ("lowest- R2") << "224.255.255.255" << false;
QTest::newRow ("lowest R2") << "225.0.0.0" << true;
QTest::newRow ("highest R2") << "225.0.0.255" << true;
QTest::newRow ("highest+ R2") << "225.0.1.0" << false;
QTest::newRow ("lowest- R2A") << "225.127.255.255" << false;
QTest::newRow ("lowest R2A") << "225.128.0.0" << true;
QTest::newRow ("highest R2A") << "225.128.0.255" << true;
QTest::newRow ("highest+ R2A") << "225.128.1.0" << false;
QTest::newRow ("lowest- R3") << "238.255.255.255" << false;
QTest::newRow ("lowest R3") << "239.0.0.0" << true;
QTest::newRow ("highest R3") << "239.0.0.255" << true;
QTest::newRow ("highest+ R3") << "239.0.1.0" << false;
QTest::newRow ("lowest- R3A") << "239.127.255.255" << false;
QTest::newRow ("lowest R3A") << "239.128.0.0" << true;
QTest::newRow ("highest R3A") << "239.128.0.255" << true;
QTest::newRow ("highest+ R3A") << "239.128.1.0" << false;
QTest::newRow ("lowest- IPv6") << "feff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" << false;
QTest::newRow ("lowest IPv6") << "ff00::" << false;
QTest::newRow ("highest") << "239.255.255.255" << false;
QTest::newRow ("highest+") << "240.0.0.0" << false;
QTest::newRow ("highest IPv6") << "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" << false;
}
Q_SLOT void is_MAC_ambiguous_multicast_address ()
{
QFETCH (QString, addr);
QFETCH (bool, result);
QCOMPARE (::is_MAC_ambiguous_multicast_address (QHostAddress {addr}), result);
}
};
QTEST_MAIN (TestQtHelpers);

View File

@ -417,6 +417,7 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple,
m_messageClient {new MessageClient {QApplication::applicationName (),
version (), revision (),
m_config.udp_server_name (), m_config.udp_server_port (),
m_config.udp_interface_name (), m_config.udp_TTL (),
this}},
m_psk_Reporter {&m_config, QString {"WSJT-X v" + version () + " " + m_revision}.simplified ()},
m_manual {&m_network_manager},
@ -785,6 +786,7 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple,
connect (&m_config, &Configuration::transceiver_failure, this, &MainWindow::handle_transceiver_failure);
connect (&m_config, &Configuration::udp_server_changed, m_messageClient, &MessageClient::set_server);
connect (&m_config, &Configuration::udp_server_port_changed, m_messageClient, &MessageClient::set_server_port);
connect (&m_config, &Configuration::udp_TTL_changed, m_messageClient, &MessageClient::set_TTL);
connect (&m_config, &Configuration::accept_udp_requests_changed, m_messageClient, &MessageClient::enable);
connect (&m_config, &Configuration::enumerating_audio_devices, [this] () {
showStatusMessage (tr ("Enumerating audio devices"));
@ -7835,7 +7837,7 @@ void MainWindow::networkError (QString const& e)
, MessageBox::Cancel))
{
// retry server lookup
m_messageClient->set_server (m_config.udp_server_name ());
m_messageClient->set_server (m_config.udp_server_name (), m_config.udp_interface_name ());
}
}