todo/Zapret2/video/02-chto-vidit-nablyudatel.md
loop-uh ae1d0be970
Some checks failed
Published content check / validate (push) Failing after 7s
Видеосерия: статьи к видео 1, 2, 3, 4, 5, 6, 16
Полный текст выпусков с анимированными SVG-схемами: «Письмо в сортировочном центре», «Что видит наблюдатель», «Читаем командную строку zapret2», «Где резать: метки», «multisplit: резать на части», «Перемешать: disorder», «Внутри программы». Список видео в Zapret2/Zapret2.md обновлён.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 14:24:56 -04:00

119 KiB
Executable file
Raw Permalink Blame History

title date tags aliases description
Видео 2. Что видит наблюдатель 2026-10-25
zapret2
видео
сценарий
paket
tls
clienthello
sni
Видео 2
Видео 2 серии о Zapret 2: байты пакета и приветствия TLS, имя сайта, игрушечный инспектор и как выглядит разрез, с анимированными схемами.

[!info] О чём заметка Это полный текст видео номер 2 из серии о том, как работает Zapret2/Zapret2, разбитый по сценам, с анимированными схемами. Каждую сцену можно читать отдельно.

[!tip] Как читать пометки «В нашем тесте» значит настоящий запуск в лаборатории проекта: клиент, роутер, сервер. «По коду» значит, что мы прочитали исходники, но не запускали. «По общим сведениям» значит, что это вне нашего кода и лаборатории. О настоящем оборудовании ТСПУ мы ничего не утверждаем: наш «инспектор» это модель.

Повтор и план выпуска

Добро пожаловать во второй выпуск. Начнём с короткого повтора, чтобы его можно было смотреть отдельно от первого.

Когда вы открываете сайт, браузер отправляет серверу запрос. Запрос режется на порции, каждая порция кладётся в конверт с адресом и едет по сети самостоятельно. Порция вместе с конвертом называется пакетом. По дороге пакеты проходят через роутеры, и на этом пути может стоять наблюдатель: оборудование, которое решает, пропускать пакет или нет. В нашей серии мы называем такого наблюдателя ТСПУ и описываем его как модель, как быстрого сортировщика, который не вскрывает конверты, а смотрит в окошко. Настоящее оборудование мы не видели и не знаем, как оно устроено внутри, поэтому всё, что мы говорим о нём, мы говорим осторожно.

В первом выпуске мы выяснили, что наш игрушечный инспектор смотрит на пакеты быстро, и что запрос, разрезанный посередине слова example, прошёл мимо нашего игрушечного инспектора. Но тогда мы только показали, что так бывает. Мы не объяснили, почему. Для этого нужно понять, что же именно наблюдатель видит в пакете: какие у пакета есть поля, где в нём лежит имя сайта, и что остаётся от этого имени, когда пакет разрезан.

В этом выпуске мы разберём настоящий пакет по байтам, поле за полем. Всё будет на настоящих данных нашей лаборатории: почти каждое число, которое вы услышите, мы прочитали в записи сетевого трафика, а остальные посчитали, и об этом мы скажем.

План такой, и он разбит на четыре части. В первой части мы разберём, из каких слоёв состоит пакет, прочитаем слой Ethernet, слой IP, и посчитаем вручную контрольную сумму. Во второй части разберём протокол TCP: порядковые номера, рукопожатие, окно и повторную отправку. В третьей части откроем самое первое сообщение защищённого соединения и найдём в нём имя сайта, байт за байтом. И в четвёртой посмотрим на всё это глазами наблюдателя: как читает пакеты тот, кто смотрит на каждый по отдельности, и как тот, кто собирает поток. В конце мы увидим, как те же самые поля читает zapret2.

!wiki-anim-video02-s01-three-questions-map.svg

Итак, цель этого выпуска: понять, что именно видит наблюдатель на пути пакетов и почему разрезанный запрос может ускользнуть от него. Начинаем с самого начала, то есть с того, из чего сделан пакет.

Слои: матрёшка из конвертов

Начнём с того, что у настоящего пакета нет одного конверта. Их несколько, и они вложены друг в друга, как матрёшка. Посмотрим на тот самый пакет из первого выпуска: запрос на сайт, кадр длиной триста шестнадцать байт.

Сначала договоримся о двух словах. Байт это единица, в которой измеряются данные: одна ячейка, в которую помещается число от нуля до двухсот пятидесяти пяти. Любой текст, любое число, любой адрес в сети это последовательность байтов. Букву w, например, записывают одним байтом со значением сто девятнадцать. А для записи самих байтов на экране мы будем использовать шестнадцатеричную запись.

Что такое шестнадцатеричная запись? В обычной записи у нас десять цифр, от нуля до девяти. В шестнадцатеричной шестнадцать: к цифрам добавлены шесть букв, от а до эф. Буква а означает десять, бэ одиннадцать, цэ двенадцать, дэ тринадцать, е четырнадцать, эф пятнадцать. Зачем так сложно? Потому что байт, то есть число до двухсот пятидесяти пяти, записывается ровно двумя такими цифрами: от ноль ноль до эф эф. Каждый байт занимает на экране одно и то же место, и в длинной ленте байтов легко ориентироваться. Наша буква w, сто девятнадцать, это семь-семь. Слово www в пакете записано как семь-семь, семь-семь, семь-семь. Любые две шестнадцатеричные цифры подряд, с буквами или без, это один байт.

!wiki-anim-video02-s02-bytes-hex.svg

Теперь вернёмся к матрёшке. Слои, от внутреннего к внешнему. В самой середине лежат данные: то, что отправила программа. У нас это запрос на двести пятьдесят байт. Это полезная нагрузка. Этот кусок кладётся в первый конверт, он называется заголовком TCP: у нас он тридцать два байта. В нём записано, как доставить данные надёжно и по порядку, и нас он будет интересовать во второй части. Получившийся кусок кладётся во второй конверт, заголовок IP, двадцать байт. В нём записано, откуда и куда ехать по сети. И наконец всё это кладётся в третий конверт, заголовок Ethernet, четырнадцать байт. Он нужен только для одного шага: от одного соседнего устройства до другого. Такое вкладывание одного в другое называется инкапсуляцией.

Складываем: четырнадцать, двадцать, тридцать два и двести пятьдесят. Выходит триста шестнадцать, ровно столько мы видели в записи. Число из заголовка IP считает всё, кроме Ethernet: двадцать плюс тридцать два плюс двести пятьдесят, это триста два. А кадр, то есть пакет вместе с внешним конвертом Ethernet, длиннее на эти четырнадцать байт. Слово «кадр» относится именно к слою Ethernet. В нашей записи, кстати, нет четырёх байт контрольной суммы кадра, которые, возможно, добавляются в конце на настоящем проводе; для нас это не важно.

!wiki-anim-video02-s02-nested-layers.svg

Зачем столько слоёв? Потому что у каждого слоя свой читатель. Коммутатор, обычное устройство, которое соединяет компьютеры в одной комнате или одном шкафу, читает только внешний конверт. Роутер смотрит глубже, на адрес в IP. Принимающая программа, наконец, открывает самый внутренний слой и читает данные. Каждое устройство читает только то, что ему положено для его работы. Запомните эту мысль: наблюдателю в роли сортировщика тоже нужно не всё, а определённое место пакета. Какое именно, мы выясним к концу выпуска.

Размеры других пакетов нашего соединения тоже полезно знать. Пакет, который начинает соединение, и ответ на него, по семьдесят четыре байта: у них в заголовке TCP больше опций, он занимает сорок байт, а данных нет. Простое подтверждение без данных, шестьдесят шесть байт: четырнадцать, двадцать и тридцать два. Короткий ответ сервера, восемьдесят байт: те же шестьдесят шесть и четырнадцать байт ответа.

Итак, пакет это вложенные конверты. Данные лежат в конверте TCP, он в конверте IP, он в конверте Ethernet. В нашем пакете это двести пятьдесят, тридцать два, двадцать и четырнадцать байт, всего триста шестнадцать. Обычному устройству на пути нужен только свой конверт. А ещё одна вещь, о которой мы будем помнить: любой байт на экране это две шестнадцатеричные цифры.

Слой Ethernet: адрес соседа

Разберём самый внешний конверт, слой Ethernet. Четырнадцать байт, и в них всего три поля.

Первое поле: адрес получателя, шесть байт. Второе: адрес отправителя, тоже шесть байт. Это так называемые MAC-адреса. MAC-адрес это номер сетевой карты, по которому устройства в одной локальной сети отличают друг друга. Он записывается шестью байтами через двоеточия. Это адрес не на всю сеть, а только на ближайший шаг: до соседа, с которым устройство соединено кабелем или Wi-Fi.

