Книга: Сетевое программирование. От основ до приложений
Назад: Глава 9. Вспомогательные данные
Дальше: Метрики и другие параметры интерфейса

Глава 10. Управление сетевыми интерфейсами

Предлагаю перестать называть эту штуку «сетью ALTO ALOHA».

Во-первых, потому что она должна поддерживать любое количество разных типов станций — NOVA, PDP-11... Во-вторых, потому что организация начинает выглядеть намного привлекательнее, чем радиосеть ALOHA <…>.

Может быть, так: «ETHERnet». Еще предложения?

Боб Меткалф, MEMO, 1973

Введение

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

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

Адаптеры — сложные устройства, которые имеют свои настройки и часто могут быть оптимально настроены только пользователем.

Когда речь идет о сетевом обмене, особенно с использованием стека TCP/IP, с точки зрения прикладного разработчика маршрутизация данных производится не через адаптеры, а через сетевые интерфейсы. Это абстракции, которые предоставляются устройствами. Они имеют адреса и являются точками входа и выхода данных из узла. Сетевые интерфейсы также имеют свои настройки.

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

Бывает и так, что интерфейс существует без адаптера. Тогда мы говорим о виртуальных интерфейсах, которые полезны для сложной фильтрации и преобразования трафика на стороне пользователя, например создания VPN, а также организации взаимодействия виртуальных машин.

Рис. 10.1. Устройства и интерфейсы

Самый распространенный вариант такого интерфейса — интерфейс локальной петли, который позволяет выполнять обмен между приложениями на локальном узле.

Отношение адаптеров и интерфейсов показано на рис. 10.1. Два адаптера — верхний с тремя физическими интерфейсами, нижний с одним — предоставляют в сумме четыре «логических интерфейса» — точки входа для стека протоколов.

Один интерфейс верхнего адаптера имеет связанный IP, второй, похоже, не используется, а третий входит в группу с интерфейсом второго адаптера. Это так называемый bonding. Два интерфейса образуют виртуальный интерфейс, а ОС распределяет данные, которые идут в этот интерфейс с адресом 10.10.1.2, по «реальным» интерфейсам, что используется для балансировки нагрузки и повышения надежности: если один из сетевых адаптеров откажет, данные все равно будут передаваться через второй.

Более широкие возможности предоставляют виртуальные интерфейсы TUN и TAP, рассматриваемые в главе 23.

Сокеты также могут быть привязаны к определенным сетевым интерфейсам, а не к адаптерам.

В этой главе мы рассмотрим перечисление и конфигурирование сетевых устройств и сетевых интерфейсов, а также назначение им адресов и других параметров и научимся работать с функцией ioctl(), предоставляющей низко­уровневый интерфейс для их настройки. Она позволяет устанавливать как параметры интерфейсов, так и параметры адаптеров, напрямую обращаясь к драйверу. Также затронем управление параметрами сокетов через ioctl-вызовы.

Данную главу можно использовать в качестве справочника при дальнейшей работе с книгой.

Функция ioctl()

Системный вызов ioctl, показанный на рис. 10.2, дает возможность напрямую общаться с драйверами и подсистемами ядра. Его выполняет функция с одноименным названием.

Рис. 10.2. Ioctl в Linux

Она позволяет устанавливать параметры устройств, напрямую обращаясь к драйверу. Функция объявлена в файле sys/ioctl.h, но для ее использования нужно подключать отдельные заголовочные файлы для каждой группы параметров.

Вызов ioctl сильно отличается от параметра к параметру, а сами параметры различаются не только в разных ОС, но и в разных версиях одной и той же ОС.

В Linux их номенклатура и параметры определяются версией ядра и загруженными модулями.

Прототип функции:

int ioctl(int fd, unsigned long request, ...);

Параметры функции:

fd — дескриптор сокета.

• request — код запроса, который зависит от устройства. Может иметь тип int или unsigned long в зависимости от реализации.

Прочие аргументы — любые, но как правило, нетипизированный указатель либо указатель и размер данных.

В случае неудачи данная функция возвращает –1, в случае успеха — обычно 0. Тем не менее она может возвращать любые неотрицательные значения от драйверов.

Структура ioctl

Код запроса имеет структуру, хотя и не стандартизованную.

В Linux данная структура описывается макросами из заголовочного файла asm/ioctl.h и представляет собой вызов макросов:

_IO(тип_подсистемы, номер_устройства).

{_IOR, _IOW, _IOWR}(тип, номер, размер_аргумента).

Например, ioctl SIOCATMARK, используя который работает одноименная функция, может быть определен так:

#define SIOCATMARK _IOR('s', 7, int)

Его идентификатор имеет значение 0x8905.

В PIP также существует пакет python-ioctl-opt, который позволяет создавать из параметров коды ioctl. Но это не кросс-платформенное решение. В книге оно не рассматривается, так как может быть полезно разве что при написании собственного модуля ядра.

Внимание! Установка параметров через ioctl может влиять на устройства и всю операционную систему. Вы можете неправильно сконфигурировать ОС или даже испортить устройство. Многие из вызовов имеют некоторые отличия в разных ОС. Перед тем как пользоваться определенным ioctl, прочитайте документацию, чтобы знать, как он работает в конкретной ОС.

Пример использования ioctl в Linux для установки флага O_ASYNC, включающего асинхронный ввод-вывод на сокете:

#include <sys/socket.h>

#include <sys/ioctl.h>

 

...

 

int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

int async_enabled = 1;

 

if (-1 == ioctl(sock, FIOASYNC, &async_enabled))

{

    perror("ioctl");

}

В ОС Windows вместо ioctl() есть несколько функций: ioctlsocket(), WSAIoctl(), DeviceIoControl(). О том, как с ними работать, будет рассказано в главе 18.

В стандартной библиотеке Python имеется модуль fcntl, в котором реализована функция ioctl():

import fcntl

 

@overload

def ioctl(fd: FileDescriptorLike, request: int, arg: int = 0,

          mutate_flag: bool = True, /) -> int

 

def ioctl(fd: FileDescriptorLike, request: int, arg: bytes,

          mutate_flag: bool = True, /) -> bytes

 

@overload

def ioctl(fd: FileDescriptorLike, request: int, arg: WriteableBuffer,

          mutate_flag: Literal[False], /) -> bytes

 

@overload

def ioctl(fd: FileDescriptorLike, request: int, arg: Buffer,

          mutate_flag: bool = True, /) -> Any

Параметры функции:

fd — файловый дескриптор сокета.

• request — операция, выполняемая над дескриптором.

• arg — аргумент запроса.

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

Функция может быть использована в Unix-подобных системах. Чтобы воспользоваться ею на объекте класса socket.socket, необходимо получить дескриптор сокета. Это позволяет сделать метод socket.fileno():

def fileno(self) -> int

Для ОС Windows непосредственно в классе socket.socket реализован метод socket.ioctl():

if sys.platform == "win32":

    def ioctl(self, control: int,

              option: int | tuple[int, int, int] | bool) -> None

Его параметры:

control — код опции. Поддерживаются коды SIO_RCVALL, SIO_KEEPALIVE_VALS, SIO_LOOPBACK_FAST_PATH. Эти опции описаны в главе 18.

