Компьютерные сети Канальный уровень: Ethernet, MAC, ARP, коммутаторы, VLAN
0%

Канальный уровень: Ethernet, MAC, ARP, коммутаторы, VLAN

Канальный уровень: Ethernet, MAC, ARP, коммутаторы, VLAN

Почти всё, что инженер знает о сети, начинается на третьем уровне: «у пода такой-то IP», «маршрут ушёл не туда», «фаервол режет порт». Канальный уровень при этом выглядит как нечто, что «просто работает»: воткнул кабель — заработало. Ровно до того дня, когда приложение начинает терять пакеты раз в час, ping до соседней машины идёт 3 секунды вместо 0,3 мс, а виртуалка после миграции пятнадцать секунд недоступна снаружи. Все три симптома живут на L2, и наверху их не видно вообще.

Канальный уровень — это ответ на один вопрос: как доставить блок данных между двумя устройствами, подключёнными к одной физической среде. Не через полмира — только до соседа по проводу или по радио. Всё остальное (глобальная адресация, маршрутизация, надёжность) строится этажами выше и разбирается в следующих статьях трека; карта уровней — в обзоре трека, а базовая интуиция про стек протоколов — в статье «Как компьютеры общаются».

Задача звучит скромно, но внутри неё четыре нетривиальные подзадачи:

  1. Разграничить поток бит на блоки — приёмник должен понять, где кадр начался и где кончился.
  2. Обнаружить порчу — среда шумит, бит переворачивается, и принять испорченные данные хуже, чем не принять никакие.
  3. Адресовать — к одной среде подключены десятки устройств, кадр должен найти своего.
  4. Разделить среду — если все говорят одновременно, не слышно никого.

Ethernet решает все четыре и делает это уже пятьдесят лет — редкий случай технологии, которая пережила смену физики (коаксиал → витая пара → оптика), скорости (10 Мбит/с → 400 Гбит/с) и топологии (шина → звезда), не поменяв формат кадра. Начнём именно с кадра: это самый честный способ понять, что реально происходит на проводе.

Кадр Ethernet: что летит по проводу

Кадр Ethernet II: раскладка байтов, структура MAC-адреса и тега 802.1Q

Разберём поля по порядку — каждое из них появилось как ответ на конкретную проблему.

Преамбула (7 байт) и SFD (1 байт). Чередование 10101010, завершающееся 10101011. Приёмник не имеет общих часов с передатчиком: он восстанавливает тактовую частоту из самого сигнала. Преамбула — это «тон настройки», по которому PLL приёмника синхронизируется, а SFD говорит «настройка кончилась, дальше данные». Решает подзадачу 1 — обозначение границы кадра.

MAC назначения и MAC источника (по 6 байт). Адресация внутри сегмента. Назначение стоит первым намеренно: приёмник должен решить «мой это кадр или чужой» как можно раньше, ещё до того, как кадр целиком приехал, — так дешевле в железе.

EtherType (2 байта). Тип полезной нагрузки: 0x0800 — IPv4, 0x86DD — IPv6, 0x0806 — ARP, 0x8100 — тег VLAN 802.1Q, 0x88A8 — QinQ, 0x88CC — LLDP, 0x8809 — служебные протоколы вроде LACP. Без него драйвер не знал бы, кому передать содержимое. Историческая тонкость: в стандарте IEEE 802.3 это поле означало длину, а в DIX-варианте (Ethernet II) — тип. Их развели по значению: всё, что меньше 1536 (0x0600), — длина, всё, что больше или равно, — EtherType. Практически весь современный трафик — Ethernet II.

Полезная нагрузка (46–1500 байт). 1500 — это и есть знаменитый MTU. Нижняя граница важнее, чем кажется: кадр не может быть короче 64 байт (заголовки + данные + FCS), и если данных мало, драйвер добивает их нулями (padding). Почему 64? Это наследие CSMA/CD: чтобы станция гарантированно успела заметить коллизию до того, как договорит кадр, время передачи кадра должно быть не меньше двойного времени распространения сигнала по максимально длинному сегменту. Для 10 Мбит/с и 2500 метров это ровно 512 бит-тактов = 64 байта. Коллизий давно нет, а минимальный размер остался — обратная совместимость.

FCS (4 байта). CRC-32 по всему кадру от MAC назначения до конца данных. Не исправляет ошибки — только обнаруживает. Кадр с несошедшимся CRC уничтожается молча, никакого уведомления отправителю: восстановление — забота вышестоящих уровней (см. TCP). Решает подзадачу 2.

Межкадровый интервал (12 байт тишины). Пауза, за которую приёмник успевает «выдохнуть» и подготовиться к следующему кадру.

Отсюда — полезная арифметика: кадр с 1500 байтами данных занимает на проводе 8 + 14 + 1500 + 4 + 12 = 1538 байт. То есть на гигабитном линке «чистых» данных не больше 1500/1538 ≈ 97,5 % от 1 Гбит/с, а если гнать мелкие пакеты по 64 байта — эффективность падает до 46/84 ≈ 55 %, и максимум составит около 1,49 млн кадров в секунду. Это не теория: именно в pps, а не в гигабитах, упираются фаерволы и виртуальные коммутаторы.

Смотрим кадры своими глазами

tcpdump по умолчанию показывает сетевой уровень и выше. Флаг -e включает печать заголовка канального уровня — это первое, что нужно нажимать при отладке L2:

