Сети для самых маленьких. Часть 12: DNS

Всё, что мы разбирали до сих пор, работало с адресами: 203.0.113.10. Люди адресами не пользуются, они набирают www.example.com. Кто-то должен перевести одно в другое, и это DNS — Domain Name System, RFC 1034 и 1035, 1987 год. DNS — первое, что делает браузер при открытии страницы, первое, что ломается при неправильной настройке сети, и самая большая распределённая база данных в мире: несколько сотен миллионов имён, ни одного центрального сервера, и ответ за десятки миллисекунд из любой точки планеты.

Как в прошлых частях, нас интересует не «DNS превращает имена в адреса», а как именно: кто хранит, кто спрашивает, что едет в пакете.


Дерево имён

Имя www.example.com. — путь в дереве от корня к листу, записанный справа налево:

                        . (корень)
          ┌───────────────┼───────────────┐
         com             org              ru
     ┌────┴────┐          │
  example   google     wikipedia
     │
    www
  • Корень — пустая метка, в полном имени обозначается точкой в конце: www.example.com.. Такое имя называется FQDN (fully qualified); без точки резолвер может попробовать дописать к имени домен поиска.
  • Первый уровень — TLD: com, org, ru, by и несколько сотен других.
  • Каждая метка до 63 байт, всё имя до 253; регистр не имеет значения: WWW.Example.COM и www.example.com — одно имя.

Дерево никто не хранит целиком. Оно разрезано на зоны — куски, за каждый из которых отвечает своя организация:

  • корневая зона — список TLD, её держит IANA, а обслуживают 13 «корневых серверов» a.root-servers.net … m.root-servers.net (на самом деле более полутора тысяч машин по всему миру с теми же 13 адресами, об этом ниже);
  • зона com. — список всех доменов второго уровня в .com, её держит регистратура Verisign;
  • зона example.com. — её держит владелец домена на своих серверах или у хостера.

Граница между зонами — делегирование: в зоне com. нет записей про www.example.com, там есть только «за example.com отвечают серверы такие-то». Поэтому владелец домена может менять что угодно внутри своей зоны, не трогая регистратуру.


Три вида участников

В разрешении имени участвуют три разных программы, и их путают чаще всего.

Stub-резолвер — часть операционной системы. Сам ничего не ищет: берёт имя у приложения, отправляет запрос рекурсивному серверу из своих настроек и возвращает ответ. Настройки — /etc/resolv.conf в Linux (или systemd-resolved, который слушает на 127.0.0.53 и сам ходит дальше), адреса DNS из DHCP прошлой части или заданные руками в свойствах адаптера Windows. Перед запросом он смотрит в /etc/hosts: имя, найденное там, в сеть не уходит.

Рекурсивный сервер (резолвер) — тот, кто делает всю работу: ходит по дереву от корня вниз, пока не найдёт ответ, и кеширует всё, что узнал. Это DNS-сервер провайдера, 8.8.8.8 Google, 1.1.1.1 Cloudflare, или ваш домашний роутер, который сам ничего не ищет, а пересылает запросы провайдеру (такой режим называется forwarding).

Авторитативный сервер — хранит зону и отвечает только за неё. Ничего не ищет и не кеширует: что в зоне есть, то и отдаёт, с флагом «ответ авторитетный». У каждой зоны минимум два таких сервера — primary, на котором зону редактируют, и secondary, которые забирают с него копию (зонная передача, AXFR целиком или IXFR изменениями, по TCP) и отвечают наравне.


Путь запроса

Браузер хочет www.example.com. Кеш рекурсивного сервера пуст:

 stub                 рекурсивный сервер R            корневой      com.          example.com.
  │                          │                            │            │               │
  ├── A? www.example.com ───►│                            │            │               │
  │  (RD = 1, «сделай всё»)  │                            │            │               │
  │                          ├── A? www.example.com ─────►│            │               │
  │                          │◄─ «не знаю, но com. — вот ─┤            │               │
  │                          │   NS a.gtld-servers.net    │            │               │
  │                          │   + их адреса (glue)»      │            │               │
  │                          ├── A? www.example.com ──────────────────►│               │
  │                          │◄── «example.com. — вот NS ──────────────┤               │
  │                          │    ns1.example.com + glue»              │               │
  │                          ├── A? www.example.com ──────────────────────────────────►│
  │                          │◄── A 203.0.113.10, TTL 3600, AA = 1 ────────────────────┤
  │◄── A 203.0.113.10 ───────┤                                                         │
  │                          │  в кеш: NS com. (2 дня), NS example.com. (2 дня),       │
  │                          │         www.example.com A (1 час)                       │
  1. Stub шлёт запрос с флагом RD (recursion desired): «найди сам». Это рекурсивный запрос.
  2. R спрашивает корневой сервер. Корневой не знает www.example.com, но знает, кто отвечает за com., и присылает отсылку (referral): NS-записи серверов com. и их адреса. Это итеративный запрос: «скажи, что знаешь, дальше я сам».
  3. R спрашивает сервер com.. Тот отсылает к серверам example.com..
  4. R спрашивает ns1.example.com и получает авторитетный ответ с флагом AA.
  5. R отдаёт ответ stub-у и кладёт в кеш каждый шаг со своим TTL.