option — значение опции.

Метод выполняет функцию WSAIoctl(), и для ioctl на дескрипторе сокета в ОС Windows необходимо использовать его.

Внимание! В Python для Unix-подобных ОС и для Windows следует использовать разные функции!

Создадим серверный сокет:

import fcntl

import socket

import sys

import time

 

# Некоторые дополнительные коды ioctl содержатся в модуле termios.

# Но иногда может потребоваться определить код вручную.

from termios import FIONBIO

 

if len(sys.argv) < 3:

    print(f'{sys.argv[0]} <port> <async_mode_flag>')

    sys.exit(1)

 

port = int(sys.argv[1])

 

# Флаг асинхронного режима получают из командной строки.

async_mode = int(sys.argv[2].lower() in ['true', 't', '1', 'y'])

 

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:

    # Желательно установить опцию, чтобы не получить исключение

    # "OSError: [Errno 98] Address already in use"

    # в случае повторного запуска.

    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)

 

    sock.bind(('', port))

    sock.listen()

 

    print(f'Async mode = {async_mode}')

Теперь проверим, существует ли метод ioctl() у класса socket.socket, и если это так, вызовем его, в противном случае будем использовать функцию ioctl() из модуля fcntl:

    # Если у сокета имеется метод ioctl(), использовать его

    # для установки значения через ioctlsocket().

    if getattr(sock, 'ioctl', None):

        print('Using socket.ioctl() method...')

        # Установка опции для Windows, метод не возвращает результат.

        sock.ioctl(FIONBIO, async_mode)

    else:

        # В противном случае попробовать функцию ioctl().

        print('Using fcntl.ioctl() function...')

        # Функция ioctl() принимает массив байтов.

        buf = int(async_mode).to_bytes(length=4, byteorder='little')

        # Вызов функции вернет результат.

        io_result = fcntl.ioctl(sock.fileno(), FIONBIO, buf)

        print(f'io_result = {io_result}')

 

        # Проверить, установлен ли асинхронный режим.

        while True:

            try:

                # В неблокирующем режиме, если в очереди нет подключений,

                # accept() сгенерирует исключение BlockingIOError.

                with sock.accept()[0] as c_sock:

                    break

            except BlockingIOError:

                print('Client not connected.')

                time.sleep(1)

 

        print('Client connected!')

В примере делается попытка установить асинхронный режим. Это одна из опций, которая поддерживается и ОС Windows, и большинством Unix-по­добных ОС.

Некоторые коды ioctl доступны в модуле termios.

Функция fcntl.ioctl() обладает гораздо более широкими возможностями, чем метод сокета, например позволяет возвращать значения. Но если вы хотите реализовать подобное для Windows, используйте сторонние модули и прямой вызов упомянутых выше функций ioctlsocket() и WSAIoctl().

Например, из модуля win32file стороннего пакета pywin32, доступного на PIP, можно вызвать функцию DeviceIoControl(), описанную в главе 18.

Любую функцию библиотек, подключаемых к приложениям на C и C++, можно вызвать, используя модуль ctypes из Стандартной библиотеки Python.

Для проверки того, что установка опции работает, запустим пример в асинхронном режиме:

src/book01/ch10/python/ioctl-async-test.py 12345 y

Async mode = 1

Using fcntl.ioctl() function...

io_result = b'\x01\x00\x00\x00'

Client not connected.

Client not connected.

Client connected!

Асинхронный режим мы рассмотрим в книге 2. При асинхронном запуске исполнение функции не блокируется.

Видим, что сервер работает в цикле, из которого его выводит подключение клиента:

nc localhost 12345

В синхронном режиме ситуация несколько иная, вызов accept() блокируется:

src/book01/ch10/python/ioctl-async-test.py 12345 n

Async mode = 0

Using fcntl.ioctl() function...

io_result = b'\x00\x00\x00\x00'

Client connected!

В ОС Windows код работает аналогично.

Вызовы к сетевым интерфейсам и модулям протоколов

Часто параметры нужно устанавливать не только для сокетов, но и для сетевых устройств. Например, установка IP-адреса на сетевой интерфейс либо изменение таблицы маршрутов не могут быть выполнены через изменение параметров сокета.

Для взаимодействия с драйверами устройств почти весь интерфейс представлен системным вызовом ioctl. В Linux старые управляющие сетевые программы, такие как ifconfig, работают, используя вызовы ioctl.

Список параметров и операций ioctl в Unix-подобных системах доступен в man 7 netdevice. В ОС Windows есть отдельная страница MSDN с ioctl для WinSock.

Большинство сетевых протоколов имеют собственные параметры ioctl, задокументированные на man-страницах протоколов, обычно в разделе Ioctls, а также в man 7 socket.

Кроме того, некоторые устройства поддерживают ioctl, специфичные для них. Ниже мы рассмотрим общие вызовы к сетевым устройствам в Linux. В других Unix-подобных ОС, например в BSD-системах, данный список вызовов будет несколько отличаться, но большая их часть все равно присутствует.

Вызовы и параметры ioctl для ОС Windows будут рассмотрены в главе 18.

Стандартные вызовы

Linux поддерживает некоторые стандартные ioctl для настройки сетевых устройств. Их можно использовать на любом дескрипторе сокета независимо от семейства или типа. Большинство из них оперируют структурой ifreq:

#include <sys/ioctl.h>

#include <net/if.h>

 

// Длина названия интерфейса в структуре ниже, как правило — 16.

#define IFNAMSIZ 16

 

struct ifreq

{

     // Название интерфейса.

    char ifr_name[IFNAMSIZ];

    // Остальные поля зависят от конкретной операции.

    union

    {

        // Адрес, привязанный к интерфейсу.

        struct sockaddr ifr_addr;

        // Адрес назначения, широковещательный адрес и маска подсети.

        struct sockaddr ifr_dstaddr;

        struct sockaddr ifr_broadaddr;

        struct sockaddr ifr_netmask;

        // Аппаратный адрес.

        struct sockaddr ifr_hwaddr;

        // Флаги интерфейса.

        short ifr_flags;

        // Индекс сетевого интерфейса.

        int ifr_ifindex;

        // Метрика устройства.

        int ifr_metric;

        int ifr_mtu;

        // Параметры областей памяти для работы с DMA.

        struct ifmap ifr_map;

        // Группа для объединяемых в bonding интерфейсов.

        char ifr_slave[IFNAMSIZ];

        // Новое имя для интерфейса.

        char ifr_newname[IFNAMSIZ];

        // Произвольные данные, которые будут переданы драйверу.

        char *ifr_data;

     };

};

Сокеты AF_INET6 — исключение, для работы с которым используется структура in6_ifreq:

struct in6_ifreq

{

    struct in6_addr ifr6_addr;

    u32 ifr6_prefixlen;

    // Индекс сетевого интерфейса.

    int ifr6_ifindex;

};

Обычно пользователь указывает, с каким устройством работать, устанавливая имя сетевого интерфейса в ifr_name либо его индекс для IPv6 в ifr6_ifindex.

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

В разных Unix-подобных ОС данная структура может несколько отличаться. Например, в NetBSD она по-другому определена и в ней отсутствуют поля ifr_hwaddr, ifr_slave, ifr_newname, а поле ifr_ifindex называется ifr_index.