sudo tcpdump -e -n -i ens1 -c 5 'arp or icmp'
14:22:31.417183 56:6f:f3:01:18:04 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806),
    length 42: Request who-has 82.38.60.1 tell 82.38.60.56, length 28
14:22:31.417602 00:00:5e:00:01:1b > 56:6f:f3:01:18:04, ethertype ARP (0x0806),
    length 60: Reply 82.38.60.1 is-at 00:00:5e:00:01:1b, length 46
14:22:31.417640 56:6f:f3:01:18:04 > 00:00:5e:00:01:1b, ethertype IPv4 (0x0800),
    length 98: 82.38.60.56 > 8.8.8.8: ICMP echo request, id 4711, seq 1, length 64

Три строки — три урока. Первая: ARP-запрос ушёл на широковещательный адрес и весит 42 байта (14 заголовок + 28 тело). Вторая: ответ пришёл уникастом и весит 60 байт — те самые 42, добитые padding-ом до минимума. Третья: сразу после разрешения адреса поехал ICMP, и MAC назначения в нём — не адрес 8.8.8.8 (у него нет MAC в нашем сегменте), а MAC шлюза. Это ключевая мысль всего канального уровня: dst MAC всегда указывает на следующий хоп, dst IP — на конечную цель.

Параметры интерфейса смотрим через ip и ethtool:

ip -br link
lo               UNKNOWN        00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
ens1             UP             56:6f:f3:01:18:04 <BROADCAST,MULTICAST,UP,LOWER_UP>
docker0          DOWN           0e:8a:b6:36:28:aa <NO-CARRIER,BROADCAST,MULTICAST,UP>

Флаг UP означает «интерфейс административно включён», LOWER_UP — «на проводе есть несущая». Именно их путают: у docker0 стоит UP, но NO-CARRIER — линка нет, потому что в мост ничего не подключено. Первый вопрос при «сеть не работает» — есть ли LOWER_UP.

sudo ethtool ens1
Settings for ens1:
	Supported link modes:   10baseT/Half 10baseT/Full
	                        100baseT/Half 100baseT/Full
	                        1000baseT/Full
	Supports auto-negotiation: Yes
	Speed: 1000Mb/s
	Duplex: Full
	Auto-negotiation: on
	Port: Twisted Pair
	Link detected: yes

Duplex: Full и Auto-negotiation: on — то, что должно быть всегда. Классическая многолетняя боль: на одной стороне линка скорость и дуплекс зафиксированы вручную, на другой включено автосогласование. Автосогласование не получает ответа, откатывается в half-duplex, и получается duplex mismatch: линк «поднят», ping работает, а под нагрузкой начинаются late collisions и потери 5–30 %. Симптом — счётчики:

sudo ethtool -S ens1 | grep -E 'crc|error|collision|dropped'
     rx_errors: 0
     tx_errors: 0
     tx_dropped: 0
     collisions: 0
     rx_length_errors: 0
     rx_crc_errors: 0
     rx_frame_errors: 0
     rx_missed_errors: 0

Ненулевой rx_crc_errors — это физика: битый патч-корд, грязный SFP, наводка, слишком длинный кабель. Никакой перезапуск приложения это не лечит. Ненулевые collisions на современном линке — почти наверняка duplex mismatch. Агрегированные счётчики видны и в ip -s link:

ip -s link show ens1
2: ens1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT
    link/ether 56:6f:f3:01:18:04 brd ff:ff:ff:ff:ff:ff
    RX:    bytes   packets errors dropped  missed   mcast
    115713958265 140480516      0   24516       0       0
    TX:    bytes   packets errors dropped carrier collsns
     89958299392  84347376      0       0       0       0

Здесь errors: 0, но dropped: 24516 — это не порча кадров, а «пришло то, что некому отдать» (обычно кадры с чужим VLAN-тегом или протоколом, который никто не слушает). Различать errors и dropped полезно: первое — про провод, второе — про софт.

MAC-адрес: 48 бит, в которых больше смысла, чем кажется

MAC (Media Access Control) — адрес сетевого интерфейса в пределах сегмента. 48 бит, обычно записываются как 56:6f:f3:01:18:04. Внутри — структура:

  • Старшие 24 бита — OUI (Organizationally Unique Identifier), купленный производителем у IEEE. По нему определяется вендор: базу можно посмотреть на oui.ieee.org, Wireshark делает это автоматически.
  • Младшие 24 бита — серийный номер карты у этого вендора.
  • Бит I/G (Individual/Group) — младший бит первого байта. 0 — одноадресный, 1 — групповой.
  • Бит U/L (Universal/Local) — второй младший бит первого байта. 1 — адрес назначен локально: виртуалка, контейнер, bond-интерфейс, рандомизация Wi-Fi.

Проверим на нашей машине: 56 = 0101 0110. Младший бит 0 → unicast. Второй бит 1 → locally administered. И действительно, это виртуальная машина, гипервизор сгенерировал адрес сам, а не купил OUI. Именно поэтому не стоит строить лицензирование или инвентаризацию на «уникальном MAC» — он меняется одной командой:

sudo ip link set dev ens1 address 02:00:00:00:00:01

Ноутбуки поступают так же по умолчанию: со времён Android 10 и iOS 14 телефон использует случайный MAC для каждой Wi-Fi-сети, чтобы его нельзя было отслеживать по торговым центрам. Все такие адреса имеют U/L = 1.