Следующий запрос про mail.example.com пойдёт сразу на шаг 4: серверы example.com уже в кеше. Запрос про www.google.com — на шаг 3: серверы com. в кеше. К корневым серверам хороший резолвер обращается раз в двое суток, и именно поэтому 13 адресов выдерживают нагрузку всей планеты. Остальное делает anycast: один адрес 198.41.0.4 анонсируется из сотен точек, и маршрутизация приводит вас к ближайшей.

Откуда R знает адреса корневых серверов, если спросить их не у кого? Из файла, вшитого в сервер (root hints), и это единственное, что нужно настроить руками.

Делегирование и glue

На шаге 2 корневой сервер прислал не только имена серверов com., но и их адреса. Иначе R пришлось бы спрашивать «а какой адрес у a.gtld-servers.net?» — у кого? У серверов net., адреса которых тоже нужно узнать. Записи с адресами серверов, которые родительская зона хранит у себя вместе с NS-записями, называются glue, и без них делегирование внутрь своей же зоны (example.com обслуживается ns1.example.com) зациклилось бы. Когда у домена меняют NS-серверы, нужно поменять и glue у регистратора — об этом забывают, и домен продолжает отвечать со старых серверов, пока те живы.


Сообщение

DNS едет в UDP, порт 53, одно сообщение в одной датаграмме. Запрос и ответ одного формата:

 ┌───────────────────────────────┐
 │ Заголовок, 12 байт            │  ID, флаги, четыре счётчика
 ├───────────────────────────────┤
 │ Question                      │  что спрашивали: имя, тип, класс
 ├───────────────────────────────┤
 │ Answer                        │  записи-ответы
 ├───────────────────────────────┤
 │ Authority                     │  NS-записи зоны (в отсылках) или SOA (в отказах)
 ├───────────────────────────────┤
 │ Additional                    │  glue, OPT и прочее полезное
 └───────────────────────────────┘

Заголовок:

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
┌───────────────────────────────┐
│              ID               │  16 бит, выбирает клиент; ответ несёт тот же
├─┬───────┬─┬─┬─┬─┬─────┬───────┤
│Q│Opcode │A│T│R│R│  Z  │ RCODE │
│R│       │A│C│D│A│     │       │
├─┴───────┴─┴─┴─┴─┴─────┴───────┤
│            QDCOUNT            │  сколько вопросов (всегда 1)
│            ANCOUNT            │  записей в Answer
│            NSCOUNT            │  в Authority
│            ARCOUNT            │  в Additional
└───────────────────────────────┘
  • QR — 0 запрос, 1 ответ. AA — ответ авторитетный. TC — ответ обрезан, не влез в датаграмму. RD — клиент просит рекурсию. RA — сервер её умеет.
  • RCODE: 0 NOERROR, 2 SERVFAIL (сервер не смог: авторитативные не отвечают, ошибка DNSSEC), 3 NXDOMAIN (такого имени нет), 5 REFUSED (сервер отказался: вы не его клиент). Отдельный случай — NODATA: имя есть, но записей такого типа нет; это NOERROR с пустым Answer.

Имена кодируются не строкой, а цепочкой меток с длиной впереди: www.example.com — это 3 w w w 7 e x a m p l e 3 c o m 0. Ноль — корень. Чтобы не повторять одно и то же имя в каждой записи ответа, есть сжатие: указатель на место в сообщении, где это имя уже было.

Запись (RR, resource record) в секциях ответа — имя, тип, класс (всегда IN), TTL, длина, данные. Так выглядит ответ в tcpdump:

$ sudo tcpdump -i eth0 -n udp port 53
IP 192.168.1.10.40321 > 192.168.1.1.53: 12345+ A? www.example.com. (33)
IP 192.168.1.1.53 > 192.168.1.10.40321: 12345 1/0/0 A 203.0.113.10 (49)

12345 — ID, + — RD, A? — вопрос, 1/0/0 — по одной записи в Answer и ни одной в Authority и Additional.


Типы записей

