О чём заметка

Это полный текст видео номер 2 из серии о том, как работает Zapret 2, разбитый по сценам, с анимированными схемами. Каждую сцену можно читать отдельно.

Как читать пометки

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

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

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

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

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

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

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

Три вопроса и четыре части выпуска Повтор выпуска 01 и план: три картинки (пакет как порция запроса в конверте с адресом, наблюдатель как модель ТСПУ, сортировщик с окошком, запрос, разрезанный пополам и прошедший мимо игрушечного инспектора) порождают три вопроса: что наблюдатель видит в пакете, где в пакете имя сайта, что он видит, если пакет разрезан; четыре части выпуска 02a-02d отвечают на них по порядку: слои пакета и IP, TCP, первое сообщение с именем сайта, взгляд наблюдателя и zapret2. Повтор выпуска 01: три картинки и три вопроса к этому выпуску Пакетпорция запроса в конвертес адресом Наблюдательмодель ТСПУ: сортировщик,смотрит в окошко Разрезанный запроспрошёл мимо игрушечногоинспектора 1Что именно видитнаблюдатель в пакете? 2Где в пакете лежитимя сайта? 3Что он видит,если пакет разрезан? Карта выпуска: четыре части 02aУстройство пакетаслои, Ethernet, IP,контрольная суммаотвечает на вопрос 1 02bПротокол TCPномера, рукопожатие,окно, повторыотвечает на вопрос 1 02cПервое сообщениеTLS ClientHello: имясайта по байтамотвечает на вопрос 2 02dВзгляд наблюдателяпакет и весь поток,чтение в zapret2отвечает на вопрос 3 Начинаем с начала: из чего сделан пакет.

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

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

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

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

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

Байт и шестнадцатеричная запись Байт это одна ячейка из восьми битов, число от 0 до 255; на экране его пишут двумя шестнадцатеричными цифрами: цифры 0-9 и буквы a-f (a=10, b=11, c=12, d=13, e=14, f=15). Пример: буква w это байт 0111 0111, то есть 77 в шестнадцатеричной записи, 7 × 16 + 7 = 119; слово www в пакете записано как 77 77 77. Байт: одна ячейка, число от 0 до 255 011101118 битов, каждый 0 или 1 77каждые полбайта = одна цифра =77так записана буква w =7 × 16 + 7 = 119то же число в обычной записи: 119 Шестнадцать цифр: 0-9 и ещё шесть букв 00112233445566778899a10b11c12d13e14f15верхняя цифра: как её пишут, нижняя: сколько она значит Байт от 00 до ff: два знака, место на экране всегда одно и то же. слово www777777три байта подряд: по одной букве w на байт

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

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

Пакет как матрёшка из конвертов Пакет как матрёшка: данные 250 байт лежат в заголовке TCP (32 байта), он в заголовке IP (20 байт), он в заголовке Ethernet (14 байт); 250 + 32 = 282, + 20 = 302 (длина в поле IP), + 14 = 316 (кадр). Тот же кадр лентой с номерами байт 0-13, 14-33, 34-65 и 66-315, а ниже размеры других кадров соединения в той же шкале: SYN и SYN-ACK по 74 байта (в TCP 40), подтверждение без данных 66, короткий ответ сервера 80. Матрёшка из конвертов: слои пакета и их размеры в байтах Ethernet · 14 байт IP · 20 байт TCP · 32 байта данные · 250 байтзапрос программы 250данные программы 250 + 32 = 282+ заголовок TCP 282 + 20 = 302+ заголовок IP: это число в поле «длина» IP 302 + 14 = 316+ заголовок Ethernet: кадр целиком Тот же кадр лентой: 316 байт, номера с нуля 142032данные: 250 байт Ethernet: 0-13IP: 14-33TCP: 34-65данные: 66-315 Другие кадры соединения, в той же шкале 142040SYN и SYN-ACK: 74 байтав TCP 40 байт: опций больше, данных нет 142032подтверждение без данных: 6614 + 20 + 32 14203214короткий ответ сервера: 8066 + 14 байт ответа

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

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

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

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

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

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

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

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

Заголовок Ethernet: 14 байт Первые 14 байт настоящего кадра с запросом: получатель 16:4d:c6:0e:f9:12 (порт роутера со стороны клиента), отправитель fe:02:bf:2a:aa:17 (сетевая карта клиента) и тип 08 00, то есть внутри IP версии 4; адрес сервера в этом кадре не встречается, потому что путь до сервера начинается с роутера. Слой Ethernet: первые 14 байт кадра с запросом 1604d1c620e3f94125fe6027bf82a9aa10171108120013 164dc60ef912получатель, 6 байт16:4d:c6:0e:f9:12порт роутера со стороны клиента fe02bf2aaa17отправитель, 6 байтfe:02:bf:2a:aa:17сетевая карта клиента 0800EtherType08 00это IPv4 Кому адресован этот кадр Клиент10.1.0.1fe:02:bf:2a:aa:17 Роутерпорт к клиенту16:4d:c6:0e:f9:12 кадр Сервер10.2.0.2его MAC в кадре нет Клиент знает IP-адрес сервера, но путь до него начинается с роутера:поэтому в заголовке Ethernet стоит адрес порта роутера, а не сервера. Ethernet-адрес действует только на ближайший шаг: до соседа по кабелю или Wi-Fi.

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

Роутер заменяет Ethernet целиком Один и тот же пакет (IP id 64005) на входе и на выходе роутера: заголовок Ethernet заменён целиком, получатель 16:4d:c6:0e:f9:12 стал ae:99:ff:5f:98:e9, отправитель fe:02:bf:2a:aa:17 стал 2e:9b:9b:dc:7e:f6, ни один байт адресов не совпал; в IP изменились только время жизни (64 на 63) и контрольная сумма (2bbf на 2cbf), а TCP и данные те же; MAC-адреса в нашем стенде создаются заново при каждом запуске, устойчива структура 6 + 6 + 2 байта. Один и тот же пакет (IP id 64005) до роутера и после него на входе в роутер Ethernet · 14 байт164dc60ef912fe02bf2aaa170800IP · 20 байтTTL 64сумма 2bbfTCP · 32 байтасумма 6970данные250 байт роутер снял старый Ethernet и надел новый на выходе из роутера Ethernet · 14 байтae99ff5f98e92e9b9bdc7ef60800 IP · 20 байтTTL 63сумма 2cbf TCP · 32 байтасумма 6970данные250 байт новые все 12 байт адресов:ни одного совпадающего изменились толькоTTL и сумма те же: ✓и TCP, и данные На каждом переходе Ethernet заменяют, поэтому MAC живёт только один шаг. MAC в нашем стенде создаются заново при каждом запуске; устойчива структура 6 + 6 + 2 байта.

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

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

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

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

