# Задача 04 — разбор IPv4-пакета и контрольная сумма (C++) ## Глава I. Общая информация - **Цель:** разобрать буфер байт из сокета в структуру заголовка вручную, безопасно и без UB — то, что реально делают сетевые железки на приёмном тракте. - **Почему это в Eltex:** это то, что реально делают сетевые железки: взять буфер из сокета и корректно разобрать заголовок, не выйдя за границы и не поверив «на слово» полям пакета. Коммутатор/маршрутизатор обязан отбросить пакет с битой контрольной суммой или враньём в `total_length`, иначе он либо читает чужую память, либо пересылает мусор дальше. - **Что сдаётся:** `solution.cpp`, проходящий `python3 grade.py 04`. ## Глава II. Что нужно знать до старта Минимальная длина IPv4-заголовка — 20 байт (без опций). Байт-порядок в заголовке смешанный: `total_length` и `header_checksum` хранятся в хост-порядке в структуре `Ipv4Header` (в задании явно указано «хост-порядок» — значит в `parse_ipv4` их нужно правильно собрать из сетевого порядка на проводе), а IP-адреса (`src_ip`, `dst_ip`) — «байты как на проводе»: `10.0.0.1 -> 0x0A000001`, то есть без перестановки байт при сборке в `uint32_t`. Почему нельзя просто `reinterpret_cast(buf)`: у `buf` нет гарантии выравнивания под `uint32_t` (нужно кратно 4 байтам) — компилятор вправе сгенерировать код, предполагающий выровненный доступ, и на невыровненном адресе это UB (UBSAN ловит как misaligned address); плюс раскладка полей в `Ipv4Header` определяется компилятором (padding), а не байтовым форматом протокола — сети всё равно, как выровнены поля в C++-структуре. Правильный путь — читать каждый байт по отдельности и собирать сдвигами (тот же приём, что в задаче 01: маски и сдвиги для сборки многобайтового числа из байт). Почему дальше: если разбор заголовка освоен, логичный следующий шаг — контрольная сумма TCP/UDP поверх псевдозаголовка, которая считается тем же алгоритмом сложения в дополнительном коде. ## Глава III. Задание ```c++ struct Ipv4Header { uint8_t version; // должно быть 4 uint8_t ihl; // длина заголовка в 32-битных словах uint8_t protocol; // 6 = TCP, 17 = UDP, 1 = ICMP uint16_t total_length; // из заголовка (хост-порядок) uint32_t src_ip; // байты как на проводе: 10.0.0.1 -> 0x0A000001 uint32_t dst_ip; uint16_t header_checksum; // поле из заголовка (хост-порядок) }; // false: буфер короче 20 байт, version != 4, ihl < 5, ihl*4 > len, // total_length < ihl*4 или total_length > len bool parse_ipv4(const uint8_t* buf, size_t len, Ipv4Header* out); // сумма в дополнительном коде по len байтам, затем инверсия — значение для записи в поле // (вызывается на буфере, где поле контрольной суммы уже обнулено) uint16_t compute_checksum(const uint8_t* buf, size_t len); // true, если контрольная сумма заголовка верна: сумма в дополнительном коде по ihl*4 байтам // заголовка (вместе с полем контрольной суммы) равна 0xFFFF; нагрузка не участвует bool checksum_valid(const uint8_t* buf, size_t len); ``` Проверка: `python3 grade.py 04`. Критерий: все `ok`, ASAN/UBSAN чистые. Запрещено приводить буфер к структуре через `reinterpret_cast` и читать поля как есть — на невыровненных адресах и в другом порядке байт это UB. ## Глава IV. Ловушки - `reinterpret_cast(buf)` вместо побайтового разбора → чтение по невыровненному адресу и/или с padding-полями структуры вместо реального формата пакета → UB, UBSAN сообщает misaligned address, а на реальных данных поля читаются со сдвигом. - Не обнулить поле `header_checksum` перед вызовом `compute_checksum` → в сумму попадает старое значение поля → результат не совпадёт с ожидаемым, тест на `compute_checksum` падает. - Пропустить перенос (carry) при сложении 16-битных слов в дополнительном коде, если промежуточная сумма считается в 16-битном типе → биты переноса теряются вместо того чтобы прибавиться обратно к младшим битам → неверная контрольная сумма на буферах, где сумма слов переполняет 16 бит. - Проверить `ihl*4 > len` до проверки `len >= 20` (или в неверном порядке) → чтение поля `ihl` из буфера короче 1 байта → выход за границы, ASAN сообщает heap-buffer-overflow. **Факты для карточек** - base | Минимальная длина IPv4-заголовка без опций? — 20 байт - base | Код `protocol` для TCP / UDP / ICMP? — 6 / 17 / 1 - core | В каком порядке лежат `src_ip`/`dst_ip` в буфере согласно заданию? — «как на проводе», без перестановки байт при сборке в `uint32_t` - core | Почему `reinterpret_cast` буфера в `Ipv4Header*` — UB? — нет гарантии выравнивания адреса под 4-байтные поля структуры - deep | Чему должна быть равна сумма в доп. коде по `ihl*4` байтам заголовка (вместе с полем checksum) при верной контрольной сумме? — 0xFFFF ## После сдачи Разбор: как поле `header_checksum` защищает именно заголовок (не данные), почему на каждом маршрутизаторе TTL уменьшается на 1 и, как следствие, контрольную сумму заголовка приходится пересчитывать заново на каждом хопе, и чем отличается контрольная сумма TCP/UDP (считается с псевдозаголовком) от чисто IP.