Тип Что в данных Для чего
A IPv4-адрес имя → адрес; у имени может быть несколько, резолвер вернёт все, клиент возьмёт любую
AAAA IPv6-адрес то же для IPv6 (часть 13); браузер спрашивает A и AAAA одновременно
CNAME другое имя псевдоним: «www.example.com — это на самом деле example.hosting.net»; сервер, встретив CNAME, продолжает искать запрошенный тип у цели и возвращает всю цепочку. У имени с CNAME не может быть других записей, поэтому CNAME нельзя поставить на вершину зоны, где обязаны быть SOA и NS
NS имя сервера кто авторитативен за зону; стоит и в родительской зоне (делегирование), и в самой
SOA primary, почта админа, serial, refresh, retry, expire, minimum паспорт зоны: serial растёт при каждом изменении, по нему secondary понимают, что пора забрать копию; refresh/retry/expire — как часто проверять и сколько жить без primary; minimum — TTL отрицательных ответов
MX приоритет, имя сервера куда слать почту для домена: почтовый сервер отправителя (он говорит по SMTP — протоколу, которым серверы передают друг другу письма) спрашивает MX, берёт с наименьшим приоритетом, для него спрашивает A
PTR имя обратное разрешение: адрес → имя, через специальное дерево in-addr.arpa: для 203.0.113.10 спрашивают 10.113.0.203.in-addr.arpa (октеты задом наперёд, потому что дерево идёт от общего к частному). Почтовые серверы по PTR проверяют отправителей
TXT произвольный текст подтверждение владения доменом, политики почты: SPF (кто имеет право слать от имени домена), DKIM (ключи подписи писем)
SRV приоритет, вес, порт, имя где искать сервис: _ldap._tcp.example.com → сервер и порт, на котором отвечает каталог LDAP (протокол доступа к справочнику пользователей и компьютеров); так машины в домене Windows находят контроллеры
CAA удостоверяющий центр кому разрешено выпускать сертификаты для домена

TTL и кеширование

У каждой записи есть TTL в секундах — сколько её можно держать в кеше. Выставляет владелец зоны, а соблюдают все рекурсивные серверы и, частично, stub-ы и браузеры. Авторитативный ответ приходит с полным TTL, из кеша — с остатком, и в dig видно, как второй запрос показывает меньшее число.

Отсюда «распространение DNS», которое длится «до 48 часов»: ничего никуда не распространяется, просто кеши по миру досматривают старую запись до конца её TTL. Правильный порядок переезда сервера: за сутки до переезда снизить TTL записи до 5 минут, дождаться, пока старый TTL везде истечёт, поменять адрес, вернуть TTL.

Кешируются и отрицательные ответы: NXDOMAIN помнится столько, сколько сказано в поле minimum SOA зоны (обычно час). Поэтому «я только что создал запись, а её нет» — сначала спросили, потом создали, и резолвер час будет отвечать «нет».

Кешируются и ошибки на уровне stub: systemd-resolved и Windows держат свой кеш, resolvectl flush-caches и ipconfig /flushdns его чистят.


UDP, TCP, EDNS

Исторический лимит датаграммы DNS — 512 байт. Ответ, который не влезает, сервер обрезает, ставит флаг TC, и клиент повторяет запрос по TCP на тот же 53 порт: рукопожатие, запрос с двухбайтовой длиной впереди, ответ любого размера. TCP нужен и для зонной передачи.

Чтобы не ходить в TCP за каждым ответом с несколькими адресами и подписями, придумали EDNS(0): клиент добавляет в Additional псевдозапись OPT с размером датаграммы, который он готов принять. Сейчас принято объявлять 1232 байта — столько гарантированно пролезает без фрагментации через любой разумный MTU, а фрагментированные датаграммы, как мы знаем из шестой части, теряются и фильтруются. В OPT же живут расширения вроде подсети клиента (чтобы CDN дал ближайший адрес) и флаг DO для DNSSEC.

Что пропускать в файрволе: UDP и TCP на 53 в обе стороны. Правило «DNS — это UDP», закрывающее TCP, ломает большие ответы, DNSSEC и зонные передачи.


Делегирование на практике: dig +trace

Весь путь запроса можно пройти самому:

$ dig +trace www.example.com

.                       518400  IN  NS  a.root-servers.net.
.                       518400  IN  NS  b.root-servers.net.
...
;; Received 525 bytes from 192.168.1.1#53(192.168.1.1) in 2 ms

com.                    172800  IN  NS  a.gtld-servers.net.
com.                    172800  IN  NS  b.gtld-servers.net.
...
;; Received 1170 bytes from 198.41.0.4#53(a.root-servers.net) in 15 ms

example.com.            172800  IN  NS  ns1.example.com.
example.com.            172800  IN  NS  ns2.example.com.
;; Received 220 bytes from 192.5.6.30#53(a.gtld-servers.net) in 20 ms

www.example.com.        3600    IN  A   203.0.113.10
;; Received 60 bytes from 192.0.2.53#53(ns1.example.com) in 25 ms

