Интересные возможности сетевой подсистемы Netgraph [2008]
Стандартные настройки сети в FreeBSD обычно не вызывают особых проблем. В интернете полно документации, процесс конфигурирования описан вдоль и поперек. Но при объединении нескольких сетей в одну, подсчете трафика и балансировке нагрузки приходится изрядно попотеть. К счастью, есть элегантный выход – использовать netgraph.
1996 - 2008
Сетевая подсистема netgraph была создана в 1996 году Джулианом Элисчером и Арчи Коббсом, как попытка решить ограниченную поддержку некоторого оборудования и протоколов в FreeBSD. Первые версии были доступны в специальной версии FreeBSD 2.2, собираемой для компании Whistle InterJet, а дебют в основном дереве состоялся в FreeBSD 3.4. Сегодня netgraph – это часть ядра ОС. Основная идея подсистемы – модульность, комбинация простых инструментов для реализации более сложного решения.
Netgraph строится на взаимодействии узлов (nodes), которые посредством крючков (hooks) создают пару захвата (одну для каждого узла). Крючок узла формируется в момент связывания и определяет, как узел может быть подключен. Данные идут в двух направлениях вдоль ребер (edges) от узла к узлу. Когда узел получает пакет данных, он его обрабатывает и затем отправляет другому узлу. Узел может быть источником/потребителем данных, например, если он связан с аппаратной частью, или может просто добавлять/удалять заголовки, выполнять мультиплексирование и т.п.
Поначалу количество узлов было небольшим, но с каждой новой версией системы их количество увеличивалось. Для FreeBSD 6.х уже были доступны модули, обеспечивающие поддержку: PPPoE, ATM, ISDN, Bluetooth, HDLC, EtherChannel, Frame Relay, L2TP и др. Система построена так, что добавить новый модуль очень легко. В FreeBSD 7.0 появились новые узлы: ng_car (алгоритмы ограничения трафика и rate-лимитов), ng_deflate (поддержка Deflate сжатия для PPP) и ng_pred1 (Predictor-1 сжатие для PPP).
Многие программы завязаны на netgraph. Например, реализация PPP для FreeBSD MPD (mpd.sf.net) использует интерфейс netgraph, благодаря чему большинство операций выполняется на уровне ядра системы и, тем самым, повышается скорость работы.
Несмотря на то, что сегодня насчитывается уже шесть десятков узлов, нормальная документация по использованию netgraph отсутствует. Можно отметить несколько устаревший документ «All about Netgraph» (русский перевод находится по адресу citrin.ru/daemonnews/netgraph.html), написанный разработчиками. В нем рассказано об общих принципах работы и дано несколько примеров, но его чтение хоть и дает некоторое понимание процесса, в конкретной ситуации помочь не сможет. В итоге остается справочная страница netgraph(4) и связанные с ним man'ы, описывающие особенности узлов, плюс несколько примеров в /usr/share/examples/netgraph.
Поддержка ядром
Как правило, специально включать поддержку netgraph в ядре не требуется. Все необходимое доступно в виде модулей. В чем можно убедиться во время работы того же MPD при помощи команды /sbin/kldstat. Просмотреть все имеющиеся KLD-модули узлов можно, введя команду:
# ls /boot/kernel/ng_*.ko
Сюда же относится модуль netgraph.ko, который является основным. При пересборке ядра следует включить группу параметров:
# grep NETGRAPH /usr/src/sys/conf/NOTES options NETGRAPH # поддержка netgraph options NETGRAPH_PPP # поддержка PPP в netgraph options NETGRAPH_PPTPGRE # поддержка gre-туннелирования …
Каждый узел netgraph может адресоваться при помощи абсолютного или относительного адреса. Абсолютный адрес получается из имени узла или его ID. Если узел сопоставлен с устройством, то он обычно их имена совпадают. В этом случае для адресации используется адрес, состоящий из имени устройства и двоеточия. Например, устройству fxp0 будет сопоставлен узел fxp0, к которому следует обращаться, введя «fxp0:». К имени узла можно добавлять связанные крючки:
fxp0:hook1.hook2
Адрес вроде «.:» или «.» всегда показывает на локальный узел. Как в имени хука, так и в имени узла использовать знаки точки и двоеточия по понятным причинам запрещено. Если узел не получил имя, к нему можно обратиться, используя его уникальный номер ID, заключенный в квадратные скобки. Например, к ID 00000002 можно обратиться так – «[00000002]:».
Каждый хук также имеет уникальное имя, отражающее его цель и использующееся только в контексте данного узла. Поэтому на разных узлах можно создавать крючки с одинаковым названием. Для некоторых типов (например, ksockets) формат имени не может быть произвольным и строго определен. Относительный адрес состоит только из имен хуков.
Отметим еще один важный момент. На сегодня существует 15 типов узлов со своими характеристиками и, соответственно, назначениями. Изучив особенности каждого типа, легко выбрать узел для решения конкретной задачи. К примеру, тип узла echo всегда принимает подключения через любой хук. Полученный пакет просто отправляется назад в виде ответа. Такой тип узла полезен при отладке. Теперь посмотрим описание узла ng_ether – «Ethernet netgraph node type». Тип Ethernet означает, что узел фактически является netgraph-копией физического интерфейса Ethernet с возможностью перехватывать и отправлять фреймы с этого интерфейса. Его можно использовать для реализации PPP поверх Ethernet (PPPoE). Узел состоит из трех хуков: lower (или divert), upper и orphans, которые отвечают за определенный уровень работы. Или другой узел – ng_bridge – «Ethernet bridging netgraph node type». Нетрудно догадаться, что этот тип позволяет организовать сетевой мост на Ethernet-интерфейсе.
У каждого типа и конкретного модуля может быть множество параметров и настроек. Поэтому управление реализовано посредством специальных команд. Такие управляющие сообщения делятся на основные, поддерживаемые всеми модулями (определены в netgraph/ng_message.h), и индивидуальные – полное их описание есть только в конкретном man (кстати, иногда оно недостоверное).
Виртуальный свич
Раз уже начали с узла ng_bridge, то разберем подробнее его в работе, соединив при помощи виртуального свича несколько Ethernet-сетей. В /usr/share/examples/netgraph/ether.bridge находится скрипт, который можно использовать в качестве примера, а после некоторой доводки – и для организации подобной функции. Кстати: если настройки выполняются удаленно через SSH, то после выполнения ряда команд соединение может прерваться.
Для управления netgraph применяются две утилиты: ngctl и nghook. Первая более функциональна, поэтому в дальнейшем будем использовать именно ее. Для начала проверим, какие узлы уже созданы в системе:
# ngctl list There are 1 total nodes: Name: ngctl850 Type: socket ID: 00000001 Num hooks: 0
В списке нет нужных нам узлов, поэтому загружаем модули вручную:
Для автоматической загрузки добавляем в /boot/loader.conf следующие строки:
$ sudo vi /boot/loader.conf netgraph_load="YES" ng_ether_load="YES" ng_bridge_load="YES"
Теперь снова смотрим список узлов:
# ngctl list There are 3 total nodes: Name: ngctl863 Type: socket ID: 00000004 Num hooks: 0 Name: le1 Type: ether ID: 00000003 Num hooks: 0 Name: le0 Type: ether ID: 00000002 Num hooks: 0
Если мы введем «ifconfig | grep le», то обнаружим, что имена Ethernet-интерфейсов совпадают именем nodes (то есть, le0 и le1). Создаем бридж и подключаем его хуком link0 к le0 на нижнем (lower) уровне.
# ngctl mkpeer le0: bridge lower link0
Параметр name команды ngctl позволяет дать имя созданному узлу:
# ngctl name le0:lower bnet0
Заметил, что к узлу обращаемся по имени «le0:lower», а к созданному узлу bnet0 – «bnet0:»? Вывод «ngctl list» должен показать наличие нового узла с именем bnet0 и типом bridge. Собираем все в кучу (le0 и link0 использовать нельзя, так как они уже задействованы):
# ngctl connect le1: bnet0: lower link1
Аналогично, используя крючки link2 и link3, соединяем le* с bnet0 по верхнему уровню:
Мост готов. Для удобства все эти команды лучше записать в файл, который выполняется во время загрузки системы. При желании можно обеспечить фильтрацию пакетов. Для этого в netgraph есть два модуля: ng_bpf и ng_ipfw, реализующие возможности соответствующих пакетных фильтров.
Соединение нескольких сетей
В предыдущем примере мы получили аналог стандартного «bridge_load="YES"». Возможности подсистемы значительно шире. Попробуй в стандартном случае добавить (при помощи net.link.ether.bridge.config) третий Ethernet-интерфейс или соединить удаленные сети в одну виртуальную, и ты столкнешься с кучей проблем. Может быть, даже придется прибегнуть к построению целой схемы использования виртуальных интерфейсов, VPN-сетей и т.д.
А при использовании netgraph для подключения третьего Ethernet достаточно добавить несколько строчек, взяв за образец подключение le1 в нашем примере. Если же необходимо подключить удаленную сеть в единый виртуальный свич, то порядок действий будет иным. И здесь применение netgraph уже не покажется таким сложным (возможно, несколько запутанным, но это только на первый взгляд).
Настроим узел ksocket (kernel socket), способный оперировать UDP-туннелями, и подключим его к созданному ранее узлу bnet0 (модуль ядра ng_ksocket в этом случае стартует автоматически). Имя хука должно быть вида «family/type/proto» (значения параметров доступны в socket(2)):
Отправляем сообщение узлу bnet0:link4 (к которому подключен ksocket), чтобы он создал сокет, куда будут поступать данные с внешнего IP-адреса (пусть адрес будет 1.2.3.4, порт выберем произвольный, выше 1024):
# ngctl msg bnet0:link4 bind inet/1.2.3.4:1111
На другом сервере повторяем эти настройки, только указываем свой IP-адрес (имена могут отличаться):
Теперь локальные и удаленная сети соединены в одну при помощи виртуального свича, причем без использования VPN.
Подсчет трафика
К netgraph часто обращаются из-за необходимости подсчета трафика. Получать статистику можно, задействуя различные модули. Например, «врежем» модуль ng_tee в le0 между lower и upper уровнями Ethernet-интерфейса. Этот модуль имеет такую же функциональность, как и стандартная утилита tee, дублирующая ввод. Вручную загружать его не требуется, ядро делает это автоматически при первом к нему обращении. Крючки right и left являются стандартными для этого модуля и показывают, с какой стороны мы его подключаем (подробности в «man 4 ng_tee»).
# ngctl mkpeer le0: tee lower right # ngctl name le0:lower le0_tee # ngctl connect le0: lower upper left
Проверяем настройки:
# ngctl show le0: Name: le0 Type: ether ID: 00000003 Num hooks: 2 Local hook Peer name Peer type Peer ID Peer hook ---------- --------- --------- ------- --------- upper le0_tee tee 00000006 left lower le0_tee tee 00000006 right
Чтобы получить статистику, нужно отправить сообщение getstats:
Более подробную информацию о проходящем трафике способен показать узел ng_netflow(4). Итоговая информация экспортируется в виде, совместимом с популярным протоколом NetFlow компании Cisco. Поэтому для ее анализа можно использовать любой коллектор, поддерживающий этот протокол. Кстати, кроме ng_netflow, есть еще ng_ipacct, но он не входит в стандартную поставку, а последняя версия на сайте ftp.wuppy.net.ru/pub/FreeBSD/local/kernel/ng_ipacct датирована декабрем 2006 года (а значит, не будем на нем останавливаться).
Существует несколько схем подключения ng_netflow. Разберем одну из них. Начинаем:
# ngctl mkpeer le0: tee lower left # ngctl connect le0: le0:lower upper right
Создаем many-узел при помощи ng_one2many (подробнее об этом типе узла читай ниже, в разделе «Балансировка нагрузки») и подключаемся к нему:
Для проверки вводим «ngctl list». Если модули загружены, запрашиваем таблицу подключений netflow:
# ngctl show netflow
Далее устанавливаем одну из утилит-коллекторов. Список некоторых коллекторов приведен на странице www.cse.wustl.edu/~cs5/567/traffic. Поиск в портах FreeBSD также даст нужный результат:
$ cd /usr/ports/ $ make search key=netflow
Например, выбираем flow-tools:
# cd /usr/ports/net-mgmt/flow-tools # make install
После этого в каталог /var/netflows будет складываться захваченная статистика; каждый час будет создаваться новый файл (-n 24). Файлы имеют бинарный формат, поэтому для просмотра используем специальные утилиты из комплекта flow-tools. Ключ '-f' указывает на формат вывода, при значении 10 будет выведен адрес источника и назначения. Ключ '-S' отвечает за поле сортировки:
Стоит упомянуть еще об одном интересном модуле – ng_one2many. Этот узел позволяет объединить несколько интерфейсов по принципу «один ко многим» (или «многие к одному»). На один из интерфейсов, который объявляется как one, перенаправляются все many-интерфейсы, входящие пакеты собираются в one. Подробности настройки с примером приведены на man-странице ng_one2many(4), перевод лежит на www.opennet.ru/base/net/ng_one2many.txt.html. С помощью ng_one2many можно объединить несколько сетевых интерфейсов (например, подключенных к разным провайдерам) в один узел, и трафик будет равномерно распределяться между интерфейсами. Для распределения нагрузки используется round-robin алгоритм. Как вариант, предлагается дублирование пакетов от one на все many-интерфейсы. К сожалению, пока он еще недостаточно хорошо определяет работоспособность канала, поэтому следует использовать его с осторожностью. Итак, объявляем le0 как one:
С этого момента ширина полосы, обеспечиваемая виртуальным интерфейсом, увеличилась.
Собери мозаику
Наличие большого количества модулей netgraph позволяет реализовывать действительно уникальные функции, складывая узлы, как мозаику, в любой комбинации, врезая при необходимости новые узлы или делая ответвления. Но, к сожалению, отсутствие полноценной документации существенно усложняет изучение netgraph.
INFO
Сегодня для netgraph существует около 60 готовых модулей, буквально на все случаи жизни.
В качестве отправной точки для изучения netgraph используй документ «All about Netgraph», перевод которого доступен по адресу citrin.ru/daemonnews/netgraph.html.
Перевод man ng_one2many и некоторые примеры можно найти на сайте www.opennet.ru.