Заголовок IP поле за полем Двадцать байт заголовка IP нашего кадра (45 00 01 2e fa 05 40 00 40 06 2b bf 0a 01 00 01 0a 02 00 02) по полям: версия 4 и длина заголовка 5 слов = 20 байт, приоритет 00, длина пакета 302, идентификатор 64005, флаги и смещение 40 00 (стоит DF), время жизни 64, протокол 6 = TCP, контрольная сумма 2bbf, отправитель 10.1.0.1, получатель 10.2.0.2; в заголовке записано, откуда и куда едет пакет, но ни слова о том, какой это сайт. Заголовок IP: 20 байт; под каждым байтом его номер в заголовке 4500010122e3fa40554060074080692b10bf110a120113001401150a16021700180219 4511версия 4; длина заголовка 5 слов × 4 = 20 байт 0022приоритет и перегрузка: 00 (нули) 012e33длина всего пакета: 01 2e = 302 байта fa0544идентификатор: fa 05 = 64005 400055флаги и смещение: 40 00; DF стоит, кусков нет 4066время жизни (TTL): 40 = 64 0677протокол внутри: 06 = TCP 2bbf88контрольная сумма: 2b bf 0a01000199отправитель: 0a 01 00 01 = 10.1.0.1 0a0200021010получатель: 0a 02 00 02 = 10.2.0.2 Откуда и куда едет пакет, здесь написано.Какой это сайт, здесь нет ни слова.

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

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

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

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

Как читаются числа в заголовке IP Как читаются числа заголовка IP: первый байт 45 это две цифры по полбайта, версия 4 и длина заголовка 5 × 4 = 20 байт; длина 01 2e = 1 × 256 + 46 = 302 = 20 + 32 + 250; идентификатор fa 05 = 250 × 256 + 5 = 64005, и пакеты клиента нумеруются подряд 64003 (SYN), 64004, 64005 (данные), 64006, 64007 (FIN), 64008; флаги и смещение 40 00 = 0100 0000 0000 0000, то есть резерв 0, DF 1, MF 0 и смещение 0: пакет делить на куски нельзя. Первый байт: 45 Длина пакета: 01 2e Идентификатор: fa 05 Флаги и смещение: 40 00 01000101= 45 4версия IP 55 × 4 = 20 байтдлина заголовка Доп. полей нет: ровно 20 байт. 01= 1 × 256 = 256 2e= 2 × 16 + 14 = 46 256 + 46 = 302 = 20 + 32 + 250: IP, TCP, данныеТак устройство знает, где пакет кончается. fa= 15 × 16 + 10 = 250 05= 5 250 × 256 + 5 = 64005 пакеты клиента нумеруются подряд: 64003SYN64004ACK64005данные64006ACK64007FIN64008ACK 4000= 0100 0000 0000 0000 01013 бит смещения: все нулирез.DFMF DF = 1: пакет нельзя делить на кускиMF = 0 и смещение 0: других кусков нет

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

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

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

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

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

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

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

Время жизни в четырёх точках Время жизни одного и того же пакета (IP id 64005) в четырёх точках записи в нашей лаборатории: на клиенте 64, на входе в роутер 64, на выходе из роутера 63, на сервере 63; роутер вычитает единицу, а контрольная сумма IP заодно меняется с 2bbf на 2cbf; пакет сервера идёт в обратную сторону так же: на сервере 64, на клиенте 63. Время жизни: один и тот же пакет (IP id 64005) в четырёх точках записи запрос клиента РОУТЕР клиентTTL 64сумма IP 2bbf вход роутераTTL 64сумма IP 2bbf выход роутераTTL 63сумма IP 2cbf серверTTL 63сумма IP 2cbf −1 сумма заголовка IP изменилась вместе с TTL: 2bbf → 2cbf (как её считают, дальше) ответ сервера, в обратную сторону серверTTL 64 роутер: −1 клиентTTL 63 TTL это запас переходов: каждый роутер вычитает единицу, а пакет с нулём выбрасывают.Так сеть защищается от пакетов, которые ходят по кругу вечно. Начальное значение выбирает отправитель: у нашего клиента и сервера оно 64.Обычно это значение операционной системы по умолчанию, а не закон.

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

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

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

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

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

Контрольная сумма IP вручную Контрольная сумма заголовка IP считается вручную: десять слов по два байта (4500, 012e, fa05, 4000, 4006, 0000, 0a01, 0001, 0a02, 0002, поле суммы взято за ноль) складываются с бегущей суммой до 1d43f, лишняя единица переносится в конец (1 + d43f = d440), биты переворачиваются (d440 даёт 2bbf), и это число записано в заголовке; после роутера слово 4006 стало 3f06 (на 0100 меньше), сумма слов упала, а контрольная сумма выросла: 2bbf стала 2cbf; функция ip4_fix_checksum в nfq2/checksum.c:109 обнуляет поле и считает сумму заново по ip_hl × 4 байтам. Контрольная сумма заголовка IP: считаем вручную, слова по 2 байта словосумма растётчто это 4500= 4500версия, длина, приоритет + 012e= 462eдлина пакета: 302 + fa05= 14033идентификатор + 4000= 18033флаги: DF + 4006= 1c039TTL 40 и протокол 06 + 0000= 1c039сумма (на время счёта 0000) + 0a01= 1ca3aотправитель, начало + 0001= 1ca3bотправитель, конец + 0a02= 1d43dполучатель, начало + 0002= 1d43fполучатель, конец 1. Сумма десяти слов1d43fэто 5 цифр: лишняя 1 впереди 2. Перенос в конец1 + d43f = d440 3. Переворот каждого битаd440 = 1101 0100 0100 0000 2bbf = 0010 1011 1011 1111 в заголовке записано 2b bf ✓ После роутера: TTL стал 63, слово 4006 стало 3f06, оно меньше на 0100 = 256Сумма слов стала меньше на столько же, а после переворота битов контрольная сумма, наоборот, выросла: 2bbf → 2cbfровно это число в записи на выходе роутера и на сервере Контрольная сумма TCP не меняется (6970): она не охватывает время жизни. Так считает zapret2: nfq2/checksum.c109void ip4_fix_checksum(struct ip *ip)111ip->ip_sum = 0;поле суммы на время счёта нули112ip->ip_sum = ip4_compute_csum(ip, ip->ip_hl << 2);

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

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

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

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

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

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

Конверты и ненадёжная сеть Пакет это вложенные конверты: Ethernet (14 байт, шаг до соседа), IP (20 байт, адрес и время жизни), TCP (32 байта, договор отправителя и получателя) и 250 байт данных, всего 316; сеть ничего не обещает: пакет может потеряться, прийти не по порядку или прийти дважды. пакет: вложенные конверты (наш кадр) Ethernet · 14 байтодин шаг до соседа IP · 20 байтадрес и время жизни: довезти по адресу TCP · 32 байтаправила отправителя и получателя данные · 250 байт 14 + 20 + 32 + 250 = 316 байт сеть ничего не обещает отправили123 потерялсяпакета 2 нет13 другой порядокпришли 2, 1, 3213 дубльпакет 2 дважды1223 роутер об этом не сообщает и не помнит, что пересылал аналогия с обычной почтой IP: почтовый индекс и адресписьмо довезут, но без обещаний TCP: договор двух сторонвсё получено, целиком и по порядку

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

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