Третье поле, два байта, называется EtherType. Оно говорит, что лежит внутри кадра. В нашем кадре это ноль восемь, ноль ноль, и это значит: внутри пакет IP версии четыре. Если бы внутри лежало что-то другое, там было бы другое число. Так принимающее устройство знает, как читать следующие байты.

Посмотрим на настоящий кадр. Первые шесть байт: один-шесть, четыре-дэ, цэ шесть, ноль е, эф девять, один-два. Это MAC-адрес порта роутера со стороны клиента. Следующие шесть: эф е, ноль два, бэ эф, два а, а а, один-семь. Это адрес сетевой карты клиента. И затем ноль восемь ноль ноль. Клиент говорит: «этот кадр для порта роутера, он от меня, внутри IP». Заметьте, что MAC-адрес сервера здесь не встречается. Клиент знает IP-адрес сервера, но путь до него начинается с роутера, и кадр уходит роутеру.

!wiki-anim-video02-s03-ethernet-14-bytes.svg

Что делает роутер? Мы записали тот же самый пакет на выходе из роутера. IP и TCP те же, кроме времени жизни и контрольной суммы IP: о них в пятой сцене. А вот Ethernet-заголовок совсем другой: получатель теперь сетевая карта сервера, отправитель теперь второй порт роутера, шесть и шесть байт, ни одного совпадающего. Роутер снял старый внешний конверт и надел новый. Так работает каждый переход через роутер, или хоп, как говорят сетевые инженеры: на каждом роутере Ethernet-конверт новый, а внутренние остаются. Поэтому MAC-адрес бесполезен для того, кто хочет узнать, кто отправил пакет в другом конце пути: он живёт только один шаг.

!wiki-anim-video02-s03-router-rewrites-ethernet.svg

Небольшое замечание о нашей лаборатории. MAC-адреса в ней создаются заново при каждом запуске, и в другом нашем опыте вы увидели бы другие числа. Устойчива только структура: шесть, шесть и два.

Итак, Ethernet это конверт на один шаг. В нём адрес получателя, адрес отправителя и два байта, которые говорят, что внутри IP. На каждом роутере его снимают и надевают новый, а всё, что внутри, остаётся прежним. Так что в нашей модели всё самое интересное для наблюдателя находится в более глубоких слоях.

Слой IP: заголовок поле за полем

Теперь следующий слой: заголовок IP. Это конверт для всей сети целиком, и в нашем пакете он занимает ровно двадцать байт. Разберём каждое поле. Вот эти двадцать байт в шестнадцатеричной записи.

!wiki-anim-video02-s04-ip-header-fields.svg

Первый байт: четыре и пять. Это два поля по половине байта. Четвёрка это версия протокола: наш пакет IP версии четыре. Существует и шестая версия, там заголовок устроен иначе, но в нашей лаборатории везде четвёрка. Пятёрка означает длину заголовка в особых единицах, по четыре байта: пять раз по четыре это двадцать. Бывают заголовки с дополнительными полями, тогда это число больше, и принимающий знает, где заканчивается заголовок и начинается следующий слой. В нашем случае дополнительных полей нет.

Второй байт: ноль ноль. Он содержит служебные пометки о приоритете и о перегрузке сети, их роутеры могут учитывать или нет. У нас там нули, и мы к этому байту возвращаться не будем. Дальше два байта: ноль один, два е. Это длина всего пакета в байтах, считая этот заголовок: триста два. То, о чём мы говорили: двадцать плюс тридцать два плюс двести пятьдесят. Заметьте, что по этому числу устройство знает, где пакет заканчивается, даже если после него в буфере лежит какой-то мусор.

Следующие два байта: эф а, ноль пять, то есть шестьдесят четыре тысячи пять. Это идентификатор. Наш клиент нумерует свои пакеты по порядку, и идентификаторы его пакетов идут подряд: шестьдесят четыре тысячи три у первого пакета, шестьдесят четыре тысячи четыре у второго, шестьдесят четыре тысячи пять у нашего с данными, и так далее. Идентификатор в протоколе IP нужен для сборки кусков, если пакет был разделён, и сам по себе ничего не гарантирует: это не порядковый номер данных, о которых мы поговорим во второй части.

Дальше два байта: четыре ноль, ноль ноль. Это флаги и смещение фрагмента. Давайте разберём. Старшие три бита этого числа: ноль, один, ноль. Первый бит зарезервирован. Второй бит это флаг DF, от слов «don't fragment», «не делить». Он стоит, и это значит: если пакет не помещается в кадр на каком-то участке пути, не нужно делить его на части, нужно его выбросить и сообщить отправителю. Третий бит говорит, что за этим куском идут ещё куски, и у нас он ноль. Остальные тринадцать бит это смещение куска внутри исходного пакета, у нас ноль. Фрагментация, то есть деление пакета на куски на лету, в нашем соединении не происходит нигде: у всех пакетов стоит DF.

!wiki-anim-video02-s04-ip-numbers-in-bits.svg

Следующий байт: четыре ноль, это шестнадцатеричная запись числа шестьдесят четыре. Это время жизни, англоязычное сокращение TTL. Каждый роутер при передаче вычитает единицу, а пакет с нулём выбрасывается. Так сеть защищается от пакетов, которые ходят по кругу вечно. Начальное значение выбирает отправитель; у нашего клиента и у нашего сервера оно шестьдесят четыре. Почему именно столько: обычно это значение по умолчанию в операционной системе, а не закон. Мы поговорим об этом подробнее в следующей сцене.

Дальше байт ноль шесть: протокол следующего слоя. Шесть означает TCP. Для другого протокола, например, UDP, который тоже встретится в нашей серии, там было бы число семнадцать. Это такое же указание, как EtherType во внешнем конверте, только на уровень глубже: что лежит внутри.

Потом два байта, два бэ и бэ эф: контрольная сумма заголовка, и ей мы посвятим отдельную сцену. И наконец, адреса. Четыре байта ноль а, ноль один, ноль ноль, ноль один: десять, один, ноль, один, это адрес клиента. Четыре байта ноль а, ноль два, ноль ноль, ноль два: десять, два, ноль, два, это адрес сервера. Адрес в IP это четыре байта, которые мы привыкли записывать четырьмя числами через точки. Это номер устройства в сети, и именно по нему роутер выбирает дорогу.

Итак, заголовок IP это двадцать байт, в которых записано: версия, длина заголовка, длина пакета, идентификатор, запрет деления, время жизни, номер протокола внутри и два адреса. Всё, что нужно роутеру, чтобы доставить пакет, и всё, что нужно наблюдателю, чтобы понять, откуда и куда он едет. Но ни слова о том, какой сайт.

Время жизни и контрольная сумма: считаем вручную

В прошлой сцене мы лишь коротко упомянули два поля заголовка IP: время жизни и контрольную сумму. Они связаны между собой, и на них очень хорошо видно, что именно делает роутер с пакетом.

Начнём со времени жизни. У клиента шестьдесят четыре, у сервера тоже. Но у нашего пакета, который дошёл до сервера, уже шестьдесят три. Роутер вычел единицу. Мы записали этот же пакет в четырёх точках: на клиенте шестьдесят четыре, на входе в роутер шестьдесят четыре, на выходе шестьдесят три, на сервере шестьдесят три. И для пакетов сервера то же самое: на сервере шестьдесят четыре, на клиенте шестьдесят три. Время жизни это запас переходов, который уменьшается на каждом роутере.

!wiki-anim-video02-s05-ttl-four-points.svg

Но если роутер изменил заголовок, он обязан был и пересчитать поле, которое защищает его от ошибок. Это и есть контрольная сумма.

Что такое контрольная сумма? Это короткое число, которое вычисляется из содержимого заголовка по строгому правилу и записывается в него же. Получатель считает по этому же правилу и сравнивает. Если число не сошлось, значит, при передаче какой-то байт испортился, и пакет выбрасывается. Это защита от случайных ошибок: от помех на линии или сбоя в устройстве. Не от злоумышленника: тот, кто изменил пакет, может заново посчитать сумму так же легко, как и мы сейчас.

Посчитаем контрольную сумму нашего заголовка, и сделаем это так, как это делает программа. Берём двадцать байт и делим их на десять чисел по два байта. Поле контрольной суммы на время подсчёта заменяем нулями. Получаем десять чисел: четыре пять ноль ноль, ноль один два е, эф а ноль пять, четыре ноль ноль ноль, четыре ноль ноль шесть, ноль ноль ноль ноль, ноль а ноль один, ноль ноль ноль один, ноль а ноль два и ноль ноль ноль два. Складываем их по очереди. Сумма растёт: четыре шесть два е, один четыре ноль три три, один восемь ноль три три, и так далее. В конце получается: один, дэ, четыре, три, эф. Это уже не помещается в два байта, лишняя единица впереди.