Кроме того, в ней есть некоторые дополнительные поля.

Обычно данные структуры Unix-подобных ОС описаны в man 7 netdevice.

Для обращения к интерфейсу можно использовать имя, поскольку оно уникально в системе.

Если системный вызов ioctl является привилегированным, для его использования требуется, чтобы эффективный идентификатор процесса был нулевым или была установлена привилегия CAP_NET_ADMIN, иначе будет возвращена ошибка EPERM.

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

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

SIOCGIFNAME — по индексу ifr_ifindex, вернуть имя интерфейса в ifr_name. Это единственный ioctl, который возвращает свой результат в ifr_name.

SIOCGIFINDEX — получить индекс интерфейса в ifr_ifindex.

В качестве параметров они используют структуру ifreq, показанную выше.

Посмотрим, как получение имени и индекса некоторого интерфейса реализуется через эти вызовы. Сначала получим индекс интерфейса по его имени:

...

extern "C"

{

#include <sys/ioctl.h>

#include <net/if.h>

}

 

...

 

    ifreq ifr = {};

    const socket_wrapper::Socket sock = {AF_INET, SOCK_DGRAM, IPPROTO_UDP};

    std::string if_param(std::strlen(argv[1]), 0);

 

    // Имя должно быть в нижнем регистре.

    std::transform(argv[1], argv[1] + std::strlen(argv[1]),

                   if_param.begin(), [](unsigned char c)

                   { return std::tolower(c); });

 

    if ("-n" == if_param)

    {

        // Скопировать имя в структуру.

        std::copy_n(argv[2],

                    std::min(strlen(argv[2]), static_cast<size_t>(IFNAMSIZ)),

                    static_cast<char*>(ifr.ifr_name));

        // Вызвать ioctl.

        if (ioctl(sock, SIOCGIFINDEX, &ifr) < 0)

        {

            throw std::system_error(errno, std::system_category(),

                                    "SIOCGIFINDEX");

        }

 

        std::cout

            << "Interface \"" << argv[2] << "\""

            << " index: " << ifr.ifr_ifindex

            << std::endl;

    }

Здесь, наоборот, получим имя по индексу:

    else if ("-i" == if_param)

    {

        const auto if_index = std::stoi(argv[2]);

        // Задать индекс сетевого интерфейса.

        ifr.ifr_ifindex = if_index;

        // Вызвать ioctl.

        if (ioctl(sock, SIOCGIFNAME, &ifr) < 0)

        {

            throw std::system_error(errno, std::system_category(),

                                    "SIOCGIFNAME");

        }

 

        std::cout

            << "interface " << if_index

            << " name: \"" << ifr.ifr_name << "\""

            << std::endl;

    }

    else

    {

        std::cerr

            << "Please use '-n' for name "

            << "or '-i' for index"

            << std::endl;

        return EXIT_FAILURE;

    }

Результат:

build/bin/b01-ch10-if_nameindex_siocifname -n "wlo1"

Interface "wlo1" index: 3

build/bin/b01-ch10-if_nameindex_siocifname -n "eno2"

Interface "eno2" index: 2

build/bin/b01-ch10-if_nameindex_siocifname -i 1

interface 1 name: "lo"

build/bin/b01-ch10-if_nameindex_siocifname -i 2

interface 2 name: "eno2"

Альтернативным вариантом работы с именами и индексами являются следующие функции:

#include <net/if.h>

 

// Получить индекс по имени.

unsigned int if_nametoindex(const char *ifname);

 

// Получить имя по индексу.

char *if_indextoname(unsigned int ifindex, char *ifname);

Буфер, на который указывает ifname, когда в него сохраняется имя интерфейса, должен иметь размер не менее IFNAMSIZ байт.

Перечисление интерфейсов реализуется через функцию if_nameindex(), объявленную в net/if.h:

#include <net/if.h>

 

struct if_nameindex *if_nameindex();

void if_freenameindex(struct if_nameindex *ptr);

Если функция завершается с ошибкой, она возвращает nullptr, в противном случае — динамически выделенный массив структур if_nameindex:

// Максимальная длина имени для интерфейса.

#define IF_NAMESIZE 16

 

struct if_nameindex

{

    // 1, 2, ...

    unsigned int if_index;

    // null-завершенная строка имени: "eth0", ...

    char *if_name;

};

После завершения работы с результатом этот массив необходимо освободить вызовом функции if_freenameindex().

Внимание! Функция и тип данных имеют одинаковое имя, поэтому ключевое слово struct для типа требуется указывать явно.

Посмотрим на примере:

extern "C"

{

#include <net/if.h>

}

 

int main()

{

    // Выделить массив и записать туда структуры.

    // Когда указатель выйдет из области видимости, будет вызван

    // пользовательский обработчик удаления (deleter):

    // функция if_freenameindex().

    std::unique_ptr<struct if_nameindex, decltype(&if_freenameindex)>

        if_ni(if_nameindex(), &if_freenameindex);

 

    if (nullptr == if_ni)

    {

        throw std::system_error(errno, std::system_category(),

                                "if_nameindex");

    }

    // Пройти по выделенному массиву.

    for (auto i = if_ni.get(); !(0 == i->if_index && nullptr == i->if_name);

         ++i)

        std::cout

            << i->if_index << ": "

            << i->if_name << "\n";

    std::cout << std::endl;

 

    return EXIT_SUCCESS;

}

Результат:

build/bin/b01-ch10-if_nameindex_if_nameindex

1: lo

2: eno2

3: wlo1

4: docker0

19: tun0

Если же требуется не только перечислить интерфейсы, но и получить по ним данные, можно использовать функцию getifaddrs():

#include <sys/types.h>

#include <ifaddrs.h>

 

int getifaddrs(ifaddrs **ifap);

void freeifaddrs(ifaddrs *ifa);

Функция getifaddrs() выделит память и в случае успеха вернет 0, а в случае неудачи –1.

В достаточно новых версиях Linux данная функция, как и все функции перечисления интерфейсов, показанные выше, работает не через ioctl, а через сокеты Netlink.

После завершения работы с данными память нужно освободить, используя функцию freeifaddrs().

Эти функции работают со структурой ifaddrs, которая определена в ifaddrs.h:

#include <ifaddrs.h>

 

struct ifaddrs

{

    // Следующий элемент списка. Или nullptr, если это последний элемент.

    struct ifaddrs *ifa_next;

    // Название интерфейса, оканчивающееся нулем.

    char *ifa_name;

    // Флаги интерфейса, возвращенные ioctl SIOCGIFFLAGS.

    unsigned int ifa_flags;

    // Указатель на структуру, содержащую адрес интерфейса или nullptr.

    struct sockaddr *ifa_addr;

    // Указатель на структуру, содержащую сетевую маску, связанную с ifa_addr,

    // если это применимо для семейства адресов.

    struct sockaddr *ifa_netmask;

    union

    {

        // Широковещательный адрес.

        struct sockaddr *ifu_broadaddr;

 

        // Адрес назначения "точка-точка".

        struct sockaddr *ifu_dstaddr;

    } ifa_ifu;

 

    #define ifa_broadaddr ifa_ifu.ifu_broadaddr

