Revision control

Copy as Markdown

// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "networktest.h"
#include <QDnsLookup>
#include <QHostAddress>
#include <QJsonDocument>
#include <iostream>
const QVersionNumber NetworkTest::m_version = QVersionNumber(1, 1);
static constexpr QLatin1StringView normalDomain(".test.qt-project.org");
QString NetworkTest::applicationName()
{
return "CiNetworkTest";
}
QString NetworkTest::packageName(const QString &extension)
{
static const QSysInfo info;
static const QString name = QString("%1-%2-%3-%4-v%5").arg(applicationName(),
info.productType(),
info.kernelType(),
info.buildCpuArchitecture(),
versionString());
return extension.isEmpty() ? name : name + "." + extension;
}
QString NetworkTest::versionString()
{
return version().toString();
}
NetworkTest::NetworkTest(const QString &fileName, bool warnOnly, bool showProgress, int timeout,
int dnsTimeout, int maxLosses, Verbosity verbosity)
: m_warnOnly(warnOnly),
m_timeout(timeout),
m_dnsTimeout(dnsTimeout),
m_maxLosses(maxLosses),
m_showProgress(showProgress),
m_verbosity(verbosity),
m_fileName(fileName)
{
QFile file(m_fileName);
const bool success = file.exists() && file.open(QIODevice::ReadOnly);
if (!success) {
qCritical() << "Couldn't read" << fileName << "=> not testing anything.";
QCoreApplication::quit();
return;
}
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
file.close();
m_array = doc.array();
}
static constexpr std::array<const char*, NetworkTest::verbosityCount> verbosityText {
"No output",
"Summary only",
"Summary and error messages",
"Summary, success and error messages"
};
QString NetworkTest::verbosityString(Verbosity verbosity)
{
static const QMetaEnum me = QMetaEnum::fromType<Verbosity>();
const int v = static_cast<int>(verbosity);
const QString key = me.valueToKey(v);
const QString text = verbosityText[v];
return QString("%1: %2 (%3)").arg(v).arg(key, text);
}
QStringList NetworkTest::verbosityStrings()
{
QStringList sl;
for (int i = 0; i < verbosityCount; ++i)
sl << verbosityString(static_cast<Verbosity>(i));
return sl;
}
bool NetworkTest::verbosityCheck(Verbosity verbosity) const
{
return static_cast<int>(m_verbosity) >= static_cast<int>(verbosity);
}
void NetworkTest::printNetworkConfig()
{
auto command = [](const QString &name, const QString &command, const QStringList &args)
{
QProcess p;
p.setProgram(command);
p.setArguments(args);
p.start();
p.waitForFinished();
qInfo() << name << p.readAll();
};
#ifdef Q_OS_LINUX
command("MAC Address", "ip", QStringList{"maddress"});
command("IP Address", "ip",QStringList{"addr", "show"});
command("Routing", "ip", QStringList{"route", "show"});
command("DNS", "cat", QStringList{"/etc/resolv.conf"});
#endif
#ifdef Q_OS_WINDOWS
command("MAC Address", "getmac", QStringList());
command("IP and DNS", "ipconfig", QStringList{"/all"});
command("Routing", "route", QStringList{"print"});
#endif
#ifdef Q_OS_MACOS
command("IP and MAC Address", "ifconfig", QStringList());
command("Routing", "netstat", QStringList{"-nr"});
command("DNS", "scutil", QStringList{"--dns"});
#endif
}
NetworkTest::Verbosity NetworkTest::toVerbosity(int verbosity, bool *ok)
{
const bool success = verbosity >= 0 && verbosity < verbosityCount;
if (ok)
*ok = success;
return success ? static_cast<Verbosity>(verbosity) : Verbosity::Summary;
}
QString domainName(const QString &input)
{
if (input.isEmpty())
return input;
if (input.endsWith(QLatin1Char('.'))) {
QString nodot = input;
nodot.chop(1);
return nodot;
}
return input + normalDomain;
}
QString reproductionCommand(QDnsLookup::Type type, const QString &domain)
{
const auto me = QMetaEnum::fromType<QDnsLookup::Type>();
const QString typeName = QString::fromLatin1(me.valueToKey(int(type)));
const QString fqdn = domainName(domain);
#ifdef Q_OS_WINDOWS
return QString("nslookup -type=%1 %2").arg(typeName, fqdn);
#else
return QString("dig %1 %2").arg(typeName, fqdn);
#endif
}
std::unique_ptr<QDnsLookup> lookupCommon(QDnsLookup::Type type, const QString &domain,
int dnsTimeout, int maxLosses)
{
const auto me = QMetaEnum::fromType<QDnsLookup::Type>();
// A timeout is treated as a presumably lost UDP packet and retried up to
// maxLosses times; ServerFailureError/ServerRefusedError are real answers
// from the server and are never retried.
for (int attempt = 0;; ++attempt) {
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
auto lookup = std::make_unique<QDnsLookup>(type, domainName(domain));
#else
auto lookup = std::make_unique<QDnsLookup>(
type, domainName(domain), QDnsLookup::Protocol::Standard, QHostAddress(), 53);
#endif
QEventLoop loop;
QObject::connect(lookup.get(), &QDnsLookup::finished, &loop, &QEventLoop::quit);
bool timeout = false;
QTimer::singleShot(dnsTimeout, &loop, [&] {
timeout = true;
loop.quit();
});
lookup->lookup();
loop.exec();
QDnsLookup::Error error = lookup->error();
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
if (timeout)
error = QDnsLookup::TimeoutError;
#endif
if (error == QDnsLookup::ServerFailureError || error == QDnsLookup::ServerRefusedError) {
const QString msg =
QString("Server refused or was unable to answer query; %1 type %3: %2")
.arg(domain, lookup->errorString(), QString(me.valueToKey(int(type))));
qCritical() << msg;
qCritical().noquote() << "Reproduce with:" << reproductionCommand(type, domainName(domain));
return { };
}
const bool timedOut =
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
timeout;
#else
error == QDnsLookup::TimeoutError;
#endif
if (timedOut) {
if (attempt < maxLosses) {
qWarning() << "Presumed UDP packet loss for" << domain << "type"
<< me.valueToKey(int(type)) << "- tolerating loss" << (attempt + 1)
<< "of" << maxLosses << "and retrying";
continue;
}
const QString msg = QString("Server refused or was unable to answer query; %1 type %3: %2")
.arg(domain, lookup->errorString(), QString(me.valueToKey(int(type))));
qCritical() << msg << "- exceeded tolerated packet loss of" << maxLosses;
qCritical().noquote() << "Reproduce with:" << reproductionCommand(type, domain);
return { };
}
return lookup;
}
}
QStringList NetworkTest::formatReply(const QDnsLookup *lookup) const
{
QStringList result;
QString domain = lookup->name();
auto shorter = [this](QString value) {
const QString &ending = normalDomain;
if (value.endsWith(ending))
value.chop(ending.size());
else
value += u'.';
return value;
};
for (const QDnsMailExchangeRecord &rr : lookup->mailExchangeRecords()) {
QString entry = QString("MX %1 %2").arg(rr.preference(), 5).arg(shorter(rr.exchange()));
if (rr.name() != domain)
entry = "MX unexpected label to " + rr.name();
result.append(std::move(entry));
}
for (const QDnsServiceRecord &rr : lookup->serviceRecords()) {
QString entry = QString("SRV %1 %2 %3 %4").arg(rr.priority(), 5).arg(rr.weight())
.arg(rr.port()).arg(shorter(rr.target()));
if (rr.name() != domain)
entry = "SRV unexpected label to " + rr.name();
result.append(std::move(entry));
}
auto addNameRecords = [&](QLatin1StringView rrtype, const QList<QDnsDomainNameRecord> &rrset) {
for (const QDnsDomainNameRecord &rr : rrset) {
QString entry = QString("%1 %2").arg(rrtype, shorter(rr.value()));
if (rr.name() != domain)
entry = rrtype + " unexpected label to " + rr.name();
result.append(std::move(entry));
}
};
addNameRecords(QLatin1StringView("NS"), lookup->nameServerRecords());
addNameRecords(QLatin1StringView("PTR"), lookup->pointerRecords());
addNameRecords(QLatin1StringView("CNAME"), lookup->canonicalNameRecords());
for (const QDnsHostAddressRecord &rr : lookup->hostAddressRecords()) {
if (rr.name() != domain)
continue; // A and AAAA may appear as extra records in the answer section
QHostAddress addr = rr.value();
result.append(QString("%1 %2")
.arg(addr.protocol() == QHostAddress::IPv6Protocol ? "AAAA" : "A",
addr.toString()));
}
for (const QDnsTextRecord &rr : lookup->textRecords()) {
QString entry = "TXT";
for (const QByteArray &data : rr.values()) {
entry += u' ';
entry += QDebug::toString(data);
}
result.append(std::move(entry));
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
for (const QDnsTlsAssociationRecord &rr : lookup->tlsAssociationRecords()) {
QString entry = QString("TLSA %1 %2 %3 %4").arg(int(rr.usage())).arg(int(rr.selector()))
.arg(int(rr.matchType())).arg(rr.value().toHex().toUpper());
if (rr.name() != domain)
entry = "TLSA unexpected label to " + rr.name();
result.append(std::move(entry));
}
#endif
result.sort();
return result;
}
int lastPercentage = -1;
void writeProgress(int count, int max)
{
static constexpr int barWidth = 70;
const float countF = count;
const float maxF = max;
const float progress = (count == max - 1) ? 1.0 : countF / maxF;
const int percentage = progress * 100;
if (percentage == lastPercentage)
return;
lastPercentage = percentage;
std::cout << "[";
const int pos = barWidth * progress;
for (int i = 0; i < barWidth; ++i) {
if (i < pos)
std::cout << "=";
else if (i == pos)
std::cout << ">";
else
std::cout << " ";
}
std::cout << "] " << percentage << " %\r";
std::cout.flush();
if (progress == 1.0) {
lastPercentage = -1;
std::cout << "\n";
}
}
#define ERROR if (verbosityCheck(Verbosity::Error)) qCritical()
#define WARNING if (verbosityCheck(Verbosity::Error)) qWarning()
#define SUCCESS if (verbosityCheck(Verbosity::All)) qInfo()
bool NetworkTest::test()
{
int errors = 0;
int ignoredRecords = 0;
const QTime started = QTime::currentTime();
if (verbosityCheck(Verbosity::Summary)) {
qInfo() << "Starting network test at" << started.toString()
<< "QT_VERSION:" << QT_VERSION_STR;
qInfo() << "WarnOnly:" << m_warnOnly;
if (m_timeout > 0)
qInfo() << "Timeout after" << m_timeout << "milliseconds";
else
qInfo() << "Never time out";
qInfo() << "DNS lookup timeout:" << m_dnsTimeout << "milliseconds";
qInfo() << "Tolerated UDP packet losses per lookup:" << m_maxLosses;
qInfo().noquote() << "Verbosity:" << verbosityString(m_verbosity);
QString progress = QString("Show progress: %1").arg(m_showProgress ? "true" : "false");
if (m_verbosity != Verbosity::Summary && m_showProgress)
progress += QString("(ignored due to verbosity != 1)");
qInfo().noquote() << progress;
}
if (m_array.isEmpty()) {
if (verbosityCheck(Verbosity::Error))
qCritical().noquote() << "Nothing to test! Check" << m_fileName;
++errors;
}
const auto me = QMetaEnum::fromType<QDnsLookup::Type>();
const int count = m_array.count();
const bool showProgress = m_verbosity == Verbosity::Summary && m_showProgress;
for (int i = 0; i < count; ++i) {
if (showProgress)
writeProgress(i, count);
if (!m_array.at(i).isObject()) {
ERROR << "JSON format error in input file, array position" << i;
++errors;
continue;
}
const QJsonObject obj = m_array.at(i).toObject();
const QByteArray typeBa = obj.value("Type").toString().toLatin1();
bool typeOk;
const int typeInt = me.keyToValue(typeBa, &typeOk);
if (!typeOk) {
WARNING.noquote() << "Ignoring record with type" << typeBa;
++ignoredRecords;
continue;
}
const auto type = static_cast<QDnsLookup::Type>(typeInt);
const QString domain = obj.value("Domain").toString();
const QString expected = obj.value("Expected").toString();
std::unique_ptr<QDnsLookup> lookup = lookupCommon(type, domain, m_dnsTimeout, m_maxLosses);
if (!lookup) {
ERROR << "Failed to create QDnsLookup object. Aborting.";
++errors;
break;
}
if (lookup->error() != QDnsLookup::NoError) {
ERROR << "DNS Lookup error" << lookup->error() << lookup->errorString();
ERROR.noquote() << "Reproduce with:" << reproductionCommand(type, domain);
++errors;
}
QString result = formatReply(lookup.get()).join(u';');
if (result == expected) {
SUCCESS << "Succeeded:" << domain << "-->" << result;
} else {
ERROR << "Expected" << expected << "and got" << result << "for" << domain;
ERROR.noquote() << "Reproduce with:" << reproductionCommand(type, domain);
++errors;
}
}
const QTime finished = QTime::currentTime();
const int duration = started.msecsTo(finished);
if (m_timeout > 0 && m_timeout < duration) {
ERROR << "Duration of" << duration << "exceeded timeout limit of" << m_timeout;
++errors;
}
if (verbosityCheck(Verbosity::Summary)) {
qInfo() << "Network test finished at" << finished.toString()
<< "Total milliseconds consumed:" << started.msecsTo(finished);
qInfo() << "Processed" << count << "records," << ignoredRecords << "ignored.";
qInfo() << errors << "error(s) occurred";
}
return (errors == 0) || m_warnOnly ;
}
#undef ERROR
#undef WARNING
#undef SUCCESS