Правило такое: лишнюю единицу в начале переносим в конец, то есть прибавляем её к остатку. Один плюс дэ четыре три эф равно дэ четыре четыре ноль. Затем делаем последний шаг: каждый бит числа переворачиваем, нулевой становится единицей и наоборот. Это называется дополнением до единицы. Из дэ четыре четыре ноль получается два бэ бэ эф. И это ровно то число, которое записано в нашем заголовке. Так определяется правильное значение, и каждый получатель проверяет именно его.

А теперь вернёмся к роутеру. После вычитания единицы пятое из десяти чисел, то самое, где живёт время жизни, стало не четыре ноль ноль шесть, а три эф ноль шесть: оно уменьшилось на шестнадцатеричное один-ноль-ноль, то есть на двести пятьдесят шесть. Сумма уменьшилась на столько же, а после переворота битов контрольная сумма, наоборот, выросла: два бэ бэ эф стало два цэ бэ эф. И в нашей записи на стороне сервера мы видим именно это число. Роутер не просто вычел единицу, он пересчитал контрольную сумму в заголовке.

!wiki-anim-video02-s05-checksum-by-hand.svg

Ещё две вещи, которые пригодятся нам позже. Первая: контрольная сумма TCP в этом же пакете не изменилась, потому что она не охватывает время жизни. Вторая: когда zapret2 строит новый или изменённый пакет, ему тоже надо посчитать эту сумму. В его коде для этого есть отдельная функция, которая обнуляет поле, считает сумму по всему заголовку IP, у нас это двадцать байт и записывает результат. Мы вернёмся к ней в четвёртой части.

Итак, время жизни уменьшается на единицу на каждом роутере, и роутер при этом пересчитывает контрольную сумму заголовка. Сумма это защита от случайной порчи: сложить пары байт, перенести лишнюю единицу и перевернуть биты. В нашем пакете получилось два бэ бэ эф, и после роутера два цэ бэ эф.

Повтор и зачем нужен TCP

Во второй части разберём самый интересный для нас слой: TCP. Начнём с короткого повтора, чтобы эту часть можно было смотреть отдельно.

Пакет это вложенные конверты. Данные лежат в конверте TCP, тот лежит в конверте IP, а тот в конверте Ethernet. Ethernet нужен для одного шага до соседа. IP нужен, чтобы довезти пакет по адресу через множество роутеров. Вместе они образуют то, что можно назвать почтовой сетью, и у этой сети есть неприятное свойство: она ничего не обещает. Пакет может потеряться. Два пакета могут прийти не в том порядке, в котором их отправили. Один и тот же пакет может прийти дважды. Обычный роутер не обязан об этом сообщать и не помнит, что он пересылал минуту назад.

Но когда вы открываете сайт, вы хотите получить всю страницу, целиком и по порядку. Значит, за эту надёжность должен отвечать кто-то другой, не сеть. Этот кто-то и есть протокол TCP. Протокол это набор правил, о которых две стороны договариваются заранее. TCP это правила, по которым отправитель и получатель вдвоём делают из ненадёжной сети надёжный канал: данные приходят все, без повторов и в нужном порядке.

!wiki-anim-video02-s06-post-no-guarantees.svg

Как это можно сделать? Придумать нужно три вещи. Первая: каждому байту данных присвоить номер, чтобы получатель мог расставить их по порядку и заметить, если какого-то нет. Вторая: получатель должен сообщать отправителю, до какого места он всё получил. Третья: если сообщения о получении нет долго, отправитель должен отправить те же байты ещё раз. Основа протокола TCP это эти три идеи, и поля его заголовка нужны именно для них.

Здесь и скрывается ответ на вопрос, который мы задали в первой части. Если получатель расставляет байты по номерам, ему всё равно, в скольких пакетах они пришли, и в каком порядке. Запрос из двухсот пятидесяти байт он соберёт и из одного пакета, и из двух, и из десяти. А вот наблюдатель, который не собирает байты, а смотрит на каждый пакет по отдельности, увидит в них только куски. Это мы и проверим на настоящих номерах к концу этой части.

!wiki-anim-video02-s06-three-ideas.svg

Итак, IP не гарантирует ничего, а надёжность создаёт TCP: он нумерует байты, получатель подтверждает, что получил, а отправитель повторяет, если подтверждения нет. Вторая часть выпуска объясняет, как именно это записано в заголовке, и на каких числах это видно.

Заголовок TCP: поле за полем

Разберём заголовок TCP нашего пакета. Тридцать два байта, и в них записано всё, что нужно двум сторонам для надёжного разговора. Идём по полям слева направо.

!wiki-anim-video02-s07-header-fields.svg

Первые четыре байта: два порта. Девять девять, три четыре это тридцать девять тысяч двести двадцать: порт отправителя, один из временных портов, которые браузер открывает для разговора. Ноль один, бэ бэ это четыреста сорок три: порт получателя, на котором сайт принимает защищённые соединения. Адрес IP выбирает устройство, порт выбирает программу на нём.

Следующие четыре байта: бэ цэ девять бэ, бэ четыре один ноль. Это порядковый номер, или по-английски sequence number. Это число равно три миллиарда сто шестьдесят четыре миллиона триста двадцать тысяч семьсот восемьдесят четыре. Оно присвоено первому байту данных в этом пакете. Что такое порядковый номер? Это адрес байта внутри потока: поток, который отправляет клиент, нумеруется от начального числа, и каждый байт получает следующий номер. О том, откуда берётся начальное число, мы поговорим в следующей сцене.

Следующие четыре байта: а девять два девять, семь пять три два, это два миллиарда восемьсот тридцать восемь миллионов шестьдесят шесть тысяч четыреста восемьдесят два. Это номер подтверждения, ACK. Он нужен для обратной связи и означает: «все байты потока от тебя до этого номера, не включая его, мной получены; следующий байт, который я жду, это байт с этим номером». Подробно эту пару, порядковый номер и номер подтверждения, мы разберём на живых числах, а сейчас запомним только, что ACK это не номер пакета, а номер следующего ожидаемого байта.

Дальше полбайта: восемь. Это длина заголовка TCP, и она, как и в IP, считается в словах по четыре байта: восемь раз по четыре это тридцать два. Из них двадцать это обязательная часть, а остальные двенадцать это опции, о которых мы скоро поговорим. Благодаря этому числу получатель знает, где заканчивается заголовок и начинаются данные.

Потом байт один-восемь: флаги. Это набор переключателей, по одному биту. Бит ACK означает «в этом пакете есть валидный номер подтверждения». Бит PSH означает «передай данные программе, не жди, пока накопится побольше». Бит SYN начинает соединение, бит FIN завершает его, а бит RST обрывает его резко, и этот бит мы запомним: именно его использует игрушечный инспектор из первого выпуска в одном из своих режимов. Ещё три бита нас пока не интересуют. В нашем пакете один-восемь означает, что включены ACK и PSH: это обычный пакет с данными посреди разговора.

!wiki-anim-video02-s07-flags-bits.svg

Дальше два байта: ноль ноль, три эф. Это окно: число шестьдесят три. Окно сообщает отправителю, сколько данных можно прислать, не дожидаясь подтверждения. Число шестьдесят три на самом деле умножается на множитель, о котором стороны договорились в начале соединения, и в итоге получается шестьдесят четыре тысячи пятьсот двенадцать байт. Подробнее об этом в десятой сцене.

Затем два байта контрольной суммы TCP: шесть девять семь ноль. Она работает так же, как в IP, но охватывает больше: заголовок TCP, данные, и несколько полей из заголовка IP. И последние два байта обязательной части, urgent-указатель: два нуля. Это старый механизм срочных данных, он почти не используется, флаг, который его включает, у нас выключен.

Наконец, двенадцать байт опций: один, один, ноль восемь, ноль а и ещё восемь байт. Это два пустых байта для выравнивания и опция «метка времени», в которой клиент записывает свои часы, а сервер потом возвращает их. Опций бывает много, и в начале соединения их больше, чем в обычном пакете: об этом тоже в следующей сцене.

Итак, заголовок TCP это порты, порядковый номер первого байта, номер следующего ожидаемого байта, длина заголовка, флаги, окно, контрольная сумма и опции. В нашем пакете: порты тридцать девять тысяч двести двадцать и четыреста сорок три, флаги ACK и PSH, окно шестьдесят три и тридцать два байта заголовка. Но больше всего нас будут занимать два числа: порядковый и подтверждения.

Флаги и рукопожатие на настоящих числах

