Я перемещаю содержимое своего блога на mdlayher.com. Пожалуйста, смотрите обновленную версию этого контента по адресу:

Https://mdlayher.com/blog/linux-netlink-and-go-part-1-netlink/

Я большой поклонник Прометея. Я довольно часто использую его как дома, так и на работе, и мне очень нравится знать, что делают мои системы в любой момент. Одним из наиболее широко используемых экспортеров Prometheus является node_exporter: демон, который может извлекать самые разные метрики из UNIX-подобных машин.

Просматривая репозиторий, я заметил открытую проблему, требующую добавления показателей WiFi в node_exporter. Идея заинтриговала меня, и я понял, что обязательно воспользуюсь такой функцией на своем ноутбуке с Linux. Я начал изучать варианты получения информации об устройстве Wi-Fi в Linux.

После пары недель экспериментов (включая устаревший ioctl() API беспроводных расширений) я создал два пакета Go, которые работают вместе для взаимодействия с устройствами Wi-Fi в Linux:

  • netlink: обеспечивает низкоуровневый доступ к сокетам netlink Linux.
  • Wi-Fi: обеспечивает доступ к действиям и статистике устройства Wi-Fi IEEE 802.11.

В этой серии публикаций будут описаны некоторые уроки, которые я извлек при реализации этих пакетов в Go, и, надеюсь, они станут хорошим справочником для тех, кто хочет поэкспериментировать с устройствами netlink и / или WiFi на своем языке.

Псевдокод в этой серии будет использовать x/sys/unix пакет Go и типы из моих пакетов netlink и wifi. Я планирую разбить серию следующим образом (ссылки появятся, когда будут опубликованы другие):

Что такое нетлинк?

Netlink - это механизм межпроцессного взаимодействия ядра Linux, обеспечивающий связь между процессом пользовательского пространства и ядром или несколькими процессами пользовательского пространства. Сокеты Netlink - это примитив, обеспечивающий эту связь.

Этот пост предоставит учебник по сокетам, сообщениям, группам многоадресной рассылки и атрибутам netlink. Кроме того, этот пост будет посвящен взаимодействию между пользовательским пространством и ядром, а не взаимодействию между двумя процессами пользовательского пространства.

Создание сокетов netlink

Netlink использует стандартный API сокетов BSD. Это должно быть хорошо знакомо любому, кто занимался сетевым программированием на C. Если вы не знакомы с сокетами BSD, я рекомендую отличное Руководство Beej по сетевому программированию в качестве основы для начинающих по этой теме.

Важно отметить, что связь по netlink никогда не выходит за пределы локального хоста. Имея это в виду, давайте приступим к изучению того, как работают сокеты netlink!

Для связи с netlink должен быть открыт сокет netlink. Это делается с помощью системного вызова socket():

fd, err := unix.Socket(
    // Always used when opening netlink sockets.
    unix.AF_NETLINK,
    // Seemingly used interchangeably with SOCK_DGRAM,
    // but it appears not to matter which is used.
    unix.SOCK_RAW,
    // The netlink family that the socket will communicate
    // with, such as NETLINK_ROUTE or NETLINK_GENERIC.
    family,
)

Параметр family определяет конкретное семейство сетевых ссылок: по сути, подсистему ядра, с которой можно взаимодействовать с помощью сокетов netlink. Эти семейства могут предлагать такие функции, как:

  • NETLINK_ROUTE: манипулирование сетевыми интерфейсами Linux, маршрутами, IP-адресами и т. Д.
  • NETLINK_GENERIC: строительный блок для упрощенного добавления новых семейств сетевых ссылок, таких как nl80211, Open vSwitch и т. Д.

После создания сокета необходимо вызвать bind(), чтобы подготовить его к отправке и получению сообщений.