    #define ifa_dstaddr   ifa_ifu.ifu_dstaddr

 

    // Буфер, содержащий данные, зависящие от семейства адресов.

    void *ifa_data;

};

Структура представляет собой элемент списка; если элемент последний, поле ifa_next будет нулевым.

Поля ifa_addr и ifa_netmask могут содержать нулевой указатель, если у интерфейса не установлены адрес или маска.

Для определения формата структуры адреса следует обращаться к полю ifa_addr->sa_family.

В зависимости от того, установлен ли бит IFF_BROADCAST или IFF_POINTOPOINT в ifa_flags, в ifa_addr будет содержаться широковещательный адрес ifa_broadaddr или адрес «точка-точка» ifa_dstaddr.

В ifa_data могут содержаться, например, счетчики принятых и отправленных данных. Для семейства адресов типа AF_PACKET в нем будет находиться низко­уровневая статистика. В BSD-системах данный буфер можно использовать для получения статистики на семействах AF_LINK.

Такое использование показано в примере ниже. Это поле может содержать нулевой указатель.

В некоторых ОС, например в IBM System i, поле ifa_data не используется.

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

Утилита strace также полезна, чтобы увидеть вызовы и возвращаемые Netlink-сокетом параметры, которые приложение читает, используя recvmsg().

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

Мы подробнее рассмотрим, как работать со strace, в главе 24.

Рассмотрим пример использования данного API.

Сначала реализуем функции для вывода адресов в читаемом виде. Выведем IPv4-адрес:

...

extern "C"

{

#include <net/if.h>

#include <ifaddrs.h>

#include <linux/if_link.h>

#include <linux/if_packet.h>

}

 

#include <socket_wrapper/socket_headers.h>

 

void print_address(const sockaddr *ia)

{

    if (!ia)

    {

        std::cout << ": nullptr, but flag is set";

        return;

    }

    // Буфер под адрес.

    std::vector<char> addr_buf;

    // Указатель на адрес.

    const void *addr = nullptr;

 

    if (AF_INET == ia->sa_family)

    {

        addr_buf.resize(INET_ADDRSTRLEN);

        addr = &(reinterpret_cast<const sockaddr_in *>(ia))->sin_addr;

        // Для отображения вызывается inet_ntop().

        std::cout

            << " IPv4: "

            << inet_ntop(ia->sa_family, addr, &addr_buf[0], addr_buf.size());

    }

Теперь выведем адреса IPv6:

    else if (AF_INET6 == ia->sa_family)

    {

        addr_buf.resize(INET6_ADDRSTRLEN);

        addr = &(reinterpret_cast<const sockaddr_in6 *>(ia))->sin6_addr;

        std::cout

            << " IPv6: "

            << inet_ntop(ia->sa_family, addr, &addr_buf[0], addr_buf.size());

    }

Для сокетов из семейства AF_PACKET выведем индекс и адрес интерфейса:

    else if (AF_PACKET == ia->sa_family)

    {

        std::cout << " AF_PACKET: ";

        std::ios_base::fmtflags f(std::cout.flags());

 

         // Структура адреса для AF_PACKET.

         // MAC-адреса интерфейса, его индексы, некоторые другие параметры.

        auto sa = reinterpret_cast<const sockaddr_ll *>(ia);

 

        for (const auto *i = sa->sll_addr;

             i < sa->sll_addr + sa->sll_halen; ++i)

            std::cout

                << std::hex << std::setfill('0') << std::setw(2)

                << int(*i) << ":";

        std::cout.flags(f);

        std::cout << "\n    if index = " << sa->sll_ifindex;

    }

    else

    {

        // Неизвестный тип адреса.

        std::cout << ia->sa_family << " addr type";

    }

}

В функции main() просто вызовем функцию getifaddrs():

int main(int argc, const char * const argv[])

{

    ifaddrs *ifa = nullptr;

 

    if (getifaddrs(&ifa) < 0)

    {

        perror("getifaddrs");

        return EXIT_FAILURE;

    }

 

    // unique_ptr вызовет пользовательский deleter при удалении, в результате

    // чего память для списка будет корректно освобождена.

    std::unique_ptr<ifaddrs, decltype(&freeifaddrs)> if_ni(ifa, &freeifaddrs);

Затем выведем поля каждого экземпляра структуры в полученном списке:

    for (auto i = if_ni.get(); i->ifa_next != nullptr; i = i->ifa_next)

    {

        std::cout

            << i->ifa_name << ":";

 

        if (i->ifa_addr)

        {

            std::cout << "\n    addr";

            print_address(i->ifa_addr);

        }

 

        if (i->ifa_netmask)

        {

            std::cout << "\n    netmask";

            print_address(i->ifa_netmask);

        }

        // В слове флагов можно проверить все флаги, которые описаны в этой

        // главе.

        // Здесь проверяется лишь тип адреса, чтобы выбрать корректное поле.

        if (i->ifa_flags & IFF_BROADCAST)

        {

            std::cout << "\n    broadcast addr";

            print_address(i->ifa_broadaddr);

        }

        else if (i->ifa_flags & IFF_POINTOPOINT)

        {

            std::cout << "\n    point to point addr";

            print_address(i->ifa_dstaddr);

        }

        else

        {

            std::cout << "\n    no addr";

        }

Рассмотрим, как печатается статистика для сокетов типа AF_PACKET:

        if (i->ifa_data)

        {

            std::cout << "\n";

            if (i->ifa_addr && AF_PACKET == i->ifa_addr->sa_family)

            {

                // В Linux для адресов AF_PACKET в ifa_data содержится

                // указатель на структуру rtl_link_stats.

                const auto stats = static_cast<const rtnl_link_stats*>(

                    ifa->ifa_data

                );

                std::cout

                    << "    tx_packets = " << stats->tx_packets << "\n"

                    << "    rx_packets = " << stats->rx_packets << "\n"

                    << "    tx_bytes   = " << stats->tx_bytes << "\n"

                    << "    rx_bytes   = " << stats->rx_bytes;

            }

            else if (i->ifa_addr)

            {

                std::cout

                    << "        " << i->ifa_addr->sa_family

                    << " — unimplemented address family to parse data";

            }

            else

            {

                std::cout

                    << "    address is null, but data is not.";

            }

        }

        std::cout << "\n\n";

    }

    std::cout << std::endl;

 

    return EXIT_SUCCESS;

}

Аналогично возможно использовать семейство AF_LINK там, где оно есть:

if (i->ifa_addr && AF_LINK == i->ifa_addr->sa_family)

{

    const auto data = static_cast<const if_data*>(ifa->ifa_data);

    std::cout

        << "    receive_packets = " << int(data.ifi_ipackets)

        << "\n";

}

Вызовем пример и посмотрим на результат:

build/bin/b01-ch10-if_nameindex_get_ifaddrs

lo:

   addr AF_PACKET: 00:00:00:00:00:00:

   if index = 1

   no addr

   tx_packets = 6225714

   rx_packets = 6225714

   tx_bytes   = 376139655

   rx_bytes   = 376139655

 