Теперь посмотрим на TCP в работе. Прежде чем данные поедут, стороны должны договориться. Это называется рукопожатием, и именно с него начинается каждое соединение.

Рукопожатие состоит из трёх пакетов. Мы записали их в лаборатории. Первый пакет клиента: флаг SYN, от английского «synchronize», «согласовать номера». Клиент говорит: «давай начнём разговор, мои байты я буду нумеровать начиная с этого числа, а сам SYN займёт его первым». Это число называется начальным порядковым номером. У нашего клиента оно три миллиарда сто шестьдесят четыре миллиона триста двадцать тысяч семьсот восемьдесят три, а в записи оно хранится в поле порядкового номера. Начальное число не ноль, а каждый раз выбирается непредсказуемо, в том числе чтобы сторонний человек не мог легко подставить свои пакеты в чужое соединение. Данных в этом пакете нет, и поэтому он короткий: семьдесят четыре байта.

Второй пакет, от сервера, через тридцать две миллионные доли секунды: флаги SYN и ACK. Сервер отвечает: «согласен, мои байты я буду нумеровать с вот этого числа, два миллиарда восемьсот тридцать восемь миллионов шестьдесят шесть тысяч четыреста восемьдесят один, а твой начальный номер я принял, и жду от тебя следующий». В поле подтверждения у него три миллиарда сто шестьдесят четыре миллиона триста двадцать тысяч семьсот восемьдесят четыре. Это на единицу больше начального номера клиента.

Почему на единицу? Потому что пакет SYN, хоть и без данных, засчитывается в нумерации как один байт. Правило такое: SYN занимает один порядковый номер, и поэтому следующий ожидаемый номер на единицу больше. Первый настоящий байт данных клиента получит номер три миллиарда сто шестьдесят четыре миллиона триста двадцать тысяч семьсот восемьдесят четыре. И, значит, пакет с данными, который мы разбирали в прошлой сцене, имел ровно этот номер. Теперь понятно, откуда он взялся.

Третий пакет, снова от клиента, через сорок пять миллионных долей секунды от начала: флаг ACK. Клиент отвечает: «слышу, жду твои байты начиная с номера два миллиарда восемьсот тридцать восемь миллионов шестьдесят шесть тысяч четыреста восемьдесят два». С этого момента соединение установлено, и можно отправлять данные. Весь этот обмен в нашей лаборатории занял сорок пять миллионных долей секунды, потому что клиент и сервер стоят рядом; в настоящем интернете это десятки миллисекунд.

!wiki-anim-video02-s08-three-packets.svg

Теперь о том, что лежит в опциях первых двух пакетов. Именно в рукопожатии стороны сообщают друг другу о своих возможностях. В нашем SYN двадцать байт опций. Первая опция: ноль два, ноль четыре, ноль пять, бэ четыре. Это число тысяча четыреста шестьдесят, и называется оно MSS, максимальный размер порции данных, которую клиент готов принять в одном пакете. Оно получилось так: наибольший размер IP-пакета, полторы тысячи байт, минус двадцать байт заголовка IP и минус двадцать байт заголовка TCP. Вторая опция: ноль четыре, ноль два. Она означает, что клиент умеет сообщать о выборочном подтверждении, и мы встретим его в конце части. Третья: метка времени, клиентские часы. Четвёртая: пустой байт для выравнивания. Пятая: ноль три, ноль три, ноль а. Это масштаб окна, число десять, о нём в десятой сцене. Сервер в своём ответе называет те же пять.

!wiki-anim-video02-s08-syn-options.svg

Итак, рукопожатие это три пакета: SYN, SYN-ACK, ACK. В них стороны выбирают начальные номера, и в нашем примере клиент начал с трёх миллиардов ста шестидесяти четырёх миллионов трёхсот двадцати тысяч семисот восьмидесяти трёх, а сервер с двух миллиардов восьмисот тридцати восьми миллионов шестидесяти шести тысяч четырёхсот восьмидесяти одного. SYN занимает один номер, поэтому данные начинаются на единицу позже. И в тех же пакетах стороны называют свои возможности: максимальный размер порции, масштаб окна и другие.

Нумерованные байты: порядковый номер и подтверждение

В предыдущей сцене мы увидели начало соединения. Теперь посмотрим на его середину и конец и разберёмся, как работают два числа: порядковый номер и номер подтверждения.

Чтобы числа были короче, давайте договоримся считать их относительно начального: на единицу больше начального это номер один. Так делают и многие программы анализа, и сама программа zapret2 хранит номера именно так. Первый байт запроса клиента получает номер один, второй номер два, и так далее. В нашем запросе двести пятьдесят байт, и они занимают номера от первого до двести пятидесятого.

[!warning] Уточнение к видео В видео сказано, что сама программа zapret2 хранит номера относительно начального. Это неточно: в коде программа хранит сырые порядковые номера, а относительные числа здесь только для удобства чтения. Договорённость «начальный номер плюс один равен единице» относится к нашим таблицам и рисункам, не к устройству программы.

Теперь пакет с данными. Порядковый номер у него один: он начинается с первого байта. Длина двести пятьдесят. А номер подтверждения тоже один: от сервера пока пришёл только SYN-ACK, он занял нулевой номер, данных ещё не было, и следующий байт, который клиент ждёт от сервера, это первый. Теперь следим за ответом сервера. Через двенадцать миллионных долей секунды после данных приходит пакет с флагом ACK и номером подтверждения двести пятьдесят один. Сервер говорит: «все байты с первого по двести пятидесятый получены, жду двести пятьдесят первый». Вот что такое номер подтверждения: он равен последнему полученному байту плюс один. Это именно номер следующего байта, которого мы ждём.

Посмотрим, как это работает в обратную сторону. Сервер отвечает коротким сообщением в четырнадцать байт, и они получают номера от первого до четырнадцатого, уже в потоке сервера: у него своя нумерация. Клиент подтверждает: «жду пятнадцатый». А потом соединение закрывается. Флаг FIN, от слова «финиш», означает «у меня больше нет данных»; он тоже занимает один номер. Клиент отправляет его с номером двести пятьдесят один, потому что это следующий свободный номер в его потоке, и сервер подтверждает двести пятьдесят два. Сервер отправляет свой FIN с номером пятнадцать, и клиент подтверждает шестнадцать. Десять пакетов, и в каждом можно проследить эти числа.

!wiki-anim-video02-s09-ten-packets.svg

А теперь вернёмся к имени сайта. В запросе имя www.example.com занимает байты с девяносто седьмого по сто одиннадцатый, если считать с нуля. Если считать так, как мы договорились в этой сцене, то есть с единицы, это байты с девяносто восьмого по сто двенадцатый. Значит, у первой буквы w порядковый номер девяносто восемь, у последней буквы m сто двенадцать. Это важный момент: имя сайта это не просто текст в пакете, а пятнадцать байтов с вполне определёнными номерами в потоке. И когда пакет разрезан на две части, эта лента разрезается на два участка по номерам.

Давайте посмотрим на разрезанный запрос. Это наш опыт с разрезанием в середине слова example. Первый пакет несёт сто четыре байта, это байты с первого по сто четвёртый, и его порядковый номер один. Второй пакет несёт сто сорок шесть байт, со ста пятого по двести пятидесятый, и его порядковый номер сто пять. В первом пакете остался кусок имени www.exa, семь байт, а во втором mple.com, восемь. Приёмник видит, что второй пакет начинается ровно там, где закончился первый, и склеивает их. В итоге все двести пятьдесят байт на месте, ничего не потеряно, и запрос получен целиком.

!wiki-anim-video02-s09-ribbon-name-cut.svg

Итак, TCP нумерует байты, а не пакеты. Подтверждение это номер следующего ожидаемого байта: после двухсот пятидесяти байт сервер подтвердил двести пятьдесят один. Если данные пришли в двух пакетах, приёмник ставит каждый на своё место по номеру и склеивает. Имя сайта в нашем запросе это байты с девяносто восьмого по сто двенадцатый.

Размер порции, окно и контрольная сумма TCP

В этой сцене три коротких вопроса про TCP, и все они пригодятся нам дальше. Как выбирается размер порции? Что такое окно? И как считается контрольная сумма TCP?

Размер порции. Мы видели, что в SYN клиент называет число тысяча четыреста шестьдесят: MSS. IP-пакет, как мы помним, ограничен примерно полутора тысячами байт, из этого вычитается двадцать байт заголовка IP и двадцать байт минимального заголовка TCP. Поэтому в одном пакете помещается не больше тысячи четырёхсот шестидесяти байт данных. Если в пакете есть опция метки времени, а она есть в каждом пакете нашего соединения после рукопожатия, то ещё двенадцать байт уходят на неё, и остаётся тысяча четыреста сорок восемь.