err := unix.Bind(fd, &unix.SockaddrNetlink{
    // Always used when binding netlink sockets.
    Family: unix.AF_NETLINK,
    // A bitmask of multicast groups to join on bind.
    // Typically set to zero.
    Groups: 0,
    // If you'd like, you can assign a PID for this socket
    // here, but in my experience, it's easier to leave
    // this set to zero and let netlink assign and manage
    // PIDs on its own.
    Pid: 0,
})

На этом этапе сокет netlink готов отправлять и получать сообщения в ядро ​​и из него.

Формат сообщения Netlink

Сообщения Netlink следуют очень особому формату. Все сообщения должны быть выровнены по 4-байтовой границе. Например, 16-байтовое сообщение должно быть отправлено как есть, а 17-байтовое сообщение должно быть дополнено до 20 байтов.

Очень важно отметить, что, в отличие от обычных сетевых коммуникаций, netlink использует порядок байтов хоста или порядок байтов, для кодирования и декодирования. целые числа вместо обычного сетевого порядка байтов (big endian). В результате код, который должен преобразовывать между байтовым и целочисленным представлениями данных, должен учитывать это.

Заголовки сообщений Netlink имеют следующий формат: (диаграмма из RFC 3549):

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          Length                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            Type              |           Flags              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Process ID (PID)                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Эти поля содержат следующую информацию:

  • Длина (32 бита): длина всего сообщения, включая заголовки и полезные данные.
  • Тип (16 бит): какая информация содержится в сообщении, например ошибка, конец сообщения, состоящего из нескольких частей и т. д.
  • Флаги (16 бит): битовые флаги, которые указывают, что сообщение является запросом, составным сообщением, подтверждением запроса и т. д.
  • Порядковый номер (32 бита): число, используемое для сопоставления запросов и ответов; увеличивается при каждом запросе.
  • Идентификатор процесса (PID) (32 бита): иногда называется идентификатором порта; номер, используемый для однозначной идентификации конкретного сокета netlink; может быть или не быть идентификатором процесса.

Наконец, полезная нагрузка может сразу следовать за заголовком netlink. Опять же, обратите внимание, что полезная нагрузка должна быть дополнена до 4-байтовой границы.

Пример сообщения netlink, которое отправляет запрос к ядру, может напоминать следующее в Go:

msg := netlink.Message{
    Header: netlink.Header{
        // Length of header, plus payload.
        Length: 16 + 4,
        // Set to zero on requests.
        Type: 0,
        // Indicate that message is a request to the kernel.
        Flags: netlink.HeaderFlagsRequest,
        // Sequence number selected at random.
        Sequence: 1,
        // PID set to process's ID.
        PID: uint32(os.Getpid()),
    },
    // An arbitrary byte payload. May be in a variety of formats.
    Data: []byte{0x01, 0x02, 0x03, 0x04},
}

Отправка и получение сообщений netlink

Теперь, когда мы знакомы с некоторыми основами сокетов netlink, мы можем отправлять и получать данные с помощью сокета.

Как только сообщение подготовлено, его можно отправить ядру с помощью sendto():

// Assume messageBytes produces a netlink request message (like the
// one shown above) with the specified payload.
b := messageBytes([]byte{0x01, 0x02, 0x03, 0x04})
err := unix.Sendto(b, 0, &unix.SockaddrNetlink{
    // Always used when sending on netlink sockets.
    Family: unix.AF_NETLINK,
})

Запросы только для чтения к netlink обычно не требуют каких-либо особых привилегий. Операции, которые изменяют состояние подсистемы с помощью netlink или требуют блокировки ее внутреннего состояния, обычно требуют повышенных привилегий. Это может означать запуск программы от имени пользователя root или использование CAP_NET_ADMIN для:

  • Отправьте запрос на запись, чтобы внести изменения в подсистему, используя netlink.
  • Отправьте запрос на чтение с флагом NLM_F_ATOMIC, чтобы получить атомарный снимок данных из netlink.

