golang

QNAME minimisation на практике: RFC 7816, реализация, грабли

  • воскресенье, 24 мая 2026 г. в 00:00:38
https://habr.com/ru/articles/1035648/

Когда вы открываете mail.google.com, ваш рекурсивный резолвер делает три-четыре шага: спрашивает root, потом TLD, потом authoritative для google.com, иногда ещё один уровень. Десятилетиями каждому из этих серверов отправлялся один и тот же вопрос целиком: «дай мне mail.google.com». Root-серверу, который понятия не имеет про google. TLD-серверу, который умеет только делегации com.. Каждый из них видел всю строку, хотя для своей работы нуждался в одной метке.

В 2016 году Стефан Бортцмайер написал RFC 7816 и сказал: ребята, это странно. Давайте резолвер будет спрашивать ровно столько, сколько нужно для следующего хопа. Идея простая до неприличия. И с этого момента началось десять лет внедрения.

Что такое QNAME minimisation

Стандартный recursive resolver работает так. Юзер спрашивает mail.google.com. Резолвер идёт к a.root-servers.net, отправляет QUERY: mail.google.com A. Root отвечает: я не знаю, спроси у gtld-servers.net (referral на NS для com.). Резолвер идёт к gtld-servers.net, отправляет QUERY: mail.google.com A. Тот отвечает: спроси у ns1.google.com. И только authoritative от Google выдаёт реальный IP.

Заметьте: каждый сервер на пути получил полное имя mail.google.com. Включая root. Root, который физически не может ответить ничего полезного про конкретный хост, всё равно знает, что вы туда лезете. Это утечка ради ничего.

С QNAME minimisation резолвер играет в более вежливую игру. Root спрашивают: «кто авторитетен за com.?». Root отвечает делегацией. TLD спрашивают: «кто авторитетен за google.com.?». TLD отвечает. И только authoritative-сервер Google узнаёт, что вы хотели именно mail. Каждый уровень получает минимум, необходимый ему для делегации, и ни байтом больше.

Почему это десять лет внедряли

RFC 7816 вышел в марте 2016 года со статусом experimental. Многие операторы резолверов отказывались включать фичу: были подозрения, что часть authoritative-серверов поломается на промежуточных запросах. Если authoritative неправильно реализует обработку qtype=NS для промежуточной метки, резолвер мог получить NXDOMAIN там, где должен был получить NODATA, и зарезолвить ничего.

Подозрения оказались частично правдой. В 2016-2018 регулярно ловили на этом старые версии BIND, экзотические enterprise-DNS, кастомные authoritative. Поэтому feature внедряли через флаг, а флаг по умолчанию ставили в off. Unbound включил по умолчанию в 2019. BIND позже. Все public resolvers (Cloudflare, Google, Quad9) подтянулись постепенно к 2020-2021.

В 2021 вышел RFC 9156, уже standards-track. К нему отношение было другое: индустрия согласилась, что QNAME minimisation это нормальная практика, а не эксперимент. К 2026 году не поддерживать его примерно как не поддерживать TLS 1.2: формально можно, но смотрят косо.

Зачем это вообще

QNAME minimisation это не security, это privacy. Никаких атак он не предотвращает напрямую. Если кто-то хочет отравить ваш кеш, его не остановит то, что root-сервер видит на одну метку меньше. Каминскому 17 лет, а сюрпризов меньше не стало, и уязвимости в DNS живут параллельным миром.

Что действительно меняется: passive DNS surveillance становится бесполезнее. Root-операторы (а их немного, и список публичный) видят только TLD-уровень запросов. TLD-операторы видят SLD. Цепочка наблюдения дробится. Никто, кроме authoritative и собственно резолвера, не знает финальное имя.

Для VPN-юзеров и Tor-юзеров это критично. Если вы подняли свой резолвер за VPN, то trafic к root-серверам идёт от вас же, и без minimisation root видит ваш трафик в полном объёме. С minimisation только зональный уровень.

Для обычного юзера, который сидит на провайдерском DNS, тоже не лишнее. Чем меньше посредников знают полную историю, тем лучше.

Реализация в Go на miekg/dns

Я пишу edge на github.com/miekg/dns. Логика minimisation поверх библиотеки выглядит так. Берём qname, разбиваем на метки. Идём от корня вниз, на каждом уровне спрашиваем NS у текущего authoritative для следующей метки. На последнем шаге спрашиваем уже реальный qtype.

func (r *Resolver) resolveMinimized(qname string, qtype uint16) (*dns.Msg, error) { qname = dns.Fqdn(qname) labels := dns.SplitDomainName(qname)

cur := "."
nameservers := r.rootNS()

for i := len(labels) - 1; i >= 0; i-- {
    cur = labels[i] + "." + cur

    if i == 0 {
        return r.queryAt(nameservers, cur, qtype)
    }

    resp, err := r.queryAt(nameservers, cur, dns.TypeNS)
    if err != nil {
        return nil, err
    }

    next, err := r.extractDelegation(resp)
    if err != nil {
        // fallback на полный qname если NS не пришёл
        return r.queryAt(nameservers, qname, qtype)
    }
    nameservers = next
}

return nil, errors.New("unreachable")

}

Это упрощённый скелет. В реальности есть кеш на каждом уровне, glue records, IPv4/IPv6 для NS, таймауты. Но идея ровно такая: цикл по меткам, на промежуточных шагах qtype=NS, на финальном реальный qtype.