Групповые адреса (I/G = 1) — отдельный мир, который стоит узнавать в лицо:

Адрес Кто это
ff:ff:ff:ff:ff:ff широковещание — «всем в сегменте»
01:00:5e:xx:xx:xx IPv4-мультикаст; младшие 23 бита IP-группы копируются в MAC
33:33:xx:xx:xx:xx IPv6-мультикаст; младшие 32 бита адреса копируются в MAC
01:80:c2:00:00:00 BPDU протокола STP
01:80:c2:00:00:02 LACP и другие «медленные протоколы»
01:80:c2:00:00:0e LLDP — обнаружение соседей

Их список для конкретного интерфейса видно прямо в системе:

bridge fdb show dev ens1
33:33:00:00:00:01 dev ens1 self permanent      # ff02::1 — все узлы IPv6
01:00:5e:00:00:01 dev ens1 self permanent      # 224.0.0.1 — все узлы IPv4
33:33:ff:01:18:04 dev ens1 self permanent      # solicited-node для нашего IPv6
01:00:5e:00:00:fb dev ens1 self permanent      # mDNS
01:80:c2:00:00:00 dev ens1 self permanent      # STP

Это фильтр, зашитый в NIC: карта принимает кадры на свой unicast-MAC, на broadcast и на явно разрешённые групповые адреса, а остальное отбрасывает в железе, не тратя такты CPU. Когда tcpdump переводит интерфейс в promiscuous mode, он просто выключает этот фильтр — поэтому захват трафика заметно нагружает машину, а в логах ядра появляется device ens1 entered promiscuous mode.

Как Ethernet стал коммутируемым

Первый Ethernet был буквально общим проводом: все станции слушали один коаксиал. Отсюда CSMA/CD (Carrier Sense Multiple Access with Collision Detection) — «слушай эфир, начинай говорить в тишине, при коллизии замолчи и повтори через случайную паузу», причём пауза удваивалась при каждой неудаче (binary exponential backoff). Схема гениально простая, но с жёстким потолком: при загрузке выше ~40 % коллизии съедали канал, а весь сегмент был одним доменом коллизий.

Хаб (1990-е) поменял топологию на звезду, но не физику: он тупо повторял каждый бит во все порты, так что домен коллизий остался общим. Настоящую революцию сделал коммутатор: он не повторяет биты, а принимает кадр целиком, читает адрес и отправляет только туда, куда надо. Каждый порт стал отдельным доменом коллизий, а с приходом полного дуплекса (передача и приём по разным парам одновременно) коллизии исчезли совсем. CSMA/CD в современных сетях мёртв — но его наследие (минимальный кадр 64 байта, счётчик collisions) живо.

Заплатили за это буферизацией: коммутатор вынужден хранить кадры в памяти, а значит, появились очереди, а с ними — задержки, джиттер и новый класс проблем вроде bufferbloat и incast в дата-центрах.

Коммутатор: обучающийся мост

Коммутатор изнутри: таблица MAC, направленная передача и flooding

Самое красивое в коммутаторе — что его никто не настраивает. Он строит таблицу пересылки сам, из наблюдаемого трафика, по одному правилу: «кадр с MAC источника X пришёл на порт N — значит, X живёт за портом N». Это и есть обучающийся мост (learning bridge), и всё поведение описывается коротким алгоритмом:

Псевдокод того же самого:

on_frame(frame, in_port):
    if not crc_ok(frame): drop; return
    if not is_group(frame.src):            # источник обязан быть уникастом
        fdb[frame.src] = (in_port, now())  # обучение
    if is_group(frame.dst) or frame.dst not in fdb or expired(fdb[frame.dst]):
        flood(frame, exclude=in_port); return
    out_port = fdb[frame.dst].port
    if out_port == in_port: drop; return   # фильтрация
    forward(frame, out_port)

Реализация на Python — ровно та модель, которую полезно держать в голове:

import time


class LearningBridge:
    """Обучающийся мост: таблица «MAC → порт» строится только из наблюдаемого трафика."""

    def __init__(self, ports, ageing=300.0):
        self.ports = set(ports)
        self.ageing = ageing          # секунды жизни записи, отраслевой дефолт — 300
        self.fdb = {}                 # mac -> (port, last_seen)

    @staticmethod
    def _is_group(mac: str) -> bool:
        # бит I/G — младший бит первого байта адреса
        return int(mac.split(":")[0], 16) & 1 == 1

    def receive(self, src: str, dst: str, in_port: int, now: float | None = None) -> list[int]:
        """Возвращает список портов, в которые уйдёт кадр."""
        now = time.monotonic() if now is None else now

        # 1. Обучение. Групповой адрес источником быть не может — такому кадру не верим.
        if not self._is_group(src):
            self.fdb[src] = (in_port, now)

        # 2. Групповые и широковещательные адреса всегда флудятся.
        if self._is_group(dst):
            return self._flood(in_port)

        # 3. Поиск получателя с проверкой возраста записи.
        entry = self.fdb.get(dst)
        if entry is None or now - entry[1] > self.ageing:
            self.fdb.pop(dst, None)           # запись протухла — забываем
            return self._flood(in_port)       # unknown unicast flooding

        out_port, _ = entry
        return [] if out_port == in_port else [out_port]   # фильтрация «сам себе»

    def _flood(self, in_port: int) -> list[int]:
        return sorted(self.ports - {in_port})