Три идеи TCP Три идеи TCP: пронумеровать каждый байт, подтверждать получение до нужного номера и повторять те же байты, если подтверждения нет; поскольку приёмник расставляет байты по номерам, ему всё равно, пришли ли 250 байт запроса одним пакетом, двумя (104 и 146 байт) или десятью, а наблюдатель, смотрящий каждый пакет отдельно, видит только куски. как сделать надёжный канал из ненадёжной сети: три идеи 1. нумероватькаждый байт получает номер 123456байта 4 нет: видно по номерам поле «порядковый номер» 2. подтверждатьдо какого места всё получено отправительполучатель«получил всё до 250, жду 251» поле «номер подтверждения» 3. повторятьесли подтверждения долго нет отправительполучательответа нет: те же байты ещё раз те же байты, тот же номер вывод: получателю всё равно, в скольких пакетах пришёл запрос один пакет250 байтприёмник: байты 1–250, всё на месте два пакета104 + 146опыт лаборатории: байты 1–104 и 105–250, каждый кусок встаёт на своё место по номеру десять пакетовпример: 10×25пример: и десять кусков по номерам собираются в те же 250 байт Наблюдатель, который смотрит на каждый пакет отдельно,а поток не собирает, увидит только куски.Так ведёт себя наш игрушечный инспектор (модель); настоящее оборудование мы не проверяли.

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

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

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

Заголовок TCP по полям Заголовок TCP нашего пакета в 32 байта поле за полем: порты 39220 и 443, порядковый номер 3 164 320 784, номер подтверждения 2 838 066 482, длина заголовка 8 слов (32 байта), флаги ACK и PSH, окно 63, контрольная сумма 0x6970, urgent-указатель 0 и 12 байт опций с меткой времени. заголовок TCP нашего пакета: 32 байта, слева направо байты 0–19934порт отправителя: 39220 (временный порт браузера) байты 2–301bbпорт получателя: 443 (защищённые соединения) байты 4–7bc9bb410порядковый номер: 3 164 320 784 байты 8–11a9297532номер подтверждения: 2 838 066 482 байт 1280длина заголовка: 8 слов × 4 = 32 байта байт 1318флаги: ACK + PSH (0x10 + 0x08) байты 14–15003fокно: 63 (× 1024 = 64 512 байт) байты 16–176970контрольная сумма: 0x6970 байты 18–190000urgent-указатель: 0 (срочных данных нет) байты 20–310101080a4259ecb083004c89опции: NOP, NOP, метка времени Больше всего нас будут занимать два числа: порядковый номер и номер подтверждения.Порт выбирает программу на устройстве, адрес IP выбирает само устройство.

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

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

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

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

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

Байт флагов TCP Байт флагов TCP равен 0x18, то есть 0001 1000: из восьми битов CWR, ECE, URG, ACK, PSH, RST, SYN, FIN включены ACK (0x10) и PSH (0x08); ACK означает, что в пакете есть номер подтверждения, PSH просит передать данные программе сразу, SYN начинает соединение, FIN завершает, а RST обрывает его резко. байт флагов в нашем пакете: 0x18 CWR0x80ECE0x40URG0x20ACK0x10PSH0x08RST0x04SYN0x02FIN0x01 00011000 0x10 (ACK) + 0x08 (PSH) = 0x18 = 0001 1000 что означает каждый бит ACKв пакете есть верный номер подтверждения PSHпередай данные программе сразу, не жди, пока накопится больше SYNначинает соединение FINзавершает соединение RSTобрывает соединение резко: этот бит запомним, его использует игрушечный инспектор CWR ECE URGостальные три бита нас пока не интересуют В нашем пакете включены ACK и PSH: обычный пакет с данными посреди разговора.

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

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

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

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

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

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

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

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

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

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

Рукопожатие из трёх пакетов Рукопожатие из трёх пакетов, записанное в лаборатории: SYN клиента (0,000 мс, начальный номер 3 164 320 783), SYN-ACK сервера (0,032 мс, начальный номер 2 838 066 481, подтверждение 3 164 320 784) и ACK клиента (0,045 мс, подтверждение 2 838 066 482); SYN занимает один номер, поэтому данные начинаются на единицу позже. клиент10.1.0.1сервер10.2.0.2время отначала 0,000 мсSYN seq 3 164 320 783 · ack нет · окно 64 240 · 74 Б «начнём разговор; мои байты нумеруются с этого числа» +0,032 мсSYN + ACK seq 2 838 066 481 · ack 3 164 320 784 · окно 65 160 · 74 Б «согласен; твой номер принял, жду следующий» SYN занимает один номер: 3 164 320 783 + 1 = 3 164 320 784 +0,045 мсACK seq 3 164 320 784 · ack 2 838 066 482 · окно 63 · 66 Б «слышу; жду твои байты с этого номера» Соединение установлено. Весь обмен занял 0,045 мс: клиент и сервер стоят рядом,в настоящем интернете это десятки миллисекунд.

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

Опции SYN Двадцать байт опций в SYN клиента в hex: MSS 1460 (1500 минус 20 минус 20), разрешение выборочного подтверждения SACK, метка времени, пустой байт для выравнивания и масштаб окна 10; SYN-ACK сервера называет те же пять опций. опции в SYN клиента: 20 байт, hex опция 1020405b4MSS: 1460 = 1500 − 20 − 20 опция 20402SACK разрешён: можно подтверждать выборочно опция 3080a4259ecaf00000000метка времени: часы клиента опция 401пустой байт для выравнивания опция 503030aмасштаб окна: 10 ответ сервера: SYN-ACK называет те же пять MSS 1460SACKметка временипустой байтмасштаб 10 у сервера в метке времени свои часы и эхо часов клиента: 83 00 4c 89 и 42 59 ec af Именно в рукопожатии стороны сообщают друг другу о своих возможностях.

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

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

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

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

Уточнение к видео

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

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

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

Десять пакетов соединения Десять пакетов соединения по записи в лаборатории с относительными номерами: запрос клиента в 250 байт (номера 1–250) подтверждён сервером номером 251, ответ сервера в 14 байт подтверждён номером 15, а флаги FIN, тоже занимающие по одному номеру, закрываются подтверждениями 252 и 16; правило: номер подтверждения равен последнему полученному байту плюс один. соединение целиком: десять пакетов, номера относительные №мсоткуда → кудафлагиseqackбайтчто видно 10,000клиент → серверSYN0–0SYN: номер 0 20,032сервер → клиентSYN+ACK010 30,045клиент → серверACK110 40,220клиент → серверPSH+ACK11250запрос: байты 1–250 50,232сервер → клиентACK12510251 = 250 + 1 (+0,012 мс) 60,353сервер → клиентPSH+ACK125114ответ: байты 1–14 70,384клиент → серверACK25115015 = 14 + 1 80,407клиент → серверFIN+ACK251150FIN занял номер 251 90,466сервер → клиентFIN+ACK152520252 = 251 + 1 100,493клиент → серверACK25216016 = 15 + 1 номер подтверждения = последний полученный байт + 1это номер следующего ожидаемого байта; FIN, как и SYN, занимает один номер

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

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

Лента байтов и имя сайта Запрос в 250 байт это лента байтов с номерами 1–250 (сырые 3 164 320 784–3 164 321 033); имя www.example.com занимает байты 98–112 (с нуля 97–111); при разрезании в середине слова пакет 1 несёт байты 1–104 с www.exa, пакет 2 байты 105–250 с mple.com, и приёмник склеивает их по номерам в целые 250 байт. запрос клиента: лента из 250 байт, у каждого байта свой номер 1250байты запроса сырые номера:3 164 320 7843 164 321 033 w98w99w100.101e102x103a104m105p106l107e108.109c110o111m112 с нуля это 97с нуля 111имя сайта: 15 байт, номера 98–112 разрезали на два пакета: опыт с разрезом посреди слова example пакет 1: 104 байта, байты 1–104 пакет 2: 146 байт, байты 105–250 www.exa: 7 байт (98–104)mple.com: 8 байт (105–112) seq 1seq 105 Приёмник видит: пакет 2 начинается с номера 105, ровно там, где закончился пакет 1.Он склеивает куски по номерам: все 250 байт на месте, запрос получен целиком. Имя сайта это не просто текст в пакете, а 15 байт с определёнными номерами в потоке.

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

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

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

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

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

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

