Сети для самых маленьких. Часть 6: IPv4 — заголовок, маршрутизация и NAT
В пятой части мы разобрали адреса и маски и дошли до момента, когда хост решает: сосед или шлюз. Если шлюз — пакет уезжает роутеру, и дальше начинается то, ради чего сетевой уровень существует: маршрутизация, передача пакета от сети к сети, пока он не окажется в сегменте получателя. Сегодня — что роутер при этом читает в пакете, как выбирает путь и что меняет, что происходит с пакетом, который не влезает в кадр, и как домашний роутер прячет всю квартиру за одним адресом.
Заголовок IPv4
Двадцать байт без опций. Рисуют его традиционно строками по 32 бита:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
┌───────┬───────┬───────────┬───┬───────────────────────────────┐
│Version│ IHL │ DSCP │ECN│ Total Length │
├───────┴───────┴───────────┴───┼─────┬─────────────────────────┤
│ Identification │Flags│ Fragment Offset │
├───────────────┬───────────────┼─────┴─────────────────────────┤
│ TTL │ Protocol │ Header Checksum │
├───────────────┴───────────────┴───────────────────────────────┤
│ Source Address │
├───────────────────────────────────────────────────────────────┤
│ Destination Address │
├───────────────────────────────────────────────────────────────┤
│ Options (если IHL > 5) │
└───────────────────────────────────────────────────────────────┘
По полям:
- Version (4 бита) —
4. По этому полю стек различает IPv4 и IPv6, даже если EtherType уже сказал то же самое. - IHL (4 бита) — длина заголовка в 32-битных словах. Обычно
5, то есть 20 байт; больше — только с опциями. - DSCP (6 бит) — класс обслуживания: метка, по которой роутеры могут обслуживать голосовой трафик раньше загрузки файлов, если их так настроили. В интернете чаще всего игнорируется и обнуляется. ECN (2 бита) — роутер, у которого очередь переполняется, вместо того чтобы выбросить пакет, может поставить здесь метку «перегрузка», и получатель передаст её отправителю через TCP. Работает, когда включено с обеих сторон.
- Total Length (16 бит) — длина всего пакета с заголовком, до 65 535 байт. Так стек узнаёт, где кончаются данные: Ethernet дописывает нули до 46 байт, и без этого поля их было бы не отличить от данных.
- Identification, Flags, Fragment Offset — для фрагментации, раздел ниже. Из флагов важен DF (Don't Fragment).
- TTL (8 бит) — Time To Live. Несмотря на имя, не время, а счётчик хопов: каждый роутер уменьшает его на единицу, и пакет с TTL, дошедшим до нуля, выбрасывается, а отправителю уходит сообщение ICMP (часть 8). Без него пакет, попавший в петлю маршрутизации, крутился бы вечно, как кадр в петле Ethernet. Начальные значения: 64 в Linux и macOS, 128 в Windows, 255 у сетевого оборудования; по TTL ответа часто можно угадать ОС и число роутеров до неё.
- Protocol (8 бит) — поле «что внутри»:
1ICMP,6TCP,17UDP. - Header Checksum (16 бит) — контрольная сумма только заголовка. Данные IP не проверяет: это забота TCP и UDP. Поскольку TTL меняется на каждом хопе, каждый роутер пересчитывает сумму заново.
- Source, Destination — адреса из прошлой части. Доходят до получателя неизменными — пока в дело не вмешается NAT.
- Options — встречаются так редко, что многие файрволы отбрасывают пакеты с ними. Исторические: записать маршрут, поставить метки времени, задать путь явно.
В tcpdump с ключом -v эти поля видны:
$ sudo tcpdump -i eth0 -n -v -c 1 icmp
12:00:00.000000 IP (tos 0x0, ttl 64, id 54321, offset 0, flags [DF], proto ICMP (1), length 84)
192.168.1.10 > 203.0.113.10: ICMP echo request, id 7, seq 1, length 64
TTL 64, флаг DF, пакет на 84 байта: 20 заголовка и 64 данных.
Таблица маршрутов
Роутер — это машина с несколькими интерфейсами и включённой пересылкой: пакет, пришедший с одного интерфейса и адресованный не ей самой, она отправляет дальше. Решение «куда» принимается по таблице маршрутов. Каждая строка — префикс и что с ним делать:
$ ip route
default via 198.51.100.1 dev eth1
10.0.0.0/8 via 192.168.1.254 dev eth0
10.1.0.0/16 via 192.168.1.253 dev eth0
10.1.2.0/24 dev eth2 proto kernel scope link src 10.1.2.1
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.1
198.51.100.0/30 dev eth1 proto kernel scope link src 198.51.100.2
Виды строк:
- Подключённые (
dev eth0безvia): сеть прямо за интерфейсом, появляются сами при назначении адреса. Пакет отдаётся получателю напрямую. - Через шлюз (
via): следующий роутер, next hop, который обязан быть в подключённой сети, иначе до него не добраться. - По умолчанию (
default, он же0.0.0.0/0): куда слать всё, что не подошло ни под одну строку. У домашнего роутера это провайдер; у роутеров в ядре интернета маршрута по умолчанию нет, у них полная таблица на ~миллион префиксов.
Откуда строки берутся: подключённые — от адресов, статические — руками администратора (ip route add), динамические — от протоколов маршрутизации, по которым роутеры рассказывают друг другу о своих сетях. Внутри организации это чаще всего OSPF: каждый роутер рассылает описание своих линков, все собирают одинаковую карту сети и считают по ней кратчайшие пути. Между провайдерами — BGP: роутеры обмениваются префиксами вместе со списком сетей, через которые префикс прошёл, и выбирают путь по этому списку и политике. Обоим протоколам можно посвятить по серии; про BGP на практике у меня есть отдельная. Для этой статьи важно, что в таблицу они кладут те же самые строки.
Самый длинный префикс
Один адрес может подходить под несколько строк. Пакет на 10.1.2.50 подходит под 10.0.0.0/8, 10.1.0.0/16, 10.1.2.0/24 и default. Правило одно: побеждает самый длинный префикс (longest prefix match). Логика в том, что более длинный префикс — более точное знание: «вся 10.0.0.0/8 вон там» — общее правило, а «10.1.2.0/24 прямо за eth2» — исключение из него.
| Назначение | Подходящие строки | Выбрана | Куда |
|---|---|---|---|
10.1.2.50 |
/8, /16, /24, default | 10.1.2.0/24 |
напрямую через eth2 |
10.1.7.1 |
/8, /16, default | 10.1.0.0/16 |
шлюзу 192.168.1.253 |
10.9.9.9 |
/8, default | 10.0.0.0/8 |
шлюзу 192.168.1.254 |
203.0.113.10 |
default | 0.0.0.0/0 |
шлюзу 198.51.100.1 |
Если и префикс одинаковый, сравнивают метрику — число, которое ставит тот, кто добавил маршрут (меньше — лучше). Так ноутбук с кабелем и Wi-Fi держит два маршрута по умолчанию и пользуется кабельным, пока он есть: у него метрика 100, у беспроводного 600.
Хост из прошлой части, принимавший решение «сосед или шлюз», делал ровно то же самое: его таблица — это подключённая сеть и маршрут по умолчанию, и алгоритм один для всех.
Что роутер делает с пакетом
Проследим пакет от ноутбука 192.168.1.10 к серверу 203.0.113.10 через домашний роутер R1 и роутер провайдера R2. У R1 интерфейсы 192.168.1.1 (в квартиру) и 198.51.100.2 (к провайдеру), у R2 — 198.51.100.1 и дальше в интернет.
ноутбук ─────── R1 ─────────── R2 ───── … ───── сервер
192.168.1.10 .1 | .2 .1
192.168.1.0/24 198.51.100.0/30
На каждом хопе происходит одно и то же:
- Снять заголовок Ethernet, проверить контрольную сумму заголовка IP.
- Адрес назначения мой? Нет — пересылать.
- TTL − 1. Стал нулём — выбросить, отправителю ICMP Time Exceeded.
- Найти маршрут по самому длинному префиксу: исходящий интерфейс и next hop. Нет ни одного — выбросить, отправителю ICMP Destination Unreachable.
- Пересчитать Header Checksum.
- Упаковать в новый кадр: MAC отправителя — свой исходящий интерфейс, MAC получателя — next hop (или сам получатель, если сеть подключённая). Отправить.
| Участок | MAC src → dst | IP src → dst | TTL |
|---|---|---|---|
| ноутбук → R1 | ноутбук → R1 eth0 | 192.168.1.10 → 203.0.113.10 | 64 |
| R1 → R2 | R1 eth1 → R2 | 192.168.1.10 → 203.0.113.10 | 63 |
| R2 → дальше | R2 → следующий | 192.168.1.10 → 203.0.113.10 | 62 |
Роутер не хранит ничего о пакете после отправки: каждый пакет — независимое решение по таблице. Он не знает о соединениях, не помнит предыдущий пакет и не заботится, дошёл ли этот. Это главное свойство IP — best effort, «постараемся»: доставка без гарантий, а гарантии, если нужны, строит TCP.
Отсюда же следствие, о которое спотыкаются: путь туда и путь обратно — два независимых решения разных роутеров. Ответ сервера может пойти другой дорогой, и это нормально, пока по дороге не стоит что-то, ожидающее увидеть оба направления. В таблице выше R1 — частный адрес 192.168.1.10 в поле отправителя. В интернет такой пакет не выпустят, а если выпустят — ответ на 192.168.1.10 не найдёт дорогу назад. Поэтому на R1 нужен NAT, и о нём ниже.
Фрагментация
MTU Ethernet — 1500 байт, и это самый частый случай, но не единственный: в PPPoE 1492, в туннелях меньше, а в старых сетях бывало и 576. Что делает роутер, если пакет длиннее MTU исходящего интерфейса?
Если флаг DF не установлен — режет пакет на фрагменты. Каждый получает копию заголовка IP с тем же Identification, в Fragment Offset пишется смещение данных фрагмента от начала исходных данных в восьмибайтовых единицах, а флаг MF (More Fragments) стоит у всех фрагментов, кроме последнего. Пакет на 4000 байт в MTU 1500:
| Фрагмент | Total Length | Данных | Offset (×8) | MF |
|---|---|---|---|---|
| 1 | 1500 | 1480 | 0 | 1 |
| 2 | 1500 | 1480 | 185 | 1 |
| 3 | 1040 | 1020 | 370 | 0 |
Данных в фрагменте 1480, потому что 20 байт уходит на заголовок, а 1480 делится на 8. Собирает фрагменты обратно только получатель: роутеры по пути этого не делают и могут резать уже порезанное. Получатель ждёт все части по Identification, складывает по Offset и, получив последний (MF = 0) без дыр, отдаёт пакет транспорту.
Почему фрагментации стараются избегать:
- Потеря одного фрагмента — потеря всего пакета, а повторит его TCP целиком, и он снова будет порезан.
- Заголовок транспорта (порты) есть только в первом фрагменте, и файрволу, NAT, балансировщику приходится либо собирать пакет у себя, либо пропускать фрагменты вслепую. Многие просто выбрасывают.
- Сборка — это буферы и таймеры на получателе; в 1990-х на этом строились атаки.
Поэтому современные стеки ставят DF на всё подряд. Пакет с DF, не влезающий в MTU, роутер выбрасывает и шлёт отправителю ICMP «Fragmentation Needed» с значением MTU, а отправитель уменьшает размер. Это Path MTU Discovery, и подробно, вместе с тем, что бывает, когда ICMP не доходит, — в части 8.
NAT
Частные адреса не маршрутизируются в интернете, а публичных на все устройства не хватает. NAT (Network Address Translation) — подмена адресов в пакете на границе между частной сетью и интернетом. В быту «NAT» означает его самый распространённый вид, который формально называется NAPT или PAT (с портами), а в Linux — masquerade.
Таблица трансляций
Роутер R1 с публичным адресом 198.51.100.2. Ноутбук 192.168.1.10 открывает соединение с порта 51234 на сервер 203.0.113.10:443.
┌──────────────┐ ┌────────────── R1 ──────────────┐ ┌──────────────┐
│ 192.168.1.10 │ ──── (1) ────► │ 192.168.1.1 198.51.100.2 │ ──── (2) ────► │ 203.0.113.10 │
│ :51234 │ │ │ │ :443 │
│ │ ◄─── (4) ───── │ таблица трансляций │ ◄─── (3) ───── │ │
└──────────────┘ └────────────────────────────────┘ └──────────────┘
(1) 192.168.1.10:51234 → 203.0.113.10:443
(2) 198.51.100.2:51234 → 203.0.113.10:443 ← src подменён
(3) 203.0.113.10:443 → 198.51.100.2:51234 ← сервер отвечает роутеру
(4) 203.0.113.10:443 → 192.168.1.10:51234 ← dst восстановлен по таблице
- Пакет приходит на R1 из частной сети. R1 ищет в таблице трансляций запись для
192.168.1.10:51234 → 203.0.113.10:443. Нет — создаёт: «192.168.1.10:51234↔198.51.100.2:51234, к203.0.113.10:443, протокол TCP». Если порт 51234 на публичном адресе уже занят другой внутренней машиной, R1 выберет другой свободный, и во внешней записи будет, например,198.51.100.2:40001. - R1 переписывает адрес отправителя на
198.51.100.2и, если нужно, порт отправителя. Пересчитывает контрольную сумму заголовка IP и — внимание — контрольную сумму TCP, потому что в неё входят адреса из заголовка IP. Роутер лезет на уровень выше своего: вот где течёт абстракция из первой части. - Сервер ничего не знает о
192.168.1.10. Он видит соединение от198.51.100.2:51234и отвечает на него. - Ответ приходит на R1. По паре «публичный порт 51234, от
203.0.113.10:443» R1 находит запись и переписывает адрес получателя обратно на192.168.1.10:51234. Снова две контрольные суммы. Отправляет в частную сеть.
Записи живут, пока по ним идёт трафик, и удаляются по таймауту: у установленного TCP-соединения в Linux это пять суток, у UDP — 30 секунд, если трафик шёл в одну сторону, и 180 секунд, если в обе. Поэтому «тихие» соединения через NAT приходится поддерживать keepalive-пакетами, иначе запись исчезнет, и следующий пакет от сервера роутер не сможет никуда отнести.
Один публичный адрес — 65 тысяч портов, значит, десятки тысяч одновременных соединений от всей квартиры. Для дома это бесконечность, и провайдер, делающий то же самое для тысяч абонентов на одном адресе (CGNAT, Carrier-Grade NAT, с адресами 100.64.0.0/10 между абонентом и провайдером), уже вынужден делить порты квотами.
Что ломается
NAT держится на том, что соединение начал кто-то изнутри. Пакет из интернета, для которого в таблице нет записи, R1 некуда отнести: внутренних адресов много, а в пакете только публичный. Он выбрасывается. Отсюда:
- Входящие соединения невозможны без ручной записи. Такая запись называется проброс портов (port forwarding, DNAT): «всё, что пришло на
198.51.100.2:8080, отправлять на192.168.1.50:80». Это постоянная строка в таблице, не зависящая от трафика. - Один порт — один внутренний сервер. Два веб-сервера за одним адресом на 443 не повесить.
- Протоколы, передающие адреса в данных. Старый FTP сообщает серверу «подключись ко мне на
192.168.1.10:5000», SIP-телефония вставляет свой адрес в сообщение. NAT их не видит. Для них в роутерах живут ALG (application layer gateway): модули, которые разбирают данные конкретного протокола и переписывают адреса и там. Работают через раз. - Два устройства за одним NAT, пытающиеся соединиться напрямую (звонок, игра), оба за своими роутерами. Ни один не может начать. Техника hole punching — оба одновременно шлют пакеты наружу, чтобы каждый NAT завёл запись для другого, подробно в части 9 — помогает не всегда, и тогда трафик идёт через сервер-посредник.
- Hairpin. Обращение изнутри сети на свой же публичный адрес: пакет приходит на R1 с внутренней стороны и адресован R1 же. Не все домашние роутеры умеют развернуть его обратно внутрь.
- Конец сквозной связности. Задумка интернета была в том, что любой адрес достижим с любого. NAT её отменил, и значительная часть современных протоколов — обход этого факта.
NAT часто путают с файрволом. Он действительно не пропускает внутрь ничего незапрошенного, но не по политике, а потому что не знает, куда. Собственно файрвол — отдельные правила, обычно на том же роутере.
В Linux NAT делает модуль conntrack, который и ведёт таблицу, а включается одним правилом:
$ sysctl -w net.ipv4.ip_forward=1
$ iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
«Всё, что уходит через eth1, подменять на адрес eth1». Таблицу видно так:
$ conntrack -L
tcp 6 431999 ESTABLISHED src=192.168.1.10 dst=203.0.113.10 sport=51234 dport=443 \
src=203.0.113.10 dst=198.51.100.2 sport=443 dport=51234 [ASSURED] use=1
Первая половина строки — как пакет выглядит изнутри, вторая — как должен выглядеть ответ снаружи. 431999 — секунды до удаления записи.
Смотрим руками
Решение таблицы маршрутов для конкретного адреса, с интерфейсом, next hop и адресом отправителя, который будет подставлен:
$ ip route get 203.0.113.10
203.0.113.10 via 192.168.1.1 dev eth0 src 192.168.1.10 uid 1000
Добавить маршрут и удалить:
$ sudo ip route add 10.1.0.0/16 via 192.168.1.253
$ sudo ip route del 10.1.0.0/16
Превратить машину в роутер — включить пересылку (по умолчанию выключена, и машина молча выбрасывает чужие пакеты):
$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
Увидеть маршрутизацию глазами пакета поможет traceroute — ему посвящён раздел в части 8, когда дойдём до ICMP, на котором он построен. А пока достаточно одного наблюдения в tcpdump -v: если захватить один и тот же пакет на входе и выходе роутера, в нём изменится TTL, контрольная сумма и оба MAC-адреса, а адреса IP — только если роутер делает NAT.
Что запомнить
- Заголовок IPv4 — 20 байт: адреса, TTL, Protocol, длина, поля фрагментации, контрольная сумма только заголовка.
- TTL — счётчик хопов против петель; каждый роутер уменьшает его и пересчитывает контрольную сумму.
- Роутер ищет в таблице самый длинный подходящий префикс, при равенстве — меньшую метрику; подключённые сети, статические маршруты и маршрут по умолчанию — три вида строк. Каждый пакет — независимое решение, без состояния.
- Пакет длиннее MTU режется на фрагменты с общим Identification, собирает их только получатель; современные стеки ставят DF и полагаются на ICMP.
- NAT подменяет адрес и порт отправителя, ведёт таблицу трансляций и восстанавливает адрес в ответах; входящие соединения требуют проброса, протоколы с адресами внутри ломаются, сквозной связности нет.
Дальше — ARP: как хост, зная IP-адрес соседа, узнаёт его MAC-адрес, что лежит в кэше соседей и как долго, что такое gratuitous ARP и почему подменить чужой адрес в локальной сети так просто.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.