diff --git a/Configuration.cpp b/Configuration.cpp index f244ecd9f..fac5564ab 100644 --- a/Configuration.cpp +++ b/Configuration.cpp @@ -163,6 +163,9 @@ #include #include #include +#include +#include +#include #include #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 (); 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) { diff --git a/Configuration.hpp b/Configuration.hpp index 8594a4a6f..403673663 100644 --- a/Configuration.hpp +++ b/Configuration.hpp @@ -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 diff --git a/Configuration.ui b/Configuration.ui index fbd53c422..fd8fb8955 100644 --- a/Configuration.ui +++ b/Configuration.ui @@ -7,7 +7,7 @@ 0 0 554 - 556 + 560 @@ -1864,12 +1864,6 @@ and DX Grid fields when a 73 or free text message is sent. - - - 0 - 0 - - <html><head/><body><p>Optional hostname of network service to receive decodes.</p><p>Formats:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">hostname</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">IPv4 address</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">IPv6 address</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">IPv4 multicast group address</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">IPv6 multicast group address</li></ul><p>Clearing this field will disable the broadcasting of UDP status updates.</p></body></html> @@ -1898,6 +1892,39 @@ and DX Grid fields when a 73 or free text message is sent. + + + + Outgoing interface: + + + udp_interface_combo_box + + + + + + + + + + 255 + + + 1 + + + + + + + Multicast TTL: + + + udp_TTL_spin_box + + + @@ -3084,6 +3111,8 @@ Right click for insert and delete options. psk_reporter_tcpip_check_box udp_server_line_edit udp_server_port_spin_box + udp_interface_combo_box + udp_TTL_spin_box accept_udp_requests_check_box udpWindowToFront udpWindowRestore @@ -3101,8 +3130,8 @@ Right click for insert and delete options. include_WAE_check_box rescan_log_push_button LotW_CSV_URL_line_edit - LotW_days_since_upload_spin_box LotW_CSV_fetch_push_button + LotW_days_since_upload_spin_box sbNtrials sbAggressive cbTwoPass @@ -3118,11 +3147,11 @@ Right click for insert and delete options. rbHound rbNA_VHF_Contest rbField_Day - Field_Day_Exchange rbEU_VHF_Contest rbRTTY_Roundup - RTTY_Exchange rbWW_DIGI + Field_Day_Exchange + RTTY_Exchange @@ -3192,13 +3221,13 @@ Right click for insert and delete options. - - - - - - + + + + + + diff --git a/Network/MessageClient.cpp b/Network/MessageClient.cpp index 3c208fd3f..57ffb2806 100644 --- a/Network/MessageClient.cpp +++ b/Network/MessageClient.cpp @@ -6,16 +6,16 @@ #include #include +#include #include #include #include #include -#include #include #include #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 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_}; diff --git a/Network/MessageClient.hpp b/Network/MessageClient.hpp index afe361fab..d0066dbe4 100644 --- a/Network/MessageClient.hpp +++ b/Network/MessageClient.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #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); diff --git a/UDPExamples/MessageServer.cpp b/UDPExamples/MessageServer.cpp index 42a9689ef..fc36aad9d 100644 --- a/UDPExamples/MessageServer.cpp +++ b/UDPExamples/MessageServer.cpp @@ -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 (); + } } } } diff --git a/UDPExamples/MessageServer.hpp b/UDPExamples/MessageServer.hpp index 184410117..449c70f4b 100644 --- a/UDPExamples/MessageServer.hpp +++ b/UDPExamples/MessageServer.hpp @@ -2,6 +2,8 @@ #define MESSAGE_SERVER_HPP__ #include +#include +#include #include #include #include @@ -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); diff --git a/UDPExamples/UDPDaemon.cpp b/UDPExamples/UDPDaemon.cpp index be1b5edeb..2668a6021 100644 --- a/UDPExamples/UDPDaemon.cpp +++ b/UDPExamples/UDPDaemon.cpp @@ -20,6 +20,9 @@ #include #include +#include +#include +#include #include #include #include @@ -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 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 (parser.value (port_option).toUInt ()), QHostAddress {parser.value (multicast_addr_option)}}; + Server server {static_cast (parser.value (port_option).toUInt ()) + , QHostAddress {parser.value (multicast_addr_option).trimmed ()} + , parser.values (network_interface_option)}; return app.exec (); } diff --git a/qt_helpers.hpp b/qt_helpers.hpp index aae8c5f07..7170bda0b 100644 --- a/qt_helpers.hpp +++ b/qt_helpers.hpp @@ -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); diff --git a/tests/test_qt_helpers.cpp b/tests/test_qt_helpers.cpp index cb6744df2..195c16e0d 100644 --- a/tests/test_qt_helpers.cpp +++ b/tests/test_qt_helpers.cpp @@ -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 ("addr"); + QTest::addColumn ("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 ("addr"); + QTest::addColumn ("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); diff --git a/widgets/mainwindow.cpp b/widgets/mainwindow.cpp index be5062fa0..e40009cb4 100644 --- a/widgets/mainwindow.cpp +++ b/widgets/mainwindow.cpp @@ -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 ()); } }