[Перевод] Пишем стек TCP/IP с нуля: основы TCP и Handshake

Первая часть: Ethernet, ARP, IPv4 и ICMPv4
Пока наш стек TCP/IP пользовательского пространства содержит минимальные реализации Ethernet и IPv4. Настало время заняться пугающим Transmission Control Protocol (TCP).
TCP, работающий на четвёртом (транспортном) сетевом слое OSI1, отвечает за починку ошибочных подключений и сбоев в доставке пакетов. TCP — «рабочая лошадка» Интернета, обеспечивающая надёжную связь практически во всех компьютерных сетях.
TCP — не особо новый протокол, первая его спецификация вышла в 1974 году2. С тех пор многое поменялось, TCP дополнился множеством расширений и исправлений3.
В этом посте мы изучим базовую теорию TCP, а также рассмотрим заголовок TCP и поговорим об установке соединения (TCP handshaking). Под конец мы продемонстрируем первую функциональность TCP в нашем сетевом стеке.
Механизмы обеспечения надёжности
Задача надёжной отправки данных может показаться тривиальной, но настоящая её реализация сложна. В сети, основанной на передаче датаграмм, возникает множество вопросов, связанных с исправлением ошибок:
Как долго должен отправитель ждать подтверждения от получателя?
Что, если получатель не может обрабатывать данные с той же скоростью, с которой их передают?
Что, если сеть между ними (например, маршрутизатор) не может обрабатывать данные с той же скоростью, с которой их отправляют?
Во всех этих сценариях проявляются опасности сетей пакетной коммуникации — подтверждение от получателя может быть повреждено или даже утеряно при передаче, что ставит отправителя в сложную ситуацию.
Для решения этих проблем можно использовать множество механизмов. Наверно, самый широко используемый из них — это техника «скользящего окна», при которой обе стороны отслеживают передаваемые данные. Данные окна считаются последовательными (как срез массива), и это окно «скользит» вперёд, когда данные обработаны (и их получение подтверждено) обеими сторонами:
Левый край окна Правый край окна
| |
| |
---------------------------------------------------------
...| 3 | 4 | 5 | 6 | 7 |...
---------------------------------------------------------
^ ^ ^ ^
| \ / |
| \ / |
Отправлено Размер окна: 3 Пока не может
и подтверждено быть отправлено
Удобное свойство применения такого скользящего окна заключается в упрощении задачи управления потоком. Управление потоком требуется, когда получатель не может обрабатывать данные со скоростью передачи. В такой ситуации размер скользящего окна путём переговоров сторон может быть снижено, что приведёт к ограничению передачи со стороны отправителя.
С другой стороны, отслеживание перегрузок помогает не перегружать сетевые стеки между отправителем и получателем. Существует две общие методики его реализации: в явной версии у протокола есть поле, специально сообщающее отправителю об уровне перегрузок. В косвенной версии отправитель пытается угадать, когда сеть перегружена, и должен ли он ограничить передачу. В целом, отслеживание перегрузок — это сложная сетевая проблема, исследования которой продолжаются и по сей день4.
Основы TCP
Внутренние механизмы TCP гораздо сложнее, чем в других протоколах наподобие UDP и IP. TCP — это протокол с установлением соединения, то есть в нём первым этапом устанавливается соединение одноадресной передачи ровно между двумя сторонами. Поддержанием этого соединения активно занимаются обе стороны: они устанавливают соединение (выполняют handshaking), информируют вторую сторону о состоянии данных и возможных проблемах.
Ещё одно важное свойство TCP заключается в том, что это потоковый протокол. В отличие от UDP, TCP не гарантирует приложениям передачу стабильных блоков данных в процессе отправки и получения. Реализации TCP приходится буферизировать данные, и в случае потери, изменения порядка или повреждения пакетов протокол должен ждать и упорядочивать данные в буфере. И только когда данные признаны целыми, TCP может передать их сокету приложения.
Так как TCP работает с данными, как с потоком, блоки из потока должны преобразовываться в пакеты, которые может передавать IP. Это называется пакетированием: заголовок TCP содержит порядковый номер текущего индекса в потоке. Так обеспечивается ещё одно удобное свойство: поток можно разбить на множество сегментов переменной длины, а TCP будет знать, как составить из них пакеты.
Аналогично IP, протокол TCP также проверяет сообщение на целостность. Для этого применяется тот же алгоритм вычисления контрольной суммы, что и в IP, но с дополнительными подробностями. Здесь используется сквозная контрольная сумма, то есть в её вычисление включаются и заголовок, и данные. Кроме того, добавляется псевдозаголовок, созданный из IP-заголовка.
Если реализация TCP получает повреждённые сегменты, то она отбрасывает их и не уведомляет отправителя. Эта ошибка исправляется установленным отправителем таймером, который можно использовать для повторной передачи сегмента, если его приём не подтверждён получателем.
Кроме того, TCP — это полнодуплексная система, то есть трафик может одновременно течь в обоих направлениях. Это значит, что обменивающиеся данными стороны должны хранить в памяти последовательность данных в обоих направлениях. При этом TCP экономит трафик, включая подтверждение трафика другой стороны в собственные отправляемые сегменты.
По сути, последовательность потока данных — основной принцип TCP. Однако задача его синхронизации очень непроста.
Формат заголовков TCP
Далее мы определим заголовок сообщения и опишем его поля. Заголовок TCP кажется простым, но содержит множество информации о состоянии связи.
Размер заголовка TCP составляет 20 октетов5:
0 15 31
-----------------------------------------------------------------
| порт-источник | порт-получатель |
-----------------------------------------------------------------
| порядковый номер |
-----------------------------------------------------------------
| номер подтверждения |
-----------------------------------------------------------------
| HL | rsvd |C|E|U|A|P|R|S|F| размер окна |
-----------------------------------------------------------------
| Контрольная сумма TCP | указатель срочности |
-----------------------------------------------------------------
Поля порт-источник и порт-получатель используются для установки множественных входящих и исходящих соединений хостов. В подавляющем большинстве случаев в качестве интерфейса привязки приложений к сетевому стеку TCP используются сокеты Беркли. Благодаря портам сетевой стек знает, куда направлять трафик. Так как размер полей составляет 16 битов, порты имеют значения от 0 до 65535.
Так как каждый байт в потоке пронумерован, порядковый номер обозначает индекс окна сегмента TCP. При выполнении handshaking это поле содержит Initial Sequence Number (ISN).
Поле номер подтверждения содержит индекс окна следующего байта, который ожидает получить отправитель. После handshake поле ACK всегда должно иметь значение.
Поле Header Length (HL) обозначает длину заголовка в 32-битных словах.
Далее в заголовке идут различные флаги. Первые 4 бита (rsvd) не используются.
Congestion Window Reduced © используется, чтобы сообщать, что отправитель уменьшил частоту отправки.
ECN Echo (E) сообщает, что отправитель получил уведомление о перегрузке.
Urgent Pointer (U) обозначает, что сегмент содержит приоритетные данные.
Поле ACK (A) используется, чтобы сообщить о состоянии TCP handshake. Оно остаётся включенным в течение всего оставшегося времени соединения.
PSH (P) используется, чтобы сообщить, что получатель должен как можно быстрее отправить данные приложению.
RST ® сбрасывает TCP-соединение.
SYN (S) используется для синхронизации порядковых номеров в первоначальном handshake.
FIN (F) обозначает, что отправитель завершил передачу данных.
Поле размер окна
используется, чтобы сообщить о размере окна. Иными словами, это количество байтов, которое способен принять получатель. Так как это 16-битное поле, максимальный размер окна составляет 65535 байтов.
Поле контрольная сумма TCP
используется для проверки целостности сегмента TCP. Алгоритм тот же, что и для Internet Protocol, но входной сегмент также содержит данные TCP и псевдозаголовок из датаграммы IP.
Указатель срочности
используется, когда задан флаг U. Указатель сообщает позицию срочных данных в потоке.
После заголовка могут передаваться различные опции. Например, опция Maximum Segment Size (MSS), которой отправитель сообщает другой стороне максимальный размер сегментов.
После опций идут сами данные, однако они необязательны. Например, handshake выполняется только заголовками TCP.
TCP Handshake
TCP-соединение обычно состоит из следующих фаз: подготовка соединения (handshaking), передача данных и закрытие соединения. На схемах ниже показана процедура handshaking для TCP:
TCP A TCP B
1. ЗАКРЫТО ПРОСЛУШИВАНИЕ
2. SYN-SENT --> --> SYN-RECEIVED
3. УСТАНОВЛЕНО <-- <-- SYN-RECEIVED
4. УСТАНОВЛЕНО --> --> УСТАНОВЛЕНО
5. УСТАНОВЛЕНО --> --> УСТАНОВЛЕНО
Сокет хоста A находится в закрытом состоянии, то есть он не принимает соединения. Сокет хоста B привязан к конкретному порту и прослушивает новые соединения.
Хост A намеревается инициировать соединение с хостом B. Для этого A создаёт сегмент TCP, у которого задан собственный флаг SYN, а полю Sequence присвоено значение (100).
Хост B отвечает сегментом TCP с заданными полями SYN и ACK, и подтверждает порядковый номер хоста A, прибавив к нему 1 (ACK=101). Аналогично, B генерирует порядковый номер (300).
Трёхсторонний handshake завершается полем ACK от источника (A) запроса соединения. Поле Acknowledgement отражает порядковый номер, который хост далее ожидает получить от другой стороны.
Так как обе стороны подтвердили номера сегментов друг друга, начинается передача данных.
Таков стандартный сценарий установки TCP-соединения. Однако возникают вопросы:
Как выбирается начальный порядковый номер?
Что, если обе стороны одновременно запросят соединение у друг друга?
Что, если сегменты будут отложены на какое-то время или на неограниченный срок?
При первом контакте Initial Sequence Number (ISN) выбирается независимо обеими сторонами обмена данными. Это очень важная часть идентификации соединения, её необходимо выбирать так, чтобы она была с высокой долей вероятности уникальной и её нелегко было угадать. Злоумышленники могут использовать TCP Sequence Number Attack6 — атаку, при которой нападающий может реплицировать TCP-соединение и, по сути, передавать данные цели, выдав себя за доверенный хост.
В первоначальной спецификации говорится, что ISN выбирается счётчиком, выполняющим инкремент каждые 4 миллисекунды. Однако это значение может угадать нападающий. В реальности современные сетевые стеки генерируют ISN более сложными способами.
Ситуация, в которой обе конечные точки получают запрос на соединение (SYN) друг от друга, называется Simultaneous Open. Эта проблема решается дополнительным обменом сообщениями в TCP handshake: обе стороны посылают ACK (не зная, что другая сторона тоже делает это) и обе выполняют SYN-ACK запросов. После этого начинается передача данных.
Также у реализации TCP должен быть таймер для того, чтобы знать, когда перестать пытаться установить соединение. Выполняются попытки повторной установки соединения, обычно с экспоненциальной задержкой, но как только достигается максимальное количество попыток или времени, соединение считается несуществующим.
Опции TCP
Последнее поле в сегменте заголовка TCP зарезервировано под возможные опции TCP. В первоначальной спецификации присутствовали три опции, но в дальнейших спецификациях добавилось множество других. Ниже мы рассмотрим наиболее распространённые.
Опция Maximum Segment Size (MSS) сообщает максимальный размер сегмента TCP, который готова принять реализация TCP. Обычно в IPv4 для этого используется значение 1460 байтов.
Опция Selective Acknowledgment (SACK) оптимизирует ситуацию, в которой многие пакеты теряются при передаче, и окно данных получателя заполняется пробелами. Чтобы компенсировать снизившуюся в результате этого пропускную способность, реализация TCP может при помощи SACK сообщить отправителю конкретных пакетов, что она их не получила. Таким образом отправитель получает информацию о состоянии данных более простым образом, чем в схеме с накапливающимися подтверждениями.
Опция Window Scale увеличивает 16-битный размер окна. Если обе стороны включают эту опцию в свои сегменты handshake, то размер окна умножается на этот коэффициент. Наличие больших размеров окна в основном важно при передачи объёмных данных.
Опция Timestamps позволяет отправителю указать в сегменте TCP метку времени, которую затем можно использовать для вычисления RTT каждого сегмента ACK. Далее эту информацию можно применить для вычисления таймаута повторной передачи данных TCP.
Тестируем TCP Handshake
Теперь, когда у нас есть макет процедуры TCP handshake, прослушивающий каждый порт, давайте его протестируем:
[saminiir@localhost ~]$ nmap -Pn 10.0.0.4 -p 1337
Starting Nmap 7.00 ( https://nmap.org ) at 2016-05-08 19:02 EEST
Nmap scan report for 10.0.0.4
Host is up (0.00041s latency).
PORT STATE SERVICE
1337/tcp open waste
Nmap done: 1 IP address (1 host up) scanned in 0.05 seconds
Так как nmap выполняет SYN-сканирование (он ожидает только SYN-ACK, чтобы решить, открыт ли порт цели), его легко хитростью убедить, что на порту есть слушающее приложение, просто вернув сегмент TCP SYN-ACK.
Заключение
Минимально работоспособную процедуру TCP handshake можно относительно легко реализовать, просто выбрав порядковый номер, задав флаги SYN-ACK и вычислив контрольную сумму получившегося сегмента TCP.
В следующем посте мы рассмотрим самую важную функцию TCP: надёжную передачу данных. Управление окном потока крайне важно для передачи данных с помощью TCP, и его логика может быть достаточно сложной.
Возможность привязки приложений к реализации TCP осуществляется при помощи сокетов. Мы изучим API сокетов Беркли и попробуем имитировать его для приложения, позволив им использовать нашу реализацию TCP.
Исходный код для этого проекта выложен на GitHub.
Источники
https://en.wikipedia.org/wiki/OSI_model
https://tools.ietf.org/html/rfc675
https://tools.ietf.org/html/rfc7414
https://en.wikipedia.org/wiki/TCP/IP_Illustrated#Volume_1:_The_Protocols
http://www.tcpdump.org/tcpdump_man.html
http://www.ietf.org/rfc/rfc1948.txt