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

За восемь частей мы научились доставлять пакет от машины к машине: Ethernet до соседа, IP через роутеры, ARP для перевода адресов и ICMP для сообщений об ошибках. Пакет пришёл на машину с нужным IP-адресом. А дальше кому? На машине десятки программ, и IP ничего о них не знает: в его заголовке есть поле Protocol, но оно скажет только «внутри UDP» или «внутри TCP».

Транспортный уровень решает ровно эту задачу — доставить данные не машине, а процессу на ней, — и, если нужно, добавляет то, чего у IP нет: надёжность, порядок, управление скоростью. В интернете два транспорта. Начнём с того, который добавляет к IP самый минимум: UDP, User Datagram Protocol, RFC 768. Весь стандарт — две страницы 1980 года.


Порты

Адрес процесса в транспортном уровне — порт, 16-битное число от 0 до 65535. Процесс, который хочет получать данные, просит у ОС порт и слушает его; отправитель указывает порт получателя в заголовке. Пара «IP-адрес, порт» однозначно задаёт точку на всей планете, куда можно отправить данные, и эта пара вместе с протоколом называется сокетом.

$ ss -ulnp
State   Recv-Q  Send-Q   Local Address:Port    Peer Address:Port  Process
UNCONN  0       0            127.0.0.53%lo:53         0.0.0.0:*      users:(("systemd-resolve",pid=600,fd=13))
UNCONN  0       0                  0.0.0.0:68         0.0.0.0:*      users:(("dhclient",pid=700,fd=6))
UNCONN  0       0                  0.0.0.0:123        0.0.0.0:*      users:(("ntpd",pid=800,fd=16))

Три процесса слушают UDP: резолвер DNS на порту 53, только на адресе 127.0.0.53; DHCP-клиент на 68 и демон времени на 123 — на 0.0.0.0, то есть на всех адресах машины. Когда приходит датаграмма, стек смотрит на порт назначения (и адрес назначения) и отдаёт данные тому сокету, который их слушает. Это последний шаг демультиплексирования из первой части: EtherType → Protocol → порт.

Диапазон поделен договорённостью:

Порты Название Что
0–1023 well-known закреплены за известными сервисами: 53 DNS, 67/68 DHCP, 123 NTP, 443 HTTPS. В Unix слушать их может только root
1024–49151 registered зарегистрированы за приложениями, но без привилегий: 3389 RDP, 5060 SIP, 4789 VXLAN
49152–65535 ephemeral временные: их ОС выдаёт отправителю, которому свой порт неважен, лишь бы ответ пришёл

Linux на практике берёт временные порты из 32768–60999 (net.ipv4.ip_local_port_range). Когда браузер открывает соединение или dig шлёт запрос, его порт отправителя — случайный из этого диапазона, а порт получателя — известный. Ответ придёт обратно на тот случайный порт, по нему стек найдёт нужный процесс.

Нумерация портов одна на протокол: UDP 53 и TCP 53 — разные сокеты, и их могут слушать разные процессы.


Заголовок

Восемь байт:

 байты:    2            2            2          2
        ┌────────────┬────────────┬──────────┬──────────┐
        │ Source     │ Destination│ Length   │ Checksum │   затем данные
        │ Port       │ Port       │          │          │
        └────────────┴────────────┴──────────┴──────────┘
  • Source Port — порт отправителя, куда слать ответ. Может быть 0, если ответа не ждут.
  • Destination Port — кому.
  • Length — длина заголовка и данных, от 8 до 65 535. Максимум данных в одной датаграмме — 65 507 байт (65 535 минус 20 байт IP и 8 UDP), но об этом ниже.
  • Checksum — контрольная сумма, и здесь есть тонкость. Она считается не только по заголовку и данным UDP, но и по псевдозаголовку: адресам отправителя и получателя, номеру протокола и длине из заголовка IP. Псевдозаголовок никуда не передаётся, он просто участвует в сумме. Смысл — поймать пакет, доставленный не той машине: если адрес назначения по дороге испортился, сумма не сойдётся, хотя заголовок UDP цел. Побочный эффект мы уже видели: NAT, меняя адрес в заголовке IP, обязан пересчитать и контрольную сумму UDP. В IPv4 сумму разрешено не считать, записав 0; в IPv6 она обязательна.