Теперь пример. В нашей лаборатории есть и второй запрос, длиннее: тысяча четыреста восемьдесят четыре байта. Один пакет столько не вместит. Ядро операционной системы режет запрос по этому правилу: первый пакет получает тысячу четыреста сорок восемь байт, и полный кадр у него тысяча пятьсот четырнадцать байт. Второй пакет получает оставшиеся тридцать шесть байт: кадр сто два. Порядковый номер первого один, второго тысяча четыреста сорок девять, и сумма длин ровно тысяча четыреста восемьдесят четыре. Здесь разрезание делает сама система, не zapret2 и не мы. Запомним это на будущее.

Окно. Каждый раз, получая пакет, приёмник сообщает, сколько байт он готов принять дальше, не дожидаясь подтверждения. Это его свободное место в приёмном буфере. Если окно маленькое, отправитель присылает мало и ждёт подтверждения. Если большое, присылает много и разгоняет канал. Число в поле окна занимает всего два байта, поэтому больше шестидесяти пяти тысяч оно быть не может. Чтобы это обойти, в начале соединения стороны договариваются о множителе: это та самая опция масштаба, десять. Теперь значение окна нужно умножить на два в десятой степени, на тысячу двадцать четыре. У клиента окно шестьдесят три, и это шестьдесят четыре тысячи пятьсот двенадцать байт, у сервера шестьдесят четыре, и это шестьдесят пять тысяч пятьсот тридцать шесть. Сам SYN всегда несёт окно без множителя, поэтому у него шестьдесят четыре тысячи двести сорок.

!wiki-anim-video02-s10-mss-window.svg

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

Посчитаем для нашего пакета. Длина TCP-части: тридцать два плюс двести пятьдесят, двести восемьдесят два. Псевдозаголовок даёт один, пять, два, шесть. Заголовок TCP с обнулённым полем суммы даёт пять бэ один е девять. Данные, двести пятьдесят байт, дают два ноль цэ эф пять а. Складываем всё и получаем два шесть девять шесть шесть девять. Переносим старшие цифры два шесть в конец и получаем девять шесть восемь эф. После переворота битов остаётся шесть девять семь ноль. Это в точности то число, которое записано в заголовке.

!wiki-anim-video02-s10-checksum-sum.svg

Итак, MSS это максимальная порция данных в пакете: у нас тысяча четыреста шестьдесят, а с меткой времени тысяча четыреста сорок восемь. Длинный запрос в тысячу четыреста восемьдесят четыре байта ядро режет на тысячу четыреста сорок восемь и тридцать шесть. Окно это размер приёмного места, и оно умножается на множитель, договорённый в начале. Контрольная сумма TCP охватывает заголовок, данные и псевдозаголовок.

Повторная отправка и итог части

Остался один из трёх механизмов TCP: повторная отправка. Её хорошо видно на опыте первого выпуска, где игрушечный инспектор выбрасывал запрос.

Напомним, что там происходило. Клиент отправляет запрос, двести пятьдесят байт, и роутер молча выбрасывает пакет. Подтверждения нет. Что делает клиент? У TCP есть таймер: отправляя данные, он запускает отсчёт, и если ответа не пришло, отправляет те же самые байты ещё раз. Это и называется повторной отправкой.

Посмотрим на настоящие числа. Первая отправка через полторы миллисекунды после начала. Вторая через двести десять миллисекунд. Те же байты, тот же порядковый номер: два миллиарда пятьсот шестьдесят шесть миллионов четыреста двадцать две тысячи триста двадцать девять, новые только идентификатор IP и метка времени. Потом через двести шестнадцать. Потом через четыреста двадцать. Потом через восемьсот тридцать. Потом через тысячу шестьсот шестьдесят семь. Всего шесть пакетов, и все шесть увидел инспектор в роутере, а за роутером, на стороне сервера, не появилось ни одного из этих шести пакетов. Сервер получил ноль байт.

Обратите внимание на ритм. Первая пауза около двухсот десяти миллисекунд, это в тысячи раз больше, чем путь пакета от клиента до сервера в нашей лаборатории: значит, это не задержка сети, а таймер. Вторая пауза почти такая же, около двухсот шестнадцати, а дальше паузы удваиваются: четыреста двадцать, восемьсот тридцать, тысяча шестьсот шестьдесят семь. Такая стратегия называется экспоненциальной задержкой: обычно так делают, чтобы не заваливать сеть повторами, если с ней что-то не в порядке. Конкретные числа зависят от операционной системы, а не только от протокола; это то, что делает именно наш клиент.

!wiki-anim-video02-s11-retransmit-timeline.svg

Теперь противоположный пример: повтор, когда никто ничего не терял. В опыте с большим запросом короткий кусок, тридцать шесть байт, ушёл в сеть, сервер подтвердил весь запрос, и вдруг этот же кусок с тем же порядковым номером пришёл ещё раз. Что делает сервер? Он узнаёт, что эти байты у него уже есть, отбрасывает дубль и отвечает подтверждением с особой опцией, в которой записано: «этот диапазон байт, от такого номера до такого, я получил повторно». Такая пометка называется D-SACK. Для нашей серии здесь важно только одно: приёмник по номерам сам замечает повторы и лишнее выбрасывает. Зачем клиент отправил дубль, мы в записи не выясняли.

!wiki-anim-video02-s11-dsack-recap.svg

Теперь соберём всю часть вместе. Что такое нумерация байтов, мы увидели. Подтверждение это следующий ожидаемый байт, повторная отправка это ответ на молчание. И из этого следует главное: приёмнику безразлично, как нарезаны байты по пакетам, потому что он собирает по номерам. Наш приёмник собирает запрос целиком, как бы его ни резали. Инспектор, который не собирает поток, увидит только куски; собирает ли поток настоящее оборудование, мы не знаем.

Повтор и первое сообщение защищённого соединения

В третьей части мы откроем то самое сообщение, которое клиент отправил в нашем запросе, и найдём в нём имя сайта. Сначала повтор, чтобы часть можно было смотреть отдельно.

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

Современные сайты работают по защищённому варианту HTTP, а под ним лежит протокол шифрования, он называется TLS. Его цель: чтобы всё, что вы отправляете на сайт и получаете с сайта, было зашифровано. Шифрование это преобразование данных по секретному ключу так, чтобы без ключа их нельзя было прочитать. Тот, кто стоит на пути пакетов, увидит поток бессмысленных байтов. Ключ знают только клиент и сервер.

Но здесь есть проблема, из которой вытекает всё остальное. Ключа ещё нет. Чтобы договориться о ключе, клиент и сервер должны обменяться несколькими сообщениями, и самое первое из них не может быть зашифровано, потому что шифровать его пока нечем. Это называется рукопожатием TLS, как рукопожатие TCP, но уже на уровне данных. Первое из таких сообщений отправляет клиент: оно называется ClientHello, «привет от клиента». В нём клиент сообщает, что умеет, и какие у него предложения. И именно это сообщение мы с вами видели как двести пятьдесят байт данных в нашем пакете.

!wiki-anim-video02-s12-before-after-keys.svg

Что же там есть интересного для наблюдателя? Клиент должен сообщить серверу не только свои возможности, но и то, какой сайт ему нужен. Почему? Потому что по одному IP-адресу может отвечать множество сайтов. Представьте дом с одним адресом, где живёт сто компаний. Курьер приходит в этот дом, и ему надо сказать, к какой компании. Для этого в сообщении ClientHello есть поле с названием желаемого сайта. Оно называется SNI, от английского «Server Name Indication», «указание имени сервера». В нашем примере там записано www.example.com.

И вот в этом всё дело. Поле SNI записано открыто, потому что оно передаётся до того, как появился ключ. Тот, кто стоит на пути, как правило, может прочитать имя сайта в обычном ClientHello, не расшифровывая ничего. Он не увидит ни страницу, ни то, что вы там ищете, всё это зашифровано позже. Но название сайта, к которому вы пришли, открыто. Это и есть то окошко с адресом на конверте, которое смотрит наш сортировщик.

!wiki-anim-video02-s12-one-address-many-sites.svg

Итак, TLS шифрует данные, но ключа ещё нет, когда клиент отправляет первое сообщение, ClientHello. В нём клиент называет сайт, и это поле называется SNI. Оно открыто, и потому наблюдатель может его прочитать. Дальше мы разберём это сообщение байт за байтом и найдём, где именно лежит имя сайта.

ClientHello, байт за байтом: заголовки

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

!wiki-anim-video02-s13-hex-fields.svg