eno2:

   addr AF_PACKET: e4:54:e8:30:20:10:

   if index = 2

   broadcast addr AF_PACKET: ff:ff:ff:ff:ff:ff:

   if index = 2

   tx_packets = 6225714

   rx_packets = 6225714

   tx_bytes  = 376139655

   rx_bytes   = 376139655

 

wlo1:

   addr AF_PACKET: 08:71:90:10:20:30:

   if index = 3

   broadcast addr AF_PACKET: ff:ff:ff:ff:ff:ff:

   if index = 3

   tx_packets = 6225714

   rx_packets = 6225714

   tx_bytes   = 376139655

   rx_bytes   = 376139655

 

...

Видим, что у каждого интерфейса есть адрес AF_PACKET. Один и тот же интерфейс появляется в списке несколько раз, только с разными типами адресов, например IPv4 и IPv6:

lo:

   addr IPv4: 127.0.0.1

   netmask IPv4: 255.0.0.0

   no addr

 

eno2:

   addr IPv4: 192.168.5.1

   netmask IPv4: 255.255.255.255

   broadcast addr IPv4: 192.168.5.1

 

wlo1:

   addr IPv4: 192.168.2.13

   netmask IPv4: 255.255.255.0

   broadcast addr IPv4: 192.168.2.255

 

...

 

lo:

   addr IPv6: ::1

   netmask IPv6: ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff

   no addr

 

wlo1:

   addr IPv6: fe80::6072:680e:bbd4:7acb

   netmask IPv6: ffff:ffff:ffff:ffff::

   broadcast addr: nullptr, but flag is set

Чтобы показать все данные по интерфейсу, целесообразно сгруппировать ­записи по именам. Из соображений краткости мы не будем этого делать.

Кроме этой функции, есть и другие, например в книге Уильяма Ричарда Стивенса «UNIX разработка сетевых приложений» (изд-во «Питер», 3-е изд., 2007) описаны функции get_ifi_info() и free_ifi_info().

Они делают то же самое, что и getifaddrs() и freeifaddrs(), но возвращают список структур ifi_info. Возвращаемые структуры содержат почти все атрибуты структуры ifaddrs, кроме data, и дополнительно содержат атрибут mtu.

Эти функции устарели, и в современных версиях Linux их уже не встретить, а функция getifaddrs(), которая их заменила, появилась еще во FreeBSD 4.8, поэтому ее допустимо использовать почти в любом случае.

В Python-модуле socket также реализованы функции для работы с интерфейсами:

# Для ОС Windows в Python 3.8 были добавлены функции

# socket.if_nameindex(), socket.if_nametoindex() и socket.if_indextoname().

if sys.platform != "win32" or sys.version_info >= (3, 8):

    def if_nameindex() -> list[tuple[int, str]]

    def if_nametoindex(name: str, /) -> int

    def if_indextoname(index: int, /) -> str

Никаких сложностей использование данных функций не представляет:

>>> import socket

>>> socket.if_nameindex()

[(1, 'lo'), (2, 'eno2'), (3, 'wlo1'), (4, 'docker0'), (19, 'tun0')]

Реализованы они через функции C API, рассмотренного нами выше.

В коде новых версий CPython для Windows получение интерфейсов реализовано через вызов функции GetIfTable2Ex().

Кроме того, для работы с интерфейсами используются функции GetAdaptersInfo() и GetAdaptersAddresses().

При желании из Python всегда можно вызвать C-функцию. Приведем пример для getifaddrs().

Сначала необходимо определить C-структуры:

import ctypes

import ctypes.util

 

# C-типы из соответствующего модуля.

from ctypes import (

    Structure, Union, POINTER,

    pointer, get_errno, cast,

    c_ushort, c_byte, c_void_p, c_char_p, c_uint, c_int, c_uint16, c_uint32

)

 

import socket

 

# Различные структуры, которых нет в модуле socket.

class sockaddr(Structure):

    _fields_ = [

        ('sa_family', c_ushort),

        ('sa_data', c_byte * 14)

    ]

 

class sockaddr_in(Structure):

    _fields_ = [

        ('sin_family', c_ushort),

        ('sin_port', c_uint16),

        ('sin_addr', c_byte * 4)

    ]

 

class sockaddr_in6(Structure):

    _fields_ = [

        ('sin6_family', c_ushort),

        ('sin6_port', c_uint16),

        ('sin6_flowinfo', c_uint32),

        ('sin6_addr', c_byte * 16),

        ('sin6_scope_id', c_uint32)

    ]

 

class ifa_ifu(Union):

    _fields_ = [

        ('ifu_broadaddr', POINTER(sockaddr)),

        ('ifu_dstaddr', POINTER(sockaddr))

    ]

 

# Структура отображает элемент списка.

class ifaddrs(Structure):

    pass

Теперь необходимо установить поля, загрузить динамическую библиотеку, содержащую функции, и получить их адреса:

# Структура ifaddrs содержит указатель того же типа, что и она сама.

# Поэтому предварительно определяется структура, и лишь после этого

# устанавливаются поля.

ifaddrs._fields_ = [

    # Вот этот указатель.

    ('ifa_next', POINTER(ifaddrs)),

    ('ifa_name', c_char_p),

    ('ifa_flags', c_uint),

    ('ifa_addr', POINTER(sockaddr)),

    ('ifa_netmask', POINTER(sockaddr)),

    ('ifa_ifu', ifa_ifu),

    ('ifa_data', c_void_p)

]

 

# Библиотека LibC.

LibC = ctypes.CDLL(ctypes.util.find_library('c'))

freeifaddrs = LibC.freeifaddrs

getifaddrs = LibC.getifaddrs

Остальное — это классы-обертки, которые упрощают использование функций.

Класс, представляющий сетевой интерфейс:

# Класс содержит данные интерфейса.

class NetworkInterface:

   def __init__(self, name):

       self.name = name.decode() if isinstance(name, bytes) else name

       self.index = socket.if_nametoindex(name)

       self.addresses = {}

       self.netmask = {}

 

   def __str__(self):

       return f'{self.name}:\n    index = {self.index}, ' \

              f'\n    addr IPv4 = {self.addresses.get(socket.AF_INET)}' \

              f'\n    netmask IPv4 = {self.netmask.get(socket.AF_INET)}' \

              f'\n    addr IPv6 = {self.addresses.get(socket.AF_INET6)}' \

              f'\n    netmask IPv6 = {self.netmask.get(socket.AF_INET6)}'

И класс для получения списка интерфейсов:

# Класс для получения списка интерфейсов.

class NetworkInterfaces:

   def __init__(self):

       # Здесь создается указатель.

       self._ifa_pointer = POINTER(ifaddrs)()

 

   def __del__(self):

       del self._ifa_pointer

 

   def _ifa_iter(self):

       p = self._ifa_pointer

       while p:

           # Вернуть генератор для обхода всего списка из структур.

           yield p.contents

           p = p.contents.ifa_next

 

   @staticmethod

   def _get_address(sa):

       try:

           # Если поле адреса нулевое, здесь будет сгенерировано исключение

           # ValueError из-за попытки доступа по нулевому указателю.

           sa = sa.contents

           family = sa.sa_family

           addr = None

           if socket.AF_INET == family:

               sa = cast(pointer(sa), POINTER(sockaddr_in)).contents

               addr = socket.inet_ntop(family, sa.sin_addr)

           elif socket.AF_INET6 == family:

               sa = cast(pointer(sa), POINTER(sockaddr_in6)).contents

               addr = socket.inet_ntop(family, sa.sin6_addr)

       except ValueError:

           # Обработка NULL pointer access.

           family, addr = None, None

       return family, addr

