Работа с сетью в Qt

QDnsLookup: программный DNS-резолвинг в Qt-приложении

Введение

В большинстве случаев Qt резолвит доменные имена автоматически и незаметно — например, при вызове QTcpSocket::connectToHost() с именем хоста вместо IP-адреса. Но иногда нужен явный контроль над DNS-резолвингом: получить все IP-адреса домена, проверить MX или TXT-записи, реализовать собственную балансировку между несколькими адресами. Для этого в Qt есть класс QDnsLookup, который выполняет асинхронные DNS-запросы напрямую.

Концепция

QDnsLookup поддерживает основные типы записей: A/AAAA (адреса), MX (почтовые серверы), TXT, CNAME, NS, PTR (обратный резолвинг). Запрос выполняется асинхронно: задаются тип записи через setType() и имя через setName(), после чего вызывается lookup(); результат приходит в сигнале finished(), а сами данные читаются через методы вроде hostAddressRecords() или mailExchangeRecords() в зависимости от типа запроса.

В отличие от блокирующих функций типа gethostbyname, QDnsLookup не блокирует поток выполнения и интегрируется в event loop приложения, что делает его уместным даже в однопоточных GUI-приложениях.

Пример кода: QDnsLookup

#include <QCoreApplication>
#include <QDnsLookup>
#include <QDebug>

class DnsResolver : public QObject
{
    Q_OBJECT
public:
    explicit DnsResolver(QObject *parent = nullptr) : QObject(parent) {}

    void resolveAddresses(const QString &hostname)
    {
        auto *lookup = new QDnsLookup(QDnsLookup::A, hostname, this);
        connect(lookup, &QDnsLookup::finished, this, [this, lookup]() {
            onAddressLookupFinished(lookup);
        });
        lookup->lookup();
    }

    void resolveMxRecords(const QString &domain)
    {
        auto *lookup = new QDnsLookup(QDnsLookup::MX, domain, this);
        connect(lookup, &QDnsLookup::finished, this, [this, lookup]() {
            onMxLookupFinished(lookup);
        });
        lookup->lookup();
    }

private:
    void onAddressLookupFinished(QDnsLookup *lookup)
    {
        lookup->deleteLater();
        if (lookup->error() != QDnsLookup::NoError) {
            qWarning() << "Ошибка DNS-резолвинга:" << lookup->errorString();
            return;
        }
        for (const QDnsHostAddressRecord &record : lookup->hostAddressRecords()) {
            qDebug() << "IP-адрес:" << record.value().toString()
                      << "TTL:" << record.timeToLive();
        }
    }

    void onMxLookupFinished(QDnsLookup *lookup)
    {
        lookup->deleteLater();
        if (lookup->error() != QDnsLookup::NoError) {
            qWarning() << "Ошибка резолвинга MX:" << lookup->errorString();
            return;
        }
        for (const QDnsMailExchangeRecord &record : lookup->mailExchangeRecords()) {
            qDebug() << "Почтовый сервер:" << record.exchange()
                      << "приоритет:" << record.preference();
        }
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    DnsResolver resolver;
    resolver.resolveAddresses("example.com");
    resolver.resolveMxRecords("example.com");
    return app.exec();
}

#include "main.moc"

Пояснения к коду

Каждый запрос создаётся как отдельный объект QDnsLookup с указанным типом записи и именем — конструктор сразу принимает оба параметра для удобства. После вызова lookup() остаётся ждать сигнал finished(), в обработчике которого важно сначала проверить error(), а затем читать результаты соответствующим геттером — hostAddressRecords() для A-записей, mailExchangeRecords() для MX и так далее по аналогии для остальных типов.

Объект lookup создаётся с this как родителем, но всё равно явно удаляется через deleteLater() в обработчике — это страховка против накопления объектов, если резолвер используется многократно в течение жизни приложения.

Подводные камни

  • TTL записей не учитывается автоматически. Если приложение кеширует результат резолвинга самостоятельно (а не полагается на ОС), нужно вручную отслеживать timeToLive() и повторно резолвить адрес по истечении TTL — иначе можно работать с устаревшим IP после смены инфраструктуры на сервере.
  • DNS-резолвинг может занимать заметное время при недоступном или медленном DNS-сервере — стоит предусмотреть таймаут на уровне приложения (Qt не предоставляет встроенный таймаут для QDnsLookup, нужно реализовывать через QTimer вручную).
  • Несколько A-записей для одного домена — обычная ситуация для балансировки нагрузки; код, рассчитывающий на единственный адрес (hostAddressRecords().first()), может постоянно использовать только первый сервер в списке и не давать отказоустойчивости.
  • На некоторых платформах поведение QDnsLookup зависит от системного резолвера — в редких случаях (например, при специфичных корпоративных DNS-настройках через VPN) результат может отличаться от nslookup/dig в терминале, что усложняет диагностику.