Это всё. Нет номеров последовательности, нет подтверждений, нет флагов, нет состояния. Отправить датаграмму — значит приписать восемь байт к данным и отдать IP.


«Без соединения» — что это значит на практике

У UDP нет понятия соединения. Каждая датаграмма — самостоятельное событие: отправитель формирует её, стек отправляет, и на этом участие UDP заканчивается. Следствия:

  • Нет рукопожатия. Первая же датаграмма несёт данные. Для запроса-ответа это один пакет туда и один обратно, без предварительного обмена.
  • Нет подтверждений и повторов. Потерялась — потерялась. Отправитель об этом не узнает, если только приложение само не ждёт ответа и не повторяет.
  • Нет порядка. Две датаграммы могут прийти в обратном порядке, если пошли разными путями. Нумеровать их — забота приложения.
  • Нет управления скоростью. Приложение может отправлять быстрее, чем сеть или получатель успевают, и лишнее просто выбросится. UDP никак не притормозит.
  • Границы сообщений сохраняются. Это плюс: один вызов sendto() — одна датаграмма — один вызов recvfrom() с теми же байтами. В TCP, как увидим в следующей части, поток байтов без границ, и приложению приходится самому размечать, где кончается сообщение.
  • Один сокет — все собеседники. DNS-сервер держит один сокет на порту 53 и получает из него датаграммы от всех клиентов сразу; от кого — говорит адрес в каждой. TCP-сервер заводит по сокету на каждого.

Про размер. Формально датаграмма может нести 65 507 байт, но IP тогда нарежет её на 45 фрагментов по 1500, и потеря любого убьёт всю датаграмму (часть 6). Поэтому приложения на UDP держат датаграммы в пределах MTU: 1472 байта данных для Ethernet без опций, а осторожные протоколы — 1200–1232, с запасом на туннели. DNS по этой причине изначально ограничивал ответ 512 байтами, а больше просил по TCP.

Что происходит, если на порту никто не слушает? Стек получателя отвечает ICMP Port Unreachable (тип 3 код 3) — то самое сообщение, по которому traceroute узнавал, что дошёл. Отправитель, если его сокет «подключён» к адресу вызовом connect(), получит ошибку ECONNREFUSED при следующей операции; если нет — ошибку стек выбросит, и приложение просто не дождётся ответа.


Кому это подходит

Кажется, что протокол без гарантий нужен только там, где на данные наплевать. На самом деле UDP выбирают, когда гарантии TCP мешают или когда их проще сделать самому.

  • DNS (часть 12): один запрос, один ответ, обычно в одну датаграмму. Рукопожатие TCP стоило бы дороже самого обмена. Потерю резолвер лечит повтором через секунду.
  • DHCP (часть 11): клиент ещё не имеет IP-адреса и шлёт запрос на широковещательный адрес. Соединение установить не с кем и не с чего: TCP здесь невозможен в принципе.
  • NTP — синхронизация времени: клиент шлёт пакет с меткой времени, сервер возвращает его со своими метками, и по четырём меткам вычисляется сдвиг часов. Повтор через TCP внёс бы задержку, которая важнее самих данных.
  • Голос и видео в реальном времени: пакет, который опоздал, бесполезен, лучше потерять кадр, чем ждать его повтора. Для этого есть RTP поверх UDP — заголовок с номером и меткой времени, по которым приёмник выстраивает порядок и замечает потери, но не просит повторить.
  • Игры — по той же причине: важно последнее состояние, а не все промежуточные.
  • syslog, SNMP, метрики: «выстрелил и забыл», потеря одной записи терпима, а накладные расходы на соединение с тысяч устройств — нет.
  • QUIC — транспорт поверх UDP, на котором работает HTTP/3, третья версия протокола, по которому браузер запрашивает страницы. Он сам, в пользовательском пространстве, делает всё, что делает TCP: подтверждения, повторы, управление перегрузкой, плюс встроенное шифрование и несколько независимых потоков в одном соединении, чтобы потеря пакета одного потока не останавливала остальные. Поверх UDP, а не нового номера протокола, потому что новый протокол не пропустили бы NAT и файрволы, а UDP пропускают все.
  • Туннели: VXLAN (часть 18) и IPsec через NAT (часть 19) заворачивают чужие пакеты в UDP, потому что UDP не добавляет своей логики и не ломает логику того, что внутри.