Сложность. Обучение и поиск — O(1) в среднем: в Python это хеш-таблица, в железе — CAM (Content Addressable Memory), которая сравнивает искомый адрес со всеми ячейками одновременно и отвечает за один такт. Flooding — O(P) по числу портов. Память — O(N) по числу известных MAC; и это главное физическое ограничение: таблица коммутатора доступа держит 8–32 тысячи записей, ToR в дата-центре — 100–300 тысяч, и это дорогая, горячая, энергоёмкая память. Отсюда же вырастает VXLAN и вся оверлейная сеть: MAC-адреса десятков тысяч виртуалок физически не помещаются в таблицы коммутаторов.

Практика: мост прямо в Linux

Linux-мост — это полноценный программный коммутатор с той же логикой. Соберём лабораторию из двух «хостов» в отдельных network namespace и моста между ними — всё воспроизводимо на любой машине:

# два изолированных «хоста» и мост
sudo ip netns add h1
sudo ip netns add h2
sudo ip link add br0 type bridge
sudo ip link set br0 up

# veth-пары: один конец внутрь namespace, второй — в мост
sudo ip link add v1 type veth peer name v1p
sudo ip link add v2 type veth peer name v2p
sudo ip link set v1 netns h1
sudo ip link set v2 netns h2
sudo ip link set v1p master br0 up
sudo ip link set v2p master br0 up

# адреса и заведомо узнаваемые MAC (02:… — locally administered unicast)
sudo ip netns exec h1 ip link set v1 address 02:00:00:00:00:01
sudo ip netns exec h1 ip addr add 10.10.0.1/24 dev v1
sudo ip netns exec h1 ip link set v1 up
sudo ip netns exec h2 ip link set v2 address 02:00:00:00:00:02
sudo ip netns exec h2 ip addr add 10.10.0.2/24 dev v2
sudo ip netns exec h2 ip link set v2 up

Теперь самое интересное — посмотреть на пустую таблицу, вызвать один пинг и посмотреть снова:

sudo bridge fdb show br br0 | grep -v permanent      # пусто
sudo ip netns exec h1 ping -c 1 10.10.0.2 >/dev/null
sudo bridge fdb show br br0 | grep -v permanent
02:00:00:00:00:01 dev v1p master br0
02:00:00:00:00:02 dev v2p master br0

Мост выучил оба адреса из одного обмена: MAC h1 — из ARP-запроса, MAC h2 — из ARP-ответа. Параметры моста видны через ip -d link show br0, и там же — ageing time (в сотых долях секунды):

ip -d link show docker0 | tr ' ' '\n' | grep -A1 ageing_time
ageing_time
30000                # = 300 секунд

Убрать за собой: sudo ip netns del h1; sudo ip netns del h2; sudo ip link del br0.

Чем платим: три фирменные болезни коммутации

1. Unknown unicast flooding. Если получателя нет в таблице, кадр уходит во все порты — то есть трафик, адресованный одному серверу, видят все. В нормальной жизни это редкость, но есть классическая ловушка: ARP-кэш живёт дольше, чем таблица MAC. У маршрутизаторов Cisco ARP-запись держится 4 часа, а MAC-запись коммутатора — 300 секунд. Хост, который молчит 5 минут, «забывается» коммутатором, но роутер продолжает слать ему кадры по известному ARP — и каждый такой кадр флудится по всему VLAN. Лечится согласованием таймеров (ARP-таймаут ≤ MAC ageing) — и это первое, что проверяют, когда «сервер видит чужой трафик».

2. Переполнение таблицы (MAC flooding). Атака macof из пакета dsniff генерирует десятки тысяч кадров со случайными MAC-адресами источника, таблица переполняется, старые записи вытесняются — и коммутатор деградирует до хаба, начиная флудить весь трафик. Защита — port-security (лимит числа MAC на порту) или 802.1X. Подробнее про подобные атаки — в треке по безопасности.

3. Широковещательный домен не масштабируется. Коммутатор не ограничивает broadcast: любой ARP-запрос доходит до всех и прерывает каждый хост (кадр надо принять, разобрать, сравнить IP). Пока хостов 50 — незаметно. На 2000 хостах в одном сегменте ARP-фон измеряется тысячами кадров в секунду, и слабые устройства начинают захлёбываться. Отраслевое правило — не больше 500–1000 хостов на широковещательный домен, то есть подсеть /23 или меньше. Именно из этой боли выросли VLAN.

ARP: мост между IP и MAC

Есть противоречие: приложение адресует по IP, а провод понимает только MAC. Кто-то должен переводить. Этим занимается ARP (Address Resolution Protocol, RFC 826, 1982) — протокол на 28 байт тела и поразительно долгую жизнь.

Логика простая: «я не знаю MAC для этого IP — спрошу у всех сразу». Кадр с dst = ff:ff:ff:ff:ff:ff и EtherType 0x0806, внутри — вопрос «у кого 10.0.0.9, скажите 10.0.0.5». Отвечает только владелец адреса, остальные молча выбрасывают.

Обратите внимание на шаг 6: получатель запоминает отправителя из самого запроса, не задавая встречного вопроса. Так ARP экономит половину обменов — и так же появляется дыра, через которую работает ARP-спуфинг.

Смотрим кэш и разрешение вживую:

ip neigh
82.38.60.1 dev ens1 lladdr 00:00:5e:00:01:1b REACHABLE
82.38.60.121 dev ens1 lladdr 56:6f:f3:01:1c:33 STALE
82.38.60.2 dev ens1 lladdr 98:49:25:51:e1:c1 STALE
fe80::b5cf:c04d:27dc:6da0 dev ens1 FAILED

Первая строка сразу многое рассказывает: MAC шлюза 00:00:5e:00:01:1b — это диапазон VRRP, то есть шлюз виртуальный, за ним пара физических маршрутизаторов, и при переключении между ними MAC не изменится (в этом весь смысл). Последняя строка — IPv6 link-local соседа в состоянии FAILED: сосед не отвечает на NDP, и это нормальная картина в сегменте, где IPv6 не настроен.

В Linux это не «ARP-кэш», а общая таблица соседей (neighbour table) с полноценным конечным автоматом NUD (Neighbour Unreachability Detection):

Ключевая тонкость — переход DELAY → REACHABLE «без запроса». Ядро считает соседа живым, если TCP подтвердил доставку данных: раз ACK пришёл, значит, кадры доходят, спрашивать не о чем. Это редкий и красивый пример того, как уровни в реальности не изолированы, а подсказывают друг другу.

Полезные ручки:

sudo ip neigh flush all                              # сбросить кэш целиком
sudo ip neigh replace 10.0.0.9 lladdr 02:0:0:0:0:2 dev v1 nud permanent   # статическая запись
sysctl net.ipv4.neigh.default.gc_thresh1             # 128  — ниже порога чистка не идёт
sysctl net.ipv4.neigh.default.gc_thresh2             # 512  — мягкий предел
sysctl net.ipv4.neigh.default.gc_thresh3             # 1024 — жёсткий предел

neighbour: arp_cache: neighbor table overflow! в dmesg — знаменитая беда узлов Kubernetes и гипервизоров: соседей в плоской сети больше тысячи, таблица упирается в gc_thresh3, и связность начинает рваться случайным образом. Лечится подъёмом порогов до 4096/8192/16384.

Разновидности ARP, которые надо знать

Gratuitous ARP — «беспричинный» ARP: хост рассылает запрос или ответ, где искомый IP равен собственному. Смысл двойной. Первый — проверка конфликта адресов при подъёме интерфейса (RFC 5227): если кто-то ответил, адрес уже занят. Второй, и самый важный на практике, — обновление чужих кэшей при переключении. Когда keepalived переносит виртуальный IP на резервный сервер, а гипервизор мигрирует виртуалку на другой хост, они шлют gratuitous ARP: «этот IP теперь за этим MAC». Коммутаторы перезаписывают таблицу, соседи обновляют кэш, и трафик едет по новому пути.

Именно здесь живёт классический инцидент: «после failover сервис недоступен 30 секунд». Почти всегда причина — gratuitous ARP потерялся (широковещание, никаких повторов и подтверждений) или его отфильтровал коммутатор с включённой защитой Dynamic ARP Inspection. Проверяется одной командой на соседнем хосте:

sudo tcpdump -e -n -i ens1 'arp and arp[6:2] = 2'   # только ARP Reply
09:14:02.771 02:00:00:00:00:0a > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 60:
    Reply 10.0.0.100 is-at 02:00:00:00:00:0a, length 46

Proxy ARP — маршрутизатор отвечает на ARP-запрос чужим адресом, притворяясь получателем, чтобы «склеить» разорванный сегмент. Сегодня это в основном источник загадочных багов: если два устройства отвечают на один и тот же ARP, трафик начинает уходить не туда. Выключается через sysctl net.ipv4.conf.all.proxy_arp=0, но применяется в VPN и некоторых схемах контейнерных сетей.

ARP-спуфинг — прямое следствие того, что в протоколе нет ни аутентификации, ни проверки: любой хост может прислать «10.0.0.1 is-at мой-MAC», и весь трафик к шлюзу пойдёт через него. Это основа man-in-the-middle в локальной сети. Защита строится не в ARP (там нечего чинить), а вокруг: Dynamic ARP Inspection на коммутаторах, DHCP snooping, 802.1X, а на прикладном уровне — шифрование, которое делает перехват бесполезным (TLS).

IPv6: ARP заменён на NDP

В IPv6 ARP не существует — его роль играет NDP (Neighbor Discovery Protocol, RFC 4861) поверх ICMPv6. Отличия принципиальные:

  • Вместо широковещания используется solicited-node multicast — группа ff02::1:ffXX:XXXX, куда попадают только узлы с совпадающими последними 24 битами адреса. То есть запрос будят не все хосты сегмента, а один-два. Прямое лечение болезни ARP-шторма.
  • Сообщения: NS (Neighbor Solicitation, тип 135) и NA (Neighbor Advertisement, 136) — аналоги запроса и ответа; RS/RA (133/134) — обнаружение маршрутизаторов и автоконфигурация адресов.
  • Дублирование адресов (DAD) встроено в протокол, а не приделано сбоку.

Практически это значит, что фильтр icmp в фаерволе, случайно вырезающий ICMPv6, ломает IPv6-сеть целиком — не «пинги не ходят», а вообще ничего не работает.

sudo tcpdump -e -n -i ens1 icmp6 -c 2
15:02:11.884 56:6f:f3:01:18:04 > 33:33:ff:00:00:07, ethertype IPv6 (0x86dd), length 86:
    fe80::546f:f3ff:fe01:1804 > ff02::1:ff00:7: ICMP6, neighbor solicitation,
    who has 2001:db8::7, length 32