Метод _get_address() переводит адреса в печатную форму, используя уже известную нам функцию inet_ntop().

В методе получения интерфейсов вызовем функцию getifaddrs() и создадим объекты класса NetworkInterface из возвращенного функцией списка:

   def _get_network_interfaces(self):

       result = getifaddrs(pointer(self._ifa_pointer))

 

       if result != 0:

           raise OSError(get_errno())

 

       try:

           retval = []

           # Заполнить Python-список интерфейсов.

           for ifa in self._ifa_iter():

               i_face = NetworkInterface(ifa.ifa_name)

               retval.append(i_face)

               addr_family, addr = \

                   NetworkInterfaces._get_address(ifa.ifa_addr)

               nm_family, netmask = \

                   NetworkInterfaces._get_address(ifa.ifa_netmask)

 

               if addr:

                   i_face.addresses[addr_family] = addr

               if netmask:

                   i_face.netmask[nm_family] = netmask

 

           return retval

       finally:

           # Python не управляет памятью, выделенной C API,

           # поэтому требуется ее освободить.

           freeifaddrs(self._ifa_pointer)

 

   @property

   def interfaces(self):

       return self._get_network_interfaces()

 

if __name__ == '__main__':

   for ni in NetworkInterfaces().interfaces:

       print(f'{str(ni)}\n')

При вызове функция getifaddrs() выделяет память, и эту память необходимо освободить, что делается в блоке finally.

Результат вызова примера в целом аналогичен результату вызова кода на C, только в этом примере выводится меньше полей.

Флаги интерфейса

Одна из характеристик сетевого интерфейса — флаги, которые представляют собой однобитовые значения и передаются как единое слово флагов.

Передается это слово через поле ifr_flags структуры ifreq. Значения флагов определены в файле net/if.h. Сейчас определено два типа слов: общее слово флагов и расширенное, или частное для устройства, слово флагов. И, соответственно, четыре вызова для работы с ними:

SIOCGIFFLAGS, SIOCSIFFLAGS — получить или установить общее слово ­флагов.

SIOCGIFPFLAGS, SIOCSIFPFLAGS — получить или установить расширенное слово флагов.

Флаги могут относиться к двум группам:

Флаги административного состояния — устанавливаются пользователем и определяют его пожелания.

Флаги оперативного состояния — устанавливаются системой и определяют реальное или рабочее состояние интерфейса.

Рассмотрим, как пользователь работает с интерфейсом. Например, требуется включить интерфейс, чтобы начать обмен данными. С этой целью пользователь устанавливает соответствующий флаг или задает административное состояние интерфейса.

То, что флаг установлен, не означает, что интерфейс работает. Например, может произойти обрыв кабеля, может быть неработоспособно устройство, могут возникнуть проблемы с драйверами — и еще множество причин, по которым интерфейс не запустится.

Более того, на инициализацию адаптера требуется какое-то время, и включение интерфейса пользователем вовсе не означает, что он сразу будет готов к работе.

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

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

Подробнее состояния описаны в RFC 2863 «The Interfaces Group MIB».

Прочитать флаги процесс может всегда. Но установка слова флагов является привилегированной операцией.

Далее рассмотрим конкретные флаги.

Флаги активности интерфейса

Флаги активности выключают и включают интерфейс, а также позволяют узнать, работает ли он.

Флаги административного состояния:

IFF_UP — интерфейс активен, то есть «поднят» администратором и готов к работе.

IFF_ECHO — интерфейс отправляет эхо-пакеты. Флаг требуется, чтобы предотвратить отправку эхо-пакетов на пакеты, отправленные в ответ данным интерфейсом. Флаг ECHO указывает, что устройству CAN необходимо зациклить пакеты, передаваемые обратно на принимающую сторону. Используется шиной CAN и устройствами IEEE 802.11, то есть Wi-Fi. Присутствует в ядре Linux выше версии 2.6.

Флаги оперативного состояния:

IFF_RUNNING — интерфейс запущен и работает. Флаг означает, что интерфейс полностью работоспособен и данные могут быть переданы.

• IFF_LOWER_UP — драйвер сигнализирует о том, что физический уровень активизирован. Для Ethernet — если драйвер находится в этом состоянии, есть несущая. Хотя это флаг оперативного состояния, некоторые драйверы, не управляющие реальными устройствами, разрешают пользователям устанавливать этот флаг. Присутствует в ядре Linux выше версии 2.6.

• IFF_DORMANT — физический уровень интерфейса активен, но ожидает внешнего события, например установления соединения по Ethernet. Пока устройство находится в таком состоянии, использовать его нельзя. Присутствует в ядре Linux выше версии 2.6.

Флаги типа среды передачи

Актуальны для устройств, которые поддерживают несколько типов среды. Например, Ethernet-адаптер может использовать коаксиальный кабель, то есть Ethernet Over Coaxical — EoC, или неэкранированную витую пару — UTP.

Как правило, это старые или промышленные адаптеры, такие как изображенный на рис. 10.3. У этого адаптера имеется два различных физических интерфейса.

Флаги позволяют выбрать тип среды:

IFF_PORTSEL — флаг сигнализирует о том, что устройство способно переключаться между несколькими типами физического носителя. Тип носителя можно выбирать вручную, используя вызов SIOCSIFMAP, описанный ниже.

IFF_AUTOMEDIA — включен автоматический выбор носителя адаптером. Для адаптеров, которые поддерживают несколько типов носителя, на что указывает IFF_PORTSEL. Устройство автоматически выберет правильный носитель.

Рис. 10.3. Сетевой адаптер с несколькими интерфейсами

Флаги типа интерфейса

Эти флаги информационные, то есть их можно только читать:

IFF_LOOPBACK — интерфейс является локальной петлей. Флаг проверяется ядром. Это более правильный способ, чем сравнивать имя с "lo".

• IFF_POINTOPOINT — интерфейс представляет собой одну из сторон для соединения «точка-точка», например, если интерфейс «подключен» к интерфейсу абонента через PPP.

Флаги работы маршрутизации и адресации

Позволяют узнать тип адреса либо задать свойства интерфейса, влияющие на обработку адресов:

IFF_BROADCAST — установлен допустимый широковещательный адрес.

• IFF_NOARP — не используется ARP. Адрес назначения канального уровня не установлен. Обычно устанавливается на интерфейсах, где активен флаг IFF_POINTOPOINT. Очевидно, что для подключений «точка-точка» ARP не требуется.

• IFF_PROMISC — интерфейс находится в неразборчивом режиме, то есть может принимать все данные. В случае Ethernet это будут все кадры, а не только содержащие MAC-адрес, равный адресу интерфейса. Используется в снифферах, которые мы подробнее рассмотрим в главе 22.

• IFF_ALLMULTI — принимать все многоадресные пакеты, даже те, чьи адреса не перечислены в списке адресов, на которые оформлена подписка. Используется в роутерах, которые осуществляют маршрутизацию много­адресных потоков данных.