Грабли

Реальность, как водится, побила меня палкой по голове. Грабли такие.

Корявые authoritative-серверы. RFC говорит: на промежуточный qtype=NS для метки, которой не существует как зоны, отвечай NODATA (пустой ANSWER, NOERROR), потому что это нормально — может, это просто узел в DNS-дереве без своей делегации. Старые серверы вместо NODATA отвечают NXDOMAIN. И резолвер, который верит NXDOMAIN, делает вывод: домена не существует. Пользователь видит ошибку, хотя mail.example.com прекрасно живёт.

Решение: при NXDOMAIN на промежуточной метке делаем fallback на полный qname. Не идеально, потому что privacy теряется именно для проблемных доменов, но альтернатива (ломать резолюцию) хуже.

CNAME-цепочки. Если на пути встретился CNAME, qname меняется, и minimisation надо начинать заново для нового имени. Это правильное поведение по RFC, но в коде добавляет уровень рекурсии. У меня в edge стоит ограничение в 8 CNAME-хопов: больше, return SERVFAIL. Без флагов тестирую на себе, что довольно унизительно: сидишь и смотришь, как резолвер уходит в петлю на криво настроенной зоне.

Wildcard-зоны. Запись *.example.com подходит на любой sublabel. QNAME minimisation спрашивает «кто authoritative для wat.example.com?», authoritative отвечает referral на самого себя или ничего, и резолвер должен корректно понять, что финальный хоп идёт на этом же сервере. Не везде это работает гладко.

Performance. На холодном кеше QNAME minimisation = 3-4 NS-запроса вместо 1-2. Хопов больше, latency выше. На горячем никакой разницы, потому что промежуточные NS закешированы. Но первый юзер, который запросит редкий домен, заплатит лишние миллисекунды.

Бенчмарки

Замерил на своём edge в Хельсинки, кеш холодный (рестарт перед каждым прогоном).

Cold resolve mail.google.com:
  без QNAME min:    35 ms
  с QNAME min:      52 ms

Cold resolve gerrit.googlesource.com:
  без QNAME min:    41 ms
  с QNAME min:      68 ms

Cold resolve www.example.org:
  без QNAME min:    28 ms
  с QNAME min:      44 ms

17-27 ms лишних. Это цена privacy на холодном кеше.

На горячем кеше 0 ms разницы, потому что и в том, и в другом режиме мы попадаем в кеш на финальном уровне. С учётом что 90%+ запросов идёт по горячему кешу, амортизированная стоимость minimisation около нуля.

Что включено по умолчанию

В VantageDNS QNAME minimisation включён по умолчанию для всех планов. Free, Plus, неважно. Можно отключить через профиль (qname_minimisation: false в API), но я не вижу причин, кроме «у меня корпоративный AD с пятнадцатилетним BIND и оно не резолвится». В таких случаях обычно достаточно отключить minimisation для конкретного домена, не для всего профиля.

Сравнение по состоянию на 2026:

NextDNS         enabled by default
AdGuard DNS     enabled by default
Cloudflare      enabled by default
Quad9           enabled by default
Google 8.8.8.8  enabled by default
VantageDNS      enabled by default

Все нормальные публичные резолверы поддерживают. Кто не поддерживает в 2026, у того явно есть и более интересные проблемы.

Тестирование

Стефан Бортцмайер же и сделал тестовый домен, чтобы проверить, работает ли minimisation у вашего резолвера.

dig +short txt qnamemintest.internet.nl

Если ответ:

"HOORAY - QNAME minimisation is enabled"

Работает. Если:

"NO - QNAME minimisation is NOT enabled"

Не работает. Удобно, прямолинейно, без церемоний.

Я гоняю это в CI после каждого изменения резолвера. Если внезапно вернулось NO, значит, что-то сломал.

Чего QNAME minimisation не решает

Чтобы не было иллюзий.

Authoritative-сервер всё равно знает полный qname. Это неизбежно: чтобы вернуть правильную запись, ему нужно полное имя. Privacy достигается только относительно промежуточных серверов (root, TLD, иногда зональные родители).

ECS leakage. Если ваш резолвер отправляет EDNS Client Subnet (ECS), authoritative узнает не только полный qname, но и вашу подсеть /24. Minimisation тут не помогает. У меня ECS отключён глобально, потому что для рекурсивного резолвера общего пользования это утечка ради CDN-оптимизации, и приоритет приватности выше.

TCP fingerprinting. Резолвер делает TCP- или TLS-handshake до authoritative. JA3-fingerprint наблюдаем. Если кому-то очень нужно отслеживать ваш резолвер в сети, minimisation от этого не защитит.

Логи самого резолвера. Если резолвер логирует запросы, он знает всё. Поэтому privacy резолвера = privacy политика резолвера + minimisation, а не одно minimisation в вакууме. У нас в ClickHouse пишется только metadata (qname, qtype, ts, action), retention 24 часа на free, до 30 дней на платных, и это enforce’ится TTL. Содержимое ответов не пишется никогда.

Что с этим делать

QNAME minimisation в 2026 это baseline, а не feature. Им не хвастаются, его ожидают. Если ваш DNS-провайдер до сих пор без него, это сигнал посмотреть, что у него ещё отсутствует. А если вы держите свой резолвер, прогоните qnamemintest.internet.nl. Десять минут на чек, и вы знаете, что не отдаёте root-операторам полный список ваших визитов.

Ссылки