Первые пять байт: шестнадцать, ноль три, ноль один, ноль ноль, эф пять. Это заголовок записи. Запись это порция, в которую TLS упаковывает свои сообщения, и у неё есть свой небольшой заголовок. Первый байт, шестнадцатеричное шестнадцать, то есть число двадцать два, означает: «это сообщение рукопожатия». Два следующих байта, ноль три, ноль один, это версия, записанная по историческим причинам как TLS один-ноль, и на самом деле почти ничего не значит: настоящая версия называется позже, в расширениях. Последние два байта, ноль ноль, эф пять, это число двести сорок пять. Это длина записи: всё, что идёт дальше. Проверим: двести пятьдесят минус пять равно двести сорок пять. Совпало.

Следующие четыре байта, смещения с пятого по восьмой: ноль один, ноль ноль, ноль ноль, эф один. Первый байт, один, означает: «это ClientHello». Следующие три байта это длина сообщения: число двести сорок один. Двести сорок пять минус четыре равно двести сорок один. Две длины, одна вложена в другую, как конверт в конверте.

Дальше два байта, ноль три, ноль три: ещё одна историческая версия, TLS один-два. И затем тридцать два байта случайного числа, смещения с одиннадцатого по сорок второй: вот они, ноль бэ, девять один, четыре шесть, и так далее. Случайное число нужно для выработки ключа, и сервер добавит к нему своё. Для нашей задачи в нём нет ничего полезного.

Потом идёт один байт: два ноль. Это длина идентификатора сессии, тридцать два. И следующие тридцать два байта, смещения с сорок четвёртого по семьдесят пятый, это сам идентификатор. Он нужен для технической совместимости, и опять же мы можем пропустить его целиком. Важно только одно: наблюдатель, который ищет поле дальше по сообщению, должен прочитать длину и перепрыгнуть через эти тридцать два байта. Позиции дальнейших полей зависят от этой длины.

Дальше список наборов шифров. Два байта длины, ноль ноль, ноль шесть: шесть байт. Это три набора по два байта: один три ноль два, один три ноль три, один три ноль один. Набор шифров это название способа шифрования, о котором клиент готов договориться. Он говорит серверу: «могу так, так или так, выбирай». В нашем лабораторном сообщении их всего три, в браузере их обычно гораздо больше. Затем идёт список способов сжатия: байт ноль один и байт ноль ноль, это значит «один способ, и он без сжатия».

Наконец, смещения восемьдесят шесть и восемьдесят семь: ноль ноль, а два. Это длина расширений, сто шестьдесят два. Все остальные байты сообщения, с восемьдесят восьмого по двести сорок девятый, ровно сто шестьдесят два, заняты расширениями. Восемьдесят восемь плюс сто шестьдесят два равно двести пятьдесят: конец данных. Сумма сходится.

!wiki-anim-video02-s13-length-walk.svg

Итак, ClientHello начинается с заголовка записи, потом идёт заголовок рукопожатия, версия, случайное число, идентификатор сессии, наборы шифров и способы сжатия. Все эти поля устроены как «длина и содержимое». Поэтому, чтобы добраться до расширений, где лежит имя сайта, нужно по очереди прочитать эти длины и перепрыгнуть через содержимое. В нашем сообщении расширения начинаются со смещения восемьдесят восемь.

Расширения ClientHello

Расширения занимают большую часть сообщения, сто шестьдесят два байта из двухсот пятидесяти. Расширение это самостоятельный блок с типом, длиной и данными. Все блоки устроены одинаково: два байта типа, два байта длины и столько байт данных, сколько сказано в длине. Поэтому программа читает их по очереди: прочитала тип, прочитала длину, перепрыгнула на следующий.

В нашем сообщении их одиннадцать. Пройдём по списку. Первое, по смещению восемьдесят восемь, это имя сервера, наше SNI. Его мы разберём в следующей сцене отдельно.

Второе и третье расширения, по смещениям сто двенадцать и сто двадцать, это форматы точек кривых и поддерживаемые группы: клиент перечисляет математические основания для обмена ключами. У нас одна группа, ноль ноль один дэ. Следующие три расширения, с коротким названием вроде «билет сессии», совсем пустые: четыре байта заголовка и ни одного байта данных. Они означают «я это умею». Дальше список поддерживаемых алгоритмов подписи, двадцать штук, это сорок два байта.

Особого внимания заслуживают три расширения. Одно из них, supported_versions, лежит по смещению сто восемьдесят шесть и занимает семь байт вместе с заголовком; данные в нём такие: ноль два, ноль три, ноль четыре. Это список версий, который поддерживает клиент: ноль три ноль четыре, то есть TLS версии один-три. Вот где записана настоящая версия, а не в заголовках, которые мы видели в прошлой сцене. Другое, режимы обмена ключами, два байта данных. И третье, самое большое из этих трёх, key_share, тридцать восемь байт данных. В нём клиент присылает свою долю будущего ключа: группа ноль ноль один дэ, длина тридцать два, а потом сами тридцать два байта открытой части. Эта часть выглядит случайной, и именно поэтому все ключи разные.

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

!wiki-anim-video02-s14-extension-list.svg

Проверим арифметику, как это делает программа. Длины блоков вместе с заголовками: двадцать четыре, восемь, восемь, четыре, четыре, четыре, сорок шесть, семь, шесть, сорок два, девять. Складываем: получается сто шестьдесят два. Это в точности то число, которое записано в поле длины всех расширений. Если разрезать сообщение посередине, эта проверка не сойдётся.

И одно важное наблюдение. В нашем клиенте имя сервера идёт первым в списке. Но это особенность этого клиента. Другие клиенты могут ставить расширения в другом порядке, и некоторые перемешивают его намеренно. Поэтому нельзя сказать «имя всегда в начале»: программе нужно пройти список по длинам и найти блок с типом ноль.

!wiki-anim-video02-s14-hops-and-sum.svg

Итак, расширения это блоки «тип, длина, данные». В нашем сообщении их одиннадцать, они занимают байты с восемьдесят восьмого по двести сорок девятый, а имя сайта лежит в самом первом. Наблюдатель, который хочет его найти, не может надеяться на постоянную позицию, а должен пройти по длинам.

Имя сайта: поле SNI и заголовок Host

Теперь возьмём расширение с именем сайта и разберём его по байтам. Это самый важный блок для всей нашей серии.

Он начинается по смещению восемьдесят восемь. Два байта, ноль ноль, ноль ноль: тип «имя сервера». Два байта, ноль ноль, один четыре: длина блока, двадцать. Дальше внутри него ещё один список, у которого тоже есть своя длина: ноль ноль, один два, это восемнадцать. Потом один байт, ноль ноль, тип имени: ноль означает «имя хоста». Потом два байта, ноль ноль, ноль эф: длина самого имени, пятнадцать. И после них идут пятнадцать байт самого имени: семь семь, семь семь, семь семь, два е, шесть пять, семь восемь, шесть один, шесть дэ, семь ноль, шесть цэ, шесть пять, два е, шесть три, шесть эф, шесть дэ. Если записать их буквами, получится www точка example точка com.

Заметьте, что это три вложенные длины. Двадцать это два байта длины списка плюс восемнадцать байт самого списка. Восемнадцать это один байт типа, два байта длины имени и пятнадцать байт имени. Три конверта, один в другом, пока не доберёшься до самих букв. Имя начинается по смещению девяносто семь и заканчивается на сто одиннадцатом, всего пятнадцать байт. Следующий байт, сто двенадцатый, это уже начало следующего блока.

!wiki-anim-video02-s15-sni-nested.svg

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

Посмотрим теперь на другой способ, которым имя сайта может оказаться в пакете: обычный запрос без шифрования. Это протокол HTTP. Запрос состоит из строк текста: первая строка говорит «хочу получить такую-то страницу», а дальше идут заголовки, по строке на каждый. Один из них называется Host, и в нём написан сайт. Вот наш запрос, восемьдесят один байт: GET, косая черта, index точка html, и затем строка Host, двоеточие, пробел, www точка example точка com.

Здесь имя лежит по смещению тридцать два и заканчивается на сорок шестом. Те же пятнадцать байт, но в другом месте и в другой упаковке: никаких вложенных длин, просто текст между пробелом и концом строки. Поэтому его легче найти: программа ищет слово Host с двоеточием и читает то, что идёт дальше. Но современные сайты работают по HTTPS, а там в обычном случае открытое имя есть только в TLS, в расширении, которое мы только что разобрали.

!wiki-anim-video02-s15-http-host.svg

Итак, в TLS имя сайта это пятнадцать байт внутри трёх вложенных длин, по смещениям от девяносто седьмого до сто одиннадцатого. В обычном HTTP то же самое имя лежит в строке Host, по смещениям от тридцать второго до сорок шестого. И в том, и в другом случае это открытый текст, который наблюдатель может прочитать, если знает, где искать.