Получение сообщений от сокета netlink с использованием recvfrom() может быть немного сложнее, в зависимости от множества факторов. Netlink может ответить так:

  • Очень маленькие или очень большие сообщения.
  • Сообщения, состоящие из нескольких частей, разбиты на несколько частей.
  • Явный номер ошибки, если тип заголовка - «ошибка».

Кроме того, необходимо проверить порядковый номер и PID каждого сообщения. При работе с необработанными системными вызовами эти случаи должен решать пользователь сокета.

Большие сообщения

Чтобы иметь дело с большими сообщениями, я использовал метод выделения одной страницы памяти, просмотра буфера (не осушая его), а затем удвоения размера буфера, если он слишком мал для чтения всего сообщения. Спасибо, Dominik Honnef за понимание этой проблемы.

Обработка ошибок опущена для краткости. Пожалуйста, проверьте свои ошибки.

b := make([]byte, os.Getpagesize())
for {
    // Peek at the buffer to see how many bytes are available.
    n, _, _ := unix.Recvfrom(fd, b, unix.MSG_PEEK)
    // Break when we can read all messages.
    if n < len(b) {
        break
    }
    // Double in size if not enough bytes.
    b = make([]byte, len(b)*2)
}
// Read out all available messages.
n, _, _ := unix.Recvfrom(fd, b, 0)

Теоретически сообщение netlink может иметь размер до ~ 4 ГБ (максимальное 32-битное целое число без знака), но на практике сообщения намного меньше.

Сообщения, состоящие из нескольких частей

Для определенных типов сообщений netlink может отвечать «сообщением, состоящим из нескольких частей». В этом случае для каждого сообщения перед последним будет установлен флаг «multi». Последнее сообщение будет иметь тип «готово».

При возврате сообщений, состоящих из нескольких частей, первое recvfrom() вернет все сообщения с установленным флагом «multi». Затем recvfrom() необходимо вызвать снова, чтобы получить последнее сообщение с типом заголовка «готово». Это очень важно, иначе netlink просто зависнет при последующих запросах, ожидая, пока вызывающий абонент истощит последнее сообщение типа заголовка «готово».

Код для этого не такой тривиальный, как другие примеры, но вы можете взглянуть на мою реализацию, если вам нужна ссылка.

Номера ошибок Netlink

Если netlink не может удовлетворить запрос по какой-либо причине, он вернет явный номер ошибки в полезной нагрузке сообщения, содержащего тип заголовка «error». Эти номера ошибок совпадают с классическими номерами ошибок Linux, например ENOENT для «нет такого файла или каталога» или EPERM для «отказано в доступе».

Если тип заголовка сообщения указывает на ошибку, номер ошибки будет закодирован как 32-битное целое число со знаком (примечание: также используется системный порядок байтов) в первых 4 байтах полезной нагрузки сообщения.

const name = "foo0"
_, err := rtnetlink.InterfaceByName(name)
if err != nil && os.IsNotExist(err) {
    // Error is result of a netlink error number, and can be
    // checked in the usual Go fashion.
    log.Printf("no such device: %q", name)
    return
}

Порядковый номер и проверка PID

Чтобы гарантировать, что ответ netlink от ядра является ответом на один из наших запросов, мы также должны проверить порядковый номер и PID для каждого полученного сообщения. В большинстве случаев они должны точно соответствовать тому, что было отправлено ядру с запросом. Последующие запросы должны увеличивать порядковый номер перед отправкой другого сообщения в netlink.

Проверка PID может незначительно отличаться в зависимости от нескольких условий.

  • Если сообщение получено в пользовательском пространстве от имени группы многоадресной рассылки, оно будет иметь PID 0, что означает, что сообщение возникло в ядре.
  • Если запрос отправляется ядру с PID, равным 0, netlink назначит PID для данного сокета при первом ответе. Этот PID следует использовать (и проверять) в последующих сообщениях.