• IFF_MULTICAST — интерфейс поддерживает многоадресную рассылку. Если флаг не установлен, интерфейс не будет обрабатывать многоадресные пакеты.

• IFF_DYNAMIC — указывает, что при деактивации интерфейса адрес на нем может измениться, например, в случае непостоянных подключений, динамически назначающих IP-адрес. Раньше это были dial-up-подключения, когда до провайдера дозванивались по телефонной линии, используя модем.

Флаги балансировки нагрузки и группировки интерфейсов

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

Связывание нескольких интерфейсов в один позволяет увеличить отказоустойчивость или пропускную способность:

IFF_MASTER — главный интерфейс в группе балансировки нагрузки. Этот флаг используется прикладным кодом, не влияя на драйвер адаптера.

IFF_SLAVE — подчиненный интерфейс в группе балансировки нагрузки.

Утилита ifenslave из одноименного пакета, которая отвечает за бондинг, или агрегацию интерфейсов, оперирует этими флагами.

Конечно, для работы агрегации каналов флагов недостаточно и, кроме утилиты, нужен еще модуль ядра bonding.

Флаги настройки передачи данных устройством

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

Трейлерная инкапсуляция позволяет уменьшить число копирований данных получателем, используя оптимизацию выравнивания данных на канальном уровне. Это может улучшить производительность, если такая возможность поддерживается обоими абонентами.

Подробнее о трейлерной оптимизации см. в RFC 893 «Trailer Encapsulations», RFC 894 «A Standard for the Transmission of IP Datagrams over Ethernet Networks» и RFC 1042 «A Standard for the Transmission of IP Datagrams over IEEE 802 Networks».

Флаги отладки

Для включения режима отладки существует флаг IFF_DEBUG. Его может использовать драйвер, чтобы отправлять в лог более подробные данные через printk(). На большинство сетевых драйверов установка данного флага не повлияет.

Расширенные флаги состояния

Вызовы SIOCGIFPFLAGS, SIOCSIFPFLAGS нужны для работы с расширенным состоянием устройства. Первый ioctl необходим для их получения, второй — для установки.

В основном эти флаги информационные, что не исключает наличия в отдельных системах другого типа флагов:

IFF_802_1Q_VLAN — интерфейс представляет устройство 802.1Q VLAN.

• IFF_EBRIDGE — интерфейс установлен как мост Ethernet.

• IFF_SLAVE_INACTIVE — интерфейс представляет собой неактивное ведомое устройство.

• IFF_MASTER_8023AD — интерфейс служит мастером соединения 802.3ad, то есть используется транкинг портов, что, по сути, то же, что и агрегация каналов через EtherChannel, и устройство является главным.

• IFF_MASTER_ALB — интерфейс служит мастером агрегированного канала в режиме balanced-alb, то есть адаптивной балансировки нагрузки.

• IFF_BONDING — интерфейс является ведущим или ведомым устройством. Иными словами, находится в связке, например, для повышения надежности или балансировки нагрузки.

• IFF_SLAVE_NEEDARP — интерфейс должен использовать ARP для проверки целостности канала.

• IFF_ISATAP — это интерфейс RFC 4214 «Intra-Site Automatic Tunnel Addressing Protocol (ISATAP)». ISATAP — экспериментальный протокол туннелирования.

Переименование интерфейса

Для переименования интерфейса можно использовать ioctl SIOCSIFNAME.

В поле ifr_name структуры ifreq задается имя того интерфейса, который требуется переименовать, а в поле ifr_newname — новое имя. Это привилегированная операция, которая может быть произведена только на неактивном интерфейсе.

Реализуем функцию для переименования:

std::string set_interface_name(const std::string &old_name,

                               const std::string &new_name)

{

    // Максимальная длина имени фиксирована размером буфера.

    if (new_name.size() >= IFNAMSIZ)

    {

        throw std::logic_error("Incorrect name size");

    }

 

    int sock = socket(PF_INET, SOCK_DGRAM, 0);

 

    if (sock < 0)

    {

        throw std::system_error(errno, std::generic_category(),

                                "Opening socket");

    }

 

    // Имя — нуль-завершенная строка.

    ifreq ifr = {0};

    // Размер буфера при копировании превышать нельзя.

    std::copy_n(old_name.begin(),

                std::min(old_name.size(), static_cast<size_t>(IFNAMSIZ)),

                ifr.ifr_name);

    std::copy_n(new_name.begin(),

                std::min(new_name.size(), static_cast<size_t>(IFNAMSIZ)),

                ifr.ifr_newname);

 

    // Переименование.

    if (ioctl(sock, SIOCSIFNAME, &ifr) != 0)

    {

        // Сохранить значение errno.

        auto e = errno;

        

        // Нам требуется errno на момент возникновения ошибки.

        // Но close() заменяет errno.

        close(sock);

        throw std::system_error(e, std::generic_category(),

                                "SIOCSIFNAME ioctl failed");

    }

 

    // Вернуть старое имя.

    return ifr.ifr_name;

}

Переименуем локальный интерфейс:

sudo build/bin/b01-ch10-if_rename eno2 eth2

SIOCSIFNAME ioctl failed: Device or resource busy

Ошибка возникает потому, что интерфейс активен:

ip a show eno2|head -1

2: eno2: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000

Переведем его в неактивное состояние и попробуем переименовать еще раз:

sudo ip link set down eno2

sudo build/bin/b01-ch10-if_rename eno2 eth2

eno2

ip a show eno2|head -1

2: eth2: <BROADCAST,MULTICAST> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000

ip a show eth2

2: eth2: <BROADCAST,MULTICAST> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000

   link/ether e4:54:...

   altname enp0s31f6

   altname eno2

Видим, что в этот раз переименование сработало.

Иногда устройство может быть занято и освободится спустя некоторое время. Чтобы обработать данную ситуацию, в udev, например, используется цикл переименования, который запускается в случае неудачной попытки:

int loop = 30 * 20;

while (loop--)

{

    retval = ioctl(sk, SIOCSIFNAME, &ifr);

    if (retval == 0)

    {

        kernel_log(ifr);

        break;

    }

 

    if (errno != EEXIST)

    {

        err("error changing net interface name %s to %s: %s\n",

            ifr.ifr_name, ifr.ifr_newname, strerror(errno));

        break;

    }

    dbg("wait for netif '%s' to become free, loop=%i\n", udev->name,

        (30 * 20) — loop);

    usleep(1000 * 1000 / 20);

}

Внимание! Интерфейс будет доступен под своим предыдущим именем, которое отображается как altname, и переименовать его обратно в eno2 без специальных действий не получится.

Чтобы обойти ограничение на длину в IFNAMESZ символов, в Linux существует механизм альтернативных имен, который также используется для того, чтобы иметь имена, основанные на разных критериях для одного интерфейса. Например, одно пользовательское, а второе — зависящее от положения адаптера на шине. Переименование через ioctl сохраняет автоматически назначенное ядром имя как альтернативное. Это имя можно удалить:

sudo ip link property del eno2 altname eno2

После этого обратное переименование будет работать:

sudo build/bin/b01-ch10-if_rename eth2 eno2