Длинное сообщение и итог части

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

Заголовки у него те же: длина записи тысяча четыреста семьдесят девять, длина расширений тысяча триста девяносто шесть. А вот список расширений почти такой же, но один блок стал огромным: key_share, доля ключа. В коротком сообщении данные этого блока занимали тридцать восемь байт, а здесь тысячу двести пятьдесят восемь. Клиент прислал сразу две доли ключа: одну на тысячу двести шестнадцать байт и одну на тридцать два. Такое, как правило, делают из соображений будущей безопасности обмена ключами, но для нашей темы это только размер. Раздувшийся ключ раздвинул всё сообщение.

Что случилось с именем сайта? Ничего. Оно по-прежнему лежит по смещениям от девяносто седьмого до сто одиннадцатого, потому что всё, что находится перед расширением имени сервера, у этого сообщения такое же, как и у короткого. А вот с остальными расширениями смещения изменились: например, key_share начинается по смещению двести тринадцать и тянется до тысяча четыреста семьдесят четвёртого.

!wiki-anim-video02-s16-big-hello-strip.svg

Теперь этот запрос не помещается в один пакет. Как мы видели во второй части, система режет его на тысячу четыреста сорок восемь байт и тридцать шесть байт. Граница лежит по смещению тысяча четыреста сорок восемь, и она попадает внутрь самого последнего, тридцатидвухбайтного ключа, где-то на шестом байте. А имя сайта целиком, байты с девяносто седьмого по сто одиннадцатый, лежит в первом пакете. Это важный вывод: обычная нарезка, которую делает система, имя сайта не скрывает. Чтобы имя оказалось разрезанным, разрез должен попасть внутрь байтов с девяносто седьмого по сто одиннадцатый. Так и получилось в первом выпуске, где мы резали посередине слова example, по смещению сто четыре.

И последнее замечание. Мы подчёркивали, что девяносто семь верно только для нашего сообщения. В примере, который мы собрали для проверки, подражая обычному браузеру, на пятьсот семнадцать байт, имя оказалось по смещению сто двадцать один. Нет такого числа, которое подходило бы всем. Всегда нужно идти по длинам.

!wiki-anim-video02-s16-cut-and-name-position.svg

Подведём итоги части. Первое сообщение защищённого соединения, ClientHello, передаётся открыто, потому что ключа ещё нет. В нём вложенные поля «длина и содержимое». Расширения идут по порядку, и в одном из них, типа ноль, лежит имя сайта: пятнадцать байт, по смещениям от девяносто седьмого до сто одиннадцатого в нашем сообщении. В обычном запросе HTTP то же имя лежит в строке Host. Дальше нам нужно выяснить, кто и как это читает.

Повтор и кто что читает на пути

В последней части мы посмотрим на пакеты глазами наблюдателя. Сначала короткий повтор.

Пакет это вложенные конверты: Ethernet, IP, TCP, данные. TCP нумерует байты, и получатель собирает запрос по этим номерам, поэтому ему безразлично, в скольких пакетах он пришёл. Первое сообщение защищённого соединения, ClientHello, передаётся открыто, и в нём лежит имя сайта: пятнадцать байт, в нашем сообщении по смещениям от девяносто седьмого до сто одиннадцатого. В обычном запросе HTTP то же имя лежит в строке Host.

Теперь вопрос: кто на пути пакета может всё это прочитать? Давайте пройдём по устройствам и посмотрим, что каждому из них в принципе нужно. Важная оговорка: ниже общая картина того, как такое оборудование обычно устроено. Данных о конкретных устройствах, и тем более о реальном ТСПУ, у нас нет, и мы описываем модель.

Коммутатор, устройство, которое соединяет компьютеры в одной локальной сети, в основном читает только внешний конверт: адрес получателя Ethernet. Остальное ему обычно не нужно. Роутер читает адрес IP, чтобы выбрать дорогу, вычитает единицу из времени жизни и пересчитывает контрольную сумму, и в нашей лаборатории мы видели ровно это: он поменял Ethernet-заголовок, время жизни и сумму, а всё остальное не тронул.

Дальше может стоять устройство трансляции адресов, по-английски NAT. Оно позволяет множеству домашних устройств выходить в интернет под одним адресом: подменяет в пакете адреса и порты, и для этого должно помнить, какой порт чей. Оно читает IP и порты TCP и обязано пересчитать обе контрольные суммы. Межсетевой экран, или файрвол, решает, какие соединения пропускать: он смотрит на адреса, порты и флаги, чтобы отличить начало соединения от его середины. Ни тому, ни другому, как правило, не нужно читать данные.

Теперь наблюдатель. В нашей серии мы называем его инспектором или ТСПУ, а оборудование такого типа называют DPI, от английских слов «deep packet inspection», «глубокая проверка пакетов». Обычный роутер смотрит на заголовки. DPI заглядывает глубже, в данные. Зачем? Затем, что решить, какой это сайт, по адресу IP не всегда возможно: на одном адресе живёт много сайтов, и узнать нужный можно только из имени в ClientHello или из строки Host. Поэтому такое оборудование может ограничиваться первыми пакетами соединения, где лежит это имя; насколько, нам неизвестно.

!wiki-anim-video02-s17-who-reads-what.svg

А вот что лежит дальше, после рукопожатия, и что мы не увидим ни в каком пакете: данные страницы, адрес внутри сайта, ваши запросы. Всё это зашифровано ключом, который знают только клиент и сервер. Наблюдатель на пути, как правило, может прочитать лишь то, что передаётся открыто: адреса, порты, размеры, время и название сайта. Поэтому всё, что мы разбираем в нашей серии, касается самого начала соединения.

!wiki-anim-video02-s17-after-handshake-locked.svg

Итак, у каждого устройства своя глубина чтения. Коммутатор читает Ethernet. Роутер читает IP. NAT и файрвол добавляют порты и флаги. А наблюдатель типа DPI заглядывает в данные и ищет имя сайта. После рукопожатия данные зашифрованы, и открытым остаётся только начало. Как именно устроено чтение у наблюдателя: пакет за пакетом или всего потока, нам неизвестно, и этому посвящены следующие сцены.

Наблюдатель, читающий пакет за пакетом

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

Устройство у него очень простое. В роутере стоит одно правило: если в пакете, идущем на порт четыреста сорок три сервера, по такому-то месту лежат ровно те пятнадцать байт, которые составляют www.example.com, пакет нужно выбросить. Что значит «по такому-то месту»? Правило ищет эти байты по фиксированному смещению: смещение сто двадцать девять от начала TCP-заголовка, то есть тридцать два байта заголовка и девяносто семь байт нагрузки. Оно не разбирает TLS, не идёт по длинам расширений и ничего не помнит о соединении. Оно просто сравнивает пятнадцать байт на одном и том же месте. Это игрушка, она нужна, чтобы показать идею, и это не копия чьего-то оборудования.

Посмотрим, что происходит с целым запросом. Пакет на двести пятьдесят байт приходит, пятнадцать байт на нужном месте совпадают, пакет выброшен. Клиент ждёт подтверждения, не дожидается и отправляет запрос ещё раз, как мы видели во второй части. Счётчик правила в этом опыте показал шесть пакетов: сам запрос и пять повторов, тысяча восемьсот двенадцать байт, то есть шесть раз по триста два байта IP-пакета: двадцать, тридцать два и двести пятьдесят. Сервер не получил ни одного байта.

!wiki-anim-video02-s18-toy-rule-whole-packet.svg

Теперь тот же запрос, разрезанный на две части в середине слова example. Первая часть, сто четыре байта. Это настоящее начало сообщения: заголовок записи, заголовок рукопожатия, всё на месте. Но дальше сообщение обрывается: в расширении имени написано, что длина имени пятнадцать, а присутствуют только семь байт: w, w, w, точка, e, x, a. Пакет кончился. Правило сравнивает пятнадцать байт, начиная со смещения девяносто семь, но в пакете их там нет: есть только семь, а на сто третьем байте пакет кончился. Совпадения нет.

Вторая часть, сто сорок шесть байт. Она начинается не с заголовка TLS, а прямо посреди слова: m, p, l, e, точка, c, o, m, а дальше сразу следующее расширение. Если бы правило искало на фиксированном месте, оно нашло бы на девяносто седьмом байте этого пакета не имя, а байты ключа: ноль ноль, два шесть, ноль ноль, два четыре, и так далее. Опять нет совпадения.

!wiki-anim-video02-s18-two-segments-search.svg

