Перевод руководства по Netgraph (netgraph freebsd socket)
Ключевые слова: netgraph, freebsd, socket, (найти похожие документы)
From: А. В. Южанинов <citrin@mail.ru.>
Date: Mon, 18 Nov 2004 14:31:37 +0000 (UTC)
Subject: Перевод руководства по Netgraph
Оригинал: http://citrin.pp.ru/daemonnews/All-About-Netgraph.txt
Все о Netgraph
Арчи Коббс (Archie Cobbs) <archie@freebsd.org.>
Перевод А. В. Южанинова <citrin@mail.ru.> Оригинал статьи опубликован
на сайте Daemon News
Часть I: Что такое Netgraph?
Обоснование
Представьте следующую ситуацию: вы разрабатываете маршрутизатор TCP/IP
основанный на FreeBSD. Продукт должен поддерживать синхронные
последовательные WAN-линии, то есть выделенные цифровые каналы,
работающие на скоростях до T1, где используется инкапсуляция HDLC. Вы
должны поддерживать следующие протоколы для передачи IP пакетов через
кабель:
* IP пакеты, передаваемые поверх HDLC (простейший путь для
транспортировки IP)
* IP пакеты, передаваемые поверх "Cisco HDLC" (по существу, пакеты
дополнены двухбайтным полем Ethertype, и периодически посылаются
keep-alive пакеты).
* IP пакеты, передаваемые поверх frame relay (frame relay
предоставляет до 1000 виртуальных каналов точка-точка поверх одой
кабельной сети).
* IP в инкапсуляции RFC 1490 поверх frame relay (RFC 1490 определяет
способ передачи нескольких протоколов через одно соединение и он
часто используется совместно frame relay).
* PPP поверх HDLC
* PPP поверх frame relay
* PPP в инкапсуляции RFC 1490, поверх frame relay
* PPP поверх ISDN
* Можно даже предположить, что вам придется поддерживать frame relay
поверх ISDN (!)
На Рисунке 1 показаны все возможные комбинации:
+------------+
+----------# IP #----------+
| +--#------#--+ |
+------#------+ | | +-------#-------+
| RFC1490 #------|------|-----# PPP |
+------#------+ | | +--#--#------#--+
| | | | | |
+------#------+ | | | | |
| Frame Relay #------|------|--------+ | |
+----#---#----+ | | | |
| | | +---#--------+ | |
| | | | Cisco HDLC | | |
| | | +---#--------+ | |
| | | | | +---#----+
| +-----------|------|-----------|--# ISDN |
| | | | +--------+
+----#---------------#------#--------+ |
| HDLC Framing #--+
| Synchronous serial |
+------------------------------------+
Рисунок 1: Способы передачи IP поверх последовательных синхронных и
ISDN WAN соединений
Эта ситуация была показана Джулианом Элисчером (Julian Elischer)
<julian@freebsd.org.> и мной в 1996 когда мы работали в компании
Whistle InterJet. В то время во FreeBSD имелась очень ограниченная
поддержка последовательного синхронного оборудования и протоколов. Мы
думали использовать OEMing от Emerging Technologies, но вместо этого
решили реализовать это сами.
Ответом был netgraph. Netgraph это сетевая подсистема в ядре,
следующая принципу UNIX достижения мощности посредством комбинации
простых инструментов, каждый их которых предназначен для выполнения
одной, вполне определенной задачи. Основная идея проста: есть узлы
(nodes) (инструменты) и ребра (edges) которые соединяют пару узлов
(отсюда и "граф" в "netgraph"). Пакеты данных идут в двух направлениях
вдоль ребер от узла к узлу. Когда узел получает пакет данных, он
обрабатывает его, и затем (обычно) отправляет его другому узлу.
Обработка может быть простейшим добавлением/удалением заголовков или
может быть более сложной или включает другие компоненты системы.
Netgraph напоминает потоки (Streams) в System V, но он разработан
более гибким и производительным.
Netgraph оказался очень полезным для работы в сети, и сейчас он
используется в Whistle InterJet для всех указанных выше комбинаций
протоколов (за исключением frame relay поверх ISDN), плюс обычный PPP
поверх асинхронных последовательных (таких как модемы и терминальные
адаптеры) и PPTP, которые включают шифрование. Во всех этих протоколах
данные полностью обрабатываются в ядре. В случае PPP, пакеты
согласования (negotiation) обрабатываются отдельно в пользовательском
режиме (смотрите порт FreeBSD для mpd).
Узлы и ребра
Глядя на рисунок выше, очевидно, что должны быть узлы и ребра. Менее
очевиден факт, что узел может иметь определенное число подключений к
другим узлам. Например, вполне возможно иметь одновременно IP, IPX, и
PPP в инкапсуляции RFC 1490; конечно, мультиплексирование это та
задача, для которой и нужен RFC 1490. В этом случае нужно три ребра
подключить к узлу RFC 1490, одно для каждого стека протоколов. Нет
требований, чтобы данные следовали строго в определенном направлении,
и нет ограничений на действия, которые узел выполняет с пакетом. Узел
может быть источником/потребителем данным, например, если он связан с
аппаратной частью, или он может просто добавлять/удалять заголовки,
мультиплексировать и т. п.
Узлы netgraph существуют в ядре и полупостоянно. Обычно узел
существует пока он подключен, к какому либо другому узлу, однако
некоторые узлы постоянные, например, узлы, связанные с аппаратной
частью; когда число ребер уменьшается до нуля, аппаратное устройство
выключается. Поскольку узлы существуют в ядре, они не связаны с каким
либо определенным процессом.
Управляющие сообщения
Эта картина все еще слишком упрощенная. В реальной жизни узел нужно
конфигурировать, запрашивать его состояние и т. д. Например, PPP
сложный протокол, с большим количеством опций. Для этого в netgraph
определены управляющие сообщения (control messages). Управляющее
сообщение это "внешние управление". Вместо следования от узла к узлу
как пакеты данных, управляющие сообщения посылаются асинхронно и
непосредственно от одного узла к другому. Два узла могут быть не
связаны (даже через другие узлы). Для обеспечения этого в netgraph
существует простая схема адресации по которой узел можно
идентифицировать, используя простую ASCII-строчку.
Управляющие сообщения это просто структуры Си с фиксированным
заголовком (структура ng_mesg) и переменной областью данных. Есть
несколько управляющих сообщений, которые все узлы должны понимать; они
называются общие управляющие сообщения (generic control messages) и
реализованы в базовой системе. Например, узлу можно указать разрушить
себя или создать/разрушить ребро. Узлы могут также иметь свои
собственные управляющие сообщения, зависящие от типа. Каждый тип узла,
определяющий свои собственные управляющие сообщения должен иметь
уникальное значение typecookie. Комбинация полей typecookie и command
в заголовке управляющего сообщения определяет, как его
интерпретировать.
На управляющие сообщения часто идут ответы в виде ответного
контрольного сообщения (reply control message). Например, чтоб узнать
состояние узла или статистику вы можете послать управляющее сообщение
"get status"; он затем пошлет вам ответ (который идентифицируется
значением token, скопированным из исходного запроса) содержащий
запрошенную информацию в поле данных. Заголовок ответного управляющего
сообщения обычно такой же, как исходный заголовок, но выставлен флаг
reply flag
Netgraph предоставляет способ преобразования этих структур в строки
ASCII и обратно для упрощения взаимодействия с человеком.
Крючки (Hooks)
В netgraph, ребра на самом деле не существуют сами по себе. Вместо
этого ребро это просто комбинация двух крючков (hooks), по одному от
каждого узла. Крючок узла определяет как узел может быть подключен.
Каждый крючок имеет уникальное, статически определенное имя, которое
часто отражает его цель. Имя имеет значение только в контексте данного
узла; два узла могут иметь крючки с одинаковым названием.
Например, рассмотрим узел Cisco. Cisco HDLC это очень простая схема
мультиплексирования протоколов посредством дополнения каждого кадра
спереди полем Ethertype перед передачей на физический уровень. Cisco
HDLC поддерживает одновременную передачу IP, IPX, AppleTalk, и т. д.
Таким образом, узел netgraph для Cisco HDLC (см ng_cisco(8))
определяет крючки, называемые inet, atalk, and ipx. Эти крючки
предназначены для подключения к соответствующим вышележащим стекам
протоколов. Он так же определяет крючок, называемый downstream который
подключается к нижележащему уровню, например узлу, связанному с
синхронной последовательной платой. К пакетам, получаемым через крючки
inet, atalk, и ipx добавляется два байта заголовка, и затем они
отправляются через крючок downstream. Наоборот, из пакетов полученных
через downstream удаляется заголовок, и они отправляются вверх через
крючок, соответствующий протоколу. Узел так же обрабатывает
периодические пакеты "tickle" и запросы, определенные протоколом Cisco
HDLC.
Крючки всегда либо подключены, либо не подключены; операция
подключения или отключения пары крючков атомарная. Когда пакет
посылается через крючок, которые не подключен, он отбрасывается.
Некоторые примеры типов узлов
Некоторые типы узлов достаточно очевидны, такие как Cisco HDLC. Другие
менее очевидны, но предоставляют некоторые интересные функции,
например, возможность обращаться непосредственно к устройству или
открытому сокету внутри ядра.
Несколько примеров типов узлов, реализованных на настоящий момент во
FreeBSD. Все эти типы узлов описаны в соответствующих страницах
справочного руководства man.
тип узла echo: ng_echo(8)
Этот тип узла принимает подключения через любой крючок. Любые
получаемые пакеты просто посылаются назад через тот же крючок.
Любые не общие управляющие сообщения так же возвращаются назад
в виде ответов.
тип узла discard: ng_disc(8)
Этот тип узла принимает подключения через любой крючок. Любые
пакеты данных и контрольные сообщения молча отбрасываются.
тип узла tee: ng_tee(8)
Этот тип узла похож на двунаправленную версию утилиты tee(1).
Он копирует данные проходящие через него в любом направлении
("right" или "left"), и полезен для отладки. Пакеты, получаемые
через "right" посылаются через "left" и копия шлется через
"right2left"; аналогично для пакетов идущих от "left" к
"right". Пакеты, получаемые через "right2left" посылаются через
"left" и пакеты получаемы через "left2right", отправляются
через "right".
+---------------+
+------------| |------------+
# right2left | | left2right #
+------------| ``tee'' node |------------+
+------------| |------------+
# left | | right #
+------------| |------------+
+---------------+
Рисунок 2: Тип узла tee
тип узла interface: ng_iface(8)
Этот тип узла одновременно узел netgraph и системный интерфейс
PPP. Он имеет (пока) три крючка, называемые "inet", "atalk" и
"ipx". Эти крючки соответствуют стекам протоколов IP, AppleTalk
и IPX соответственно. Первый раз, при создании интерфейсного
узла, интерфейс ng0 показывается в выводе ifconfig -a. Вы
можете прописать на этом интерфейсе адрес, как на другом PPP
интерфейсе, пинговать удаленную сторону, и т. д. Конечно, узел
должен быть подключен к чему либо, иначе пакеты ping будут
выходить через крючок inet и исчезать.
К сожалению, FreeBSD в настоящий момент не поддерживает
удаление интерфейсов, т. о. однажды создав узел ng_iface(8) он
будет существовать до следующей перезагрузки (однако это будет
скоро исправлено).
$ ifconfig ng0 inet 1.1.1.1 2.2.2.2
USER +-------------------------+
---------------| Interface node ``ng0'' |---------------
KERNEL +-------------------------+
| i | | i | | a |
| n | | p | | t |
| e | | x | | a |
| t | | | | l |
| | | | | k |
+---+ +---+ +---+
Рисунок 3: Тип узла interface
тип узла TTY: ng_tty(8)
Этот тип узла одновременно узел netgraph и дисциплина
асинхронной последовательной линии (line discipline) (см.
tty(4)). Вы создаете узел установкой дисциплины линии
NETGRAPHDISC на последовательной линии. Узел имеет одни крючок
называемый "hook". Пакеты, получаемые через "hook" передаются
(как последовательные байты) через соответствующее
последовательное устройство; данные, получаемые от устройства,
формируются в пакеты и посылаются через "hook". Нормальное
чтение и запись в последовательную линию блокируются.
+---+
| h |
| o |
| o |
| k |
+-----------------+
| tty node |
|-----------------|
| /dev/cuaa0 |
+-----------------+
Рисунок 4: Тип узла TTY
тип узла socket: ng_socket(8)
Этот тип узла очень важен, поскольку позволяет программам
пользовательского режима взаимодействовать с системой netgraph.
Каждый узел одновременно узел netgraph и пара сокетов из
семейства PF_NETGRAPH. Узел создается, когда программа
пользовательского режима создает соответствующий сокет через
системный вызов socket(2). Один сокет используется для передачи
и получения пакетов данных, а второй для контрольных сообщений.
Этот узел поддерживает крючки с произвольными именами, например
"hook1", "hook2" и т. д.
s1 = socket(PF_NETGRAPH, SOCK_DGRAM, NG_CONTROL);
s2 = socket(PF_NETGRAPH, SOCK_DGRAM, NG_DATA);
+----+ +----+
| | | |
| s1 | | s2 |
| | | |
+-----------------------+
USER | |
------------| socket node |-------------
KERNEL | |
+-----------------------+
| h | | h |
| o | | o |
| o | | o |
| k | | k |
| 1 | | 2 |
+---+ +---+
Рисунок 5: Тип узла socket
тип узла BPF: ng_bpf(8)
Этот тип узла выполняет сравнение с шаблоном и фильтрацию
пакетов так, как будто они следуют через bpf(4).
тип узла ksocket: ng_ksocket(8)
Этот тип узла противоположен ng_socket(8). Каждый узел
одновременно является сокетом, полностью расположенном в ядре.
Данные, получаемые узлом, записываются в сокет и наоборот.
Нормальные bind(2), connect(2), и т. д. операции осуществимы
вместо контрольных сообщений. Этот тип узла полезен для
туннелирования пакетов через сокет (например, туннелирование IP
поверх UDP).
тип узла ethernet: ng_ether(8)
Если вы скомпилировали ваше ядро с options NETGRAPH, то каждый
интерфейс Ethernet так же является узлом netgraph с таким же
именем как интерфейс. Каждый узел имеет два крючка "orphans" и
"divert"; только один крючок может быть подключен одновременно.
Если "orphans" подключен, то устройство продолжает работать
нормально, за исключением того, что все пакеты Ethernet с
неизвестным или неподдерживаемым типом, доставляются через этот
крючок (в нормальном режиме эти пакеты просто отбрасываются).
Когда крючок "divert" подключен то все входящие пакеты
доставляются через этот крючок. Пакет полученный через любой из
этих крючков передается в кабель. Все пакеты "сырые" кадры
Ethernet со стандартным 14-байтным заголовком (но без
контрольной суммы). Этот тип узла полезен, например для PPP
поверх Ethernet (PPPoE).
Синхронные драйверы: ar(4) and sr(4)
Если вы скомпилировали ваше ядро с options NETGRAPH, то
драйвера ar(4) и sr(4) перестанут работать в нормальном режиме
и вместо этого будут работать как постоянные узлы netgraph (с
таким же именем как название устройства). Сырые кадры HDLC
могут быть прочитаны и записаны через крючок "rawdata".
Метаинформация
В некоторых случаях пакеты данных могут иметь связанную метаинформацию
которую нужно передать вместе с пакетом. Хотя это редко используется,
netgraph предоставляет механизм, чтоб сделать это. Пример
метаинформации - приоритеты: некоторые пакеты могут иметь более
высокий приоритет чем другие. Типы узлов могут определять свою
собственную, специфичную метаинформацию, и netgraph для этой цели
определяет структуру ng_meta. Мета информация не воспринимается
базовой системой netgraph
Адресация узлов netgraph
Каждый узел netgraph адресуем через строку ASCII, называемую адрес
узла (node address) или путь (path). Адрес узла используется только
для отправки контрольных сообщений.
Многие узлы имеют имена. Например, узел, ассоциированный с устройством
будет обычно иметь такое же имя как устройство. Когда узел имеет имя,
он всегда может быть адресован, используя абсолютный адрес, состоящий
из имени устройства и двоеточия. Например, если вы создали
интерфейсный узел, названный "ng0" его адрес будет "ng0:".
Если узел не имеет имени, вы можете составить его из уникального
номера ID узла заключив его в квадратные скобки (каждый узел имеет
уникальный номер ID). Таким образом, если узел ng0: умеет номер ID
1234, тогда "[1234]:" так же является адресом этого узла.
Наконец, адрес ".:" или "." всегда указывает на локальный узел
(источник).
Относительная адресация так же возможно когда два узла соединены
опосредовано. Относительный адрес использует имена последовательных
крючков в пути от одного узла к другому. Рассмотрим рисунок:
+--------------+ +-----------------+
| |--------++--------| |
| node 1 | hook1a || hook2a | |
| |--------++--------| |
+--------------+ | |
| node 2 |
+--------------+ | |
| |--------++--------| |
| node 3 | hook3a || hook2b | |
| |--------++--------| |
+--------------+ +-----------------+
Рисунок 6: Простая конфигурация узлов
Если узел node1 хочет послать контрольное сообщение узлу node2, он
может использовать адрес ".:hook1a" или просто "hook1a". Для обращения
к узлу node3, он может использовать адрес ".:hook1a.hook2b" или просто
"hook1a.hook2b". Аналогично, узел node3 может обратиться к узлу node1,
используя адрес ".:hook3a.hook2a" или просто "hook3a.hook2a".
Относительные и абсолютные адреса можно сочетать, например,
"node1:hook1a.hook2b" будет указывать на узел node3.
Часть II: Использование Netgraph
Netgraph поставляется с утилитами командной строки и пользовательской
библиотекой, которые позволяют взаимодействовать с системой ядра
netgraph. Необходимы привилегии root для работы с netgraph из
пользовательской режима.
Из командной строки
Есть две утилиты командой строки для взаимодействия с netgraph,
nghook(8) и ngctl(8). nghook(8) очень проста: она подключается к
любому неподключенному крючку любого узла и позволяет вам передавать и
получать пакеты данных через стандартный ввод и стандартный вывод.
Вывод может быть дополнительно декодирован в читаемый человеком формат
hex/ASCII. В командной строке вы указываете абсолютный адрес узла и
имя крючка.
Например, если ваше ядро собрано с options NETGRAPH и вы имеете
сетевой интерфейс fxp0, следующая команда перенаправит все сетевые
пакеты получаемые картой и выведет их через стандартный вывод в
формате hex/ASCII:
nghook -a fxp0: divert
ngctl(8) более функциональная программа, которая позволяет вам делать
практически все с netgraph из командной строки. Она работает в
пакетном или интерактивном режиме, и поддерживает несколько команд,
которые выполняют интересующую работу, в том числе:
connect Соединить пару крючков для объединения двух узлов
list Вывести список всех узлов в системе
mkpeer Создать узел и подключить его к существующему узлу
msg Послать форматированное ASCII сообщение узлу
name Назначит узлу имя
rmhook Отключить два подключенных крючка
show Показать информацию об узле
shutdown Удалить/сбросить узел, разрушив все подключения
status Получить статус узла в удобочитаемом виде
types Показать типы установленных узлов
quit Выйти из программы
Эти команды могут быть объединены в скрипт, который делает что-то
полезное. Например, предположим, что у вас есть две частные сети,
которые разделены, но обе подключены к интернету через машины с
FreeBSD. Сеть A имеет внутренние адреса из диапазона 192.168.1.0/24 и
внешний IP адрес 1.1.1.1, в то время как сеть B имеет адреса
192.168.2.0/24 и внешний адрес 2.2.2.2. Используя Netgraph, вы можете
легко сделать UDP для IP трафика между двумя частными сетями. Пример
скрипта, который это может сделать (его можно так же найти в
/usr/share/examples/netgraph):
#-----------------------------------------------------------------------
#!/bin/sh
# Этот скрипт устанавливает виртуальный канал точка-точка между двумя
# подсетями, используя UDP пакеты в качестве "глобального канала".
# Эти две подсети могут иметь адреса немаршрутизируемые между двумя
# файрволами.
# Определение локальной и удаленной внутренней сетей, также как и
# локального и удаленного внешних IP адресов и номера UDP порта,
# которые будут использованы для туннеля
#
LOC_INTERIOR_IP=192.168.1.1
LOC_EXTERIOR_IP=1.1.1.1
REM_INTERIOR_IP=192.168.2.1
REM_EXTERIOR_IP=2.2.2.2
REM_INSIDE_NET=192.168.2.0
UDP_TUNNEL_PORT=4028
# Создать интерфейсный узел "ng0", если его еще нету,
# если есть, просто убедиться, что он ни к чему не подключен
#
if ifconfig ng0 >/dev/null 2>&1; then
ifconfig ng0 inet down delete >/dev/null 2>&1
ngctl shutdown ng0:
else
ngctl mkpeer iface dummy inet
fi
# Присоединить UDP сокет к крюку "inet" интерфейсного узла использую
# узел типа ng_ksocket(8).
#
ngctl mkpeer ng0: ksocket inet inet/dgram/udp
# Присоединить UDP сокет к локальному внешнему IP и порту
#
ngctl msg ng0:inet bind inet/${LOC_EXTERIOR_IP}:${UDP_TUNNEL_PORT}
# Установить соединение с внешним IP и портом на удаленном сервере
#
ngctl msg ng0:inet connect inet/${REM_EXTERIOR_IP}:${UDP_TUNNEL_PORT}
# Настроить интерфейс точка-точка
#
ifconfig ng0 ${LOC_INTERIOR_IP} ${REM_INTERIOR_IP}
# Добавить маршрут к удаленной частной сети через туннель
#
route add ${REM_INSIDE_NET} ${REM_INTERIOR_IP}
#-----------------------------------------------------------------------
Далее рассмотрим как можно работать с ngctl(8) в интерактивном режиме.
Пользовательский ввод выделен синим.
Запустим ngctl в интерактивном режиме. Будет показан список доступных
команд...
$ ngctl
Available commands:
connect Connects hook <peerhook> of the node at <relpath> to <hook>
debug Get/set debugging verbosity level
help Show command summary or get more help on a specific command
list Show information about all nodes
mkpeer Create and connect a new node to the node at "path"
msg Send a netgraph control message to the node at "path"
name Assign name <name> to the node at <path>
read Read and execute commands from a file
rmhook Disconnect hook "hook" of the node at "path"
show Show information about the node at <path>
shutdown Shutdown the node at <path>
status Get human readable status information from the node at <path>
types Show information about all installed node types
quit Exit program
ngctl создает при запуске узел типа ng_socket(8). Это наш локальный
узел netgraph, который используется для взаимодействия с другими
узлами в системе. Посмотрим на него. Мы видим, что ngctl назначил ему
имя "ngctl652" и его тип "socket", номер ID 45 и он имеет ноль
подключенных крючков, т. е. он не подключен к другим узлам.
+ show .
Name: ngctl652 Type: socket ID: 00000045 Num hooks: 0
Теперь мы создадим узел "tee" и подключим его к локальному узлу. Мы
подключим крючок "right" узла "tee" к крючку "myhook" на локальном
узле. Мы можем использовать любое имя для нашего крючка, так как узел
типа ng_socket(8) поддерживает крючки с произвольными именами. После
этого снова посмотрим на наш локальный узел, чтобы убедиться, что он
имеет безымянного соседа типа "tee".
+ help mkpeer
Usage: mkpeer [path] <type> <hook> <peerhook>
Summary: Create and connect a new node to the node at "path"
Description:
The mkpeer command atomically creates a new node of type "type"
and connects it to the node at "path". The hooks used for the
connection are "hook" on the original node and "peerhook" on
the new node. If "path" is omitted then "." is assumed.
+ mkpeer . tee myhook right
+ show .
Name: ngctl652 Type: socket ID: 00000045 Num hooks: 1
Local hook Peer name Peer type Peer ID Peer hook
---------- --------- --------- ------- ---------
myhook <unnamed> tee 00000046 right
Аналогично, если мы посмотрим на вывод узла tee, мы увидим, что он
подключен к нашему локальному узлу через крючок "right". Узел "tee"
все еще безымянны, но мы можем его указать используя абсолютный адрес
"[46]:" или относительный адрес ".:myhook" или "myhook"...
+ show .:myhook
Name: <unnamed> Type: tee ID: 00000046 Num hooks: 1
Local hook Peer name Peer type Peer ID Peer hook
---------- --------- --------- ------- ---------
right ngctl652 socket 00000045 myhook
Теперь назначим ему имя и убедимся, что можем по нему обратиться к
этому узлу...
+ name .:myhook mytee
+ show mytee:
Name: mytee Type: tee ID: 00000046 Num hooks: 1
Local hook Peer name Peer type Peer ID Peer hook
---------- --------- --------- ------- ---------
right ngctl652 socket 00000045 myhook
Теперь подключим узел Cisco HDLC к другой стороне узла "tee" и снова
проверим узел "tee". Мы подключимся к крючку "downstream" узла Cisco
HDLC, как будто бы узел tee соответствует подключению к WAN. Cisco
HDLC слева (крючек "left") от узла tee наш локальный узел справа
(крючок "right") от узла tee...
+ mkpeer mytee: cisco left downstream
+ show mytee:
Name: mytee Type: tee ID: 00000046 Num hooks: 2
Local hook Peer name Peer type Peer ID Peer hook
---------- --------- --------- ------- ---------
left <unnamed> cisco 00000047 downstream
right ngctl652 socket 00000045 myhook
+
Rec'd data packet on hook "myhook":
0000: 8f 00 80 35 00 00 00 02 00 00 00 00 00 00 00 00 ...5............
0010: ff ff 00 20 8c 08 40 00 ... ..@.
+
Rec'd data packet on hook "myhook":
0000: 8f 00 80 35 00 00 00 02 00 00 00 00 00 00 00 00 ...5............
0010: ff ff 00 20 b3 18 00 17 ... ....
Эй, что это такое?! Выглядит так, будто мы получаем какие то пакеты
данных через наш крючок "myhook". Узел Cisco каждые 10 секунд посылает
периодические пакеты keep-alive. Эти пакеты проходят через узел tee
(слева направо от крючка "left" к крючку "right") и принимаются
крючком "myhook", где ngctl показывает их в консоли.
Теперь посмотрим список всех узлов, существующих в системе. Заметим,
что два наших интерфейса Ethernet так же показаны, поскольку это
постоянные узлы и мы собирали ядро с options NETGRAPH...
+ list
There are 5 total nodes:
Name: <unnamed> Type: cisco ID: 00000047 Num hooks: 1
Name: mytee Type: tee ID: 00000046 Num hooks: 2
Name: ngctl652 Type: socket ID: 00000045 Num hooks: 1
Name: fxp1 Type: ether ID: 00000002 Num hooks: 0
Name: fxp0 Type: ether ID: 00000001 Num hooks: 0
+
Rec'd data packet on hook "myhook":
0000: 8f 00 80 35 00 00 00 02 00 00 00 00 00 00 00 00 ...5............
0010: ff ff 00 22 4d 40 40 00 ..."M@@.
OK, давайте выключим (то есть удалим) узел Cisco HDLC, таким образом
мы остановим получение данных...
+ shutdown mytee:left
+ show mytee:
Name: mytee Type: tee ID: 00000046 Num hooks: 1
Local hook Peer name Peer type Peer ID Peer hook
---------- --------- --------- ------- ---------
right ngctl652 socket 00000045 myhook
Теперь, давайте посмотрим статистику узла tee. Мы пошлем управляющее
сообщение и немедленно получим ответ. Команда и ответ конвертируются
в/из ASCII автоматически с помощью ngctl, так как управляющие
сообщение это двоичная структура...
+ help msg
Usage: msg path command [args ... ]
Aliases: cmd
Summary: Send a netgraph control message to the node at "path"
Description:
The msg command constructs a netgraph control message from the
command name and ASCII arguments (if any) and sends that
message to the node. It does this by first asking the node to
convert the ASCII message into binary format, and re-sending the
result. The typecookie used for the message is assumed to be
the typecookie corresponding to the target node's type.
+ msg mytee: getstats
Rec'd response "getstats" (1) from "mytee:":
Args: { right={ outOctets=72 outFrames=3 } left={ inOctets=72 inFrames=3 }
left2right={ outOctets=72 outFrames=3 } }
Ответ это просто строковая версия структуры ng_tee_stats возвращаемой
в ответном управляющем сообщении (Эта структура определена в
ng_tee.h). Мы видим, что три кадра (и 72 байта) прошли через узел
слева направо. Каждый кадр был скопирован и отправлен через крючок
"left2right" (но поскольку этот крючок не подключен эти кадры были
отброшены).
OK, теперь проиграемся с узлом ng_ksocket(8)...
+ mkpeer ksocket myhook2 inet/stream/tcp
+ msg .:myhook2 connect inet/127.0.0.1:13
ngctl: send msg: Operation now in progress
Rec'd data packet on hook "myhook":
0000: 54 75 65 20 46 65 62 20 20 31 20 31 31 3a 30 32 Tue Feb 1 11:02
0010: 3a 32 38 20 32 30 30 30 0d 0a :28 2000..
Мы создали в ядре TCP сокет, используя узел ng_ksocket(8), и
подключили его к сервису "daytime" на локальной машине, который
возвращает текущее время. Как мы узнали, что нужно использовать
"inet/127.0.0.1:13" в качестве аргумента команды "connect"? Это
описано в странице справочного руководства man ng_ksocket(8).
OK, поигрались и хватит...
+ quit
libnetgraph(3)
Существует так же пользовательская библиотека libnetgraph(3) для
использования в программах netgraph. Она предоставляет много полезных
вызовов, которые описаны в справочном руководстве man. Пример
использования их можно посмотреть в исходном коде
/usr/src/usr.sbin/ngctl.
Часть III: Реализация
Функциональная сущность
Как netgraph реализован? Одна из главных целей netgraph это скорость,
поэтому он полностью работает в ядре. Другое конструктивное решение в
том, что netgraph полностью функциональный. То есть пакеты не ставятся
нигде в очередь при перемещении от узла к узлу. Вместо этого
используется прямой вызов функций. Пакеты данных это packet header
mbuf'ы, в то время как мета-данные и управляющие сообщения
Си-структуры расположенные в куче (используя malloc типа M_NETGRAPH).
Объектно-ориентированная сущность
Netgraph отчасти имеет объектно-ориентированную архитектуру. Каждый
тип узла определен как массив указателей на методы, или функции Си,
которые определяют специфическое поведение узлов данного типа. Каждый
метод может быть оставлен NULL для того, чтобы оставить поведение по
умолчанию.
Аналогично, есть несколько управляющих сообщений, которые понимают
узлы всех типов и которые обрабатываются базовой системой (они
называются общими управляющими сообщениями, generic control messages).
Каждый тип узлов может дополнительно определять свои собственные
управляющие сообщения. Управляющие сообщения всегда содержат
typecookie и команду, которые вместе определяют, как интерпретировать
это сообщение. Каждый тип узлов должен определить свое уникальное
значение typecookie если предполагается, что он будет получать свои
управляющие сообщения. Общие управляющие сообщения имеют
предопределенные значения typecookie.
Память
Netgraph использует подсчет ссылок для структур узлов и крючков.
Каждый указатель на узел или крючок считается как одна ссылка. Если
узел имеет имя, оно тоже считается ссылкой. Вся связанная с netgraph
область памяти выделяется и освобождается используя malloc типа
M_NETGRAPH.
Синхронизация
Выполнение кода в ядре требует внимательной синхронизации. Узлы
netgraph обычно выполняются через splnet() (см. spl(9)). Для
большинства типов узлов не требуется дополнительного внимания.
Некоторые узлы, однако, взаимодействуют с другими частями ядра,
которые выполняются с другим приоритетом. Например, последовательный
порт работает через spltty() и поэтому ng_tty(8) должен это учитывать.
На этот случай в netgraph есть альтернативные вызовы передачи данных,
которые обрабатывают все необходимые очереди авто-магически. (см.
ng_queue_data() ниже).
Как реализовать тип узлов
Для реализации нового типа узлов, нужно сделать только две вещи:
1. Определить структуру ng_type.
2. Связать её используя макрос NETGRAPH_INIT().
Второй шаг простой, поэтому мы обратим внимание на первый шаг.
Структура ng_type, из netgraph.h:
/*
* Structure of a node type
*/
struct ng_type {
u_int32_t version; /* должна совпадать с NG_VERSION */
const char *name; /* Уникальное имя типа */
modeventhand_t mod_event; /* Модуль обработки событий (не обязательно
) */
ng_constructor_t *constructor; /* Конструктор узла */
ng_rcvmsg_t *rcvmsg; /* сюда поступают управляющие сообщения */
ng_shutdown_t *shutdown; /* сброс и освобождение ресурсов */
ng_newhook_t *newhook; /* первое сообщение о новом крючке */
ng_findhook_t *findhook; /* только если вы имеете несколько крючков
*/
ng_connect_t *connect; /* заключительное сообщение о новом крючке
*/
ng_rcvdata_t *rcvdata; /* сюда поступают данные */
ng_rcvdata_t *rcvdataq; /* или сюда, если через очередь */
ng_disconnect_t *disconnect; /* предупреждение об отключении */
const struct ng_cmdlist *cmdlist; /* команды, которые мы можем конвер
тировать */
/* R/W данные базового кода netgraph, НЕ ТРОГАТЬ! */
LIST_ENTRY(ng_type) types; /* связанный список всех типов */
int refs; /* число экземпляров */
};
Поле version должно совпадать с NG_VERSION. Это для избежания
связывания несовместимых типов. Поле name уникальное имя типа узлов,
например "tee". mod_event необязательный модуль обработки событий
(когда узел загружается и выгружается) - похоже на статические
инциализаторы в C++ или Java.
Далее идут методы типа узлов, описано подробнее ниже. cmdlist
предоставляет (дополнительно) информацию по конвертированию
управляющих сообщений в/из ASCII (см. ниже), и оставшаяся часть
используется только в базовом коде netgraph.
Методы типа узлов
Каждый тип узлов должен реализовать методы, определенные в структуре
ng_type. Каждый метод имеет реализацию по умолчанию, которая
используется, если тип узлов не определяет данные метод.
int constructor(node_p *node);
Цель: Инициализировать новый узел вызвав ng_make_node_common()
и установив node->private если необходимо. Инициализация узла и
выделение памяти для данного экземпляра узла должно
производиться здесь. Сначала нужно вызвать
ng_make_node_common(); он создаст узел и установит число
указателей в 1.
Действие по умолчанию: Просто вызывает ng_make_node_common().
Когда переопределять: Если требуется специфичная для данного
узла инициализация или выделение ресурсов.
int rcvmsg(node_p node, struct ng_mesg *msg,
const char *retaddr, struct ng_mesg **resp);
Цель: Получает и обрабатывает управляющее сообщение. Адрес
отправителя в retaddr. Функция rcvmsg() ответственна за
освобождение msg. Ответ, если есть, может возвращен синхронно,
если resp != NULL установкой *resp так, чтоб он указывал на
ответ. Общие управляющие сообщения (за исключением
NGM_TEXT_STATUS) обрабатываются базовой системой, и нет
необходимости обрабатывать их здесь.
Действие по умолчанию: Обрабатывает все общие контрольные
сообщения; иначе возвращает EINVAL.
Когда переопределять: Если вы определяете управляющие сообщения
специфичные для данного типа, или если вы хотите реализовать
управляющие сообщения определенные некоторыми другими типами
узлов.
int shutdown(node_p node);
Цель: Выключить узел. Должен отключить все крючки посредством
вызова ng_cutlinks(), освободить всю частную память данного
экземпляра узла, освободить присвоенное имя (если было) через
ng_unname(), и освободить сам узел вызвав ng_unref() (этот
вызов освобождает ссылку добавленную в ng_make_node_common()).
В случае постоянного узла, все крючки должны быть отключены и
связанное устройство (или что там) сбрасывается, но узел не
должен удаляться (т. е., используется только вызов
ng_cutlinks()).
Действие по умолчанию: Вызвать ng_cutlinks(), ng_unname(), и
ng_unref().
Когда переопределять: Когда вы должны отменить то, что вы
сделали в конструкторе.
int newhook(node_p node, hook_p hook, const char *name);
Цель: Подтвердить подключение крючка и инициализировать ресурсы
для данного крючка. Узел должен проверить, что имя крючка
действительно поддерживается узлом этого типа. Уникальность
имени уже проверена (но не будет хуже если проверить это еще
раз).
Если узлу нужна информация по данному крючку, этот метод должен
инициализировать соответственно hook->private.
Действие по умолчанию: Ничего; подключение крючка всегда
разрешено.
Когда переопределять: Всегда, если вы не планируете разрешать
крючки с произвольными именами без инициализации и выделения
ресурсов, и рассматривать все крючки одинаково при подключении.
hook_p findhook(node_p node, const char *name);
Цель: Найти подключенные к данному узлу крючки. Нет
необходимости переопределять этот метод, если вам не нужно
поддерживать большое количество крюков, когда линейный поиск
становится слишком медленным.
Действие по умолчанию: Выполняет линейный поиск по списку
крючков, подключенных к данному узлу.
Когда переопределять: Когда ваш узел поддерживает большое число
одновременно подключенных крючков (скажем, больше чем 50).
int connect(hook_p hook);
Цель: Заключительная проверка вновь подключенного крючка. Этот
метод дает узлу последний шанс проверть только что подключенный
крючок. Например, узел может проверить к кому он подключился.
Если этот метод возвращает ошибку соединение обрывается.
Действие по умолчанию: Ничего; подключение крючка принимается.
Когда переопределять: У меня никогда не было причин
переопределять этот метод.
int rcvdata(hook_p hook, struct mbuf *m, meta_p meta);
Цель: Получить пакет данных через подключенный крючок. Узел
ответственен за освобождение mbuf если он возвращает ошибку,
или если он решит отбросить пакет данных. Хотя сейчас это не
имеет значение, в будущем, возможно, что иногда m == NULL
(например, если посылается только meta), таким образом, узлы
должны учитывать эту возможность.
Действие по умолчанию: Отбросить пакет и метаинформацию.
Когда переопределять: Всегда, если вы не хотите игнорировать
полученные пакеты.
int rcvdataq(hook_p hook, struct mbuf *m, meta_p meta);
Цель: Поставить входящий пакет данных в очередь на получение
подключенным крючком. Узел ответственен за освобождение mbuf
если он возвращает ошибку, или если он решит отбросить пакет
данных.
Замысел в том, что некоторые узлы могут захотеть посылать
данные используя механизм очереди, вместо функционального
механизма. Это требует взаимодействия с типом узлов получателя,
который должен реализовать этот метод надлежащим образом для
того, чтобы делать что-то отличное от rcvdata().
Действие по умолчанию: Вызвать метод rcvdata().
Когда переопределять: Никогда, если у вас нет причин
рассматривать очередь входящих данных отдельно от данных без
очереди.
int disconnect(hook_p hook);
Цель: Уведомление узла об отключении крючка. Узел должен
освободить ресурсы, выделенные крючку в ходе connect().
Хотя функция возвращает int, она должна в действительности
возвращать void поскольку возвращаемое значение игнорируется;
отключение крючка не может быть блокировано узлом.
Функция должна проверять есть ли еще крючки
(hook->node->numhooks == 0) и если был отключен последний
крючок, вызывать ng_rmnode() для самоликвидации, если так надо.
Это позволяет избежать полностью неподключенных узлов, которые
задерживаются в системе после завершения своей работы.
Действие по умолчанию: Ничего не делает.
Когда переопределять: Почти всегда.
int mod_event(module_t mod, int what, void *arg);
Цель: Обрабатывать событие загрузки и выгрузки типа узлов.
Заметим, что оба события обрабатываются этим методом,
разделяются через параметр what который может быть либо
MOD_LOAD либо MOD_UNLOAD. Параметр arg указатель на структуру
ng_type, определяющую тип узлов.
Этот метод никогда не вызывается для MOD_UNLOAD пока существуют
узлы данного типа.
В настоящий момент только вызывается только со значение
MOD_UNLOAD когда вызывается kldunload(2). Однако в будущем
выгрузка типа узлов может быть реализована как мера по "уборке
мусора".
Действие по умолчанию: Ничего не делает. Если не переопределен,
MOD_LOAD и MOD_UNLOAD нормально завершаются.
Когда переопределять: Если ваш тип нуждается в специфической
инициализации или выделении ресурсов при загрузке, или откате
этого при выгрузке. Также, если ваш тип не поддерживает
выгрузку (может быть из-за неразрушимых связей с другими
частями ядра) возвращение ошибки в MOD_UNLOAD предотвратит
выгрузку типа.
Заголовочные файлы netgraph
Каждый тип узлов включает два заголовочных файла. Заголовочный файл
netgraph.h определяет базовые структуры netgraph (хороший
объектно-ориентированный дизайн диктует, что определения структур
ng_node и ng_hook здесь фактически нет; вместо этого они должны быть
скрыты внутри базового кода netgraph). Структуры узлов освобождаются
когда счетчик указателей уменьшается до нуля после вызова ng_unref().
Если узел имеет имя, оно считается ссылкой; для удаления имени (и
ссылка), вызывается ng_unname(). Особенный интерес представляет
структура ng_type, поскольку она должна быть предоставлена для каждого
типа узлов.
Заголовочный файлng_message.h определяет структуры и макросы, имеющие
отношение к обработке управляющие сообщений. В нем определена
структура ng_mesg, с которой начинается любое управляющее сообщение.
Он также является "публичным заголовочным файлом" для всех общих
управляющих сообщений, которые имеют значение typecookie
NGM_GENERIC_COOKIE. Общие управляющие сообщения:
NGM_SHUTDOWN Отключает от целевого узла все крючки и удаляет узел (или
сбрасывает его, если он постоянный)
NGM_MKPEER Создает новый узел и подключается к нему
NGM_CONNECT Подключить крючок целевого узла к другому узлу
NGM_NAME Назначить имя целевому узлу
NGM_RMHOOK Отключить целевой узел от другого узла
NGM_NODEINFO Получить информацию по целевому узлу
NGM_LISTHOOKS Получить список крючков, подключенных к данному узлу
NGM_LISTNAMES Получить список всех именованных узлов*
NGM_LISTNODES Получить список всех узлов, с именем и без имени*
NGM_LISTTYPES Получить список всех установленных типов узлов*
NGM_TEXT_STATUS Получить в удобочитаемом виде информацию о состоянии
узла (может быть не реализовано)
NGM_BINARY2ASCII Преобразовать управляющее сообщение из двоичного вида
в ASCII
NGM_ASCII2BINARY преобразовать управляющее сообщение из ASCII в
двоичный вид
* Не связано в каким либо конкретным узлом
Для большинства перечисленных команд в ng_message.h определены
соответствующие структуры Си.
Заголовочные файлы netgraph.h и ng_message.h некоторые широко
используемые функции и макросы:
int ng_send_data(hook_p hook, struct mbuf *m, meta_p meta);
Действие: Доставляет mbuf m и связанные метаданные meta наружу
через крючок hook и возвращает в error код ошибки. Оба или один
из параметров m и meta могут бытьNULL. В любом случае,
необходимо освободить m и meta при вызове этой функции, поэтому
переменные должны быть сброшены в NULL после вызова (это
производится автоматически, если вместо функции вы используете
макрос NG_SEND_DATA()).
int ng_send_dataq(hook_p hook, struct mbuf *m, meta_p meta);
Действие: Такое же как для ng_send_data(), за исключением того,
что узел-получатель получает данные через его метод rcvdataq()
вместо rcvdata(). Если тип узлов не переопределяет rcvdataq(),
его вызов эквивалентен ng_send_data().
int ng_queue_data(hook_p hook, struct mbuf *m, meta_p meta);
Действие: Такое же как ng_send_data(), за исключением того, что
его безопасно вызывать вне контекста splnet(). mbuf и
метаинформация будут поставлены в очередь и доставлены позже, в
splnet().
int ng_send_msg(node_p here, struct ng_mesg *msg, const char *address,
struct ng_mesg **resp);
Действие: Посылаем контрольное сообщение, указываемое msg от
локального узла here узлу с адресом address, который может быть
абсолютным или относительным адресом. Если resp не NULL, и
получатель желает послать ответ синхронно, он устанавливает
указатель *resp на него. В этом случае вызывающий узел должен
обработать и освободить*resp.
int ng_queue_msg(node_p here, struct ng_mesg *msg, const char
*address);
Действие: Такое же как ng_send_msg(), за исключением того, что
его можно вызывать вне контекста splnet(). Сообщение будет
поставлено в очередь и доставлено позже splnet(). Синхронный
ответ невозможен.
NG_SEND_DATA(error, hook, m, meta)
Действие: Несколько более безопасный вариант ng_send_data(). Он
просто вызывает ng_send_data() и потом устанавливает m и meta в
NULL. Один или оба параметра m и meta могут быть NULL, но они
должны быть переменными (они не могут быть константой NULL
из-за природы работы макроса).
NG_SEND_DATAQ(error, hook, m, meta)
Действие: Несколько более безопасный вариант ng_send_dataq().
Он просто вызывает ng_send_dataq() и потом устанавливает m и
meta в NULL. Один или оба параметра m и meta могут быть NULL,
но они должны быть переменными (они не могут быть константой
NULL из-за природы работы макроса).
NG_FREE_DATA(m, meta)
Действие: Освобождает m и meta и устанавливают в NULL. Один или
оба параметра m и meta могут быть NULL, но они должны быть
переменными (они не могут быть константой NULL из-за природы
работы макроса).
NG_FREE_META(meta)
Действие: Освобождает meta и устанавливает в NULL. Параметр
meta может иметь значение NULL, он должен быть переменной (он
не может быть константой NULL из-за природы работы макроса).
NG_MKRESPONSE(rsp, msg, len, how)
Действие: Выделяет память и инициализирует новое управляющее
сообщение, которое должно быть ответом на msg. Этот ответ имеет
len байт места для аргументов (len должна быть нулевой, если
аргументов нет).msg должен быть указателем на существующую
структуру ng_mesg в то время как rspдолжен иметь тип ng_mesg *.
how это M_WAITили M_NOWAIT (безопаснее использовать M_NOWAIT).
Устанавливает rspв NULL если выделение памяти прошло неудачно.
Инициализирует поля сообщения нулем.
int ng_name_node(node_p node, const char *name);
Действие: Назначает глобальное имя name узлу node. Имя должно
быть уникальным. функция часто вызывается внутри конструктора
узла для узлов, которые соответствуют другой именованной
сущности ядра, например устройству или интерфейсу. Назначение
имени увеличивает на один счетчик ссылок на узлы.
void ng_cutlinks(node_p node);
Действие: Разрывает все подключения крючков node. Обычно
вызывается в ходе выключения узла.
void ng_unref(node_p node);
Действие: Уменьшает на один счетчик ссылок узла и освобождает
узел, если он уменьшился до нуля. Обычно вызывается из метода
shutdown() для освобождения ссылки созданной
ng_make_node_common().
void ng_unname(node_p node);
Действие: Удаляет глобальное имя, назначенное узлу и уменьшает
счетчик ссылок. Если имени не было, то функция ничего не
делает. Должна вызываться из метода shutdown() перед
освобождением узла (через ng_unref()).
Пример из реальной жизни
Достаточно теории, рассмотрим пример. Это реализация узла типа tee.
Как было решено, реализация состоит из открытого заголовочного файла,
Си файла и страницы man. Заголовочный файл ng_tee.h и Си файл
ng_tee.c.
Нужно сделать несколько замечаний по поводу заголовочного файла:
* В заголовочном файле определены следующие важные вещи:
+ Уникальное имя типа "tee" как NG_TEE_NODE_TYPE.
+ Уникальное значение typecookie для узла специфичных
управляющих сообщений узлов "tee", NGM_TEE_COOKIE.
+ Имена четырех крючков, поддерживаемых узлом "tee".
+ Два управляющих сообщения, понимаемых узлами "tee",
NGM_TEE_GET_STATS и NGM_TEE_CLR_STATS.
+ Структура, возвращаемая по NGM_TEE_GET_STATS, которая
определена в struct ng_tee_stats.
Эта информация общедоступна, поскольку другие типы узлов должны
знать это для обмена сообщениями и подключения к узлам tee.
* Всякий раз, когда в формат сообщений вносятся несовместимые
изменения, значение typecookie должно быть изменено для избежания
трудно обнаружимых проблем. Общепринятый способ для генерации
уникальных значений typecookies - использовать вывод команды "date
-u +%s".
* Вместе с структурами Си определены соответствующие макросы,
которые используются при конвертировании между двоичным видом и
ASCII. Хотя эта информация лучше подходит для Си файла, она
помещена в заголовочный файл, для того чтоб было проще
поддерживать соответствие между структурой и макросом.
Несколько замечаний по Си файлу:
* узлы обычно хранят информацию внутреннюю для узла или для каждого
крючка. Для узлов типа ng_tee(8) эта информация хранится в
структуре privdata для каждого узла, и в структуре hookdata для
каждого крючка.
* Массив ng_tee_cmds определяет, как преобразовывать специфичные для
данного типа управляющие сообщения из двоичного вида в ASCII и
обратно. Смотрите ниже.
* Структура ng_tee_typestruct в начале, фактически определяет узел
типа tee. Эта структура содержит версию системы netgraph (для
избежания несовместимости), уникальное имя типа
(NG_ECHO_NODE_TYPE), указатели на методы типа узлов, и указатель
на массив ng_tee_cmds. Некоторые методы нет необходимости
переопределять, поскольку достаточно поведения по умолчанию.
* Макрос NETGRAPH_INIT() нужен для связывания типа. Этот макрос
нужен и при загрузке в виде модуля и при непосредственной
компиляции в ядре (в данном случае, используя options
NETGRAPH_TEE).
* Структуры узла netgraph (тип struct ng_node) содержат счетчик
ссылок, чтоб убедиться, что они будут освобождены вовремя. Скрытый
эффект в вызове ng_make_node_common() из конструктора в том, что
одна ссылка создается. Ссылка освобождается вызовом ng_unref() в
методе ngt_rmnode().
* Также изngt_rmnode() вызывается ng_bypass(). Здесь небольшой
клудж, в том, что два ребра объединяются при отключении узла между
ними (в данном случае узла tee).
* Заметим, что функция ngt_disconnect() разрушает сам узел, при
отключении последнего крючка. Это нужно чтоб узлы не повисали на
неопределенное время, после того как им не осталось работы.
* Синхронизационные вызовы spl не требуются; все выполняется в
пределахsplnet().
Конвертирование управляющих сообщений в/из ASCII
Netgraph простой способ конвертирования управляющих сообщений (по
сути, любых структур Си) между двоичным и ASCII видом. Подробное
описание выходит за рамки данной статьи, но мы дадим обзор.
Вспомним, что управляющее сообщение имеет фиксированный заголовок
(struct ng_mesg) за которым идет полезная нагрузка переменной длины с
определенной структурой и содержанием. Вдобавок заголовок управляющего
сообщения содержит флаг, показывающий, что сообщение является командой
или ответом. Обычно полезная часть сообщения имеет разную структуру в
команде и в ответе. Например, для узла "tee" определено управляющее
сообщение NGM_TEE_GET_STATS. Когда мы посылаем команду
((msg->header.flags & NGF_RESP) == 0), полезная нагрузка пустая. Когда
посылается ответ на команду ((msg->header.flags& NGF_RESP) != 0),
полезная нагрузка содержит структуру ng_tee_stats в которой находится
статистика.
Для каждого управляющего сообщения, которое тип узлов понимает,
определено как конвертировать полезную нагрузку в (в обоих случаях,
команды и ответа) между родной двоичной формой и удобочитаемой ASCII
версией. Эти определения называются типы разбора (netgraph parse
types).
Поле cmdlist в структуре ng_type, определяющей тип узлов указывает на
массив структур ng_cmdlist. Каждый элемент в этом массиве
соответствует специфичному для данного типа сообщению, понимаемому
этим узлом. В соответствие с typecookie и ID команды (которые
однозначно определяют контрольное сообщение), сопоставлено имя ASCII и
два типа разбора которые определяют как полезная нагрузка
структурирована, по одному для каждого направления (команда и ответ).
Типы разбора строятся на основе типов разбора предопределенных в
ng_parse.h. Используя эти типы разбора, вы можете описать структуры Си
любой сложности, даже содержащие массивы переменной длины и строки. В
узле "tee" есть пример как это сделать для структуры ng_tee_stats
возвращаемой управляющим сообщением NGM_TEE_GET_STATS (см. ng_tee.h и
ng_tee.c).
Вы можете так же определить собственные типы разбора с нуля, если
необходимо. Например, тип узлов "ksocket" содержит специальный код для
преобразования структуры sockaddr в адреса семейства AF_INET и
AF_LOCAL, чтобы сделать их более удобочитаемыми. Код, имеющий
отношение к этому отношение, может быть найден в ng_ksocket.h и
ng_ksocket.c, особенно в секции "STRUCT SOCKADDR PARSE TYPE".
Типы разбора удобный и эффективный способ конвертирования
двоичного/ASCII в ядре без большого объема коду по ручному разбору и
работы со строками. Когда это действительно сильно влияет на
производительность, всегда могут быть использованы двоичные сообщения
непосредственно, которые не нужно конвертировать.
Детально информацию о типах разбора можно посмотреть в ng_parse.h и
ng_parse.c.
Советы программистам
Несколько вещей, которые нужно иметь виду, если вы собираетесь писать
свой собственный тип узлов:
* Сначала убедитесь, что вы ясно понимаете как mbuf'ы работают и
используются.
* Все пакеты данных должны быть mbuf'ом заголовка пакета, т. е.,
флаг M_PKTHDR должен быть установлен.
* Не забывайте обновить m->m_pkthdr.len когда вы изменяете m->m_len
для любого mbuf в цепочке.
* Не забывайте проверить m->m_len и вызвать m_pullup() перед
доступом к данным в mbuf. Не вызывайте m_pullup() если это не
нужно. Всегда следует придерживаться следующего шаблона:
struct foobar *f;
if (m->m_len < sizeof(*f) && (m = m_pullup(m, sizeof(*f))) == NULL) {
NG_FREE_META(meta);
return (ENOBUFS);
}
f=mtod(m, struct foobar *);
...
* Не забывайте вовремя освобождать все ресурсы, например в
методах disconnect() и shutdown() для предотвращения утечек памяти
т.п. Я случайно оставил таймер запущенным, и это имело гибельный
результат.
* Если вы используете таймер (см. timeout(9)), проверьте что первым
в вашем обработчике стоит вызовsplnet() (и splx() перед выходом,
конечно). Вызов timeout() не сохраняет уровень SPL в обработчике
событий.
* Убедитесь, что ваш узел исчезает, если все его ребра были
разрушены, если только у вас нет веских причин не делать так.
Часть IV: Планы на будущее
Работа над Netgraph все еще продолжается, и помощники приветствуются!
Есть несколько идей, по поводу будущей работы.
Типы узлов
еще много типов узлов не написано:
* Узлы типа "slip", которые будут реализовывать протокол SLIP. Это
должно быть достаточно легко и возможно скоро будет сделано.
* Больше узлов сжатия и шифрования PPP, которые могут быть
подключены к узлу ng_ppp(8) например, сжатие Deflate для PPP, 3DES
шифрование PPP и т. д.
* Реализация ipfw(4) как узла netgraph.
* Реализация Динамического Пакетного Фильтра (DPF) как узла
netgraph. DPF это разновидность высокоскоростной, компилирующей
налету (JIT compiling) версии BPF.
* Общий узел типа "mux", где каждый крючок может быть настроен
добавлять/удалять определенный заголовок из пакетов.
Во FreeBSD сейчас имеется четыре реализации PPP: sppp(4), pppd(8),
ppp(8), и порт MPD. Это достаточно глупо. Используя netgraph, это
может быть объединено в один демон, работающий в пользовательском
режиме, который будет выполнять все согласования и настройки, в то
время как маршрутизация данных будет полностью происходить в ядре,
через узлы ng_ppp(8). Это позволит объединить гибкость и удобство
настройки демонов, работающих в пользовательском режиме, со скоростью
работы в ядре. Сегодня MPD единственная реализация которая полностью
основана на netgraph, но есть планы так же переработать ppp(8).
Перевод управляющих сообщений в ASCII
Не все типы узлов, которые определяют свои собственные управляющие
сообщения поддерживают преобразование между двоичным видом и ASCII.
Одна из задач - завершить эту работу для узлов, которые в этом еще не
сделано.
управление потоком
Одна из проблем, к которой возможно придется обратиться - это
управление потом. Сейчас когда вы посылаете пакет данных, если
конечный получатель узла не может принять его из-за переполнения
очереди передачи или по другой причине, все что может быть сделано это
отбросить пакет и вернуть ENOBUFS. Возможно, мы сможем определить
новый код возврата ESLOWDOWN или что-то, что будет означать "пакет
данных не отброшен; очередь полна; уменьшите скорость и попробуйте
позже." Другой вариант определить типы метаинформации эквивалентные
XOFF (остановить передачу) и XON (возобновить передачу).
Чистка кода
Netgraph объектно-ориентированный, но преимущества
объектно-ориентированной архитектуры должны использоваться более полно
без ущерба производительности. Пока слишком много видимых полей в
структурах, которые не должны быть доступны, и т. д., так же как много
других разных доработок.
Также, страницы man для всех узлов (например, ng_tee(8)) в
действительности должны находиться в разделе 4, а не 8.
Выключение по цепочке
Было бы удобно сделать новое общее управляющее сообщение
NGM_ELECTROCUTE, которое если послать его узлу выключит узел вместе со
всеми узлами связанными с ним непосредственно или через другие узлы.
Это позволит выполнить быструю очистку сложного графа в netgraph одним
ударом. В дополнение можно сделать новую опцию сокета (см.
setsockopt(2)) которую нужно установить для сокета ng_socket(8) чтоб
при его закрытии автоматически посылалось сообщение NGM_ELECTROCUTE.
Вместе две этих вещи позволят более надежно избежать в netgraph
"утечку узлов".
Обнаружение бесконечных петель
В базовую систему netgraph несложно включить "обнаружение бесконечных
петель". Каждый узел должен иметь свой закрытый счетчик. Счетчик
должен увеличиваться перед каждым обращением к методу rcvdata()
данного узла, и уменьшаться потом. Если счетчик достиг нереально
большого значения, мы считаем что обнаружена бесконечная петля (и
избегаем паники ядра).
Новые типы узлов
Можно создать и улучшить много узлов:
* Маршрутизирующий тип узлов. Каждый подключенный крючок
соответствует маршруту назначения, например комбинации адреса и
маски. Маршруты могут изменяться через управляющие сообщения.
* Узел, реализующий пакетный фильтр с сохранением состояния
(stateful)/файрвол/НАТ (замена для ipfw и/или ipfirewall)
* Типы узлов для ограничения полосы пропускания и/или её учета
* Добавление поддержки VLAN к существующим узлам Ethernet.
Если вы хотите сойти с ума
Теоретически, сетевая подсистема BSD может быть полностью заменена на
netgraph. Конечно, скорее всего, это никогда не случиться, но это
хороший мысленный эксперимент. Каждое сетевое устройство должно быть
постоянным узлом netgraph (как устройства Ethernet). Вверху каждого
устройства Ethernet должен быть мультиплексор Ethertype. К нему должны
быть подключены узлы IP, ARP, IPX, AppleTalk и т. д. Узел IP должен
быть просто мультиплексором IP протокола, над которым должны
находиться узлы TCP, UDP, и т. д. Узлы TCP и UDP должны, наконец,
иметь узлы, похожие на сокеты сверху. И т. д., и т. д.
Другие сумасшедшие идеи (отречение: это сумасшедшие идеи):
* Сделать все устройства в виде узлов netgraph. Преобразование между
ioctl(2) и управляющими сообщениями. Обращайтесь напрямую к вашему
SCSI диску через ngctl(8)! Полная интеграция между netgraph и
devfs(8).
* Узел netgraf также являющийся слоем VFS? файловая система в виде
пространства узлов netgraph?
* Если NFS может работать поверх UDP, это может работать поверх
netgraph. Можно будет иметь диски NFS удаленно смонтированные
через ATM, или просто сделать NFS поверх чистого Ethernet без
посредника в виде UDP.
* Программируемы типу узлов, чья реализация зависит от конфигурации,
используя некую разновидность псевдокода.
Конечно есть еще много сумасшедших идей о которых мы еще не подумали.
Автор обладает всеми правами на эту статью.
Иллюстрации и верстка Copyright © 1998-2003 Daemon News. All Rights Reserved.