Сетевые протоколы: Ethernet и Go

29 minute read

Перевод статьи “Network Protocol Breakdown: Ethernet and Go".

Если вы читаете эту статью, то есть очень большая вероятность, что прямо сейчас вы пользуетесь Ethernet (IEEE 802.3) соединением где-то между вашими устройствами и хостингом, на котором размещен этот блог. Семейство Ethernet технологий - это строительные блоки для современных компьютерных сетей.

Было бы не плохо разобраться как именно Ethernet работает на физическом уровне, но в этой статье я сфокусируюсь на фреймах Ethernet канального уровня (“Ethernet frames”). Этот уровень описывает каким образом два компьютера взаимодействуют посредством Ethernet соединения.

В этой статье мы подробно рассмотрим структуру фреймов Eathernet, вплоть до значения каждого поля. А также разберемся, как можно манипулировать Ethernet фреймами в простой Go программе, используя пакет github.com/mdlayher/ethernet.

Структура Ethernet фрейма

Фундаментальная часть второго уровня Ethernet - это фреймы. Структура фрейма достаточно простая и является основой для построения более сложных протоколов поверх Ethernet.

В первых двух полях указывается MAC адрес получателя и MAC адрес отправителя. MAC адрес - это уникальный идентификатор сетевого интерфейса для работы на канальном уровне. Размер MAC адреса 48 бит (6 байт).

В поле адреса получателя указывается MAC адрес сетевого интерфейса, для которого предназначен этот фрейм. В некоторых случаях это может быть специальный броадкаст адрес: FF:FF:FF:FF:FF:FF. Некоторые протоколы, такие как ARP, всегда используют броадкаст адрес для отправки сообщения всем машинам в сегменте сети. Когда свитч получает фрейм с таким адресом, то он дублирует его на все подключенные порты.

В адресе отправителя указывается MAC адрес сетевого интерфейса, с которого был отправлен фрейм. Это позволяет другим машинам в сети идентифицировать машину отправителя и оправить сообщение в ответ.

Следующее поле это 16 битное целочисленное поле, которое называется “Тип Ethernet”(EtherType). Оно определяет протокол более высокого уровня, который должен использоваться для работы с данными(полезной нагрузкой), инкапсулированными в фрейме. Как пример, это могут быть протоколы ARP, IPv4 и IPv6.

Полезная нагрузка - это набор данных, размером от 46 до 1500 (в некоторых случаях и больше) байтов. Размер этого поля зависит от настроек канального уровня. В качестве данных может передаваться все что угодно, в том числе и заголовки протоколов более высоких уровней.

Последний элемент Ethernet фрейма - специальное поле, проверочная последовательность(“FCS”). По сути это CRC32 проверочная сумма использующая IEEE многочлен. С ее помощью можно определять повреждены ли данные фрейма. Как только фрейм полностью сформирован, проверочная сумма рассчитывается и записывается в последние 4 байта фрейма. Как правило, это делается автоматически операционной системой или сетевым интерфейсом. Но иногда бывает необходимым посчитать FCS в самой программе.

Создание Ethernet фрейма с помощью Go

С помощью пакета ethernet можно создавать сами Ethernet фреймы, отправлять и получать их через сеть.

В этом примере мы сделаем фрейм у которого в качестве полезной нагрузки будет простая фраза “hello world”. Также, у этого фрейма будет кастомный EtherType. Фрейм будет рассылаться на все машины того же сегмента на канальном уровне, для этого будем использовать адрес: FF:FF:FF:FF:FF:FF

// Фрейм будет рассылаться по сети.
f := &ethernet.Frame{
    // Рассылаем фрейм на все машины в сегменте.
    Destination: ethernet.Broadcast,
    // Указываем нашу машину как отправителя.
    Source: net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad},
    // Указываем неиспользуемое значение EtherType.
    EtherTypexcccc,
    // Отправляем простое сообщение.
    Payload: []byte("hello world"),
}
// Кодируем структуру в бинарный формат Ethernet фрейма.
b, err := f.MarshalBinary()
if err != nil {
    log.Fatalf("failed to marshal frame: %v", err)
}

// Отправляем данные по сети.
sendEthernetFrame(b)

Как я уже писал, операционная система или сетевой интерфейс сами выполнят расчет FCS. В некоторых случаях можно воспользоваться методом ethernet.Frame.MarshalFCS, выполнить расчет FCS в “ручном режиме” и добавить эти данные к фрейму.

Введение в VLAN теги

Если вы работали с компьютерными сетями в прошлом, то вы вероятно знакомы с концепцией VLAN: виртуальные LAN сегменты. VLANs (IEEE 802.1Q) позволяет разбивать один сегмент сети на множество различных сегментов. Для это хитро используется поле EtherType в фрейме.

Когда добавляется VLAN тег, то первые 16 бит поля EtherType становится идентификатором протокола(Tag Protocol Identifier). Если точнее, то что бы указать, что VLAN тег присутствует, используется резервное значение для поля EtherType, например 0x8100.

Значения этих то 16 бит, следующие за идентификатором протокола в поле EtherType используются для задания специальных параметров:

  • Приоритет (3 бита) - один из классов IEEE P8021.p, используется на сервисном уровне.
  • DEI(Drop Eligible Indicator) (1 бит) - определяет можно ли исключить пакет если в сети есть проблемы.
  • VLAN ID (VID) (12 бит) - указывает на VLAN к которому относится этот фрейм. Каждый VID создает новый сетевой сегмент.