Размер порции и окно Размер порции и окно: MSS 1460 это 1500 минус 20 байт заголовка IP минус 20 байт заголовка TCP, а с 12 байтами метки времени в пакете помещается 1448 байт данных; запрос в 1484 байта ядро режет на 1448 и 36 байт (номера 1 и 1449, кадры 1514 и 102 байта); окно это свободное место приёмника, число в поле умножается на 2 в десятой степени: 63 × 1024 = 64 512 у клиента и 64 × 1024 = 65 536 у сервера, а в самом SYN окно без множителя (64 240 и 65 160). размер порции: сколько данных помещается в один пакет 1500пакет IP, предел−20заголовок IP −20заголовок TCP =1460MSS 1460MSS−12метка времени=1448данных в пакете метка времени естьв каждом пакете после рукопожатия запрос длиннее: большой ClientHello, 1484 байта 1448 байт · seq 1 · кадр 1514 36 байт · seq 1449 · кадр 102 1448 + 36 = 1484 Здесь запрос режет сама система, не zapret2 и не мы. Запомним это на будущее. окно: сколько байт приёмник готов принять, не дожидаясь подтверждения свободное местов приёмном буфере масштаб 10 договорились в начале: множитель 2 в десятой степени = 1024 клиент: 63 × 1024 = 64 512 байт сервер: 64 × 1024 = 65 536 байт Сам SYN несёт окно без множителя: клиент 64 240, сервер 65 160.В поле всего два байта, поэтому без множителя окно не больше 65 тысяч.

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

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

Контрольная сумма TCP Контрольная сумма TCP нашего пакета: псевдозаголовок (адреса 10.1.0.1 и 10.2.0.2, протокол 6, длина 282) даёт 0x1526, заголовок TCP с обнулённой суммой даёт 0x5b1e9, 250 байт данных дают 0x20cf5a; сумма 0x269669, после переноса старших цифр 0x968f, после переворота битов 0x6970, и это в точности число из заголовка. контрольная сумма TCP нашего пакета: что складываем псевдозаголовок10.1.0.1 → 10.2.0.2, протокол 6длина 32 + 250 = 282 0x1526 заголовок TCP32 байта, само поле суммына время счёта равно 0000 0x5b1e9 данные250 байт = 125 пар байтзапрос клиента 0x20cf5a 1сложить всё0x1526 + 0x5b1e9 + 0x20cf5a = 0x269669 2перенести старшие цифры0x26 + 0x9669 = 0x968f 3перевернуть биты0xffff − 0x968f = 0x6970 4сравнить с заголовком0x6970 в поле: совпало Адрес входит в сумму, хотя в заголовке TCP его нет: чужой пакет не пройдёт проверку.Так считается и сумма IP, но TCP охватывает больше: заголовок, все данные и псевдозаголовок.

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

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

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

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

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

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

Повторная отправка: шесть пакетов Повторная отправка в опыте, где роутер выбрасывает запрос в 250 байт: клиент отправляет те же байты с одним порядковым номером 2 566 422 329 шесть раз, через 1,6; 212,2; 428,2; 848,4; 1678,0 и 3345,0 мс от начала; паузы между отправками 210,6; 216,0; 420,1; 829,7 и 1667,0 мс, то есть сначала почти одинаковые, потом примерно вдвое длиннее каждая; инспектор в роутере увидел все шесть пакетов (счётчик 6), за роутером не появилось ни одного, сервер получил 0 байт, а первая пауза в тысячи раз больше пути пакета по лаборатории, значит это таймер клиента. опыт первого выпуска: запрос в 250 байт, роутер выбрасывает каждый пакет Клиент10.1.0.1Роутер с инспекторомигрушечный инспекторСервер10.2.0.2запрос, 250 байтвыброшен время от начала опыта, мс; один пакет = одна отправкамс 1,6 212,2 428,2 848,4 1678,0 3345,0 Все шесть пакетов несут одни и те же байты с одним порядковым номером 2 566 422 329меняются только идентификатор IP и метка времени В роутере до правилавидны все шесть, счётчик = 6 За роутеромне появился ни один из шести У сервераполучено 0 байт 210,6 216,0×1,03 420,1×1,9 829,7×2,0 1667,0×2,0 ← паузы, мс× к предыдущей паузе Рукопожатие заняло 0,05 мс, а первая пауза 210 мс: в тысячи раз больше. Это таймер клиента, не задержка сети. Дальше паузы примерно удваиваются (экспоненциальная задержка): так не заваливают сеть повторами. Числа зависят от системы, а не только от протокола: это наш клиент, Linux 6.18.Закрытие соединения сервером около 2 с лабораторный сервер сделал сам, по таймауту; к опыту это не относится.

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

Дубль и пометка D-SACK Повтор, когда ничего не терялось, в опыте с большим запросом: сегмент в 1448 байт (номер 1) и сегмент в 36 байт (номер 1449) дошли, сервер подтвердил весь запрос номером 1485, и тот же 36-байтный сегмент с тем же порядковым номером 2 856 733 027 пришёл ещё раз; сервер отбросил дубль и ответил подтверждением в 78 байт с опцией SACK от 2 856 733 027 до 2 856 733 063, это пометка D-SACK; итог части: нумерация байтов, подтверждение как следующий ожидаемый байт и повторная отправка как ответ на молчание значат, что приёмник собирает запрос по номерам, выбрасывает дубли, и ему безразлично, как запрос нарезан на пакеты. повтор без потери: большой запрос, 1484 байта в двух сегментах КлиентСервер сегмент 1: 1448 байт, номер 1130,6 мс сегмент 2: 36 байт, номер 1449166,7 мс подтверждение: всё получено, жду байт 1485222,0 мс тот же сегмент 2 ещё раз: 36 байт, номер 1449235,4 мс ответ в 78 байт: подтверждение 1485 и SACK235,5 мс D-SACK: «диапазон от 2 856 733 027 до 2 856 733 063 я получил повторно»Приёмник сам заметил по номерам, что эти байты у него уже есть, и выбросил дубль.1449 это относительный номер, в пакете он записан как 2 856 733 027. Зачем клиент прислал дубль, не выясняли. итог части: что мы узнали про TCP 1. Нумерация байтову каждого байта свой номер 2. Подтверждениеэто следующий ожидаемый байт 3. Повторная отправкаэто ответ на молчание Приёмник собирает запрос по номерам и выбрасывает дубли: ему всё равно, как он нарезан.Инспектор, не собирающий поток, видит только куски; собирает ли поток настоящее оборудование, мы не знаем.

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

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

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

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

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

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