eth2

 

ip link show eth2

Device "eth2" does not exist.

 

ip link show eno2

2: eno2: <BROADCAST,MULTICAST> mtu 1500 qdisc fq_codel state DOWN mode DEFAULT group default qlen 1000

   link/ether e4:54:...

   altname enp0s31f6

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

Зачем нужно переименование интерфейса

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

После обновления сервера имена могут измениться, но администратор не должен перенастраивать систему, особенно если таких серверов много. Поэтому в Linux есть утилита ifrename, которая может использоваться для переименования интерфейсов по определенным правилам. Эта утилита использует данный ioctl.

Интерфейс можно переименовать и через команду ip, например:

ip link set eno2 name eth2

Интерфейс может именоваться, например, на основе MAC. Администратор добавляет соответствующее правило udev, что значительно облегчает его работу.

Система устройства udev также использует переименование в процессе своей работы.

Адреса интерфейса

Несколько ioctl служат для установки и получения различных адресов. В них также используются поля вышеприведенной структуры ifreq.

Рассмотрим эти вызовы:

SIOCGIFADDR, SIOCSIFADDR, SIOCDIFADDR — получить, установить или удалить адрес интерфейса. Для совместимости SIOCGIFADDR возвращает только адреса AF_INET, SIOCSIFADDR принимает адреса AF_INET и AF_INET6, а SIOCDIFADDR удаляет только адреса AF_INET6. Адрес AF_INET можно удалить, установив его в 0 через SIOCSIFADDR. В зависимости от типа адреса используются поля ifr_addr или ifr6_addr с ifr6_prefixlen.

SIOCGIFDSTADDR, SIOCSIFDSTADDR — получить или установить адрес назначения в соединении «точка-точка». Используется поле ifr_dstaddr. Получить или изменить IPv6-адреса соединения «точка-точка» можно только через сокеты RTNetlink.

SIOCGIFBRDADDR, SIOCSIFBRDADDR — получить или установить широковещательный адрес. Используется поле ifr_brdaddr.

SIOCGIFNETMASK, SIOCSIFNETMASK — получить или установить сетевую маску. Используется поле ifr_netmask.

SIOCGIFHWADDR, SIOCSIFHWADDR — получить или установить аппаратный адрес адаптера. Используется поле ifr_hwaddr, которое представляет собой структуру sockaddr:

sa_family — содержит тип устройства ARPHRD_*, например ARPHRD_ETHER для Ethernet. В Linux константы объявлены в net/if_arp.h или linux/if_arp.h.

sa_data — аппаратный адрес L2, начиная с байта 0.

SIOCSIFHWBROADCAST — установить широковещательный аппаратный адрес устройства из ifr_hwaddr.

• SIOCADDMULTI, SIOCDELMULTI — добавить адрес в многоадресные фильтры канального уровня или удалить его оттуда. Используется поле ifr_hwaddr.

SIOCGIFCONF — возвращает адреса сетевого уровня. С целью совместимости возвращает только адреса AF_INET. В отличие от других этот ioctl передает структуру ifconf.

Структура ifconf определена следующим образом:

#include <net/if.h>

 

struct ifconf

{

    // Размер буфера в байтах.

    int ifc_len;

    union

    {

        // Адрес буфера.

        char *ifc_buf;

        // Массив структур для получения сетевых адресов.

        struct ifreq *ifc_req;

    };

};

Этот вызов сложнее остальных. Чтобы его выполнить, требуются следующие действия:

1. Сначала вызвать SIOCGIFCONF с нулевым полем ifc_req. Он вернет количество байтов в ifc_len, необходимое под буфер.

2. Затем выделить буфер размера ifc_len и передать его адрес в ifc_req, а размер — в ifc_len.

3. Ioctl вернет указатель на массив структур ifreq, каждая из которых будет содержать имя интерфейса в ifr_name и IP-адрес в ifr_addr. Поле ifc_len будет содержать количество переданных байтов.

Этот алгоритм вызова показан на рис. 10.4.

Рис. 10.4. Вызов SIOCGIFCONF для получения адресов

Внимание! Если размер, указанный параметром ifc_len, недостаточен для получения всех адресов, ядро пропустит лишние данные и вернет статус успешного завершения. Поэтому всегда заранее выделяйте буфер достаточного размера, как это показано выше.

Рассмотрим на примере. Сначала получим размер буфера:

int main()

{

    // Все поля нулевые.

    ifconf ifc = {0};

 

    const socket_wrapper::SocketWrapper sock_wrap;

    const auto sock = socket_wrapper::Socket(AF_INET, SOCK_DGRAM, 0);

 

    if (sock < 0)

    {

        throw std::system_error(errno, std::system_category(), "socket");

    }

 

    // Вызвать для получения размера.

    if (ioctl(sock, SIOCGIFCONF, &ifc) < 0)

    {

        throw std::system_error(errno, std::system_category(),

                                "SIOCGIFCONF with 0 buffer");

    }

 

    // Вывести размер буфера.

    std::cout << "Buffer size = " << ifc.ifc_len << std::endl;

 

    assert(ifc.ifc_len);

Повторный вызов заполнит переданный буфер структурами ifreq, содержимое которых мы выведем в цикле:

    // Создать буфер нужного размера.

    std::vector<char> buffer(ifc.ifc_len);

    // Передать его адрес в структуру.

    ifc.ifc_buf = buffer.data();

    // Сделать повторный вызов.

    if (ioctl(sock, SIOCGIFCONF, &ifc) < 0)

    {

        throw std::system_error(errno, std::system_category(), "SIOCGIFCONF");

    }

    // Вывести имена и адреса интерфейсов.

    for (auto i = ifc.ifc_req;

         i != ifc.ifc_req + ifc.ifc_len / sizeof(ifreq); ++i)

    {

        std::cout << i->ifr_name << ": ";

        if (AF_INET == i->ifr_addr.sa_family)

        {

            std::array<char, INET_ADDRSTRLEN> buf;

            std::cout

                << inet_ntop(AF_INET,

                    &(reinterpret_cast<sockaddr_in*>(&i->ifr_addr)->sin_addr),

                    buf.data(), buf.size());

        }

        else if (AF_INET6 == i->ifr_addr.sa_family)

        {

            std::array<char, INET6_ADDRSTRLEN> buf;

            std::cout

                << inet_ntop(AF_INET6,

                  &(reinterpret_cast<sockaddr_in6*>(&i->ifr_addr)->sin6_addr),

                  buf.data(), buf.size());

        }

 

        std::cout << std::endl;

    }

 

    return EXIT_SUCCESS;

}

Если при доступе к структурам ifconf либо ifreq возникнет ошибка, будет возвращен EFAULT.

Вывод примера:

build/bin/b01-ch10-if_list

Buffer size = 160

lo: 127.0.0.1

wlo1: 192.168.2.13

docker0: 172.18.0.1

tun0: 10.32.51.216

Все перечисленные операции являются привилегированными. Для ioctl получения и установки адресов «точка-точка», широковещательных адресов и сетевой маски с целью совместимости принимаются и возвращаются только адреса AF_INET.

Назад: Глава 9. Вспомогательные данные
Дальше: Метрики и другие параметры интерфейса