Files
eduplan-cpp-eltex/diag/tasks/13_subnet/task.md
T

153 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Задача 13 — арифметика подсетей IPv4 (C++)
Адреса передаются в **host byte order** (`0x0A000001` = 10.0.0.1), маска — длиной префикса.
```c++
struct Subnet {
uint32_t network; // адрес сети
uint32_t broadcast; // широковещательный адрес
uint32_t first_host; // первый адрес для узла
uint32_t last_host; // последний адрес для узла
long long host_count; // сколько адресов узлов доступно (для /0 не влезает в int)
};
Subnet subnet_of(uint32_t ip, int prefix); // 0 <= prefix <= 32
bool same_subnet(uint32_t a, uint32_t b, int prefix);
int prefix_for_hosts(int hosts); // самая узкая подсеть, куда влезает hosts
```
Определения, которые нужно соблюсти:
- для `prefix <= 30`: `host_count = 2^(32-prefix) - 2`, `first_host = network + 1`,
`last_host = broadcast - 1`;
- `/31` (RFC 3021): `host_count = 2`, `first_host = network`, `last_host = broadcast`;
- `/32`: `host_count = 1`, `first_host = last_host = network = broadcast = ip`;
- `prefix_for_hosts(h)`: **наибольший** `prefix` из диапазона `0..30` (то есть самая узкая
подсеть), при котором `host_count >= h`. `h < 1` или `h > 2^32-2` → `-1`.
Исключения `/31` и `/32` в подборе не участвуют — они для линков и одиночных адресов.
Эталонные примеры, которые обязаны сойтись: `/26` → 62 узла; `/24` → 254; `/30` → 2;
`10.0.1.130/26` → сеть `10.0.1.128`, broadcast `10.0.1.191`, узлы `129..190`;
`prefix_for_hosts(62)` → 26, `prefix_for_hosts(63)` → 25, `prefix_for_hosts(254)` → 24,
`prefix_for_hosts(1)` → 30.
Проверка: `python3 grade.py 13`. Критерий: все `ok`, сборка без предупреждений.
**Факты для карточек**
- base | Сколько узлов даёт /24? — 254
- base | Сколько узлов даёт /26? — 62
- base | Сколько узлов даёт /30? — 2
- base | Сеть, broadcast и диапазон узлов для `10.0.1.130/26`? — сеть `10.0.1.128`, broadcast `10.0.1.191`, узлы `129..190`
Почему дальше: чтобы получить эти числа программно, а не подбором, нужен механизм вычисления
маски, границ сети и обратной задачи — подбора префикса по числу узлов.
## Механизм: маска, границы сети, обратный подбор префикса
**Маска по префиксу.** Маска — это `prefix` единиц в старших битах и `32-prefix` нулей в
младших: `mask = 0xFFFFFFFFu << (32 - prefix)` для `prefix > 0`. Для `prefix == 0` формула
не годится напрямую — сдвиг на 32 бита для 32-битного типа в C/C++ является неопределённым
поведением (UB), поэтому этот случай обрабатывается отдельно: `mask = 0`.
**Границы сети.** `network = ip & mask` — обнуляет все биты хостовой части (сохраняет
только биты сети). `broadcast = network | ~mask` — выставляет все биты хостовой части в
единицу (`~mask` — это как раз маска хостовой части, инвертированная сетевая). Число бит,
отданных под узлы, — `32 - prefix`; всего адресов в блоке — `2^(32-prefix)`.
**Разбор эталонного примера `10.0.1.130/26`.** `/26` оставляет `32-26=6` бит под узлы,
блок из `2^6 = 64` адресов. Адрес `.130` попадает в блок `128..191` (границы блоков кратны
64): `network = 10.0.1.128`, `broadcast = 10.0.1.191`. Обычные узлы — `first_host =
network+1 = .129`, `last_host = broadcast-1 = .190`. Из 64 адресов блока минус 2 служебных
(сеть и broadcast) — 62 узла, что совпадает с `/26 → 62 узла`.
**Почему `/26` даёт именно 62 узла.** `host_count = 2^(32-26) - 2 = 2^6 - 2 = 64 - 2 = 62`.
Аналогично `/24`: `2^8 - 2 = 254`; `/30`: `2^2 - 2 = 2` — ровно минимум для соединения
точка-точка между двумя маршрутизаторами.
**`/31` — исключение (RFC 3021).** По общей формуле `/31` дал бы `2^1 - 2 = 0` узлов —
бессмысленный результат для блока из 2 адресов. RFC 3021 разрешает для каналов
точка-точка (ровно 2 узла на линке) отдавать под хосты оба адреса блока: широковещательный
адрес такому каналу физически не нужен (получателей всего два, и оба известны заранее), а
резервировать половину 2-адресного блока под network/broadcast — чистая потеря адресного
пространства. Поэтому `host_count = 2`, `first_host = network`, `last_host = broadcast`.
**`/32` — единичный адрес.** Блок из одного адреса: `network = broadcast = first_host =
last_host = ip`, `host_count = 1`. Используется для маршрутов на конкретный узел
(host route) или loopback-адресов маршрутизатора.
**Обратная задача — `prefix_for_hosts(h)`.** Нужен наибольший `prefix` (самая узкая
подсеть), при котором `2^(32-prefix) - 2 >= h`, то есть `2^(32-prefix) >= h+2`, то есть
`32-prefix >= log2(h+2)`. Так как число хостовых бит — целое, наименьшее подходящее
значение — `32-prefix = ceil(log2(h+2))`, откуда:
```
prefix_for_hosts(h) = 32 - ceil(log2(h+2))
```
Проверка на эталонных примерах: `h=62` → `h+2=64`, `log2=6`, `prefix=26` ✓. `h=63` →
`h+2=65`, `log2(65)≈6.02`, `ceil=7`, `prefix=25` ✓ (62 узла `/26` уже не хватает на 63-й
хост, нужен на бит шире — `/25` с 126 узлами). `h=254` → `h+2=256`, `log2=8`, `prefix=24` ✓.
`h=1` → `h+2=3`, `log2(3)≈1.58`, `ceil=2`, `prefix=30` ✓.
**Факты для карточек**
- base | Формула маски для префикса p (0<p≤32)? — `0xFFFFFFFF << (32-p)`; для p=0 маска = 0 отдельным случаем (сдвиг на 32 — UB)
- base | Как получить network и broadcast из ip и mask? — `network = ip & mask`, `broadcast = network | ~mask`
- core | Формула host_count для prefix ≤ 30? — `2^(32-prefix) - 2`
- core | Почему /31 — исключение и даёт 2 узла вместо 0 по общей формуле? — RFC 3021: у канала точка-точка ровно 2 узла, broadcast не нужен, поэтому оба адреса 2-адресного блока отдаются под хосты вместо потери половины блока
- core | Что возвращает subnet_of для /32? — network=broadcast=first_host=last_host=ip, host_count=1 (host route/loopback)
- core | Формула `prefix_for_hosts(h)`? — `32 - ceil(log2(h+2))`, из условия `2^(32-prefix) >= h+2`
- deep | Почему `prefix_for_hosts(63) = 25`, а не 26, хотя 62 меньше 63 всего на 1? — /26 даёт только 62 узла, этого не хватает даже на 1 хост меньше требуемых 63; нужно расширить хостовую часть на 1 бит — /25 с 126 узлами
## Ловушки
- Вычислить маску как `0xFFFFFFFF << (32 - prefix)` при `prefix == 0` без отдельной ветки
→ сдвиг на 32 бита для 32-битного типа — неопределённое поведение в C/C++ (компилятор
может дать любое значение, не обязательно 0) → UBSan ловит это как сдвиг за пределы
разрядности типа.
- Применить общую формулу `host_count = 2^(32-p) - 2` к `/31` без проверки исключения →
получится 0 узлов вместо 2 → тест на `/31` (RFC 3021) падает.
- Использовать `int` вместо `long long` для `host_count` → для `/0` значение `2^32 - 2` не
влезает в `int` (переполнение со знаком — UB) → в условии структуры явно указан `long long`
именно из-за этого случая.
- Перепутать host byte order с network byte order при сравнении с реальным трафиком или
утилитами вроде `tcpdump` → `10.0.0.1` в host order этой задачи — это `0x0A000001`, но в
сетевом порядке байты переставлены (`0x0100000A` при little-endian хосте) — конвертация
через `htonl`/`ntohl`, а не прямое сравнение чисел.
## Проверь себя
<details>
<summary>1. Почему /31 не подчиняется общей формуле «минус 2 служебных адреса»?</summary>
Потому что у 2-адресного блока резервирование network и broadcast по общей формуле оставило
бы 0 узлов — бессмысленно для канала точка-точка, где всего два участника и оба заранее
известны, широковещание не нужно. RFC 3021 явно отдаёт оба адреса блока под хосты.
</details>
<details>
<summary>2. Дано `ip=10.0.1.130`, `prefix=26`. Посчитать network, broadcast, first_host,
last_host по шагам.</summary>
Хостовых бит: `32-26=6`, размер блока `2^6=64`. Адрес `.130` попадает в блок, начинающийся
на границе, кратной 64: `128 <= 130 < 192`, значит `network=10.0.1.128`,
`broadcast=10.0.1.128+63=10.0.1.191`, `first_host=network+1=10.0.1.129`,
`last_host=broadcast-1=10.0.1.190`.
</details>
<details>
<summary>3. Почему `prefix_for_hosts(63) = 25`, а не 26, хотя 62 меньше 63 только на
единицу?</summary>
`/26` физически даёт ровно 62 адреса под хосты — этого не хватает даже на один хост меньше
требуемых 63. Следующий шаг «расширения» подсети — не +1 узел, а удвоение блока: снятие
одного бита из префикса (`/25`) сразу даёт 126 узлов. Промежуточных вариантов между /26 и
/25 не существует, поэтому единственный подходящий ответ — /25.
</details>
<details>
<summary>4. Почему для `prefix=0` нельзя вычислять маску как `0xFFFFFFFF << (32-0)`?</summary>
Это сдвиг на 32 бита для 32-битного беззнакового типа — стандарт C/C++ определяет сдвиг
только для величины меньше ширины типа в битах, сдвиг на саму ширину или больше — UB
(на практике часто даёт исходное значение без сдвига вместо ожидаемого 0). Поэтому
`prefix == 0` обрабатывается отдельной веткой: `mask = 0` напрямую.
</details>
Разбор после сдачи: как считать префикс по числу узлов за O(1) (`32 - ceil(log2(h+2))`),
почему `/31` — исключение, что такое маска в бинарном виде.