До ключа и после ключа Наши 250 байт запроса с номерами TCP от 1 до 250 и есть сообщение ClientHello, имя сайта в нём занимает байты 98–112 (с нуля 97–111); защищённое соединение делится на «до ключа» и «после ключа»: до ключа идёт рукопожатие TLS из трёх действий (клиент здоровается сообщением ClientHello, сервер отвечает и доказывает, кто он, стороны договариваются о ключе), и первое сообщение шифровать нечем, поэтому оно открыто; после ключа данные зашифрованы, и ключ знают только клиент и сервер. наши 250 байт запроса: это и есть первое сообщение защищённого соединения 1250 имя сайта: байты 98–112 (с нуля 97–111) 250 байт = ClientHello защищённое соединение делится на две части: до ключа и после ключа До ключа: рукопожатие TLSключа ещё нет, шифровать нечем 1Клиент здоровается: сообщение ClientHelloпервый пакет с данными; что клиент умеет 2Сервер отвечает и доказывает, кто он 3Стороны договариваются о ключе не зашифрован ключготов После ключа: данныеДанные зашифрованы.Ключ знают только клиенти сервер. На пути виденпоток бессмысленных байтов. ClientHello идёт до появления ключа, поэтому открыт: чтобы прочитать, расшифровывать нечего.Как правило, наблюдатель на пути может прочитать его; настоящее оборудование мы не разбираем.

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

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

Один адрес, много сайтов На одном IP-адресе может отвечать много сайтов (пример: адрес 10.2.0.2 с сайтами example.com, example.org, example.net, www.example.com и другими), поэтому в ClientHello есть поле SNI, «указание имени сервера», в нашем сообщении там www.example.com, как курьеру в доме с сотней компаний говорят, к какой идти; поле записано открыто, потому что передаётся до появления ключа, так что наблюдатель на пути, как правило, видит адрес и имя сайта, но не страницу и не то, что вы ищете: это зашифровано позже. один IP-адрес, много сайтов: поэтому клиент называет нужный сайт Клиент10.1.0.1 Один адрес 10.2.0.2, на нём отвечает много сайтовпример: дом с одним адресом и сотней компаний, не наш сервер example.com example.org example.net www.example.com mail.example.com … и ещё сотни ClientHello курьеру нужно сказать, к какой компании: имя сайта что лежит в ClientHelloПоле SNI, «Server Name Indication», «указание имени сервера»: название нужного сайта. заголовки и возможности клиентаSNI: www.example.comостальные расширения Наблюдательна пути Как правило, видит адрес 10.2.0.2 и имя www.example.comSNI записан открыто: он передаётся до появления ключа Не видит страницу и то, что вы там ищетевсё это зашифровано позже Это и есть окошко с адресом на конверте, которое смотрит наш сортировщик из первого выпуска.Что настоящее оборудование делает с этим окошком, мы не знаем: наблюдатель здесь модель.

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

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

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

Поля ClientHello по байтам Двести пятьдесят байт нашего ClientHello по 16 в строке со смещениями от нуля и поля по порядку: заголовок записи (смещения 0–4: 16 03 01 00 f5, длина 245), заголовок рукопожатия (5–8: 01 00 00 f1, длина 241), версия 03 03 (9–10), случайное число (11–42), длина идентификатора сессии 0x20 = 32 (43) и сам идентификатор (44–75), длина наборов шифров 6 (76–77) и три набора 1302, 1303, 1301 (78–83), способы сжатия 01 00 (84–85), длина расширений 00 a2 = 162 (86–87) и сами расширения (88–249); поля длины выделены отдельно, потому что программа читает их, чтобы перепрыгнуть через содержимое. наши 250 байт по 16 в строке: слева смещение первого байта строки, считаем с нуля 0123456789101112131415016030100f5010000f103030b91466a5e1695b5ee25d7f1d4987039c398876c94ba32aa1a46262c42e5ae21582a206392f695485ff03abfd3a8e210677728dfdce9e5b564ede051766bde75026770d791000613028013031301010000a20000001400120000960f7777772e6578616d706c652e636f6d112000b000403000102000a00040002001d128002300000016000000170000000d002a144002809050906090404030503060308071600808081a081b081c0809080a080b080417608050806040105010601002b0003020319204002d00020101003300260024001d0020820d2a1e450c78b4967d7ec4ea3eed8582246eb3e6a50c4f7e49bc651ed5d4283c6a24007001b00050400010003 заголовки и версиидлина: её читаемсодержимое: можно пропуститьрасширения 0–4заголовок записи16 = «рукопожатие» · 03 01 · длина 00 f5 = 245 5–8заголовок рукопожатия01 = ClientHello · длина 00 00 f1 = 241 9–10версия03 03: метка TLS 1.2, настоящая версия позже 11–42случайное число32 байта, для поиска имени не нужны 43длина идентификатора сессии20 = 32 44–75идентификатор сессии32 байта: через них перепрыгиваем 76–77длина наборов шифров00 06 = 6 байт 78–83три набора шифров13 02 · 13 03 · 13 01 84–85способы сжатия01 = один способ · 00 = без сжатия 86–87длина расширений00 a2 = 162 88–249расширения162 байта до конца данных Каждая длина говорит, сколько байт содержимого за ней идёт: на этом держится весь разбор.

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

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

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

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

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

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

Длины и прыжки через содержимое Длины вложены друг в друга и сходятся: 250 = 5 + 245 (длина записи 00 f5), 245 = 4 + 241 (длина сообщения 00 00 f1) и 250 = 88 + 162 (длина расширений 00 a2); чтобы добраться до расширений, программа читает длины и перепрыгивает через содержимое: по смещению 43 стоит 32, прыжок на 76; по 76–77 стоит 6, прыжок на 84; по 84 стоит 1, прыжок на 86; по 86–87 стоит 162, и расширения идут с 88 до конца данных; так же ходит и zapret2 (nfq2/protocol.c:524). длины вложены одна в другую, и сумма всегда сходится запись: 250 = 5 + 245длина записи 00 f5 = 245 рукопожатие: 245 = 4 + 241длина сообщения 00 00 f1 = 241 расширения: 250 = 88 + 162длина расширений 00 a2 = 16288 байт до расширений синий = заголовок, жёлтый = то, что покрывает длина как программа перепрыгивает до расширений расширения: 162 байтаслучайное0437688250 1 2 3 4 1по смещению 43 стоит 20, то есть 32: перепрыгиваем идентификатор сессии, попадаем на 76 (43 + 1 + 32) 2по 76–77 стоит 00 06, то есть 6: перепрыгиваем наборы шифров, попадаем на 84 (76 + 2 + 6) 3по 84 стоит 01: перепрыгиваем один байт способа сжатия, попадаем на 86 (84 + 1 + 1) 4по 86–87 стоит 00 a2 = 162: расширения идут с 88 до 249 (88 + 162 = 250, конец данных) Позиции дальнейших полей зависят от длины идентификатора сессии: её надо прочитать.Так ходит и zapret2: nfq2/protocol.c:524, функция TLSFindExtLenOffsetInHandshake.

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

Расширения ClientHello

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

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

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

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

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

