12 KiB
Задача 13 — арифметика подсетей IPv4 (C++)
Адреса передаются в host byte order (0x0A000001 = 10.0.0.1), маска — длиной префикса.
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, broadcast10.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, а не прямое сравнение чисел.
Проверь себя
1. Почему /31 не подчиняется общей формуле «минус 2 служебных адреса»?
Потому что у 2-адресного блока резервирование network и broadcast по общей формуле оставило бы 0 узлов — бессмысленно для канала точка-точка, где всего два участника и оба заранее известны, широковещание не нужно. RFC 3021 явно отдаёт оба адреса блока под хосты.2. Дано `ip=10.0.1.130`, `prefix=26`. Посчитать network, broadcast, first_host, last_host по шагам.
Хостовых бит: `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`.3. Почему `prefix_for_hosts(63) = 25`, а не 26, хотя 62 меньше 63 только на единицу?
`/26` физически даёт ровно 62 адреса под хосты — этого не хватает даже на один хост меньше требуемых 63. Следующий шаг «расширения» подсети — не +1 узел, а удвоение блока: снятие одного бита из префикса (`/25`) сразу даёт 126 узлов. Промежуточных вариантов между /26 и /25 не существует, поэтому единственный подходящий ответ — /25.4. Почему для `prefix=0` нельзя вычислять маску как `0xFFFFFFFF << (32-0)`?
Это сдвиг на 32 бита для 32-битного беззнакового типа — стандарт C/C++ определяет сдвиг только для величины меньше ширины типа в битах, сдвиг на саму ширину или больше — UB (на практике часто даёт исходное значение без сдвига вместо ожидаемого 0). Поэтому `prefix == 0` обрабатывается отдельной веткой: `mask = 0` напрямую.Разбор после сдачи: как считать префикс по числу узлов за O(1) (32 - ceil(log2(h+2))),
почему /31 — исключение, что такое маска в бинарном виде.