Общее правило: UDP там, где каждая датаграмма самодостаточна, где задержка важнее полноты, где нужен broadcast или multicast (TCP для них не существует: у него всегда ровно две стороны), и где приложение знает лучше стека, что делать с потерей.

Обратная сторона отсутствия рукопожатия: адрес отправителя никто не проверяет. Можно послать DNS-серверу запрос от имени жертвы, и ответ, в десятки раз больше запроса, уйдёт жертве. Так устроены атаки с усилением (amplification), и защита от них — не в UDP, а у провайдеров: не выпускать из своей сети пакеты с чужими адресами отправителя.


UDP за NAT

Вернёмся к NAT из шестой части. Там таблица трансляций заполнялась по первому исходящему пакету, а у TCP, забегая вперёд, есть явное начало и конец соединения, по которым NAT понимает, когда запись создать и когда убрать. У UDP ни того, ни другого нет. Поэтому NAT создаёт запись по первой исходящей датаграмме и удаляет по таймеру: в Linux через 30 секунд, если трафик шёл только наружу, и через 180, если ответы тоже были. Приложение, которое ждёт датаграмм от сервера дольше, должно само слать что-нибудь, чтобы запись не умерла. Поэтому VPN-клиенты и голосовые приложения шлют keepalive каждые 20–25 секунд: это не для сервера, это для NAT по дороге.

Два устройства за разными NAT

Задача: два телефона, каждый за своим домашним роутером с NAT, хотят передавать голос напрямую, не гоняя его через сервер. Ни один не может начать: входящая датаграмма без записи в таблице отбрасывается. Решение — hole punching, и оно держится на двух фактах: запись создаёт исходящая датаграмма, и для большинства домашних NAT эта запись не зависит от того, кому датаграмма адресована.

          A  192.168.1.10:5000                               B  10.0.0.7:5000
          │                                                        │
    NAT-A │ 198.51.100.2:40001                    203.0.113.5:40002│ NAT-B
          │                                                        │
          │ (1) датаграмма на сервер S ──►  S  ◄── датаграмма на S │
          │ (2) S сообщает A: «тебя видно как 198.51.100.2:40001;  │
          │     B — это 203.0.113.5:40002», и наоборот для B       │
          │                                                        │
          │ (3) A шлёт на 203.0.113.5:40002 ──────────────────────►│  NAT-B ещё не знает A — отбросит
          │◄──────────────────────── B шлёт на 198.51.100.2:40001  │  NAT-A уже открыл запись для B: пройдёт
          │                                                        │
          │ (4) A ещё раз ────────────────────────────────────────►│  теперь и NAT-B пропускает
  1. Оба отправляют датаграмму серверу-посреднику S, у которого публичный адрес. Каждый NAT создаёт запись и подменяет адрес.
  2. Сервер видит, откуда пришли датаграммы — публичные адреса и порты после трансляции — и сообщает их обеим сторонам. Эта часть называется STUN: протокол, в котором сервер просто возвращает клиенту адрес и порт, с которых получил его датаграмму.
  3. Обе стороны одновременно шлют датаграммы на публичный адрес друг друга. Первая датаграмма от A к B упрётся в NAT-B, у которого ещё нет записи для A. Но она же создала в NAT-A запись «A:5000 ↔ 40001, для 203.0.113.5:40002». Датаграмма от B, придя на 40001, этой записи соответствует и проходит.
  4. Следующая датаграмма от A проходит и NAT-B: у него уже есть запись от исходящей датаграммы B.

Дыра пробита, и дальше обе стороны разговаривают напрямую, продлевая записи keepalive-ами.

Работает это не со всяким NAT. Если NAT выдаёт разные внешние порты для разных адресатов (такой называют симметричным; так часто делает CGNAT), то порт, который увидел сервер, не совпадёт с портом для соседа, и предсказать его нельзя. Тогда остаётся TURN — сервер-ретранслятор, через который идёт весь трафик, и звонок получается дороже и медленнее. Процедура, которая перебирает кандидатов «напрямую, через STUN, через TURN» и выбирает лучший рабочий путь, называется ICE, и именно её выполняет браузер в первые секунды видеозвонка.


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

Кто слушает UDP и кто сейчас разговаривает:

$ ss -ulnp          # слушающие
$ ss -uanp          # все, включая «подключённые» сокеты клиентов

Самый доступный обмен по UDP — DNS-запрос. В одном окне захват, в другом dig example.com:

$ sudo tcpdump -i eth0 -n udp port 53
12:00:00.000000 IP 192.168.1.10.40321 > 192.168.1.1.53: 12345+ A? example.com. (29)
12:00:00.012345 IP 192.168.1.1.53 > 192.168.1.10.40321: 12345 1/0/0 A 203.0.113.10 (45)

Порт отправителя 40321 — временный, порт назначения 53, ответ вернулся на 40321. 29 и 45 — длины данных UDP. Между запросом и ответом ничего: ни рукопожатия, ни подтверждений.

Отправить датаграмму руками и посмотреть, что бывает с закрытым портом:

$ echo hello | nc -u -w1 192.168.1.1 9999

и в захвате tcpdump -n 'udp port 9999 or icmp':

IP 192.168.1.10.51000 > 192.168.1.1.9999: UDP, length 6
IP 192.168.1.1 > 192.168.1.10: ICMP 192.168.1.1 udp port 9999 unreachable, length 42

Датаграмма дошла, никто не слушал, стек ответил ICMP. Если ICMP не пришло, а порт точно закрыт, — между вами файрвол, который его режет (часть 8). На этом же основано сканирование UDP-портов: нет ответа — порт «открыт или фильтруется», и однозначного ответа сканер дать не может.

Узнать, как вас видно из интернета, можно любым STUN-клиентом, например stunclient stun.example.com из пакета stuntman, или просто открыв в браузере страницу проверки WebRTC (встроенного в браузеры механизма звонков, который и выполняет ICE): она покажет публичный адрес и порт после NAT.


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

  • Транспортный уровень доставляет данные процессу: адрес процесса — порт, пара «адрес, порт» плюс протокол — сокет. Отправитель берёт временный порт, получатель слушает известный.
  • Заголовок UDP — 8 байт: два порта, длина, контрольная сумма с псевдозаголовком из адресов IP (поэтому NAT её пересчитывает).
  • Без соединения: ни рукопожатия, ни повторов, ни порядка, ни притормаживания, зато границы сообщений и один сокет на всех. Датаграммы держат в пределах MTU.
  • UDP выбирают, когда задержка важнее полноты, когда нужен broadcast или multicast, когда приложение само знает, что делать с потерей, или когда делает свой транспорт поверх (QUIC).
  • За NAT запись живёт по таймеру, отсюда keepalive; hole punching через STUN открывает прямой путь между двумя NAT, а если не выходит — ретранслятор TURN.

Дальше — Часть 10: TCP, самая длинная часть серии: заголовок, номера последовательности, установление соединения в три шага и закрытие в четыре, как достигается надёжность, что такое окно, медленный старт и почему потеря одного пакета режет скорость, и одно соединение целиком в Wireshark от SYN до FIN.

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

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