15:02:11.885 02:00:5e:10:00:07 > 56:6f:f3:01:18:04, ethertype IPv6 (0x86dd), length 86:
    2001:db8::7 > fe80::546f:f3ff:fe01:1804: ICMP6, neighbor advertisement,
    tgt is 2001:db8::7, length 32

Обратите внимание на MAC назначения 33:33:ff:00:00:07 — это и есть отображение solicited-node группы в Ethernet: префикс 33:33 плюс последние 32 бита адреса группы.

Что видно в Wireshark, а что нет

Wireshark на канальном уровне — это в первую очередь честные фильтры отображения:

Фильтр Что покажет
eth.dst == ff:ff:ff:ff:ff:ff весь широковещательный трафик — оценить фон в сегменте
eth.ig == 1 все групповые кадры: broadcast + multicast
eth.src.lg == 1 кадры от локально назначенных MAC — виртуалки и контейнеры
arp.opcode == 1 только ARP-запросы; частота = здоровье сегмента
arp.duplicate-address-detected конфликт IP: два MAC отвечают за один адрес
vlan.id == 100 трафик конкретного VLAN на trunk-порту
stp BPDU — есть ли рядом коммутаторы и не мигает ли топология
eth.len < 60 подозрительно короткие кадры (runt)

Как выглядит рукопожатие на L2. Ethernet не устанавливает соединений — «рукопожатие» здесь физическое: автосогласование скорости и дуплекса, невидимое в дампе. Единственное, что похоже на переговоры и видно в захвате, — LACP (обмен кадрами 01:80:c2:00:00:02 раз в секунду или раз в 30 секунд) и STP (BPDU каждые 2 секунды). Если в дампе с обычного серверного порта внезапно пошли BPDU — кто-то воткнул в сеть коммутатор, и это стоит проверить прежде, чем искать проблему в приложении.

Как выглядит потеря пакета. Ключевой момент, который ломает интуицию новичкам: потерянного кадра в захвате нет. Ethernet не подтверждает доставку и не ретранслирует — кадр с битым CRC уничтожается сетевой картой до того, как его увидит libpcap. Поэтому потеря на L2 диагностируется косвенно:

  • В дампе — «дыра» в номерах последовательности TCP и последующие [TCP Retransmission], [TCP Dup ACK], [TCP Out-Of-Order] (фильтр tcp.analysis.flags). Это следствие, а не причина.
  • Настоящая причина — в счётчиках: ethtool -S на хосте (rx_crc_errors, rx_missed_errors) и show interface на порту коммутатора (input errors, CRC, output drops).
  • Wi-Fi — исключение: 802.11 подтверждает каждый кадр на канальном уровне и повторяет потерянные сам. В захвате с монитор-режима видны кадры с флагом Retry, и именно эти ретрансмиссии дают тот джиттер в 100+ мс, который наверху выглядит как «интернет тормозит».

Три артефакта захвата, из-за которых теряют часы.

  1. FCS не показывается. NIC срезает контрольную сумму до передачи в стек. Если Wireshark вдруг рисует поле FCS — это редкий драйвер с включённым сохранением.
  2. VLAN-тег пропадает. При включённом аппаратном разборе тегов карта снимает тег и передаёт его в метаданных — в дампе кадр выглядит нетегированным. Отладка VLAN становится невозможной, пока не сделать sudo ethtool -K ens1 rxvlan off.
  3. Кадры больше MTU. Из-за TSO/GSO/GRO в захвате появляются «кадры» по 20–60 килобайт: ядро отдаёт карте один большой сегмент, а нарезает его на кадры уже железо (и наоборот при приёме). Перед измерением реального числа пакетов офлоады выключают:
sudo ethtool -K ens1 gro off gso off tso off
sudo ethtool -k ens1 | grep -E 'segmentation|receive-offload'
tcp-segmentation-offload: off
generic-segmentation-offload: off
generic-receive-offload: off
large-receive-offload: off [fixed]

Не забудьте вернуть обратно — без офлоадов на 10G-линке процессор съедается целиком.

Петли, широковещательный шторм и STP

У кадра Ethernet нет TTL. В IP-заголовке счётчик прыжков ограничивает жизнь пакета, а здесь — нет ничего. Это делает петлю на канальном уровне катастрофой, а не неудобством: единственный широковещательный кадр, попавший в кольцо из трёх коммутаторов, будет размножаться и циркулировать вечно, за секунды забив линки под завязку. Это и есть broadcast storm: сеть встаёт целиком, коммутаторы перестают отвечать даже по консоли, а таблицы MAC сходят с ума, потому что один адрес «виден» то с одного порта, то с другого (MAC flapping в логах — верный признак петли).

STP (Spanning Tree Protocol, 802.1D) — изобретение Радии Перлман 1985 года: коммутаторы обмениваются служебными кадрами BPDU, выбирают корень (по наименьшему bridge ID = приоритет

  • MAC) и вычисляют покрывающее дерево, а лишние линки логически отключают. Физическая избыточность сохраняется, петли — нет; при обрыве заблокированный порт открывается.