Одиннадцать расширений Расширение TLS это блок из двух байт типа, двух байт длины и данных такой длины; в нашем сообщении одиннадцать расширений в байтах 88–249 по порядку: server_name 88–111 (данные 20 байт), ec_point_formats 112–119 (4), supported_groups 120–127 (4), session_ticket 128–131 (0), encrypt_then_mac 132–135 (0), extended_master_secret 136–139 (0), signature_algorithms 140–185 (42), supported_versions 186–192 (3, данные 02 03 04, то есть TLS 1.3), psk_key_exchange_modes 193–198 (2), key_share 199–240 (38, группа 001d и 32 байта ключа) и compress_certificate 241–249 (5); первое из них, server_name, несёт имя сайта. расширение: самостоятельный блок «тип, длина, данные» тип · 2 байта длина · 2 байта данные: столько байт, сколько сказано в длине программа читает блоки по очереди: прочитала тип, прочитала длину, перепрыгнула на следующий байты 88–249 по расширениям, номер в ленте = номер в списке88249 смещениярасширениедлиначто это 1188..111server_name20имя сайта (SNI): разберём в следующей сцене 22112..119ec_point_formats4форматы точек кривых для обмена ключами 33120..127supported_groups4группа 00 1d, одна 44128..131session_ticket0пусто: «я это умею» 55132..135encrypt_then_mac0пусто: «я это умею» 66136..139extended_master_secret0пусто: «я это умею» 77140..185signature_algorithms42список из 20 алгоритмов подписи 88186..192supported_versions3данные 02 03 04: версия 03 04 = TLS 1.3 99193..198psk_key_exchange_modes2режимы обмена ключами 1010199..240key_share38доля ключа: группа 00 1d, длина 32, 32 байта 1111241..249compress_certificate5умеет принимать сжатые сертификаты, два способа жёлтое: имя сайта; зелёные три: на них особое внимание. «Длина» это длина данных без заголовка.

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

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

Прыжки по длинам и сумма 162 Чтобы пройти список расширений, программа с каждого начала блока прыгает на 4 байта заголовка плюс длину данных: 88, 112, 120, 128, 132, 136, 140, 186, 193, 199, 241 и конец на 250; длины блоков вместе с заголовками 24 + 8 + 8 + 4 + 4 + 4 + 46 + 7 + 6 + 42 + 9 дают 162, ровно число из поля длины расширений, а 88 + 162 = 250; если разрезать сообщение посередине, проверка не сойдётся. Порядок расширений зависит от клиента: у нашего клиента имя первое, у другого (условный пример, не захвачено) оно может стоять где угодно, поэтому программа проходит по длинам и ищет блок с типом 0, как цикл в nfq2/protocol.c:575. пройти список расширений: от начала блока прыгаем на 4 + длина данных в рамках: смещение начала блока (последняя рамка: конец данных)над дугой: на сколько прыгаем; под дугой: 4 байта заголовка + длина данных 88 112+244+20 120+84+4 128+84+4 132+44+0 136+44+0 140+44+0 186+464+42 193+74+3 199+64+2 241+424+38 250+94+5 24 + 8 + 8 + 4 + 4 + 4 + 46 + 7 + 6 + 42 + 9 = 162это ровно число из поля длины расширений (смещения 86–87); а 88 + 162 = 250, конец данных Если разрезать сообщение посередине, эта проверка не сойдётся. порядок зависит от клиента; в рамках типы блоков наш клиент, из записи: имя первое00 0000 0b00 0a00 2300 1600 1700 0d00 2b00 2d00 3300 1b условный пример другого клиента (не захвачен): имя где-то в середине00 2b00 3300 0a00 0000 0d00 2d00 0b00 2300 1b00 1600 17 Поэтому «имя всегда в начале» сказать нельзя: нужно пройти по длинам и найти блок с типом 0.Так делает и zapret2: цикл в nfq2/protocol.c:575 (TLSFindExtInHandshake) читает тип и длину и сравнивает тип.

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

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

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

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

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

Поле SNI по байтам Расширение с именем сайта по байтам, смещения 88–111: тип 00 00 «имя сервера» (88–89), длина расширения 00 14 = 20 (90–91), длина списка 00 12 = 18 (92–93), тип имени 00 «имя хоста» (94), длина имени 00 0f = 15 (95–96) и 15 байт имени 77 77 77 2e 65 78 61 6d 70 6c 65 2e 63 6f 6d, то есть www.example.com (97–111); длины вложены как конверты: 24 = 4 + 20, 20 = 2 + 18, 18 = 1 + 2 + 15; следующий байт, 112, уже начало следующего блока. расширение «имя сервера»: байты 88–111, три вложенные длины 1 2 3 4 5 6 00000014001200000f7777772e6578616d706c652e636f6d888990919293949596979899100101102103104105106107108109110111 www.example.com имя: 15 байт запись об имени: длина списка 18 = 1 + 2 + 15 данные расширения: длина 20 = 2 + 18 расширение целиком: 24 байта = 4 (тип и длина) + 20 188–8900 00тип расширения: 0 = «имя сервера» 290–9100 14длина расширения: 20 392–9300 12длина списка имён: 18 49400тип имени: 0 = «имя хоста» 595–9600 0fдлина имени: 15 697–11177 … 6dимя: www.example.com, 15 байт Имя занимает смещения 97–111; байт 112 уже начало следующего блока (00 0b).Так читает и zapret2: nfq2/protocol.c:646 (TLSAdvanceToHostInSNI).

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

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

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

Имя сайта в заголовке Host Тот же 81-байтный запрос HTTP текстом, смещения с нуля: строка GET /index.html HTTP/1.1 (0–23), строка Host: www.example.com (26–46, само имя 32–46), User-Agent: lab (49–63), Accept: */* (66–76) и пустая строка (79–80), после каждой строки два байта конца строки; те же 15 байт имени лежат в TLS по смещениям 97–111 внутри трёх вложенных длин, а в HTTP по смещениям 32–46 просто текстом между пробелом и концом строки, поэтому его легче найти: программа ищет строку Host с двоеточием и читает дальше (nfq2/protocol.c:361); современные сайты работают по HTTPS, где открытое имя, как правило, есть только в TLS. обычный запрос HTTP: 81 байт текста, смещения с нуля GET /index.html HTTP/1.10–23CRLF 24–25 Host: www.example.com26–46CRLF 47–48 User-Agent: lab49–63CRLF 64–65 Accept: */*66–76CRLF 77–78 пусто: конец заголовковCRLF 79–80 324626 Host: имя заголовка (26–30), пробел (31), значение 32–46CRLF: два байта 0d 0a, конец строки одно и то же имя в двух упаковках TLS: расширение SNI00 0000 1400 120000 0fwww.example.comсмещения 97–111, перед ним три длины HTTP: строка HostHost:www.example.comсмещения 32–46, обычный текст до конца строки Как найти имяTLS: пройти по длинам до блока с типом 0HTTP: найти «Host:» и прочитать слово после негопоэтому в HTTP имя искать проще Современные сайты работают по HTTPS, а там открытое имя, как правило, только в TLS. И в TLS, и в HTTP это открытый текст: наблюдатель может его прочитать, если знает, где искать. zapret2: nfq2/protocol.c:361 (HttpExtractHost, ищет «\nHost:»), nfq2/protocol.c:646 (TLSAdvanceToHostInSNI).

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

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

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

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

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

Большой ClientHello на 1484 байта Большой ClientHello в лаборатории, 1484 байта, в одном масштабе с коротким на 250 байт: заголовки те же (длина записи 1479, длина расширений 1396), но расширение key_share выросло с 38 до 1258 байт данных (две доли ключа: 1216 и 32 байта) и занимает байты 213..1474, а имя сайта осталось на тех же местах 97..111, потому что перед расширением имени всё то же. тот же ClientHello, но большой: байты в одном масштабе, нумерация с нуля короткое сообщение: 250 байтkey_share: 38 байт данных, байты 199..240 большое сообщение: 1484 байта 0881122131484 запись 1479 = 1484 − 5, расширения 1396 = 1484 − 88 key_share: 1258 байт данных внутри key_share: две доли ключа доля 1: ключ 1216 байт доля 2: ключ 32 байта имя сайта по-прежнему на местах 97..111: перед ним всё, как раньше Имя не сдвинулосьвсё до расширения имени такое же, как в коротком Сдвинулись остальные расширенияkey_share начинается с 213 вместо 199

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

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