После VLAN тега указывается EtherType, который уже идентифицирует данные в полезной нагрузке пакета.

В некоторых случаях, может быть использовано несколько VLAN тегов(IEEE 802.1ad, также известный как “Q-in-Q”). К примеру, это может быть полезно, когда провайдер инкапсулирует трафик пользователя в одном VLAN, в то время как пользователь также может инкапсулировать свой трафик в множестве различных VLANов

Добавление тегов в фрейм с помощью Go

Как правило, сетевой интерфейс сам заботится о добавлении VLAN тегов в Ethernet фрейм. Но иногда бывает необходимым добавить VLAN тег из самого приложения. Давайте посмотрим, как это можно сделать на примере нашего приложения.

// Фрейм будет отправляться по сети.
f := &ethernet.Frame{
    // Рассылаем фрейм на все машины в сегменте сети.
    Destination: ethernet.Broadcast,
    // Идентифицируем нашу машину как отправителя.
    Source: net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad},
    // Добавляем тег VLAN 10. Если необходимо, указываем
    // несколько тегов для for Q-in-Q.
    VLAN: []*ethernet.VLAN{{
        ID: 10,
    }},
    // Указываем неиспользуемый EtherType.
    EtherTypexcccc,
    // Отправляем простое сообщение.
    Payload: []byte("hello world"),
}

В моем пример не используются поля Priority и DEI в VLAN тегах. Если в этих полях нет необходимости, можно просто оставить их пустыми.

Отправка и получение Ethernet фреймов по сети

Большинство сетевых приложений работают поверх TCP или UDP. Но Ethernet фреймы используются на более низком уровне, и для работы с ним нужно соответствующее API и права.

Под API, как правило, имеются ввиду “сырые сокеты” (“raw sockets” или “packet sockets”). Эти низкоуровневые сокеты позволяют отправлять и получать Ethernet фреймы напрямую, но необходимы повышенные привилегии.

На Linux и BSD системах можно использовать пакет github.com/mdlayher/raw для отправки фреймов через сетевой интерфейс. Ниже я привел пример, как можно рассылать наши самодельные фреймы с сообщением “hello world”:

// Выбираем интерфейс eth0.
ifi, err := net.InterfaceByName("eth0")
if err != nil {
    log.Fatalf("failed to open interface: %v", err)
}
// Открываем новый сокет. Используем EtherType 
// что и в самом фрейме.
c, err := raw.ListenPacket(ifi, 0xcccc)
if err != nil {
    log.Fatalf("failed to listen: %v", err)
}
defer c.Close()
// Кодируем фрейм в бинарный формат.
f := newEthernetFrame("hello world")
b, err := f.MarshalBinary()
if err != nil {
    log.Fatalf("failed to marshal frame: %v", err)
}
// Рассылаем фрейм на все девайсы в сегменте сети.
addr := &raw.Addr{HardwareAddr: ethernet.Broadcast}
if _, err := c.WriteTo(b, addr); err != nil {
    log.Fatalf("failed to write frame: %v", err)
}

На другой машине мы можем использовать похожую программу для приема Ethernet фреймов с нашим кастомным EtherType.

// Выбираем интерфейс eth0 с которым будем работать.
ifi, err := net.InterfaceByName("eth0")
if err != nil {
    log.Fatalf("failed to open interface: %v", err)
}
// Открываем сокет с указанием EtherType как в фрейме.
c, err := raw.ListenPacket(ifi, 0xcccc)
if err != nil {
    log.Fatalf("failed to listen: %v", err)
}
defer c.Close()
// Принимаем сокеты на интерфейсе.
b := make([]byte, ifi.MTU)
var f ethernet.Frame
// Считываем фреймы.
for {
    n, addr, err := c.ReadFrom(b)
    if err != nil {
        log.Fatalf("failed to receive message: %v", err)
    }
    // Парсим Ethernet в Go структуру.
    if err := (&f).UnmarshalBinary(b[:n]); err != nil {
        log.Fatalf("failed to unmarshal ethernet frame: %v", err)
    }
    // Отображаем полученное сообщение.
    log.Printf("[%s] %s", addr.String(), string(f.Payload))
}

И это, собственно, все. Если у вас есть несколько машин с Linux, то вы можете попробовать запустить все примеры у себя. Исходный код можно найти на github.

Заключение

Низкоуровневые сетевые примитивы, такие как сокеты и Ethernet фреймы, очень мощные инструменты. Используя их, можно полностью контролировать весь трафик который отправляет и получает ваше приложение.

Если вы находите подобные программы захватывающими, так же как и я, то вам пригодятся мои пакеты ethernet и raw. В своих будущих постах я покажу как можно реализовать различные протоколы поверх Ethernet фреймов.

Спасибо, что у вас хватило терпения прочитать эту статью. Надеюсь, вам понравилось и вы нашли что-то новое для себя. Если это так, то вам могут понравиться другие мои статьи про использование низкоуровневых сетевых механизмов.

Если у вас есть вопросы, задавайте их в комментариях.

Ссылки:

comments powered by Disqus