7.8 KiB
Задача 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.