Первый блок — корневые серверы из кеша вашего резолвера; второй — ответ корневого: отсылка к com.; третий — ответ com.: отсылка к example.com.; четвёртый — авторитетный ответ. Если на каком-то шаге отсылка ведёт на серверы, которые не отвечают или отвечают другим — вот где сломано делегирование.


Разное, что встретится

Домены поиска. Stub дописывает к коротким именам домены из search в resolv.conf или опции 119 DHCP: ping server → server.example.com. Правило ndots решает, пробовать ли сначала имя как есть; в Kubernetes с его ndots:5 каждый внешний запрос сначала проходит по четырём внутренним доменам и только потом идёт как есть, отсюда лишние запросы и странные задержки.

Split-horizon. Один и тот же www.example.com изнутри сети резолвится в 192.168.1.50, снаружи — в 203.0.113.10: внутренний DNS-сервер держит свою версию зоны. Нормальная практика, пока ноутбук не принёс домой кеш с внутренним адресом.

Round-robin. Несколько A-записей у одного имени — сервер перемешивает порядок в каждом ответе, и нагрузка распределяется. Не балансировка: мёртвый сервер из списка не исчезает.

Безопасность. Подделать ответ, зная ID (16 бит) и порт запроса, было легко, пока резолверы не начали брать случайный порт отправителя на каждый запрос: это отравление кеша, и после атаки Камински 2008 года так делают все. Настоящая защита — DNSSEC: владелец зоны подписывает записи закрытым ключом (записи RRSIG), публикует открытый (DNSKEY), а хеш открытого ключа кладёт в родительскую зону (DS) — так от корня до зоны строится цепочка доверия, и резолвер, проверяющий подписи, не примет подделку. DNSSEC защищает подлинность, но не приватность: запросы по-прежнему в открытом виде. Для приватности есть DoT — тот же DNS внутри TLS-соединения на порту 853, и DoH — DNS-сообщения как запросы HTTPS (протокола HTTP, по которому браузер получает страницы, внутри того же TLS) на порту 443, неотличимые от веб-трафика; и то и другое закрывает запросы от провайдера, но не от самого резолвера.


Смотрим руками

Главный инструмент — dig:

$ dig www.example.com

;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.example.com.               IN      A

;; ANSWER SECTION:
www.example.com.        3600    IN      A       203.0.113.10

;; Query time: 12 msec
;; SERVER: 192.168.1.1#53(192.168.1.1) (UDP)
;; MSG SIZE  rcvd: 60

Читаем: status: NOERROR; флаги qr rd ra — ответ, рекурсию просили, сервер умеет, aa нет — ответ из кеша или через рекурсию, не от авторитативного; EDNS с 1232; TTL 3600 — полный, значит, резолвер только что сходил за записью. Повторите через минуту — TTL станет 3540.

Полезные варианты:

$ dig @ns1.example.com www.example.com        # спросить конкретный сервер (ответ будет с aa)
$ dig example.com MX                          # другой тип
$ dig -x 203.0.113.10                         # PTR
$ dig +short www.example.com                  # только данные
$ dig +norecurse @192.168.1.1 www.example.com # есть ли в кеше: без RD сервер искать не пойдёт
$ dig example.com SOA                         # serial и TTL отрицательных ответов

Что настроено у stub-а: resolvectl status (systemd), cat /etc/resolv.conf, на Windows ipconfig /all и Resolve-DnsName www.example.com в PowerShell, которая показывает и цепочку CNAME.

И tcpdump на udp port 53, когда непонятно, уходит ли запрос вообще и откуда приходит ответ: половина «DNS не работает» — это запросы, уходящие не на тот сервер.


Что запомнить

  • Дерево имён разрезано на зоны; каждую обслуживают свои авторитативные серверы, родительская зона хранит делегирование (NS) и glue (адреса).
  • Три участника: stub в ОС просит рекурсию, рекурсивный сервер ходит по дереву от корня и кеширует, авторитативный отвечает только за свою зону.
  • Сообщение: 12-байтовый заголовок с ID и флагами (QR, AA, TC, RD, RA, RCODE), секции Question, Answer, Authority, Additional. NXDOMAIN — имени нет, NODATA — нет записей такого типа.
  • Записи: A/AAAA адреса, CNAME псевдоним (не на вершине зоны), NS и SOA — устройство зоны, MX почта, PTR обратное, TXT, SRV, CAA.
  • TTL правит кешем: «распространение» — это истечение старых записей; отрицательные ответы кешируются по SOA minimum.
  • UDP 53 с EDNS 1232, при TC — TCP 53; оба должны быть открыты. DNSSEC — подлинность, DoT/DoH — приватность.

На этом служебные протоколы IPv4 закончены. Дальше — IPv6: 128-битные адреса, их запись и типы, почему на интерфейсе всегда несколько адресов, что убрали из заголовка по сравнению с IPv4 и зачем, и почему там нет широковещания, фрагментации на роутерах и NAT.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.