А что, если инспектор умнее и ищет имя в любом месте пакета? Мы проверили и это. Строка www.example.com не содержится ни в первом, ни во втором пакете. Слова example тоже нет ни в одном: оно разрезано. Кусок www.exa есть в первом, кусок mple.com во втором. Целого имени нет нигде. Поэтому и такой инспектор, который не склеивает пакеты, имя не найдёт.

Результат опыта мы уже видели. Счётчик правила ноль. Оба пакета прошли. Сервер получил сто четыре байта и ещё сто сорок шесть, собрал их по номерам и увидел запрос целиком: принятый поток совпал с отправленным. Мы разрезали запрос, и ничего не потеряли.

Итак, инспектор, который смотрит на каждый пакет отдельно, ищет имя целиком, и в разрезанном запросе его нет ни в одном пакете: в первом семь байт, во втором восемь. Но важно понимать границы вывода. Это поведение нашей модели. Какие правила у настоящего оборудования, неизвестно, и следующая сцена про инспектора с совсем другим устройством.

Наблюдатель, собирающий поток, и подделка сброса

Теперь модель наблюдателя посложнее: тот, кто собирает поток. Мы знаем, как это делает приёмник. Нумерация байтов позволяет поставить пакеты на свои места, склеить их и получить целый запрос. Может ли то же самое сделать наблюдатель? Технически да. Но давайте посмотрим, чего это стоит.

Чтобы собрать поток, нужно помнить каждое соединение отдельно. Для каждого запомнить начальные номера обеих сторон, чтобы считать относительные. Класть приходящие пакеты в буфер по номерам. Ждать, если какого-то куска ещё нет. Следить за повторами, за перестановками, за концом соединения. И делать это для всех соединений сразу, на скорости сети. Это дорого. Поэтому устройства, которым надо справляться с огромным потоком, могут упрощать задачу. Насколько, нам неизвестно: у нас нет данных о настоящем оборудовании, только общие соображения о том, что собирать поток дороже, чем читать пакеты. Мы не утверждаем, что какое-то устройство собирает или не собирает.

Мы покажем, как выглядит сборка, на нашем разрезанном запросе. Первый пакет, сто четыре байта, ложится в буфер на места с первого по сто четвёртый. Второй, сто сорок шесть, должен лечь на места начиная со ста пятого. Он начинается ровно там, где кончился первый, и склеивается. Теперь в буфере двести пятьдесят байт, и имя сайта лежит на местах с девяносто восьмого по сто двенадцатое целиком, как в целом запросе. Читающий поток находит его.

И это даёт нам вторую модель, с обратным выводом. Если наблюдатель собирает поток, то разрезание запроса на части ему не помешает, и нужны другие приёмы. Если не собирает, то помешает. Какая из этих картин ближе к реальности, для конкретного оборудования нам это неизвестно, и мы вернёмся к нему в серии много раз. Но теперь вы знаете, из какого именно различия возникает вся идея: приёмник собирает, а быстрый наблюдатель, возможно, нет.

!wiki-anim-video02-s19-reassembly-buffer.svg

Теперь о том, что наш игрушечный инспектор умеет не только читать, и, возможно, настоящее оборудование тоже. В первом выпуске игрушечный инспектор в одном из опытов отвечал клиенту сбросом соединения. Сброс это пакет с флагом RST, который сообщает «разговор окончен». Инспектор подделывает его: пишет в нём адрес сервера, как будто это сервер оборвал соединение. Посмотрим на поддельный сброс, записанный в лаборатории. Пакет в пятьдесят четыре байта. В нём порядковый номер, который настоящий сервер отправил бы следующим, поэтому клиент его принимает.

Но у подделки есть отличия от пакетов настоящего сервера, которые видны в самой записи. Время жизни шестьдесят четыре, а у всех настоящих пакетов сервера, дошедших до клиента, шестьдесят три: подделку отправил роутер, который стоит в одном шаге от клиента, а настоящий сервер дальше на один шаг. Опций в пакете нет, а у настоящих пакетов сервера есть метка времени. Окно ноль. Флага ACK нет. Всё это отпечатки: по ним внимательный получатель, возможно, мог бы отличить подделку. Но это наблюдение для нашей лаборатории, в настоящей сети числа могут быть другими, и мы вернёмся к этой теме, когда будем говорить о приёмах с подделкой.

!wiki-anim-video02-s19-forged-rst-vs-real.svg

Итак, у наблюдателя две возможные модели. Читающий пакеты по одному ищет имя целиком и не находит его в разрезанном запросе. Собирающий поток находит его, но платит памятью и временем. Что делает конкретное оборудование, мы не знаем. И, кроме чтения, наблюдатель может вмешаться, например, прислать поддельный сброс: в нашей лаборатории он отличается от настоящего временем жизни, опциями и окном.

Как те же поля читает zapret2, и итог выпуска

Остался вопрос, ради которого мы всё это разбирали. Как с теми же самыми байтами работает zapret2? Здесь мы впервые заглянем в настоящий исходный код программы. Всё, что вы услышите, мы открыли и проверили в файлах.

Каждый пакет, который перехватывает программа, первым делом проходит разбор. Эта функция называется proto_dissect_l3l4, и работает она ровно так, как мы с вами разбирали пакет. Она получает сырые байты пакета и заполняет структуру, где записаны указатели: на заголовок IP, на заголовок TCP, на данные. Сначала проверяет, что это IPv4 с правильной длиной заголовка, берёт длину пакета из самого заголовка и пропускает заголовок IP, у нас это двадцать байт. Затем проверяет заголовок TCP и пропускает, сколько сказано в поле длины. Всё, что осталось, это данные. То есть программа читает те же поля, о которых мы говорили в первой и второй частях.

Результат разбора доступен и в скриптах на языке Lua, где пишутся стратегии: пакет приходит туда как таблица с полями. У нашего пакета время жизни в ней шестьдесят четыре, флаги ноль-икс один-восемь, порядковый номер лежит в отдельном поле, а нагрузка это двести пятьдесят байт. Те же числа, что мы читали в записи. Опции TCP программа находит отдельной функцией, которая идёт по ним, как мы шли по расширениям: тип, длина, перепрыгнуть.

Если программа меняет пакет, она пересчитывает контрольные суммы. Для IP это функция, которая обнуляет поле, складывает слова заголовка и записывает результат, а для TCP такая же, но с псевдозаголовком. Точно то, что мы считали вручную: сумма, перенос, переворот битов.

!wiki-anim-video02-s20-dissect-and-lua.svg

Теперь имя сайта. Чтобы его найти, программа делает ровно то, что мы делали по байтам в третьей части. Сначала проверяет, что это ClientHello: первый байт один-шесть, второй три, а третий не больше трёх. Потом проходит по длинам: прыгает через идентификатор сессии, через наборы шифров, через способы сжатия и находит длину расширений. Потом идёт по расширениям, тип, длина, перепрыгнуть, пока не найдёт тип ноль. Потом читает три вложенные длины и копирует пятнадцать байт имени. Ответ: www.example.com. Для обычного HTTP есть отдельная функция, она ищет слово Host. Вот и всё, никакой магии.

И последнее. Когда мы говорили в первом выпуске о разрезе посередине слова example, откуда программа знает, где это? У неё есть метки позиций. Метка «sld», что значит «домен второго уровня», в нашем случае это слово example, которое начинается по смещению сто один и имеет длину семь байт. Метка «середина слова», midsld, вычисляется как сто один плюс семь, делённое нацело на два, то есть сто один плюс три, сто четыре. Поэтому разрез по этой метке даёт две части: сто четыре байта и сто сорок шесть. Те самые числа из нашего опыта. Подробно эти метки мы разберём в десятом выпуске.

!wiki-anim-video02-s20-name-walk-and-mark.svg

[!warning] Уточнение к видео Фраза «в десятом выпуске» устарела: порядок серии изменился. Метки позиций разбирает отдельное видео 04-gde-rezat-metki.

Подведём итог всего выпуска. Пакет это вложенные конверты: Ethernet на один шаг, IP для адреса, времени жизни и контрольной суммы, TCP для нумерации байтов. Запрос это двести пятьдесят байт с номерами от первого до двести пятидесятого, и имя сайта в нём байты с девяносто восьмого по сто двенадцатый. Первое сообщение защищённого соединения передаётся открыто и содержит имя в расширении. Приёмник собирает запрос по номерам, как бы его ни резали. Наблюдатель, который читает пакеты по одному и ищет имя целиком, в разрезанном запросе его не найдёт. Что делает настоящее оборудование, мы не знаем. А zapret2 читает те же поля теми же способами и умеет менять пакеты, например резать их по меткам, как мы только что видели на байтах.

См. также


[!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование доступно в Forgejo: исходник этой заметки · скачать весь репозиторий одним zip-архивом.