92 lines
7.8 KiB
Markdown
92 lines
7.8 KiB
Markdown
# Задача 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. Задание
|
||
|
||
```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<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.
|