484 lines
48 KiB
Markdown
484 lines
48 KiB
Markdown
# D4, часть 2. L2/L3: Ethernet, IP, подсети (урок)
|
||
|
||
Это не проверка, а урок: сначала разбираем механизм, потом решаешь `tasks/04_ipv4` и
|
||
`tasks/13_subnet`. Тема дня — то, что физически лежит в байтах пакета от момента, когда
|
||
приложение вызвало `send`, до момента, когда кадр ушёл в провод.
|
||
|
||
## 1. Модель OSI и модель TCP/IP
|
||
|
||
OSI — эталонная модель из **семи** независимых уровней: физический, канальный, сетевой,
|
||
транспортный, сеансовый, представления, прикладной. Каждый уровень решает свою задачу и
|
||
разговаривает только с соседними уровнями через фиксированный интерфейс, не зная деталей их
|
||
реализации — физическую среду можно сменить с меди на оптику, ничего не трогая в IP и TCP,
|
||
потому что канальный уровень скрывает эту деталь от сетевого. На практике верхние три уровня
|
||
(сеансовый, представления, прикладной) отдельно не реализуют — приложение само решает вопросы
|
||
сессии и кодирования данных.
|
||
|
||
**TCP/IP** — практическая **четырёхуровневая** модель, которой реально пользуется стек Linux:
|
||
канальный, интернет, транспортный, прикладной. Соответствие: канальный уровень TCP/IP = L1+L2
|
||
OSI (доставка кадра внутри сегмента), интернет-уровень = L3 OSI (IP, маршрутизация между
|
||
сетями), транспортный = L4 OSI (TCP/UDP, порты), прикладной уровень TCP/IP поглощает сразу
|
||
L5–L7 OSI. Сокет создаётся на транспортном уровне — отдельного программного «уровня сессии»
|
||
в реальном стеке не существует.
|
||
|
||
**Факты для карточек**
|
||
- base | Сколько уровней в модели OSI? — 7
|
||
- base | Сколько уровней в модели TCP/IP? — 4
|
||
- core | Какие три уровня OSI объединяет прикладной уровень TCP/IP? — сеансовый, представления, прикладной
|
||
- core | На каком уровне создаётся сокет? — транспортном (L4 OSI / транспортный TCP/IP)
|
||
|
||
Почему дальше: раз данные проходят через уровни сверху вниз при отправке, нужно понять, что
|
||
физически происходит с байтами на каждом уровне — это инкапсуляция.
|
||
|
||
## 2. Инкапсуляция: во что заворачиваются данные
|
||
|
||
Каждый нижний уровень оборачивает данные верхнего уровня в свой заголовок (а канальный —
|
||
ещё и в трейлер) при передаче:
|
||
|
||
```
|
||
данные приложения
|
||
→ сегмент TCP (заголовок 20 байт)
|
||
→ пакет IP (заголовок 20 байт)
|
||
→ кадр Ethernet (заголовок 14 байт + трейлер FCS 4 байта)
|
||
```
|
||
|
||
На приёмной стороне процесс идёт в обратном порядке: каждый уровень снимает свой заголовок и
|
||
передаёт содержимое выше, ориентируясь на поле типа протокола (EtherType в Ethernet-кадре
|
||
говорит, что внутри IP; поле `protocol` в IP-заголовке говорит, что внутри TCP/UDP/ICMP).
|
||
Так устроено потому, что каждый уровень должен работать независимо от содержимого —
|
||
коммутатору не нужно знать про TCP, чтобы передать кадр дальше, ему хватает MAC-адреса в
|
||
заголовке L2. В дампе `tcpdump` эта вложенность видна буквально как последовательность байт:
|
||
Ethernet, затем IP, затем TCP, затем данные.
|
||
|
||
**Факты для карточек**
|
||
- base | Размер заголовка TCP-сегмента (без опций)? — 20 байт
|
||
- base | Размер заголовка IP-пакета (без опций)? — 20 байт
|
||
- base | Размер заголовка Ethernet-кадра и трейлера FCS? — 14 байт заголовок + 4 байта FCS
|
||
- core | По какому полю верхний уровень при приёме узнаёт, что лежит внутри нижнего? — по полю типа протокола (EtherType в Ethernet, `protocol` в IP)
|
||
|
||
Почему дальше: раз данные заворачиваются в Ethernet-кадр последними перед проводом, логично
|
||
разобрать этот заголовок по байтам первым.
|
||
|
||
## 3. Ethernet и MAC-адрес
|
||
|
||
Ethernet-заголовок — **14 байт**: 6 байт MAC-адрес получателя, 6 байт MAC-адрес отправителя,
|
||
2 байта EtherType (тип содержимого кадра — например, IPv4 или ARP). После полезной нагрузки
|
||
кадр завершается 4-байтовым трейлером **FCS** (frame check sequence) — контрольной суммой для
|
||
проверки целостности при приёме; трейлер частью заголовка не считается. Фиксированная и
|
||
компактная структура нужна для того, чтобы коммутатор мог разобрать заголовок на аппаратной
|
||
скорости без анализа содержимого выше — все поля имеют строго фиксированную длину и позицию.
|
||
|
||
**MAC-адрес** — 48-битный (6-байтный) физический адрес интерфейса, действующий только внутри
|
||
одного сегмента (широковещательного домена). Первые **3 байта (OUI)** назначаются
|
||
производителю оборудования организацией IEEE, оставшиеся 3 байта производитель присваивает
|
||
конкретному интерфейсу. Когда пакет покидает сегмент через маршрутизатор, MAC-адрес
|
||
получателя в заголовке меняется на MAC следующего узла на пути, а IP-адрес остаётся прежним —
|
||
L2 отвечает за доставку «из рук в руки» внутри сегмента, L3 — за доставку через множество
|
||
сегментов.
|
||
|
||
**Факты для карточек**
|
||
- base | Длина Ethernet-заголовка? — 14 байт
|
||
- base | Длина трейлера FCS? — 4 байта
|
||
- base | Длина MAC-адреса в битах и байтах? — 48 бит, 6 байт
|
||
- core | Из скольки байт состоит OUI в MAC-адресе и кто его назначает? — 3 байта, назначает IEEE производителю
|
||
- core | Что меняется в заголовках при переходе пакета через маршрутизатор — MAC или IP получателя? — MAC-адрес получателя, IP остаётся прежним
|
||
|
||
Почему дальше: чтобы отправить кадр внутри сегмента, нужен MAC получателя, а известен обычно
|
||
только его IP — протокол, который их связывает, это ARP.
|
||
|
||
## 4. ARP: разрешение IP в MAC
|
||
|
||
ARP (Address Resolution Protocol) по известному IP-адресу узла в локальном сегменте находит
|
||
его MAC-адрес. Механизм: отправитель рассылает **широковещательный (broadcast)** кадр «кто
|
||
владеет IP X, сообщите свой MAC»; получают его все узлы сегмента, но отвечает только владелец
|
||
адреса — уже адресным **unicast**-пакетом со своим MAC. Полученную пару IP-MAC отправитель
|
||
кладёт в локальный **ARP-кэш (таблицу)**, чтобы не повторять broadcast на каждый пакет —
|
||
именно поэтому первый пакет к новому соседу в сети чуть медленнее последующих, он ждёт
|
||
ARP-ответа.
|
||
|
||
**Gratuitous ARP** — ARP-запрос или ответ, который узел рассылает не в ответ на чужой запрос,
|
||
а сам, без повода, про собственный IP-адрес (запрашивает MAC для своего же IP). Назначение —
|
||
объявить остальным узлам сегмента свою пару IP-MAC заранее (обновить их кэши до первого
|
||
реального обмена) или обнаружить конфликт IP-адресов: если кто-то в сегменте уже отвечает за
|
||
тот же IP, это сразу видно. Используется при старте интерфейса и при переключении на резервный
|
||
узел (failover), чтобы соседи сразу обновили устаревшую запись в ARP-кэше на MAC нового узла.
|
||
|
||
Если ARP не проходит (узел выключен, неверная подсеть, фильтрация), это видно в `tcpdump` как
|
||
повторяющиеся «who has X» без ответа, а приложение получит таймаут соединения без объяснения
|
||
причины.
|
||
|
||
**Факты для карточек**
|
||
- base | Как рассылается ARP-запрос — broadcast или unicast? — broadcast
|
||
- base | Как отправляется ARP-ответ? — unicast, от владельца адреса
|
||
- core | Зачем нужен ARP-кэш? — не повторять broadcast-запрос для каждого исходящего пакета к уже известному узлу
|
||
- core | Что такое gratuitous ARP и для чего он нужен? — незапрошенный ARP про собственный IP; обновление чужих кэшей заранее и обнаружение конфликта адресов
|
||
|
||
Почему дальше: ARP работает внутри одного L2-сегмента. Если в сети настроены виртуальные
|
||
сегменты поверх одной физической — VLAN, — это меняет сам Ethernet-заголовок.
|
||
|
||
## 5. VLAN 802.1Q
|
||
|
||
VLAN (Virtual LAN) по стандарту **802.1Q** делит один физический сегмент на несколько
|
||
логически изолированных широковещательных доменов — кадры из разных VLAN не видят
|
||
широковещательный трафик друг друга, хотя идут по одному кабелю и через один коммутатор.
|
||
Механизм — тег из **4 дополнительных байт**, вставляемый в Ethernet-заголовок между
|
||
MAC-адресом отправителя и полем EtherType:
|
||
|
||
- **TPID** (Tag Protocol Identifier) — 2 байта, фиксированное значение **0x8100**, сигнализирует
|
||
коммутатору, что дальше идёт VLAN-тег, а не обычный EtherType;
|
||
- **TCI** (Tag Control Information) — 2 байта, из них: **PCP** (Priority Code Point, 3 бита) —
|
||
приоритет кадра для QoS, **DEI** (Drop Eligible Indicator, 1 бит) — кандидат на отбрасывание
|
||
первым при перегрузке, **VID** (VLAN ID, **12 бит**) — номер VLAN, диапазон 0–4095 (0 и
|
||
4095 зарезервированы, реально используется 1–4094).
|
||
|
||
Из-за тега заголовок Ethernet-кадра с VLAN вырастает с 14 до **18 байт**. Если это не
|
||
учесть при расчёте MTU или максимального размера кадра, получится ошибка ровно на 4 байта —
|
||
типично ловится сравнением дампа `tcpdump` с ожидаемой длиной.
|
||
|
||
**Факты для карточек**
|
||
- base | Сколько байт добавляет VLAN-тег 802.1Q к Ethernet-заголовку? — 4 байта (14 → 18)
|
||
- base | Значение поля TPID для VLAN-тега? — 0x8100
|
||
- core | Из каких трёх полей состоит TCI и сколько бит занимает VID? — PCP (3 бита), DEI (1 бит), VID (12 бит)
|
||
- core | Диапазон реально используемых VLAN ID? — 1–4094 (0 и 4095 зарезервированы)
|
||
|
||
Почему дальше: заголовок Ethernet ограничивает не только формат, но и максимальный размер
|
||
полезной нагрузки в одном кадре — MTU.
|
||
|
||
## 6. MTU и фрагментация
|
||
|
||
MTU (Maximum Transmission Unit) — максимальный размер полезной нагрузки, который канальный
|
||
уровень передаёт в одном кадре; для стандартного Ethernet это **1500 байт**. Если IP-пакет
|
||
крупнее MTU исходящего интерфейса, происходит одно из двух:
|
||
|
||
- **фрагментация** — пакет режется на несколько IP-пакетов меньшего размера, каждый со своим
|
||
IP-заголовком, собираются они уже на узле-получателе;
|
||
- если в заголовке выставлен флаг **DF** (Don't Fragment) — узел на пути отбрасывает пакет и
|
||
отправляет источнику ICMP-сообщение о необходимости фрагментации (см. раздел про ICMP).
|
||
|
||
Ограничение в 1500 байт исторически идёт из спецификации Ethernet и балансирует накладные
|
||
расходы заголовка против задержки и вероятности ошибки на длинном кадре: чем крупнее кадр,
|
||
тем дороже обходится его повторная передача при ошибке. Фрагментация — дорогая операция:
|
||
она нагружает маршрутизаторы на пути и делает передачу уязвимой к потере одного фрагмента, из-
|
||
за которого теряется весь исходный пакет целиком. Поэтому современные стеки заранее подбирают
|
||
размер пакета под MTU всего пути — механизм **PMTUD** (Path MTU Discovery), разобранный
|
||
дальше вместе с ICMP.
|
||
|
||
**Факты для карточек**
|
||
- base | Значение MTU для стандартного Ethernet? — 1500 байт
|
||
- core | Что происходит с IP-пакетом крупнее MTU, если флаг DF не выставлен? — фрагментируется на несколько IP-пакетов меньшего размера
|
||
- core | Что происходит, если пакет крупнее MTU и выставлен флаг DF? — пакет отбрасывается, отправителю летит ICMP о необходимости фрагментации
|
||
- deep | Почему фрагментация считается дорогой операцией? — нагружает маршрутизаторы на пути, и потеря одного фрагмента роняет весь исходный пакет
|
||
|
||
Почему дальше: и флаг DF, и размер, и адреса — это конкретные поля одной структуры,
|
||
IP-заголовка. Разберём его целиком по байтам.
|
||
|
||
## 7. IPv4-заголовок по полям
|
||
|
||
IPv4-заголовок — минимум **20 байт** (без опций), опции могут увеличить его до 60 байт.
|
||
Поля в порядке следования:
|
||
|
||
| Поле | Размер | Смысл |
|
||
|---|---|---|
|
||
| Version | 4 бита | версия протокола, для IPv4 всегда 4 |
|
||
| IHL | 4 бита | длина заголовка в 32-битных словах (минимум 5 → 20 байт) |
|
||
| TOS (Type of Service) | 8 бит | приоритет/качество обслуживания пакета |
|
||
| Total Length | 16 бит | общая длина пакета (заголовок + данные), байты |
|
||
| Identification | 16 бит | идентификатор для сборки фрагментов одного исходного пакета |
|
||
| Flags | 3 бита | бит DF (не фрагментировать), бит MF (есть ещё фрагменты) |
|
||
| Fragment Offset | 13 бит | смещение этого фрагмента в исходном пакете |
|
||
| TTL | 8 бит | время жизни пакета в хопах |
|
||
| Protocol | 8 бит | протокол следующего уровня: 6 = TCP, 17 = UDP, 1 = ICMP |
|
||
| Header Checksum | 16 бит | контрольная сумма заголовка (не данных) |
|
||
| Source / Destination IP | по 32 бита | адреса отправителя и получателя |
|
||
|
||
Это ровно поля структуры `Ipv4Header` из `tasks/04_ipv4`. Задача требует парсить их из сырого
|
||
буфера побайтово (через сдвиги и маски), а не приведением указателя `reinterpret_cast` — на
|
||
невыровненных адресах и при другом порядке байт это UB. Обязательные проверки при разборе:
|
||
буфер короче 20 байт, `version != 4`, `ihl < 5`, `ihl*4 > len`, `total_length < ihl*4` или
|
||
`total_length > len` — пакет с любым из этих условий отбрасывается как некорректный, ещё до
|
||
попытки читать поля выше заголовка.
|
||
|
||
**Факты для карточек**
|
||
- base | Минимальный размер IPv4-заголовка? — 20 байт
|
||
- base | Значение поля Protocol для TCP / UDP / ICMP? — 6 / 17 / 1
|
||
- core | Сколько бит занимает поле IHL и что оно означает? — 4 бита, длина заголовка в 32-битных словах
|
||
- core | Какие условия делают IPv4-пакет некорректным при разборе (по `tasks/04_ipv4`)? — буфер <20 байт, version≠4, ihl<5, ihl·4>len, total_length<ihl·4 или >len
|
||
- deep | Почему в `tasks/04_ipv4` запрещён `reinterpret_cast` на буфер? — невыровненный адрес и другой порядок байт на проводе дают UB при чтении полей как структуры напрямую
|
||
|
||
Почему дальше: одно из полей заголовка — TTL — существует специально для защиты от
|
||
зацикленной маршрутизации, и с ним напрямую связан отдельный протокол ошибок, ICMP.
|
||
|
||
## 8. TTL и ICMP Time Exceeded: как работает traceroute
|
||
|
||
**TTL** (Time To Live) уменьшается на единицу **на каждом маршрутизаторе**, через который
|
||
проходит пакет. Это сделано специально: чтобы зацикленный по ошибке маршрут не гонял пакет по
|
||
сети бесконечно. Когда TTL достигает нуля, пакет отбрасывается, а отправителю отправляется
|
||
**ICMP Time Exceeded** (тип 11).
|
||
|
||
Именно на этом механизме построен **traceroute**: он последовательно отправляет пакеты с
|
||
TTL 1, 2, 3, ... Пакет с TTL 1 гарантированно "умирает" на первом же маршрутизаторе — тот
|
||
шлёт ICMP Time Exceeded с собственным адресом, это и есть первая строка вывода traceroute.
|
||
Пакет с TTL 2 доходит до второго маршрутизатора и умирает там, и так далее, пока пакет не
|
||
дойдёт до конечного получателя. Так по цепочке ICMP-ответов восстанавливается список всех
|
||
промежуточных маршрутизаторов на пути, без какого-либо специального протокола обнаружения
|
||
маршрута — только манипуляция TTL и стандартный побочный эффект его истечения.
|
||
|
||
**Факты для карточек**
|
||
- base | На сколько уменьшается TTL на каждом маршрутизаторе? — на 1
|
||
- base | Какой ICMP-тип отправляется при обнулении TTL? — Time Exceeded, тип 11
|
||
- core | Как traceroute находит промежуточные маршрутизаторы, не имея отдельного протокола обнаружения пути? — последовательно шлёт пакеты с TTL=1,2,3..., каждый умирает на очередном хопе и присылает ICMP Time Exceeded с адресом этого хопа
|
||
|
||
Почему дальше: Time Exceeded — лишь один из типов ICMP-сообщений. Остальные закрывают другие
|
||
сценарии ошибок доставки и обнаружения MTU пути.
|
||
|
||
## 9. ICMP: echo, destination unreachable, PMTUD
|
||
|
||
ICMP (Internet Control Message Protocol) — протокол сетевого уровня для диагностических
|
||
сообщений; у него нет портов, он не переносит пользовательские данные приложений. Ключевые
|
||
типы:
|
||
|
||
- **Echo Request / Echo Reply** (тип 8 / тип 0) — основа команды `ping`;
|
||
- **Time Exceeded** (тип 11) — TTL обнулился (раздел выше, основа traceroute);
|
||
- **Destination Unreachable** (тип 3) — пакет физически не может быть доставлен; код внутри
|
||
этого типа уточняет причину, в частности код **"fragmentation needed and DF set"** —
|
||
посылается, когда пакет крупнее MTU промежуточного линка, а флаг DF запрещает
|
||
фрагментацию.
|
||
|
||
Именно код "fragmentation needed" лежит в основе **PMTUD** (Path MTU Discovery): отправитель
|
||
шлёт пакеты с выставленным DF, начиная с MTU своего интерфейса; если по пути встречается
|
||
линк с меньшим MTU, тот роутер отбрасывает пакет и присылает ICMP Destination Unreachable с
|
||
этим кодом (в современных реализациях — вместе со значением MTU узкого места); отправитель
|
||
уменьшает размер пакета и повторяет попытку. Так стек заранее подбирает размер, не полагаясь
|
||
на фрагментацию по пути.
|
||
|
||
ICMP существует отдельно от TCP/UDP потому, что диагностика нужна на уровне, где ещё нет
|
||
понятия соединения или порта — маршрутизатор должен уметь сообщить об ошибке доставки, даже не
|
||
зная, что за протокол был внутри. Именно поэтому `ping` и `traceroute` работают даже к узлу,
|
||
на котором не открыт ни один сервис поверх TCP/UDP. Если ICMP заблокирован файрволом
|
||
(частая практика), `ping` не пройдёт, хотя TCP-соединение на конкретный порт может работать
|
||
нормально — это видно, если сравнить `ping host` и `curl host` с разным результатом.
|
||
|
||
**Факты для карточек**
|
||
- base | Номера типов Echo Request и Echo Reply? — 8 и 0
|
||
- base | Тип ICMP-сообщения Destination Unreachable? — тип 3
|
||
- core | Какой код Destination Unreachable запускает PMTUD? — "fragmentation needed and DF set"
|
||
- core | Почему ICMP не имеет портов? — диагностика работает на сетевом уровне, где ещё нет понятия транспортного соединения
|
||
- deep | Почему `ping` может не проходить, а `curl` на тот же хост — работать? — ICMP заблокирован файрволом отдельно от TCP-порта, это два разных уровня фильтрации
|
||
|
||
**Ловушки**
|
||
- Судить о доступности хоста только по `ping` → файрвол может глушить ICMP, не трогая
|
||
реальный TCP-сервис — вывод «хост недоступен» будет ложным.
|
||
- Не учитывать PMTUD при жёстко заданном MTU в туннеле (VPN, GRE) → пакеты с DF молча
|
||
теряются на узком месте, если ICMP-ответы блокируются по пути — проявляется как
|
||
«маленькие пакеты проходят, большие — зависают».
|
||
|
||
Почему дальше: контрольная сумма заголовка из раздела про IPv4-поля устроена нетривиально —
|
||
разберём отдельно, как она считается и почему покрывает только заголовок.
|
||
|
||
## 10. Контрольная сумма IPv4-заголовка
|
||
|
||
Алгоритм — сумма в **дополнительном коде (one's complement)** по 16-битным словам заголовка,
|
||
затем **инверсия** результата (побитовое НЕ) — это и есть значение, которое пишут в поле
|
||
`header_checksum`. При сложении в дополнительном коде перенос из старшего бита не отбрасывается,
|
||
а прибавляется обратно к младшему биту суммы (end-around carry).
|
||
|
||
`tasks/04_ipv4` требует ровно эту схему в двух функциях:
|
||
|
||
```c++
|
||
// сумма в дополнительном коде по len байтам, затем инверсия — значение для записи в поле
|
||
// (вызывается на буфере, где поле контрольной суммы уже обнулено)
|
||
uint16_t compute_checksum(const uint8_t* buf, size_t len);
|
||
|
||
// true, если контрольная сумма заголовка верна: сумма в дополнительном коде по ihl*4 байтам
|
||
// заголовка (вместе с полем контрольной суммы) равна 0xFFFF; нагрузка не участвует
|
||
bool checksum_valid(const uint8_t* buf, size_t len);
|
||
```
|
||
|
||
Асимметрия проверки и вычисления не случайна: при **вычислении** поле суммы ещё не заполнено
|
||
(обнулено), поэтому оно не участвует своим значением; при **проверке** поле уже содержит
|
||
записанную сумму, и если она верна, повторное суммирование всех 16-битных слов заголовка
|
||
(включая это поле) в дополнительном коде обязано дать **все единицы — 0xFFFF**. Это свойство
|
||
дополнительного кода: сумма X и инверсии X всегда даёт все единицы.
|
||
|
||
**Почему сумма считается только по заголовку, а не по всему пакету с данными**: заголовок
|
||
меняется на каждом хопе — как минимум TTL уменьшается на 1 на каждом маршрутизаторе, а значит
|
||
контрольную сумму заголовка пришлось бы пересчитывать на каждом хопе заново. Пересчитывать её
|
||
ещё и по всей полезной нагрузке на каждом маршрутизаторе было бы избыточно дорого и не нужно:
|
||
целостность самих данных уже проверяется отдельно контрольными суммами более высокого уровня
|
||
(TCP/UDP-заголовок содержит свою контрольную сумму, покрывающую данные) и трейлером FCS на
|
||
канальном уровне. Если контрольная сумма заголовка не сходится, пакет молча отбрасывается —
|
||
ошибка ловится счётчиками ошибок интерфейса или отсутствием ожидаемого ответа в `tcpdump`.
|
||
|
||
**Факты для карточек**
|
||
- base | Алгоритм контрольной суммы IPv4-заголовка? — сумма в дополнительном коде по 16-битным словам, затем инверсия
|
||
- core | Какое значение должна давать сумма при проверке валидности (с учётом поля суммы)? — 0xFFFF
|
||
- core | Почему контрольная сумма покрывает только заголовок, а не данные? — заголовок (минимум TTL) меняется на каждом хопе и пересчитывается заново; целостность данных проверяют TCP/UDP-checksum и FCS канального уровня
|
||
- deep | Что такое end-around carry в one's complement сложении? — перенос из старшего бита не отбрасывается, а прибавляется обратно к младшему биту суммы
|
||
|
||
**Ловушки**
|
||
- Считать контрольную сумму по буферу с уже заполненным полем `header_checksum` вместо
|
||
обнулённого → результат `compute_checksum` окажется неверным, потому что старое значение
|
||
поля участвует в сумме.
|
||
- Включить в сумму данные после заголовка (payload) → сумма не сойдётся ни у отправителя, ни
|
||
при проверке — контрольная сумма IPv4 считается строго по `ihl*4` байтам, не по всей длине
|
||
пакета.
|
||
|
||
Почему дальше: сами IP-адреса в заголовке не существуют сами по себе — узел должен понимать,
|
||
какая их часть определяет сеть, а какая — конкретный хост. Это маска и подсеть.
|
||
|
||
## 11. Маски, подсети и CIDR
|
||
|
||
Маска подсети делит 32-битный IPv4-адрес на две части: номер сети и номер узла внутри неё —
|
||
это определяет, какие адреса «свои» для локальной доставки внутри сегмента, а какие требуют
|
||
выхода через шлюз. Запись `/N` (**CIDR**, Classless Inter-Domain Routing) — это и есть длина
|
||
префикса сети в битах вместо устаревшей классовой адресации (A/B/C), что позволяет выделять
|
||
подсети произвольного размера, а не только фиксированных 8/16/24 бит.
|
||
|
||
**Широковещательный адрес** подсети — адрес, где все биты хостовой части выставлены в 1; он
|
||
зарезервирован для рассылки всем узлам сегмента и не выдаётся конкретному устройству. Первый
|
||
адрес подсети (все хостовые биты — 0) зарезервирован как адрес самой сети.
|
||
|
||
Число доступных хостов по маске (для обычных подсетей — минус 2 служебных адреса, сеть и
|
||
broadcast):
|
||
|
||
| Маска | Хостовых бит | Всего адресов | Доступно хостам |
|
||
|---|---|---|---|
|
||
| /24 | 8 | 256 | **254** |
|
||
| /26 | 6 | 64 | **62** |
|
||
| /30 | 2 | 4 | **2** |
|
||
| /31 | 1 | 2 | **2** (RFC 3021, оба адреса — хосты, без сети/broadcast) |
|
||
| /32 | 0 | 1 | **1** (сеть = broadcast = сам адрес) |
|
||
|
||
`/31` и `/32` — исключения из общего правила «минус 2»: `/31` (RFC 3021) используется на
|
||
линках точка-точка между двумя маршрутизаторами, где резервировать сеть и broadcast из
|
||
всего двух адресов расточительно — оба адреса становятся хостовыми; `/32` — адрес единственного
|
||
узла, маршрут на конкретный хост.
|
||
|
||
По `tasks/13_subnet`: для `prefix <= 30` — `host_count = 2^(32-prefix) - 2`, `first_host =
|
||
network + 1`, `last_host = broadcast - 1`. Эталонный пример: `10.0.1.130/26` → сеть
|
||
`10.0.1.128`, broadcast `10.0.1.191`, диапазон хостов `129..190`. Обратная задача —
|
||
`prefix_for_hosts(h)`: найти **самую узкую** (наибольший `prefix`) подсеть, вмещающую `h`
|
||
хостов — считается как `32 - ceil(log2(h+2))` за O(1), без перебора. Примеры: `62` → `/26`,
|
||
`63` → `/25`, `254` → `/24`, `1` → `/30` (`/31` и `/32` в подборе не участвуют — они не для
|
||
произвольного числа хостов, а под конкретные сценарии линка и одиночного адреса).
|
||
|
||
**Факты для карточек**
|
||
- base | Сколько хостов доступно в подсети /24? — 254
|
||
- base | Сколько хостов доступно в подсети /26? — 62
|
||
- base | Сколько хостов доступно в подсети /30? — 2
|
||
- core | Чем /31 отличается от остальных масок по числу служебных адресов? — оба адреса хостовые (RFC 3021), нет отдельного сетевого/broadcast адреса
|
||
- core | Что означает запись `/N` в CIDR? — длина префикса сети в битах вместо классовой адресации A/B/C
|
||
- deep | Формула подбора самой узкой подсети под h хостов за O(1)? — `32 - ceil(log2(h+2))`
|
||
|
||
**Ловушки**
|
||
- Забыть исключить /31 и /32 из общего расчёта `2^(32-prefix) - 2` → для /31 формула даст 0
|
||
хостов вместо верных 2, для /32 — отрицательное число.
|
||
- Спутать адрес сети (все хостовые биты 0) с первым доступным хостом (`network + 1`) → off-by-one
|
||
в диапазоне выдаваемых адресов.
|
||
|
||
Почему дальше: маска определяет, доставлять ли пакет напрямую внутри сегмента или через
|
||
устройство более высокого уровня. Логично развести устройства L1/L2/L3 по тому, что каждое
|
||
из них реально делает с кадром/пакетом.
|
||
|
||
## 12. Хаб, коммутатор, маршрутизатор; таблица MAC-адресов (FDB)
|
||
|
||
Три устройства работают на разных уровнях и с разным объёмом понимания трафика:
|
||
|
||
- **Хаб (L1)** — просто повторитель: любой бит, пришедший на один порт, электрически
|
||
копируется на все остальные порты без какого-либо разбора кадра. Все порты хаба — один
|
||
общий домен коллизий; трафик одной пары узлов виден всем остальным. Практически вытеснен
|
||
коммутаторами.
|
||
- **Коммутатор (L2)** — разбирает Ethernet-заголовок и пересылает кадр только на порт, где
|
||
находится MAC-адрес получателя, а не на все порты сразу. Каждый порт — отдельный домен
|
||
коллизий, но все порты (без VLAN) остаются одним широковещательным доменом.
|
||
- **Маршрутизатор (L3)** — разбирает IP-заголовок и пересылает пакет между разными подсетями
|
||
по таблице маршрутов, уменьшая TTL на 1 на каждом пересланном пакете. Разделяет
|
||
широковещательные домены — broadcast из одной подсети не проходит через маршрутизатор в
|
||
другую.
|
||
|
||
Коммутатор знает, на какой порт слать кадр, благодаря **таблице MAC-адресов** (FDB, Forwarding
|
||
Database, также называют CAM-таблицей) — по сути хеш-таблице, где ключ — MAC-адрес, значение —
|
||
номер порта. Заполняется она самообучением: коммутатор смотрит **source MAC** каждого
|
||
входящего кадра и запоминает пару (MAC, порт, на который кадр пришёл) — так через обычный
|
||
трафик таблица заполняется без отдельного протокола объявления адресов. Если адреса
|
||
получателя нет в таблице (адрес ещё не встречался как источник), коммутатор пересылает кадр
|
||
на все порты, кроме входного (flooding) — точно так же, как хаб, но только для этого одного
|
||
кадра, пока адрес не станет известен.
|
||
|
||
Записи в FDB не хранятся вечно — у каждой запись есть **aging**: таймер, который сбрасывается
|
||
при каждом новом кадре с этим source MAC, и если запись не обновлялась дольше таймаута
|
||
(типичное значение в реализациях — 300 секунд, 5 минут), она удаляется из таблицы. Это нужно,
|
||
чтобы таблица не разрасталась записями отключённых или перемещённых на другой порт устройств
|
||
— переключить сетевой кабель с одного порта на другой без aging означало бы, что коммутатор
|
||
продолжал бы слать кадры на старый, уже неверный порт.
|
||
|
||
**Факты для карточек**
|
||
- base | На каком уровне работает хаб / коммутатор / маршрутизатор? — L1 / L2 / L3
|
||
- base | Что такое FDB коммутатора по структуре данных? — хеш-таблица MAC-адрес → порт
|
||
- core | Как коммутатор заполняет FDB без отдельного протокола объявления? — самообучением по source MAC каждого входящего кадра
|
||
- core | Что делает коммутатор с кадром, чей MAC получателя не найден в FDB? — рассылает на все порты кроме входного (flooding)
|
||
- core | Зачем нужен aging записей FDB? — удалять устаревшие пары MAC-порт, если устройство отключилось или переехало на другой порт
|
||
- deep | Что разделяет маршрутизатор, чего не делает коммутатор? — широковещательные домены (домены коллизий разделяет уже коммутатор)
|
||
|
||
**Ловушки**
|
||
- Ожидать, что коммутатор ограничивает broadcast-трафик → он передаёт broadcast-кадры на все
|
||
порты одного широковещательного домена так же, как хаб; ограничивает его только
|
||
маршрутизатор (или разбиение на VLAN).
|
||
- Перепутать основание пересылки: коммутатор решает по MAC (L2), маршрутизатор — по IP (L3) →
|
||
попытка «настроить маршрутизацию на коммутаторе» без функции L3 физически невозможна.
|
||
|
||
Ссылки на задачи этого дня: `tasks/04_ipv4` (разбор IPv4-заголовка и контрольная сумма),
|
||
`tasks/13_subnet` (арифметика подсетей).
|
||
|
||
<details>
|
||
<summary>Проверь себя</summary>
|
||
|
||
1. Кадр Ethernet с VLAN-тегом занимает 18 байт заголовка вместо 14. Откуда взялись эти лишние
|
||
4 байта и что конкретно в них закодировано?
|
||
<details><summary>Ответ</summary>VLAN-тег 802.1Q: 2 байта TPID (фиксированное значение
|
||
0x8100) + 2 байта TCI, где TCI = PCP (3 бита приоритета) + DEI (1 бит) + VID (12 бит номер
|
||
VLAN, диапазон 1–4094 реально используемых значений).</details>
|
||
|
||
2. Почему PMTUD зависит от ICMP, и что происходит, если файрвол на пути блокирует ICMP
|
||
целиком?
|
||
<details><summary>Ответ</summary>PMTUD узнаёт об узком месте по ICMP Destination
|
||
Unreachable с кодом «fragmentation needed and DF set» от промежуточного маршрутизатора;
|
||
если ICMP заблокирован, отправитель никогда не получит этот сигнал и будет слать пакеты,
|
||
которые молча теряются на узком месте — классическая причина «маленькие пакеты проходят,
|
||
большие зависают» через VPN/туннель.</details>
|
||
|
||
3. При проверке контрольной суммы IPv4-заголовка сумма всех 16-битных слов (включая само
|
||
поле суммы) даёт 0xFFFF. Почему именно это число, а не 0?
|
||
<details><summary>Ответ</summary>Поле контрольной суммы — это инверсия суммы остальных
|
||
слов; сумма числа и его инверсии в дополнительном коде всегда даёт все единицы (0xFFFF),
|
||
это математическое свойство one's complement, а не произвольный выбор.</details>
|
||
|
||
4. Подсеть `10.0.1.130/26`. Назови адрес сети, broadcast и диапазон хостов, не считая заново
|
||
с нуля — по правилу из этого урока.
|
||
<details><summary>Ответ</summary>Сеть `10.0.1.128`, broadcast `10.0.1.191`, хосты
|
||
`10.0.1.129`–`10.0.1.190` (62 адреса, /26 = 64 адреса минус 2 служебных).</details>
|
||
|
||
5. Почему `/31` — единственная маска (кроме /32) с `host_count`, который не считается по
|
||
формуле `2^(32-prefix) - 2`?
|
||
<details><summary>Ответ</summary>RFC 3021: на линке точка-точка из всего двух адресов
|
||
резервировать отдельно сеть и broadcast бессмысленно — оба адреса используют как хостовые,
|
||
поэтому `host_count = 2`, а не `2^1 - 2 = 0`.</details>
|
||
|
||
6. Коммутатор получил кадр с MAC получателя, которого нет в его FDB. Что он сделает и чем
|
||
это временно похоже на поведение хаба?
|
||
<details><summary>Ответ</summary>Разошлёт кадр на все порты, кроме входного (flooding) —
|
||
как и хаб, который всегда рассылает на все порты; разница в том, что у коммутатора это
|
||
разовое поведение для конкретного неизвестного адреса, а не постоянный режим работы для
|
||
всего трафика.</details>
|
||
|
||
</details>
|
||
|
||
## Материалы
|
||
|
||
- RFC 791, Internet Protocol (IPv4) — https://datatracker.ietf.org/doc/html/rfc791
|
||
- RFC 826, An Ethernet Address Resolution Protocol (ARP) — https://datatracker.ietf.org/doc/html/rfc826
|
||
- RFC 792, Internet Control Message Protocol (ICMP) — https://datatracker.ietf.org/doc/html/rfc792
|
||
- RFC 4632, Classless Inter-domain Routing (CIDR) — https://datatracker.ietf.org/doc/html/rfc4632
|
||
- RFC 3021, Using 31-Bit Prefixes on IPv4 Point-to-Point Links — https://datatracker.ietf.org/doc/html/rfc3021
|
||
- man7.org, `ip(7)` — https://man7.org/linux/man-pages/man7/ip.7.html
|