Предполагая, что вы не указали PID в bind(), при открытии нескольких сокетов netlink в одном приложении первому будет назначен PID идентификатора процесса. Последующие будут иметь случайное число, выбранное netlink. По моему опыту, гораздо проще просто позволить netlink самому назначать все PID и следить за тем, какие номера он присваивает каждому сокету.

Группы многоадресной рассылки

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

К группе многоадресной рассылки можно присоединиться двумя разными способами:

  • Указание битовой маски группы в течение bind(). Это считается «устаревшим» методом.
  • Присоединение к группам и выход из них с помощью setsockopt(). Это наиболее предпочтительный современный метод.

Присоединение к группе и выход из нее с использованием setsockopt() - это вопрос замены одной константы. В Go это делается с помощью uint32 «групповых» значений.

// Can also specify unix.NETLINK_DROP_MEMBERSHIP to leave
// a group.
const joinLeave = unix.NETLINK_ADD_MEMBERSHIP
// Multicast group ID. Typically assigned using predefined
// constants for various netlink families.
const group = 1
err := syscall.SetSockoptInt(
    fd,
    unix.SOL_NETLINK,
    joinLeave,
    group,
)

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

Атрибуты Netlink

Чтобы завершить наше руководство по сокетам netlink, мы обсудим очень распространенный формат данных для полезной нагрузки сообщения netlink: атрибуты.

Атрибуты Netlink необычны тем, что они имеют формат LTV (длина, тип, значение), а не типичный TLV (тип, длина, значение). Как и любое другое целое число в сокетах netlink, значения типа и длины также кодируются с использованием порядка байтов хоста. Наконец, атрибуты netlink также должны быть дополнены до 4-байтовой границы, как и сообщения netlink.

Каждое поле содержит следующую информацию:

  • Длина (16 бит): длина всего атрибута, включая поля длины, типа и значения. Не может быть установлен на 4-байтовую границу. Например, если длина составляет 17 байтов, атрибут будет дополнен до 20 байтов, но 3 байта заполнения не следует интерпретировать как значимые.
  • Тип (16 бит): тип атрибута, обычно определяемый как константа в некотором семействе сетевых ссылок или заголовке.
  • Значение (байты переменной): необработанные полезные данные атрибута. Может содержать вложенные атрибуты, которые хранятся в том же формате. Эти вложенные атрибуты могут содержать еще больше вложенных атрибутов!

Есть два специальных флага, которые могут присутствовать в атрибутах netlink, хотя я еще не встречал их в своей работе.

  • NLA_F_NESTED: указывает вложенный атрибут; используется как подсказка для синтаксического анализа. Не всегда используется, даже если присутствуют вложенные атрибуты.
  • NLA_F_NET_BYTEORDER: данные атрибутов хранятся в сетевом порядке байтов (с прямым порядком байтов), а не с порядком байтов хоста.

Обратитесь к документации данного семейства netlink, чтобы определить, следует ли проверять какой-либо из этих флагов.

Резюме

Теперь, когда мы знакомы с использованием сокетов и сообщений netlink, следующий пост в этой серии будет основываться на этих знаниях, чтобы углубиться в общие netlink.

Надеюсь, вам понравился этот пост! Если у вас есть вопросы или комментарии, не стесняйтесь обращаться к ним через комментарии, Twitter или Gophers Slack (имя пользователя: mdlayher).

Обновления

  • 22.02.2017: перенесена справочная информация об API сокетов BSD в раздел «Создание сокетов netlink».
  • 22.02.2017: отмечена потребность в root или CAP_NET_ADMIN для многих операций записи netlink, а также при использовании NLM_F_ATOMIC. Спасибо Стивену Хартланду из болванки-нити.
  • 23.02.2017: отмечена возможность указывать PID для сокета в bind(). Спасибо, Дэн Уильямс из ветки libnl.
  • 27.02.2017: изменен псевдокод на x/sys/unix вместо syscall, поскольку syscall заморожен.

использованная литература

Следующие ссылки часто использовались в качестве справочных, когда я создавал netlink пакета и писал этот пост: