105. Сеть снизу вверх: Ethernet, MAC, IP, подсети, ARP
Сокеты в разделе 78 пользовались сетью как готовой: адрес, порт, и данные пошли. Здесь — что под ними. Прибор шлёт в провод кадр Ethernet, а чтобы отправить его соседу, нужен MAC-адрес соседа — знает же прибор только его IP. Между ними стоят маска подсети, шлюз и ARP. Все кадры раздела собирает переносимый код на C, и на компьютере он же разговаривает с настоящим ядром Linux в соседнем контейнере: ARP-запрос и ping уходят через сырой сокет, ответы разбирают те же функции. Кадр с неверной контрольной суммой ядро выбрасывает молча — раз оно ответило, суммы посчитаны верно.
Сокеты — раздел 78, типы сетей — раздел 86, контейнеры — раздел 87.
Как это называется в документации
Оригинал Перевод frame кадр preamble, start frame delimiter (SFD) преамбула, начало кадра MAC address физический адрес сетевого интерфейса organizationally unique identifier (OUI) код производителя в MAC-адресе locally administered address локально назначенный адрес EtherType тип содержимого кадра frame check sequence (FCS) контрольная сумма кадра, CRC-32 interframe gap межкадровая пауза subnet mask, prefix length маска подсети, длина префикса default gateway шлюз по умолчанию broadcast address широковещательный адрес address resolution protocol (ARP) поиск MAC-адреса по IP-адресу ones' complement checksum контрольная сумма в обратном коде time to live (TTL) время жизни пакета: сколько маршрутизаторов он ещё пройдёт raw socket сырой сокет: кадры целиком, мимо стека
105.1 Кадр Ethernet
| Поле | Байт | Что это |
|---|---|---|
| преамбула и начало кадра | 8 | семь байт 0x55 и 0xD5: приёмник подстраивает часы |
| адрес назначения | 6 | MAC получателя или FF:FF:FF:FF:FF:FF — всем |
| адрес отправителя | 6 | свой MAC |
| тип | 2 | 0x0800 — IPv4, 0x0806 — ARP |
| данные | 46 … 1500 | короче 46 — дополняют нулями |
| FCS | 4 | CRC-32 от адреса назначения до конца данных |
| пауза | 12 | тишина перед следующим кадром |
Кадр — от 64 до 1518 байт, а в проводе с преамбулой и паузой занимает от 84 до 1538 байтовых интервалов. На 100 Мбит/с это 148 810 самых коротких кадров в секунду или 8127 самых длинных. Полезные данные занимают в длинном кадре — 97,53 % времени линии, в коротком — 54,76 %. Самый короткий кадр длится 5,12 мкс.
У F407 MAC встроен, а приёмопередатчик (PHY) — отдельная микросхема. Между ними RMII — 50 МГц по 2 бита или MII — 25 МГц по 4 бита: оба дают 100 Мбит/с. У F103 MAC нет — сеть подключают внешним контроллером на SPI.
Собрать кадр — значит сложить поля по порядку:
unsigned eth_frame(uint8_t *f, const uint8_t dst[6], const uint8_t src[6], uint32_t type,
const uint8_t *data, unsigned n)
{
memcpy(f, dst, 6);
memcpy(f + 6, src, 6);
put16(f + 12, type);
memcpy(f + ETH_HDR, data, n);
unsigned len = ETH_HDR + n;
if (len < ETH_MIN) {
memset(f + len, 0, ETH_MIN - len);
len = ETH_MIN;
}
uint32_t fcs = eth_crc32(f, len);
for (unsigned i = 0; i < ETH_FCS; i++)
f[len++] = (uint8_t)(fcs >> (8 * i)); /* FCS — младшим байтом вперёд */
return len;
}
Числа в полях идут старшим байтом вперёд, а Cortex-M хранит их младшим —
put16 раскладывает по байтам явно. На плате FCS и дополнение до 60 байт
MAC F407 делает сам, а преамбулу добавляет PHY.
105.2 FCS: CRC-32
uint32_t eth_crc32(const uint8_t *p, unsigned n)
{
uint32_t c = 0xFFFFFFFFu;
while (n--) {
c ^= *p++;
for (unsigned k = 0; k < 8; k++)
c = (c >> 1) ^ (0xEDB88320u & (0u - (c & 1u)));
}
return ~c;
}
Контрольное значение по строке «123456789» — 0xCBF43926, как у всех
реализаций этого CRC-32. Приёмник не вынимает FCS из кадра: CRC по кадру
вместе с ним всегда равна 0x2144DF1C. Испорчен один бит — и сумма уже
другая (для этого кадра — 0x0B6CE77E): всё, что не 0x2144DF1C, означает
ошибку. Кадр выбрасывают молча, а повтор — забота протоколов выше.
У блока CRC в F1, F2, F4 и L1 другой порядок бит и нет финальной инверсии
(раздел 12), так что для FCS он без перестановки бит не годится. У F0, F3, F7,
L4, G0, G4 и H7 порядок задают биты REV_IN и REV_OUT: с разворотом входа
по байтам, разворотом выхода и инверсией результата блок считает FCS сам.
105.3 MAC-адрес
MAC — 6 байт. У купленного адреса первые три — код производителя. Два младших бита первого байта служебные: бит 0 — групповой адрес, у адреса устройства он ноль; бит 1 — адрес назначен локально, а не производителем. У STM32 своего MAC нет: адрес покупают блоком, берут из микросхемы с зашитым адресом или назначают локально. Для прибора в своей сети локальный адрес строят из уникального номера кристалла:
#define MAC_LOCAL 0x02u /* бит 1 первого байта: адрес назначен локально */
#define MAC_GROUP 0x01u /* бит 0: групповой; у адреса устройства — ноль */
void mac_from_uid(const uint8_t uid[12], uint8_t mac[6])
{
uint64_t h = 0xCBF29CE484222325u; /* FNV-1a, 64 бита: 96 бит номера → 46 бит адреса */
for (unsigned i = 0; i < 12; i++)
h = (h ^ uid[i]) * 0x100000001B3u;
for (unsigned i = 0; i < 6; i++)
mac[i] = (uint8_t)(h >> (8 * i));
mac[0] = (uint8_t)((mac[0] & ~(MAC_LOCAL | MAC_GROUP)) | MAC_LOCAL);
}
void net_mac(uint8_t mac[6])
{
uint8_t uid[12];
memcpy(uid, (const void *)NET_UID_ADDR, sizeof uid);
mac_from_uid(uid, mac);
}
Номер лежит по адресу 0x1FFFF7E8 у F103 и 0x1FFF7A10 у F407. Номер для примера 30 00 3C 00 0F 51 33 30 35 39 32 37 даёт адрес 06:42:8F:70:AA:C7: младшие биты первого байта — 10, локальный индивидуальный. Совпадение не исключено, но маловероятно: 46 бит — это 70 368 744 177 664 адресов, и вероятность совпадения в сети из 1000 приборов — около 7,1·10⁻⁹.
105.4 IPv4: заголовок и контрольная сумма
| Байты | Поле |
|---|---|
| 0 | версия 4 и длина заголовка в словах: 0x45 |
| 1 | класс обслуживания, обычно 0 |
| 2–3 | длина всего пакета |
| 4–5 | номер пакета |
| 6–7 | флаги (0x40 — «не дробить») и смещение фрагмента |
| 8 | TTL: сколько маршрутизаторов пакет ещё пройдёт |
| 9 | протокол: 1 — ICMP, 6 — TCP, 17 — UDP |
| 10–11 | контрольная сумма заголовка |
| 12–15 | адрес отправителя |
| 16–19 | адрес получателя |
Сумма — 16-битные слова, сложенные с переносом по кругу и в конце инвертированные:
uint32_t ip_checksum(const uint8_t *p, unsigned n)
{
uint32_t s = 0;
for (unsigned i = 0; i + 1 < n; i += 2)
s += get16(p + i);
if (n & 1u)
s += (uint32_t)p[n - 1] << 8;
while (s >> 16)
s = (s & 0xFFFFu) + (s >> 16);
return ~s & 0xFFFFu;
}
Заголовок 45 00 00 73 00 00 40 00 40 11 00 00 C0 A8 00 01 C0 A8 00 C7 с нулём в поле суммы даёт 0xB861. В нём «40 00» — «не дробить» и смещение 0, «40» в байте 8 — TTL 64, «11» — UDP. Приёмник считает сумму по заголовку вместе с этим полем и получает ноль. Та же функция считает сумму ICMP, UDP и TCP — у последних двух ещё и с псевдозаголовком из адресов.
105.5 Подсети и шлюз
Адрес пишут с длиной префикса: 192.168.1.130/26 — это 26 единиц маски слева. Сеть — адрес, умноженный на маску по битам; широковещательный — сеть с единицами во всех битах узла:
| Префикс | Маска | Сеть | Широковещательный | Узлов |
|---|---|---|---|---|
| /24 | 255.255.255.0 | 192.168.1.0 | 192.168.1.255 | 254 |
| /26 | 255.255.255.192 | 192.168.1.128 | 192.168.1.191 | 62 |
| /30 | 255.255.255.252 | 192.168.1.128 | 192.168.1.131 | 2 |
| /31 | 255.255.255.254 | 192.168.1.130 | 192.168.1.131 | 2 |
| /32 | 255.255.255.255 | 192.168.1.130 | 192.168.1.130 | 1 |
У /31 адресов сети и широковещательного нет — это два узла на линии точка-точка.
uint32_t ip_mask(unsigned prefix)
{
return prefix ? 0xFFFFFFFFu << (32u - prefix) : 0u;
}
uint32_t next_hop(uint32_t me, unsigned prefix, uint32_t gw, uint32_t dst)
{
return ((me ^ dst) & ip_mask(prefix)) ? gw : dst;
}
Решение «сам или через шлюз» — одна операция: адреса совпадают под маской — адресат рядом, и кадр идёт ему; нет — кадр уходит MAC-адресу шлюза, а в IP-заголовке остаётся адрес адресата. Прибор 192.168.1.130/26 со шлюзом .129 до .150 достаёт сам, а до .20 — через шлюз.
105.6 ARP: MAC по IP
Прежде чем послать кадр, нужен MAC. ARP спрашивает всех: «у кого 192.168.1.129? Ответьте 192.168.1.130».
unsigned arp_build(uint8_t *a, const arp_t *r)
{
put16(a, 1); /* оборудование: Ethernet */
put16(a + 2, ETH_IPV4); /* протокол: IPv4 */
a[4] = 6; /* длина MAC */
a[5] = 4; /* длина адреса IPv4 */
put16(a + 6, r->op);
memcpy(a + 8, r->sha, 6);
put32(a + 14, r->spa);
memcpy(a + 18, r->tha, 6);
put32(a + 24, r->tpa);
return ARP_LEN;
}
ARP — 28 байт; с заголовком кадра — 42, с дополнением до 60 и FCS — 64. Весь запрос:
ff ff ff ff ff ff 06 42 8f 70 aa c7 08 06 кому — всем, от кого, тип ARP
00 01 08 00 06 04 00 01 Ethernet, IPv4, длины 6 и 4, запрос
06 42 8f 70 aa c7 c0 a8 01 82 MAC и IP отправителя
00 00 00 00 00 00 c0 a8 01 81 искомый MAC — нули, искомый IP
00 × 18 дополнение до 60 байт
3b 6c 8b c2 FCS, младшим байтом вперёд
Отвечают уже не всем, а спросившему: операция 2, и MAC владельца стоит в поле отправителя. Ответ хранят в таблице ARP несколько минут, чтобы не спрашивать перед каждым пакетом.
105.7 Опыт: ARP и ping настоящему ядру
Два контейнера в своей сети 10.105.105.0/24: в .10 работает код раздела,
в .20 — обычный Linux. Код открывает сырой сокет AF_PACKET на eth0
и отправляет кадры, собранные функциями раздела, — без FCS: его добавляет
сетевой интерфейс. Сначала ARP-запрос о .20, потом ping по MAC-адресу
из ответа. Ответы разбирают те же функции:
- ARP-ответ от 10.105.105.20: операция 2, 42 байта, адресован нашему MAC. Кадр короче 60 байт: на виртуальном интерфейсе дополнять некому, это делает сетевая карта перед проводом.
- Эхо-ответ: тип 0, TTL 64, пакет 60 байт — 20 заголовка, 8 ICMP и 32 наших.
Обе суммы ответа сошлись по нашей
ip_checksum, данные вернулись те же.
Ядро выбрасывает пакет с неверной суммой без ответа, так что сборка заголовка и суммы проверены самим получателем. TTL 64 — начальное значение Linux; пройди ответ через маршрутизатор, пришло бы 63.
105.8 Грабли
Кадры уходят, но никто не отвечает. Тип записан младшим байтом вперёд: 0x0608 вместо 0x0806. Числа в полях — старшим байтом вперёд.
Два прибора дерутся за адрес. Одинаковые MAC: у обоих адрес из примера. Строить адрес из номера кристалла с битом «локальный».
Прибор видит соседей, но не интернет. Нет шлюза или маска шире настоящей: чужой адрес прибор ищет ARP-ом сам, и никто не отвечает.
ARP есть, а ping молчит. Неверная сумма IP или ICMP — ядро выбрасывает пакет молча. Считать сумму при нуле в её поле.
FCS, посчитанная блоком CRC STM32, не сходится. У F1 и F4 у блока другой
порядок бит и нет финальной инверсии; у семейств с REV_IN/REV_OUT их не включили.
Короткий кадр не принимают. Меньше 60 байт без FCS: дополнить нулями. MAC F407 делает это сам, программная сборка — нет.
105.9 Итоги
- Кадр — 14 байт заголовка, 46–1500 данных и 4 FCS, всего 64–1518 байт; в проводе ещё 20: 148 810 коротких кадров в секунду на 100 Мбит/с.
- FCS — CRC-32, по кадру вместе с ней выходит 0x2144DF1C.
- MAC: бит 0 — групповой, бит 1 — локальный; у STM32 адрес строят из номера кристалла.
- IPv4: 20 байт заголовка и сумма в обратном коде; по заголовку вместе с суммой — ноль.
- Маска решает, слать ли адресату или шлюзу, а ARP находит MAC по IP.
- Настоящее ядро Linux ответило на наши ARP и ping: 42 байта, TTL 64, суммы верны.
Дальше: раздел 106 «TCP и UDP изнутри: рукопожатие, окна, повторы, NAT» — что добавляет транспорт поверх IP и почему соединение переживает потерю пакетов.