Куда попадает разрез Куда попадает разрез: у большого сообщения обычная нарезка по границе 1448 даёт сегменты 1448 и 36 байт, граница лежит внутри 32-байтного ключа (на его 6-м байте), а имя сайта 97..111 целиком в первом сегменте и не скрыто; скрыть имя может только разрез внутри 97..111, как в первом выпуске по смещению 104 (7 и 8 байт имени); позиция имени не постоянна: 97..111 в наших сообщениях на 250 и 1484 байта и 121..135 в собранном для примера сообщении на 517 байт. обычная нарезка системы и разрез внутри имени то же сообщение на 1484 байта, имя сайта выделено сегмент 1: байты 0..1447 (1448 Б)сегмент 2: 1448..1483 (36 Б) последние 32 байта-ключа (байты 1443..1474), ячейка = байт 5 байт ключа | 27 байт ключа + расширение сжатия сертификата (9 Б) граница 1448 попала внутрь ключа, на его 6-й байт: сегмент 1 | сегмент 2 Имя сайта, байты 97..111, целиком в первом сегменте.Обычная нарезка системы имя не скрывает: разрез должен попасть внутрь 97..111. разрез из первого выпуска: по смещению 104, посередине слова examplewww.example.com7 | 8 байт байты 97..103 | 104..111: имя разрезано, целиком его нет ни в одном пакете позиция имени не постоянна: «97» верно только для наших двух сообщений 250 Б: наше короткое97..111 1484 Б: наше большое97..111 517 Б: пример, не захвачено121..135 показаны первые 140 байт каждого сообщения; числа, подходящего всем, нет: всегда идём по длинам

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

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

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

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

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

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

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

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

Кто что читает на пути пакета Кто на пути пакета что обычно читает (модель, а не данные о конкретном оборудовании): коммутатор читает Ethernet; роутер читает IP и меняет Ethernet, время жизни и сумму; NAT читает и подменяет адреса IP и порты TCP; межсетевой экран смотрит адреса, порты и флаги; наблюдатель типа DPI заглядывает ещё и в данные, где лежит имя сайта; сервер читает всё. путь пакета и глубина чтения: типичная картинаEthernetадресIPадреса, TTLTCPпортыTCPфлагиTCPномера, окноданныеимя сайта, Host Коммутаторсоединяет компьютерычитает Роутервыбирает дорогу по IPзаменяетTTL, сумма NATодин адрес для многихадресапорты Межсетевой экранрешает, что пропускатьчитаетчитаетчитает Наблюдатель (DPI)глубокая проверка пакетовчитаетчитаетчитаетчитаетчитаетищет имя сайта Серверполучательчитаетчитаетчитаетчитаетчитаетвсё в нашей лаборатории роутер изменил ровно это: Ethernet, время жизни и сумму IP, остальное не тронул читаетменяет в пакетеобычно не нужно В реальной сети состав устройств другой: это типичная картина, а не данные о конкретном оборудовании.

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

Что зашифровано после рукопожатия Что видно наблюдателю на пути: начало соединения (адреса, порты, размеры, время и открытое первое сообщение с названием сайта) и что скрыто после рукопожатия: данные страницы, адрес внутри сайта и запросы зашифрованы ключом, который знают только клиент и сервер; поэтому всё, что разбирает серия, касается самого начала соединения. один разговор клиента и сервера, пакеты по порядку SYN SYN+ACK ACK ClientHello … данные данные данные данные начало: открыто после рукопожатия: зашифровано Наблюдатель на пути, как правило, видитадреса IPпортыразмеры пакетоввремяназвание сайта: из ClientHello или строки Host Не видно: зашифровано ключомего знают только клиент и серверданные страницыадрес внутри сайтаваши запросы Поэтому всё, что мы разбираем в серии, касается самого начала соединения.Открытыми остаются адреса, порты, размеры и время; название сайта видно в самом начале.

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

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

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

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

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

Игрушечный инспектор и целый запрос Игрушечный инспектор из первого выпуска сравнивает 15 байт по фиксированному смещению 129 от начала заголовка TCP (32 + 97) с www.example.com; в целом запросе на 250 байт эти байты совпали, пакет выброшен, клиент повторил его ещё пять раз, и счётчик правила показал 6 пакетов, 1812 байт (6 × 302), а сервер не получил ни одного байта. Правило в роутере (игрушка для показа идеи, не копия чьего-то оборудования)tcp dport 443 @th,1032,120 0x7777772e6578616d706c652e636f6d counter drop@th,1032,120 = 15 байт с байта 129 заголовка TCP; константа 0x7777772e… это www.example.com129 = 32 (заголовок TCP) + 97 (смещение имени в данных). Правило ничего не разбирает и ничего не помнит. целый запрос: заголовок TCP и 250 байт данных, нумерация с нуляTCPданные: 250 байт 129143 032281 15 байт на местах129..143:77w77w77w2e.65e78x61a6dm70p6cl65e2e.63c6fo6dm = www.example.comсовпало Совпадение: пакет выброшен, счётчик правила +1. Сервер этот пакет не получил. клиент не дождался подтверждения и отправляет запрос ещё раз; каждый раз то же самое правило запрос302 Б, выброшен повтор 1302 Б, выброшен повтор 2302 Б, выброшен повтор 3302 Б, выброшен повтор 4302 Б, выброшен повтор 5302 Б, выброшен Счётчик правила: 6 пакетов, 1812 байт = 6 × 302 (20 + 32 + 250). Сервер получил 0 байт.

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

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

Разрезанный запрос и инспектор Тот же запрос, разрезанный на 104 и 146 байт: в первом пакете от смещения 97 есть только 7 из 15 байт имени (www.exa), во втором на местах 97..111 лежат байты ключа 00 26 00 24 00 1d 00 20 d2 a1 e4 50 c7 8b 49, поэтому правило с фиксированным смещением не совпало; поиск подстрок тоже не находит www.example.com и example ни в одном пакете (www.exa только в первом, mple.com и .com только во втором); счётчик правила 0, сервер получил 104 и 146 байт и собрал запрос целиком. пакет 1: 104 байта, байты запроса 0..103 нужно 15 байт с места 97, в пакете только 7: он кончился77 77 77 2e 65 78 61 = www.exa, совпадения нет пакет 2: 146 байт, байты запроса 104..249; начинается посреди слова: «mple.com» место 97..111: не имя 00260024001d0020d2a1e450c78b49байты ключа, совпадения нет а если инспектор умнее и ищет строку в любом месте пакета?пакет 1 (104 Б)пакет 2 (146 Б) www.example.comнетнет exampleнетнет www.exaесть: смещение 97нет mple.comнетесть: смещение 0 .comнетесть: смещение 4 Целой строки нет ни в одном пакете: слово example разрезано. Счётчик правила: 0. Оба пакета прошли.Сервер получил 104 и 146 байт, собрал их по номерам и увидел запрос целиком: поток совпал с отправленным.

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

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

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

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

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

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

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

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

