Files
eduplan-cpp-eltex/diag/tasks/04_ipv4/task.md
T

7.8 KiB
Raw Blame History

Задача 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<const Ipv4Header*>(buf): у buf нет гарантии выравнивания под uint32_t (нужно кратно 4 байтам) — компилятор вправе сгенерировать код, предполагающий выровненный доступ, и на невыровненном адресе это UB (UBSAN ловит как misaligned address); плюс раскладка полей в Ipv4Header определяется компилятором (padding), а не байтовым форматом протокола — сети всё равно, как выровнены поля в C++-структуре. Правильный путь — читать каждый байт по отдельности и собирать сдвигами (тот же приём, что в задаче 01: маски и сдвиги для сборки многобайтового числа из байт).

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

Глава III. Задание

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<Ipv4Header*>(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.