Цена — время сходимости. Классический STP переводит порт через состояния Blocking → Listening → Learning → Forwarding с таймерами по 15 секунд, итого 30–50 секунд недоступности. Для сервера, который перезагружается, это означает: линк поднялся, а сеть не работает ещё полминуты — с ошибками DHCP и падением приложений при старте. Отсюда portfast / edge port на портах, куда включены только хосты: такой порт сразу переходит в Forwarding.

RSTP (802.1w, ныне часть 802.1D-2004) сократил набор состояний до Discarding → Learning → Forwarding, ввёл роли портов (Root, Designated, Alternate, Backup) и явные согласования — сходимость упала до долей секунды. Современные дата-центры чаще уходят от STP вовсе: MLAG/стекирование позволяют подключить сервер двумя линками к двум коммутаторам без петли, а в фабриках Clos L2-домен схлопывают до одной стойки и маршрутизируют всё остальное на L3 (см. IP и маршрутизацию).

Агрегация каналов (LACP, 802.3ad) — родственная тема: несколько физических линков объединяются в один логический, отказ одного не рвёт связность. Важная деталь, о которую регулярно спотыкаются: балансировка идёт по потокам, а не по пакетам — коммутатор считает хеш от MAC/IP/портов и всегда отправляет один поток в один линк, чтобы не переставлять пакеты местами. Поэтому агрегат из четырёх гигабитных линков не даёт 4 Гбит/с одному TCP-соединению: одна копия scp упрётся в 1 Гбит/с, сколько кабелей ни воткни.

VLAN: как разрезать один коммутатор на несколько

Проблема, которую решает VLAN: широковещательный домен нельзя делать большим, но покупать отдельный коммутатор бухгалтерии, отдельный — гостям и отдельный — серверам разорительно и негибко. 802.1Q (1998) добавляет в кадр 4 байта тега с 12-битным номером VLAN — и один физический коммутатор превращается в 4094 независимых логических.

Два типа портов:

  • Access-порт смотрит на конечное устройство. Кадры приходят и уходят без тега: хост о VLAN ничего не знает. Коммутатор сам добавляет тег на входе и снимает на выходе.
  • Trunk-порт соединяет коммутаторы. Кадры идут с тегом, по одному проводу — несколько VLAN.

Тонкое место — native VLAN: один VLAN на транке разрешено передавать без тега. Он существует ради совместимости, но именно на нём строится атака double tagging: злоумышленник в native VLAN отправляет кадр с двумя тегами, первый коммутатор снимает внешний (свой native) и отправляет дальше кадр, у которого внутренний тег указывает на чужой VLAN. Односторонний, но рабочий обход изоляции. Отсюда правило: native VLAN на транке должен быть отдельным неиспользуемым номером, а не VLAN 1. Вторая атака — switch spoofing: компьютер притворяется коммутатором и через DTP уговаривает порт стать транком; лечится явным switchport mode access и выключением DTP.

Linux умеет и то и другое. Подынтерфейс для тегированного трафика:

sudo ip link add link ens1 name ens1.100 type vlan id 100
sudo ip addr add 10.100.0.5/24 dev ens1.100
sudo ip link set ens1.100 up
ip -d link show ens1.100
7: ens1.100@ens1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 56:6f:f3:01:18:04 brd ff:ff:ff:ff:ff:ff
    vlan protocol 802.1Q id 100 <REORDER_HDR>

Важная деталь: MTU подынтерфейса остался 1500, а тег добавляет ещё 4 байта — на проводе кадр станет 1522 байта. Оборудование обязано это принимать (baby giant), но старые коммутаторы с жёстким лимитом 1518 такие кадры молча режут. Симптом узнаваем: мелкие пакеты ходят, большие — нет.

Настоящее VLAN-разграничение на Linux-мосте включается фильтрацией:

sudo ip link set br0 type bridge vlan_filtering 1
sudo bridge vlan add dev v1p vid 10 pvid untagged   # access-порт в VLAN 10
sudo bridge vlan add dev v2p vid 20 pvid untagged   # access-порт в VLAN 20
sudo bridge vlan add dev uplink vid 10              # trunk: оба VLAN с тегами
sudo bridge vlan add dev uplink vid 20
bridge vlan show
port              vlan-id
v1p               10 PVID Egress Untagged
v2p               20 PVID Egress Untagged
uplink            10
                  20

PVID Egress Untagged — это и есть определение access-порта: входящий кадр без тега получает VID 10, исходящий тег снимается. Строка без этих слов — тегированный член VLAN.

Чем платим за VLAN: таблица MAC теперь ключуется парой (VLAN, MAC), а не одним MAC; между VLAN нужен маршрутизатор, и весь межсегментный трафик проходит через него (или через L3-коммутатор); 4094 — маленький потолок для облачного провайдера, откуда и родились VXLAN с 24-битным VNI и Geneve. И, наконец, VLAN — это про изоляцию, а не про безопасность: неверно настроенный транк рушит всю схему одной командой.

MTU: где ломается «у меня работает, а у клиента нет»

Максимальный размер полезной нагрузки кадра — 1500 байт. Jumbo frames (обычно 9000) дают меньше накладных расходов и меньше прерываний на гигабайт: для NFS, iSCSI и репликации баз это заметный выигрыш. Но jumbo нужно включить на всех устройствах сегмента — на хостах, на коммутаторах, на всех транках. Стоит ошибиться в одном месте — получаем классику:

ping -c 2 10.10.0.2                  # работает: 84 байта
ping -c 2 -M do -s 1472 10.10.0.2    # 1472 + 8 (ICMP) + 20 (IP) = ровно 1500
ping -c 2 -M do -s 8972 10.10.0.2    # проверка jumbo
PING 10.10.0.2 (10.10.0.2) 8972(9000) bytes of data.
ping: local error: message too long, mtu=1500

Флаг -M do запрещает фрагментацию, -s задаёт размер данных. Именно так проверяют реальный MTU пути: бинарным поиском по размеру. Если маленькие пакеты ходят, а большие пропадают без всякой ошибки — почти наверняка сломан Path MTU Discovery: где-то по пути фаервол режет ICMP «Fragmentation Needed», отправитель не узнаёт о снижении MTU и продолжает слать слишком большие пакеты в пустоту. Снаружи это выглядит издевательски: ping идёт, SSH-логин проходит, но scp или большой HTTP-ответ виснет намертво. Подробный разбор — в статьях про NAT и фаерволы и диагностику. Всё это особенно актуально в туннелях: VXLAN отъедает 50 байт, WireGuard — 60, IPsec — до 73, и MTU внутри туннеля обязан быть меньше.

Типичные ошибки и как их узнавать

Симптом Что искать на L2 Команда
Потери 1–30 %, скорость «плавает» duplex mismatch, битый кабель ethtool ens1, ethtool -S ens1 | grep -i err
Работает 5 минут, потом пропадает конфликт IP, два MAC на один адрес arping -D, ip neigh, фильтр arp.duplicate-address-detected
Сеть встала целиком, консоль не отвечает петля и broadcast storm tcpdump -e -n 'ether broadcast', логи MAC flapping
После failover 30 секунд недоступности не дошёл gratuitous ARP tcpdump -e -n arp на клиентской стороне
«Наш сервер видит чужой трафик» unknown unicast flooding, ARP-таймаут > MAC ageing bridge fdb show, таймеры на коммутаторе
Пинг есть, передача файла виснет MTU mismatch, PMTUD blackhole ping -M do -s 1472, tracepath
Соединение рвётся хаотично на большом кластере переполнение таблицы соседей dmesg | grep neighbor, gc_thresh3
Виртуалка не видна после миграции коммутатор не обновил таблицу MAC bridge fdb show, gratuitous ARP
VLAN настроен, а трафика нет native VLAN, тег снят офлоадом карты bridge vlan show, ethtool -K ens1 rxvlan off

Чеклист диагностики канального уровня

Порядок принципиален: каждый следующий шаг имеет смысл только если предыдущий прошёл.

  1. Физика. ip -br link — есть ли LOWER_UP? Нет — кабель, порт, SFP.
  2. Согласование. ethtool ens1 — ожидаемые скорость и Full дуплекс?
  3. Ошибки. ethtool -S и ip -s link — растут ли crc/errors под нагрузкой?
  4. Свой адрес. ip -br link — тот ли MAC (не рандомизированный, не дубль)?
  5. Сосед. ip neigh — есть ли шлюз в состоянии REACHABLE? FAILED означает, что ARP-запрос не долетает или не возвращается.
  6. Кадры. tcpdump -e -n -i ens1 arp — уходят ли запросы, приходят ли ответы, не отвечают ли двое.
  7. Коммутация. bridge fdb show (или таблица на коммутаторе) — виден ли MAC соседа и на том ли порту.
  8. VLAN. bridge vlan show, tcpdump -e — совпадают ли теги на обеих сторонах транка.
  9. Размер. ping -M do -s 1472 — проходит ли полноразмерный кадр.

Если все девять шагов зелёные, а проблема осталась — она не на канальном уровне, и дальше идти нужно вверх по стеку.

Мини-итог

  • Канальный уровень доставляет кадр между соседями по среде и делает ровно четыре вещи: обозначает границы кадра, обнаруживает порчу через CRC, адресует по MAC, разделяет среду.
  • Кадр Ethernet II неизменен с 1980 года: MAC назначения, MAC источника, опциональный тег 802.1Q, EtherType, 46–1500 байт данных, FCS. dst MAC — это всегда следующий хоп, не конечный адресат.
  • MAC — 48 бит: OUI вендора, серийный номер, биты I/G и U/L. Он не уникален и не является идентичностью: меняется одной командой, а телефоны его рандомизируют.
  • Коммутатор — обучающийся мост: O(1) поиск по CAM, обучение по MAC источника, flooding при незнании адреса, ageing 300 секунд. Никакой настройки не требует и никому свою таблицу не рассказывает.
  • ARP переводит IP в MAC широковещательным вопросом, ответ кэшируется конечным автоматом NUD. Протокол доверчив по конструкции — отсюда спуфинг, отсюда же gratuitous ARP как механизм переключения.
  • VLAN режут широковещательный домен на 4094 логических; STP платит секундами сходимости за право иметь избыточные линки без петель.
  • Всё, что ломается на L2, наверху выглядит как «приложение тормозит»: дуплекс, CRC, MTU, флудинг и потерянный gratuitous ARP не видны ни в логах сервиса, ни в APM.

Источники

Что дальше

Мы научились доставлять кадр соседу по проводу. Но соседей в интернете — миллиарды, и широковещательный вопрос «у кого такой адрес» физически не может дойти до каждого. Нужен уровень, который умеет строить путь через чужие сети, ничего не зная об их устройстве: IP и маршрутизация: адресация, подсети, IPv6, таблицы маршрутов, BGP.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов