Поля заголовка IPv4-пакета, механизм фрагментации и Path MTU Discovery. Демонстрация в Wireshark.
Каков минимальный размер заголовка IPv4?
Что происходит с полем TTL при прохождении пакета через маршрутизатор?
Почему фрагментации IP-пакетов следует избегать?
Какую защиту обеспечивает поле Header Checksum?
Какой механизм позволяет определить максимальный размер пакета на всём маршруте без фрагментации?
Зачем нужно поле TTL, если маршрутизаторы и так выбирают правильный путь?
Оба урока разбирают поля заголовка IPv4, фрагментацию и механизмы TTL/Protocol — в разных контекстах
Сравнение заголовков IPv4 и IPv6: упрощённая структура, Flow Label и отсутствие контрольной суммы в IPv6
Все поля IPv4-заголовка выровнены по границе 32 бит (4 байта). Сделано это для того, чтобы процессорам было легче обрабатывать заголовки.
Машина работает с ноликами и единичками, но она не обрабатывает их по одному. Процессор берет сразу пачку бит и оперирует ими одновременно. Размер этой пачки определяет разрядность процессора. 32-разрядный процессор за один такт обрабатывает 32 бита, 64-разрядный -- 64 бита.
IPv4-пакеты выровнены по 32 битам именно для удобства обработки:
Все поля, как правило, выровнены и по границе 8 бит, так что даже 8-битный процессор сможет относительно легко работать с таким заголовком.
Пачку из 32 бит мы будем называть машинным словом.
Первые 4 бита заголовка -- поле версии. В него можно вписать число от 0 до 15.
Краткая история версий:
| Значение | Протокол |
|---|---|
| 0 | Первая версия протокола IP (тогда ещё назывался TCP) |
| 1 | TCP версии 3 |
| 2 | IP версии 2 (отдельно выделенный) |
| 3 | IP версии 3 (отдельно выделенный) |
| 4 | IPv4 -- тот, которым мы пользуемся до сих пор |
Версии 0, 1, 2, 3 вы в продакшене не увидите никогда. Это были экспериментальные версии, которые не прижились -- меньше чем через пару месяцев выходила новая, и каждый раз говорили "нет, предыдущая плохая, давайте новую". Четвёртая версия устоялась.
Когда к вам приходит последовательность бит, обработчик Ethernet видит EtherType 0x0800 и передаёт данные обработчику IP. Тот смотрит на первые 4 бита: если там число 4 (двоичное 0100), значит это действительно IPv4-пакет.
У IPv4 есть фиксированные поля заголовка и необязательное поле Options. Поле IHL указывает размер всего заголовка, включая опции.
Минимальный заголовок -- 5 машинных слов (5 строчек по 32 бита = 20 байт). Меньше 20 байт заголовок IPv4 быть не может.
Максимальный заголовок -- ограничен 4 битами поля IHL. Максимальное значение 15, то есть 15 x 4 = 60 байт. Из них 20 байт -- штатные поля, и ещё до 40 байт может занимать поле опций.
В реальности поле Options никто особо не использует, поэтому в подавляющем большинстве пакетов заголовок будет ровно 20 байт.
Современные названия:
Оба поля нужны для управления качеством обслуживания.
Изначально на месте этих двух полей было одно 8-битное поле -- Type of Service (ToS). Оно не имело строго определённого смысла: можно было что-нибудь туда записать, а узлы могли это читать и принимать решения о маршрутизации, а могли и не читать. По умолчанию туда не нужно было смотреть, и обычно туда писали восемь нулей.
Наиболее популярная реализация использовала только первые 3 бита для указания приоритета трафика (число от 0 до 7, где 7 -- самый важный). Остальные биты широкого распространения не получили.
Модель с Type of Service оказалась не очень жизнеспособной, поэтому 8-битное поле разбили на две части:
Большинство узлов не смотрят ни в DSCP, ни в ECN. Просто игнорируют эти 8 бит и идут дальше.
Указывает суммарный размер IPv4-пакета: заголовок + опции + данные.
Зачем это нужно? Представьте: IPv4-пакет с 4 байтами полезной нагрузки (20 + 4 = 24 байта) приходит в Ethernet-кадре. Минимальный размер Ethernet-кадра -- 64 байта, полезная нагрузка -- минимум 46 байт. Значит, к 24 байтам IP-пакета добавляется мусор (padding), чтобы добить кадр до минимального размера.
Когда получатель получает такой кадр, обработчику IP передаётся 46 байт. По полю Total Length он понимает, что полезных из них только 24, а остальное -- мусор.
Максимум в 16 бит можно вписать 65 535 байт. Но далеко не любой узел способен принять пакет такого размера. Спецификация говорит, что любое устройство обязано поддерживать пакеты до 576 байт -- это минимальный MTU (Maximum Transmission Unit) на уровне IP.
На практике большинство современных систем поддерживают пакеты до 32 килобайт.
Здесь всё просто: 32 бита адреса отправителя и 32 бита адреса получателя.
Эти поля не меняются при маршрутизации. Отправитель пишет свой IP и IP получателя. Как на почтовом конверте: от кого и кому. Вы не указываете, через какие промежуточные почтовые отделения пройдёт конверт.
То же самое и в IP-пакете: нигде не указываются IP-адреса или MAC-адреса промежуточных узлов. Промежуточная адресация работает на канальном уровне -- в Ethernet-кадре указывается MAC-адрес следующего маршрутизатора.
Да, в поле Options можно указать маршрутное пожелание, чтобы пакет прошёл через определённые маршрутизаторы, но в реальности этого не происходит.
Разумеется, всё это -- при отсутствии NAT. NAT будет менять адреса источника и получателя, но это отдельная тема.
В это поле вписывается число, которое постоянно уменьшается. Пока оно не дошло до нуля -- пакет живёт. Дошло -- пакет "протух".
История поля. Первые маршрутизаторы работали очень медленно, обработка одного пакета могла занимать секунды. Поэтому в TTL хранилось время жизни пакета в секундах. Каждый маршрутизатор вычитал время обработки из этого поля. Но существовало правило: вычитать нужно минимум единицу.
Сегодня обработка пакета занимает ничтожно малое время, и хранить секунды бессмысленно. Поэтому каждый маршрутизатор просто вычитает единицу. Фактически это поле стало счётчиком прыжков (hops). В IPv6 его так и переименовали -- Hop Limit.
Зачем TTL нужен в современном мире? Для защиты от петель маршрутизации. Если два маршрутизатора по ошибке считают, что другой ближе к получателю, они начнут перекидывать пакет друг другу, как волейбольный мячик. Без TTL такой пакет бегал бы бесконечно. С TTL он рано или поздно обнулится, и маршрутизатор скажет: "Стоп, этот пакет протух -- стираем".
Кто формирует TTL? Отправитель. Он может вписать любое значение от 0 до 255:
Некоторые провайдеры ставят TTL в сторону абонента равным 1, чтобы тот не мог раздавать интернет через свой роутер -- маршрутизатор абонента вычтет единицу, получит 0, и пакет будет отброшен.
Указывает, что лежит внутри пакета -- какому обработчику передать данные после заголовка.
Основные значения:
| Код | Протокол |
|---|---|
| 1 | ICMP |
| 6 | TCP |
| 17 | UDP |
| 47 | GRE |
| 50 | ESP (IPsec) |
Значения присваивает IANA (Internet Assigned Numbers Authority). Поскольку в 8 бит помещается всего 256 значений, от балды туда вписывать ничего нельзя.
Нужно для проверки целостности заголовка. Первые 4 бита (поле версии) позволяют быстро, но очень грубо определить, похоже ли это на IP-пакет -- вероятность случайного совпадения около 6%. Header Checksum даёт более надёжную проверку.
Используется алгоритм Internet Checksum:
0xFFFF (инвертировать)Пример:
Данные: 45 00 00 2C 70 7E 00 00
Сумма: 1E4FF
Разбиваем: 0001 + E4FF = E500
Инверсия: FFFF - E500 = 1AFF
Checksum: 0x1AFF
Проверка: если сложить оригинальные данные вместе с контрольной суммой, всегда должно получиться 0.
Механизм очень простой -- достаточно уметь складывать и вычитать. Он защищает от случайных ошибок (перевернулся бит по дороге), но не защищает от умышленных искажений -- злоумышленник легко пересчитает checksum. Это не электронно-цифровая подпись.
Этот же алгоритм используется в TCP и UDP -- потому он и называется Internet Checksum.
Три поля: Identification, Flags и Fragment Offset. Все три используются для управления фрагментацией.
Фрагментация -- это разделение IP-пакета на несколько маленьких пакетов. Допустим, вы создали пакет размером 32 килобайта. Отправили его, ближайший маршрутизатор принял, а дальше нужно передать в Ethernet-интерфейс с MTU 1500 байт. Пакет не пролезает. Тогда маршрутизатор режет его на части.
При разрезании:
Накладные расходы фрагментации:
Нагрузка на процессор. Фрагментация выполняется на центральном процессоре, не ускоряется специальными схемами. Реальный пример: D-Link с 100-мегабитным каналом выдавал 70 Мбит/с, а после включения jumbo frames на внутреннем интерфейсе -- упал до 15 Мбит/с
Дополнительные байты. Вместо одного пакета с одним заголовком вы получаете несколько, каждый со своим заголовком, Ethernet-заголовком, преамбулой, interframe gap
Вывод: избегайте фрагментации. Протокол IP умеет её выполнять, но это не значит, что стоит.
По поводу MTU в интернете: стандартные каналы сейчас -- 1500 байт (Ethernet). IP-пакет 1500 байт, вложение в IP -- 1480 байт, вложение TCP (MSS) без опций -- 1460 байт. Такие пакеты проходят нормально.
Если у провайдера добавляются дополнительные заголовки (VLAN, туннели), крупные пакеты могут не пролезать. Решение -- немного снизить MTU на своём интерфейсе (например, до 1480 или 1460 байт).
Проставляется отправителем для каждого пакета. Каждый следующий пакет получает значение на единицу больше. При фрагментации у всех кусочков сохраняется одинаковое значение Identification -- так получатель понимает, что это части одного и того же оригинального пакета.
Изначально поле предполагалось использовать ещё и для обнаружения дублированных пакетов. Но сегодня это запрещено. Причина: на быстрых каналах одинаковые значения Identification появляются слишком часто. На 100-мегабитном канале при отправке минимальных пакетов можно генерировать около миллиона пакетов в секунду, а поле 16-битное (65 536 значений) -- значит, примерно 15 пакетов с одинаковым Identification каждую секунду. Отличить дубликат от нового пакета с совпавшим Identification невозможно.
Указывает смещение данных фрагмента относительно начала оригинального пакета.
Проблема: в 13 бит можно вписать только 8192 значения, а смещение может быть до 65 535. Решение: значение в поле Fragment Offset нужно умножить на 8. При фрагментации пакет всегда режется по границе 8 байт.
Пример: в заголовке записано 0xB9 (185 в десятичном). Умножаем на 8 -- получаем 1480. Значит, данные в этом фрагменте начинаются с 1480-го байта.
| Бит | Название | Описание |
|---|---|---|
| 0 | Reserved | Всегда 0, не используется |
| 1 | DF (Don't Fragment) | 1 = запрет фрагментации |
| 2 | MF (More Fragments) | 1 = есть ещё фрагменты после этого |
Флаг DF -- отправитель запрещает резать пакет. Применения:
Флаг MF -- выставляется в 1 для всех фрагментов, кроме последнего.
Как определить состояние пакета по флагам и offset:
| Fragment Offset | MF | Значение |
|---|---|---|
| 0 | 0 | Единственный (нефрагментированный) пакет |
| 0 | 1 | Первый фрагмент, есть ещё |
| > 0 | 1 | Промежуточный фрагмент |
| > 0 | 0 | Последний фрагмент |
Не все реализации отправляют фрагменты в естественном порядке. Некоторые отправляют сначала последний, потом предпоследний и так далее. Логика: получив последний фрагмент первым, получатель сразу знает полный размер данных и может подготовить буферы. Впрочем, рекомендуется отправлять в естественном порядке.
Исходный пакет: 2000 байт, ICMP.
Оригинальный заголовок:
| Поле | Значение |
|---|---|
| Version | 4 |
| IHL | 20 байт (5 слов) |
| Total Length | 2000 |
| Identification | (какое-то) |
| Flags | 0, 0, 0 (фрагментация разрешена) |
| Fragment Offset | 0 |
| TTL | 128 |
| Protocol | 1 (ICMP) |
Данные: 20 байт заголовка + 8 байт ICMP-заголовка + 1972 байта данных ICMP = 2000 байт.
Фрагмент 1 (1500 байт):
| Поле | Значение |
|---|---|
| Total Length | 1500 |
| Identification | (то же) |
| Flags | 0, 0, 1 (MF = ещё есть) |
| Fragment Offset | 0 |
| Protocol | 1 (ICMP) |
Содержит: ICMP-заголовок (8 байт) + первые 1472 байта данных.
Фрагмент 2 (520 байт):
| Поле | Значение |
|---|---|
| Total Length | 520 (500 данных + 20 заголовок) |
| Identification | (то же) |
| Flags | 0, 0, 0 (MF = последний) |
| Fragment Offset | 0xB9 (185 x 8 = 1480) |
| Protocol | 1 (ICMP) |
Содержит: оставшиеся 500 байт данных. Обратите внимание: в заголовке написано Protocol = ICMP, но заголовка ICMP в этом фрагменте нет -- только хвост данных. Это нормально при фрагментации.
Необязательное поле переменного размера. Всегда кратно 4 байтам (если содержимое не кратно -- добивается мусором до границы).
Изначально предполагалось расширять функциональность заголовка. В RFC 791 были определены некоторые опции, позже выходили дополнительные RFC. Но почти все опции либо экспериментальны, либо устарели, либо небезопасны.
Например, есть опция Source Routing -- отправитель указывает маршрут прохождения пакета. Провайдеры такое не принимают: они не позволяют пользователям указывать, как маршрутизировать пакеты.
С нестандартными (вендорскими) опциями ещё хуже: оборудование другого производителя их не прочитает, а при нескольких опциях подряд невозможно понять, где заканчивается одна и начинается другая.
Итог: поле Options не используется в реальности. В 100% современных пакетов опций нет, заголовок -- ровно 20 байт. Если попытаться отправить пакет с опциями в интернет -- его либо отфильтруют, либо опции отрежут.
В захваченном UDP-пакете (голосовой трафик Skype) видно:
0x45 -- версия 4, IHL = 5 (20 байт)101110) -- метка приоритетного голосового трафика0xD30EПри отправке ICMP-пакета размером 2000 байт через Ethernet (MTU 1500) пакет разрезается на два фрагмента:
Identification у обоих одинаковый. Интересно, что некоторые реализации (как в данном случае) отправляют последний фрагмент первым.
При отправке 3000 байт данных ICMP (+ 8 байт заголовка ICMP + 20 байт IP = 3028 байт):
При отправке 1460-байтного пинга на Google с флагом DF:
При попытке отправить 1500-байтный пакет с DF -- драйвер сам отказывается: пакет не пролезает в Ethernet.
При отправке 1472-байтного пакета с DF через VPN-туннель (MTU 1450):
Хорошие маршрутизаторы возвращают ICMP с указанием MTU. Плохие -- тихо отбрасывают пакет.
На оборудовании Cisco есть штатная возможность в расширенном ping отправлять пакеты возрастающего размера с DF для автоматического определения Path MTU. Windows такого не умеет.
В заголовке IPv4-пакета нет ничего, кроме описанных полей. Там нет маски подсети, нет MAC-адресов маршрутизаторов -- только то, что видно в Wireshark. Никакой магии, всё достаточно простое и понятное.