Наблюдатель, собирающий поток Наблюдатель, который собирает поток: для этого нужно помнить каждое соединение, начальные номера обеих сторон, класть пакеты в буфер по номерам, ждать недостающие куски и следить за повторами, причём для всех соединений сразу; в нашем примере пакет 1 (104 байта) ложится на места 1..104, пакет 2 (146 байт) на 105..250, и имя www.example.com лежит на местах 98..112 целиком, так что такому наблюдателю разрезание не мешает, а читающему пакеты по одному мешает; что делает настоящее оборудование, неизвестно. цена сборки потока и сама сборкачтобы собрать поток, нужно: помнить каждое соединение запомнить стартовые номера сторон класть пакеты в буфер по номерам ждать недостающие куски следить за повторами и порядком всё это для каждого соединения сразу что делают настоящие устройства,неизвестно: мы не утверждаем,что какое-то собирает или нет буфер соединения: байты запроса, номера 1..250 1105250 пакет 1: 1..104 пакет 2: 105..250 стык: пакет 2 начался,где кончился пакет 1 www.example.comна местах 98..112 целиком Читающий поток находит имя, как в целом запросе. Наблюдатель собирает потокразрезание запроса ему не мешает,нужны другие приёмы Наблюдатель читает пакеты по одномуразрезание ему мешает: имени целикомнет ни в одном пакете

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

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

Поддельный сброс и настоящие пакеты Подделанный сброс из лаборатории: роутер с игрушечным правилом выбрасывает запрос и сам присылает клиенту RST на 54 байта с адресом сервера и номером, который сервер отправил бы следующим (1859754862), но у подделки время жизни 64 против 63 у настоящих пакетов сервера, нет опций TCP (у настоящих есть метка времени), окно 0 против 64 или 65160 и нет флага ACK; клиент получил Connection reset by peer, сервер получил 0 байт; это наблюдение нашей лаборатории, в настоящей сети числа могут быть другими. клиент10.1.0.1роутер с правиломигрушечный инспекторсервер10.2.0.2 запрос, 250 Б выброшен поддельный RST: TTL 64 через 0,010 мс; отправитель: адрес сервера TTL 64TTL 63для сравнения: настоящие пакеты сервера, дошедшие до клиента, роутер вычитает единицу поддельный RST (54 Б)настоящие пакеты сервера отправитель10.2.0.2, адрес сервера10.2.0.2 номер seq1859754862 = начальный номер сервера + 1тот же номер отправил бы сервер время жизни TTL6463 опции TCPнет (заголовок 20 байт)есть, метка времени окно064 или 65160 флаг ACKнет, только RSTстоит Совпадает то, что нужно, чтобы клиент принял сброс; отличается всё остальное. Клиент получил «Connection reset by peer», сервер получил 0 байт. Это наблюдение нашей лаборатории: в настоящей сети числа могут быть другими.

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

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

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

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

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

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

Как zapret2 разбирает пакет Как zapret2 читает те же поля: функция proto_dissect_l3l4 (nfq2/darkmagic.c:554) разбирает пакет в 302 байта и заполняет структуру dissect (nfq2/darkmagic.h:191-208): заголовок IP (20 байт), заголовок TCP (32 байта) и данные (250 байт); в Lua-стратегиях пакет приходит таблицей с теми же числами (ip_ttl 64, th_flags 0x18, нагрузка 250 байт), опции TCP находит tcp_find_option (тип, длина, перепрыгнуть), а после изменений программа пересчитывает суммы функциями ip4_fix_checksum и tcp_fix_checksum (nfq2/checksum.c:109 и :133). zapret2 читает те же поля, что мы разбирали вручнуюproto_dissect_l3l4(data, len, &dis, false)nfq2/darkmagic.c:554; вызывается из nfq2/desync.c:2202 для каждого перехваченного пакета входные байты: IP-пакет в 302 байта, длина берётся из самого заголовка IPIP 20TCP 32данные: 250 байт dis.ip → начало пакетаlen_l3 = 20 dis.tcp → байт 20len_l4 = 32 (из поля длины) dis.data_payload → байт 52len_payload = 250 struct dissect (nfq2/darkmagic.h:191-208): указатели и длины, которыми дальше пользуются все стадии в Lua-стратегиях пакет приходит таблицейnfq2/lua.c:1496, 1366, 1660 dis.ip.ip_ttl= 64 dis.tcp.th_flags= 0x18 dis.tcp.th_seq= 3164320784 #dis.payload= 250 tcp_find_option: идёт по опциям, как мы шлипо расширениям: тип, длина, перепрыгнуть01NOP01NOP08 0a …метка времени, длина 10nfq2/darkmagic.c:58; у нашего пакета 12 байт опций если программа меняет пакет, она пересчитывает контрольные суммы обнулить поле суммы сложить 16-битные слова добавить перенос перевернуть биты ip4_fix_checksum (nfq2/checksum.c:109): заголовок IP, 20 байтtcp_fix_checksum (nfq2/checksum.c:133): то же плюс псевдозаголовок

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

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

Как zapret2 находит имя и метку разреза Как zapret2 находит имя сайта и откуда берёт позицию разреза: функции IsTLSHello, TLSFindExtLenOffsetInHandshake, TLSFindExtInHandshake, TLSAdvanceToHostInSNI и TLSExtractHostFromExt (nfq2/protocol.c:510, 524, 575, 646, 658) проходят по длинам до расширения типа 0 и копируют 15 байт имени в ps.host = www.example.com (nfq2/desync.c:1642); метки позиций считаются по найденному имени: host = 97, sld = 101 (слово example, длина 7), midsld = 101 + 7 / 2 = 104, и разрез по этой метке даёт части 104 и 146 байт. поиск имени: шаги по длинам в nfq2/protocol.c IsTLSHellonfq2/protocol.c:510первый байт 16, второй 03,третий не больше 0316 03 01: подходит TLSFindExtLenOffsetInHandshakenfq2/protocol.c:524прыгает через сессию, шифрыи сжатие к длине расширений32, 6, 1: длина на смещении 86 TLSFindExtInHandshakenfq2/protocol.c:575идёт по расширениям: тип, длина,перепрыгнуть; ищет тип 0расширение 88..111 TLSAdvanceToHostInSNInfq2/protocol.c:646читает вложенные длины и выходитна первый байт именидлины 20, 18, 15; имя с 97 TLSExtractHostFromExtnfq2/protocol.c:658копирует байты именив строку15 байт ps.host = www.example.com (nfq2/desync.c:1642)HTTP: HttpExtractHost (nfq2/protocol.c:361) ищет слово Host откуда программа знает, где середина слова example байты имени 97..111 (слово example: 101..107)w97w98w99.100e101x102a103m104p105l106e107.108c109o110m111 host 97 sld 101 midsld 104 endsld 108 endhost 112 sld = 101, длина 7midsld = 101 + 7 / 2= 101 + 3 = 104 части: 104 + 146 = 250protocol.c:198, :691; FindNLD :16 метки позиций подробно разберём в десятом выпуске

Уточнение к видео

Фраза «в десятом выпуске» устарела: порядок серии изменился. Метки позиций разбирает отдельное видео «Где резать: метки».

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

См. также


🤖 Эти статьи открыты — можно обучать на них ИИ

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